From a773e1a7405a2a32ce3169fc7b3ead1f260f0298 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 22:22:21 +0100 Subject: [PATCH 01/11] WebSocket receiver --- package-lock.json | 3 +- package.json | 7 +- server.mjs | 60 ++++++++++++ src/app/runs/[runId]/components/Editor.tsx | 104 +++++++++++---------- src/app/runs/[runId]/page.tsx | 2 +- src/modules/live-run/core.ts | 1 - 6 files changed, 121 insertions(+), 56 deletions(-) create mode 100644 server.mjs diff --git a/package-lock.json b/package-lock.json index 0b86856..d2f890d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,8 @@ "react-dom": "19.2.3", "recharts": "^2.15.4", "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "ws": "^8.19.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/package.json b/package.json index db20ba8..c1ebad0 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "node server.mjs", "build": "next build", - "start": "next start" + "start": "NODE_ENV=production node server.mjs" }, "dependencies": { "@codemirror/lang-go": "^6.0.1", @@ -61,7 +61,8 @@ "react-dom": "19.2.3", "recharts": "^2.15.4", "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "ws": "^8.19.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..add4783 --- /dev/null +++ b/server.mjs @@ -0,0 +1,60 @@ +import { createServer } from 'http'; +import next from 'next'; +import { WebSocketServer } from 'ws'; + +const dev = process.env.NODE_ENV !== 'production'; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +app.prepare().then(() => { + const server = createServer((req, res) => { + handle(req, res); + }); + + const clients = {}; + + const wss = new WebSocketServer({ noServer: true }); + + // Handle WebSocket connections + wss.on('connection', (ws, req) => { + ws.on('message', (msg) => { + const runId = msg.toString(); + if (!clients[runId]) clients[runId] = [] + clients[runId].push(ws); + }); + }); + + // Handle POST requests + server.on('request', async (req, res) => { + console.log(req.method, req.url) + if (req.method === 'POST' && req.url === '/api/run/move') { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + const move = JSON.parse(body); + + clients[move.runId]?.forEach(client => { + if (client.readyState === client.OPEN) { + client.send(JSON.stringify(move.moves)); + } + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok' })); + }); + } + }); + + // Upgrade HTTP connections to WebSocket when needed + server.on('upgrade', (req, socket, head) => { + if (req.url === '/api/view-run') { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req); + }); + } + }); + + server.listen(3000, () => { + console.log('Server running on http://localhost:3000'); + }); +}); diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index c2160bf..0f49df9 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -8,7 +8,7 @@ import { go } from "@codemirror/lang-go"; import { json } from "@codemirror/lang-json"; import { python } from "@codemirror/lang-python"; import { java } from "@codemirror/lang-java"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import useAction from "@/hook/use-action"; import { getLiveRunMoves } from "@/modules/live-run/actions"; import { LiveRun, LiveRunMove } from "@/modules/live-run/types"; @@ -43,66 +43,70 @@ function getLanguageExtension(lang: Language) { export default function Editor({ run }: { run: LiveRun | null | undefined }) { const [editorView, setEditorView] = useState(); - const [rawMoves] = useAction( - async () => (run ? await getLiveRunMoves(run.id) : []), - [run], - ); + const [isConnected, setConnected] = useState(false); - const [_, setLastMoveId] = useState(-1); - const [moves, setMoves] = useState([]); + const [rawMoves, setRawMoves] = useState([]); const [movesLoading, setMovesLoading] = useState(false); + const scheduledTimeouts = useRef([]); + // WebSocket connection useEffect(() => { - if (!rawMoves || rawMoves.length === 0 || movesLoading) return; + if (!run) return; - setLastMoveId((prevLastMoveId) => { - const newMoves = rawMoves.filter((move) => move.moveId > prevLastMoveId); + const ws = new WebSocket(`ws://${window.location.host}/api/view-run`); - if (newMoves.length === 0) return prevLastMoveId; - - setMoves(newMoves); - - return Math.max(...newMoves.map((m) => m.moveId)); - }); - }, [rawMoves, movesLoading]); - - useEffect(() => { - if (!editorView || !moves) return; - - let cancelled = false; - - const executeMove = async (move: LiveRunMove) => { - return new Promise((resolve) => { - const timeout = setTimeout(() => { - if (!editorView || cancelled) return resolve(); - - editorView.dispatch({ - selection: { anchor: move.cursor }, - scrollIntoView: true, - changes: move.changes, - }); - - resolve(); - }, move.latency); - - return () => clearTimeout(timeout); - }); + ws.onopen = () => { + ws.send(run.id); }; - (async () => { - if (!moves) return; - setMovesLoading(true); - for (const move of moves) { - if (cancelled) break; - await executeMove(move); - } - setMovesLoading(false); - })(); + ws.onmessage = (m) => { + const data: LiveRunMove[] = JSON.parse(m.data); + setRawMoves(data); + }; + + return () => ws.close(); + }, [run]); + + // Scheduler + useEffect(() => { + if (!editorView || !rawMoves.length) return; + + setMovesLoading(true); + + // Clear any existing scheduled timeouts + scheduledTimeouts.current.forEach((t) => clearTimeout(t)); + scheduledTimeouts.current = []; + + const startTime = Date.now(); + + rawMoves.forEach((move) => { + const timeout = setTimeout(() => { + if (!editorView) return; + + editorView.dispatch({ + selection: { anchor: move.cursor }, + scrollIntoView: true, + changes: move.changes, + }); + }, move.latency); + + scheduledTimeouts.current.push(timeout); + }); + + // Stop loading after the last move + const lastMoveLatency = rawMoves[rawMoves.length - 1].latency; + const finishTimeout = setTimeout( + () => setMovesLoading(false), + lastMoveLatency + 50, + ); + scheduledTimeouts.current.push(finishTimeout); return () => { - cancelled = true; + scheduledTimeouts.current.forEach((t) => clearTimeout(t)); + scheduledTimeouts.current = []; + setMovesLoading(false); }; - }, [moves]); + }, [rawMoves, editorView]); return ( (); - const [run] = useActionInterval(() => getLiveRun(params.runId), 1000, [ + const [run] = useActionInterval(() => getLiveRun(params.runId), 5000, [ params, ]); diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 845215a..039bb6f 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -4,7 +4,6 @@ import "server-only"; import { LiveRun, LiveRunMove } from "./types"; import { SupabaseClient } from "@supabase/supabase-js"; import { redis } from "@/lib/redis"; -import { randomInt } from "crypto"; /** -------------------- Live Runs in Redis -------------------- */ From 01452e0e9f910c02ee3f096ab6a7947428adae4c Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 22:24:39 +0100 Subject: [PATCH 02/11] Removed run moves in redis --- src/app/api/run/move/route.ts | 35 ---------------------- src/app/runs/[runId]/components/Editor.tsx | 2 -- src/modules/live-run/actions.ts | 4 --- src/modules/live-run/core.ts | 29 ------------------ 4 files changed, 70 deletions(-) delete mode 100644 src/app/api/run/move/route.ts diff --git a/src/app/api/run/move/route.ts b/src/app/api/run/move/route.ts deleted file mode 100644 index d691e2b..0000000 --- a/src/app/api/run/move/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { createClient } from "@/lib/supabase/server"; -import * as Core from "@/modules/live-run/core"; -import { getSession } from "@/modules/account/core"; - -export async function POST(req: NextRequest) { - const { runId, moves } = await req.json(); - - if (!runId || !moves) - return NextResponse.json( - { error: "body requires runId and moves" }, - { status: 400 }, - ); - - const supabase = await createClient(); - - const session = await getSession(supabase); - - if (!session) - return NextResponse.json({ error: "Invalid session" }, { status: 401 }); - - const liveRun = await Core.getLiveRun(runId); - - if (!liveRun) - return NextResponse.json( - { error: "Run is not live or does not exist" }, - { status: 401 }, - ); - - console.log(moves); - - const run = await Core.addLiveRunMoves(liveRun.id, moves); - - return NextResponse.json({ ok: true, run }); -} diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index 0f49df9..ff63e0c 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -9,8 +9,6 @@ import { json } from "@codemirror/lang-json"; import { python } from "@codemirror/lang-python"; import { java } from "@codemirror/lang-java"; import { useEffect, useRef, useState } from "react"; -import useAction from "@/hook/use-action"; -import { getLiveRunMoves } from "@/modules/live-run/actions"; import { LiveRun, LiveRunMove } from "@/modules/live-run/types"; type Language = diff --git a/src/modules/live-run/actions.ts b/src/modules/live-run/actions.ts index ce70ac6..19d4cc5 100644 --- a/src/modules/live-run/actions.ts +++ b/src/modules/live-run/actions.ts @@ -10,7 +10,3 @@ export async function getLiveRuns(): Promise { export async function getLiveRun(id: string): Promise { return Core.getLiveRun(id); } - -export async function getLiveRunMoves(id: string) { - return Core.getLiveRunMoves(id); -} diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 039bb6f..a90e73d 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -43,35 +43,6 @@ export async function removeLiveRun(id: string) { await redis.del(`liveRun:${id}`, `liveRunMoves:${id}`); } -/** -------------------- Live Run Moves -------------------- */ - -/** - * Add moves to a live run (ephemeral) - */ -export async function addLiveRunMoves(id: string, moves: LiveRunMove[]) { - const runExists = await redis.exists(`liveRun:${id}`); - if (!runExists) throw new Error("Invalid run"); - - const pipeline = redis.pipeline(); - moves.forEach((move) => { - pipeline.rpush(`liveRunMoves:${id}`, JSON.stringify(move)); - }); - - setTimeout(() => { - redis.del(`liveRunMoves:${id}`); - }, 3000); - - await pipeline.exec(); -} - -/** - * Get live moves for a run - */ -export async function getLiveRunMoves(id: string): Promise { - const moves = await redis.lrange(`liveRunMoves:${id}`, 0, -1); - return moves.map((m) => JSON.parse(m)) as LiveRunMove[]; -} - /** -------------------- Submit Run -------------------- */ /** From eae062a23a8047ac8f70d0503ad85023a3511752 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 8 Feb 2026 22:28:53 +0100 Subject: [PATCH 03/11] Fixed issues --- src/app/runs/[runId]/components/Editor.tsx | 60 +++++----------------- 1 file changed, 14 insertions(+), 46 deletions(-) diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index ff63e0c..c82da63 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -41,10 +41,6 @@ function getLanguageExtension(lang: Language) { export default function Editor({ run }: { run: LiveRun | null | undefined }) { const [editorView, setEditorView] = useState(); - const [isConnected, setConnected] = useState(false); - - const [rawMoves, setRawMoves] = useState([]); - const [movesLoading, setMovesLoading] = useState(false); const scheduledTimeouts = useRef([]); // WebSocket connection @@ -59,53 +55,25 @@ export default function Editor({ run }: { run: LiveRun | null | undefined }) { ws.onmessage = (m) => { const data: LiveRunMove[] = JSON.parse(m.data); - setRawMoves(data); + + data.forEach((move) => { + const timeout = setTimeout(() => { + if (!editorView) return; + + editorView.dispatch({ + selection: { anchor: move.cursor }, + scrollIntoView: true, + changes: move.changes, + }); + }, move.latency); + + scheduledTimeouts.current.push(timeout); + }); }; return () => ws.close(); }, [run]); - // Scheduler - useEffect(() => { - if (!editorView || !rawMoves.length) return; - - setMovesLoading(true); - - // Clear any existing scheduled timeouts - scheduledTimeouts.current.forEach((t) => clearTimeout(t)); - scheduledTimeouts.current = []; - - const startTime = Date.now(); - - rawMoves.forEach((move) => { - const timeout = setTimeout(() => { - if (!editorView) return; - - editorView.dispatch({ - selection: { anchor: move.cursor }, - scrollIntoView: true, - changes: move.changes, - }); - }, move.latency); - - scheduledTimeouts.current.push(timeout); - }); - - // Stop loading after the last move - const lastMoveLatency = rawMoves[rawMoves.length - 1].latency; - const finishTimeout = setTimeout( - () => setMovesLoading(false), - lastMoveLatency + 50, - ); - scheduledTimeouts.current.push(finishTimeout); - - return () => { - scheduledTimeouts.current.forEach((t) => clearTimeout(t)); - scheduledTimeouts.current = []; - setMovesLoading(false); - }; - }, [rawMoves, editorView]); - return ( Date: Mon, 9 Feb 2026 00:37:40 +0100 Subject: [PATCH 04/11] typescript server.ts --- package.json | 7 ++-- server.mjs | 8 ++--- server.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++ server.tsconfig.json | 16 +++++++++ 4 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 server.ts create mode 100644 server.tsconfig.json diff --git a/package.json b/package.json index c1ebad0..365da54 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "node server.mjs", - "build": "next build", - "start": "NODE_ENV=production node server.mjs" + "dev": "npm run buildserver && node ./.next/server.js", + "build": "npm run buildserver && next build", + "start": "npm run buildserver && NODE_ENV=production node ./.next/server.js", + "buildserver": "tsc -p server.tsconfig.json" }, "dependencies": { "@codemirror/lang-go": "^6.0.1", diff --git a/server.mjs b/server.mjs index add4783..988b8c6 100644 --- a/server.mjs +++ b/server.mjs @@ -11,7 +11,7 @@ app.prepare().then(() => { handle(req, res); }); - const clients = {}; + const runs = {}; const wss = new WebSocketServer({ noServer: true }); @@ -19,8 +19,8 @@ app.prepare().then(() => { wss.on('connection', (ws, req) => { ws.on('message', (msg) => { const runId = msg.toString(); - if (!clients[runId]) clients[runId] = [] - clients[runId].push(ws); + if (!runs[runId]) runs[runId] = { clients: [] } + runs[runId].clients.push(ws); }); }); @@ -33,7 +33,7 @@ app.prepare().then(() => { req.on('end', () => { const move = JSON.parse(body); - clients[move.runId]?.forEach(client => { + runs[move.runId]?.clients.forEach(client => { if (client.readyState === client.OPEN) { client.send(JSON.stringify(move.moves)); } diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..56f1e16 --- /dev/null +++ b/server.ts @@ -0,0 +1,86 @@ +import { createServer, IncomingMessage } from "http"; +import next from "next"; +import { WebSocketServer, WebSocket } from "ws"; + +const dev = process.env.NODE_ENV !== "production"; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +/** + * One run = many websocket clients + */ +type LiveRun = { + clients: Set; +}; + +const runs: Record = {}; + +app.prepare().then(() => { + const server = createServer((req, res) => { + handle(req, res); + }); + + const wss = new WebSocketServer({ noServer: true }); + + // WebSocket connections + wss.on("connection", (ws: WebSocket, req: IncomingMessage) => { + ws.on("message", (msg: Buffer) => { + const runId = msg.toString(); + + if (!runs[runId]) { + runs[runId] = { clients: new Set() }; + } + + runs[runId].clients.add(ws); + + ws.once("close", () => { + runs[runId]?.clients.delete(ws); + }); + }); + }); + + // Handle POST requests + server.on("request", (req, res) => { + if (req.method === "POST" && req.url === "/api/run/move") { + let body = ""; + + req.on("data", (chunk: Buffer) => { + body += chunk.toString(); + }); + + req.on("end", () => { + try { + const move: { + runId: string; + moves: unknown; + } = JSON.parse(body); + + runs[move.runId]?.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(move.moves)); + } + }); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + } catch (err) { + res.writeHead(400); + res.end("Invalid JSON"); + } + }); + } + }); + + // Upgrade HTTP → WebSocket + server.on("upgrade", (req, socket, head) => { + if (req.url === "/api/view-run") { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + } + }); + + server.listen(3000, () => { + console.log("Server running on http://localhost:3000"); + }); +}); diff --git a/server.tsconfig.json b/server.tsconfig.json new file mode 100644 index 0000000..aaced07 --- /dev/null +++ b/server.tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./.next", + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2020"], + "types": ["node"], + "skipLibCheck": true, + "incremental": false, + "composite": false + }, + "include": ["server.ts"] +} From 07156ec598a5820c9736f08fa9b7fbc3291cd7f8 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 13:04:14 +0100 Subject: [PATCH 05/11] Testing next-ws --- package-lock.json | 1168 +++++++++++++++++++++++++++++- package.json | 9 +- src/app/api/view-run/ws/route.ts | 14 + 3 files changed, 1180 insertions(+), 11 deletions(-) create mode 100644 src/app/api/view-run/ws/route.ts diff --git a/package-lock.json b/package-lock.json index d2f890d..e2bb445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "ioredis": "^5.9.2", "lucide-react": "^0.563.0", "next": "16.1.6", + "next-ws": "^2.1.16", "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.13.0", @@ -87,6 +88,523 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.27.1.tgz", + "integrity": "sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-flow-strip-types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.28.6.tgz", + "integrity": "sha512-pgcbbEl/dWQYb6L6Yew6F94rdwygfuv+vJ/tXfwIOYAfPB6TNWpXUMEtEq3YuTeHRdvMIhvz13bkT9CNaS+wqA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", @@ -96,6 +614,51 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", @@ -790,7 +1353,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -801,7 +1363,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -812,7 +1373,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -822,14 +1382,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3474,6 +4032,18 @@ "node": ">=10" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3500,6 +4070,57 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -3551,6 +4172,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3612,6 +4247,18 @@ "node": ">= 0.8" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -3855,6 +4502,12 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "license": "ISC" + }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", @@ -3942,6 +4595,28 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -3957,6 +4632,53 @@ "node": ">=6.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flow-parser": { + "version": "0.299.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.299.0.tgz", + "integrity": "sha512-phGMRoNt6SNglPHGRbCyWm9/pxfe6t/t4++EIYPaBGWT6e0lphLBgUMrvpL62NbRo9R549o3oqrbKHq82kANCw==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -4002,6 +4724,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4064,7 +4795,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-symbols": { @@ -4115,6 +4845,15 @@ "node": ">=20.0.0" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -4148,6 +4887,36 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -4164,6 +4933,79 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jscodeshift": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.3.0.tgz", + "integrity": "sha512-LjFrGOIORqXBU+jwfC9nbkjmQfFldtMIoS6d9z2LG/lkmyNXsJAySPT+2SWXJEoE68/bCWcxKpXH37npftgmow==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.7", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -4425,6 +5267,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -4455,6 +5310,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/lucide-react": { "version": "0.563.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", @@ -4474,6 +5338,28 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4483,6 +5369,19 @@ "node": ">= 0.4" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4504,6 +5403,15 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4528,6 +5436,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/next": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", @@ -4581,6 +5495,25 @@ } } }, + "node_modules/next-ws": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/next-ws/-/next-ws-2.1.16.tgz", + "integrity": "sha512-Kbzo4tHwVj3BJAE0YfZpKu1/Z1MCLi+LrOnMy7b6MItpDS3f1W6qgoeohtKSh8xmTCXOCFnP9ajQyyqXrjjC1w==", + "license": "MIT", + "dependencies": { + "jscodeshift": "^17.3.0", + "minimist": "^1.2.8", + "semver": "^7.7.2" + }, + "bin": { + "next-ws": "dist/cli.cjs" + }, + "peerDependencies": { + "next": ">=13.5.1", + "react": "*", + "ws": "*" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4609,6 +5542,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4618,12 +5557,99 @@ "node": ">=0.10.0" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5039,6 +6065,22 @@ "react-dom": ">=16.6.0" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -5103,7 +6145,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -5111,6 +6152,18 @@ "node": ">=10" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -5156,6 +6209,27 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5165,6 +6239,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5245,6 +6329,27 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5281,6 +6386,36 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -5361,6 +6496,19 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -5381,6 +6529,12 @@ "optional": true } } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 365da54..a52c187 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "npm run buildserver && node ./.next/server.js", - "build": "npm run buildserver && next build", - "start": "npm run buildserver && NODE_ENV=production node ./.next/server.js", - "buildserver": "tsc -p server.tsconfig.json" + "dev": "next dev --turbopack", + "build": "npm run build:lib && next build", + "postinstall": "npm run ws -- -y", + "ws": "npx --yes next-ws-cli@latest patch" }, "dependencies": { "@codemirror/lang-go": "^6.0.1", @@ -56,6 +56,7 @@ "ioredis": "^5.9.2", "lucide-react": "^0.563.0", "next": "16.1.6", + "next-ws": "^2.1.16", "radix-ui": "^1.4.3", "react": "19.2.3", "react-day-picker": "^9.13.0", diff --git a/src/app/api/view-run/ws/route.ts b/src/app/api/view-run/ws/route.ts new file mode 100644 index 0000000..71fc7f3 --- /dev/null +++ b/src/app/api/view-run/ws/route.ts @@ -0,0 +1,14 @@ +import { WebSocketServer, WebSocket } from "ws"; + +export function UPGRADE(client: WebSocket, server: WebSocketServer) { + console.log("A client connected"); + + client.on("message", (message) => { + console.log("Received message:", message); + client.send(message); + }); + + client.once("close", () => { + console.log("A client disconnected"); + }); +} From 6c9b703ed1f00dd15aa9cc37fe8b24691c76b2d4 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 13:41:03 +0100 Subject: [PATCH 06/11] Using next-ws for better code structure --- server.mjs | 60 ---------------------- server.ts | 86 -------------------------------- server.tsconfig.json | 16 ------ src/app/api/run/move/route.ts | 35 +++++++++++++ src/app/api/view-run/route.ts | 47 +++++++++++++++++ src/app/api/view-run/ws/route.ts | 14 ------ src/modules/live-run/core.ts | 17 +++++++ 7 files changed, 99 insertions(+), 176 deletions(-) delete mode 100644 server.mjs delete mode 100644 server.ts delete mode 100644 server.tsconfig.json create mode 100644 src/app/api/run/move/route.ts create mode 100644 src/app/api/view-run/route.ts delete mode 100644 src/app/api/view-run/ws/route.ts diff --git a/server.mjs b/server.mjs deleted file mode 100644 index 988b8c6..0000000 --- a/server.mjs +++ /dev/null @@ -1,60 +0,0 @@ -import { createServer } from 'http'; -import next from 'next'; -import { WebSocketServer } from 'ws'; - -const dev = process.env.NODE_ENV !== 'production'; -const app = next({ dev }); -const handle = app.getRequestHandler(); - -app.prepare().then(() => { - const server = createServer((req, res) => { - handle(req, res); - }); - - const runs = {}; - - const wss = new WebSocketServer({ noServer: true }); - - // Handle WebSocket connections - wss.on('connection', (ws, req) => { - ws.on('message', (msg) => { - const runId = msg.toString(); - if (!runs[runId]) runs[runId] = { clients: [] } - runs[runId].clients.push(ws); - }); - }); - - // Handle POST requests - server.on('request', async (req, res) => { - console.log(req.method, req.url) - if (req.method === 'POST' && req.url === '/api/run/move') { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { - const move = JSON.parse(body); - - runs[move.runId]?.clients.forEach(client => { - if (client.readyState === client.OPEN) { - client.send(JSON.stringify(move.moves)); - } - }); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok' })); - }); - } - }); - - // Upgrade HTTP connections to WebSocket when needed - server.on('upgrade', (req, socket, head) => { - if (req.url === '/api/view-run') { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req); - }); - } - }); - - server.listen(3000, () => { - console.log('Server running on http://localhost:3000'); - }); -}); diff --git a/server.ts b/server.ts deleted file mode 100644 index 56f1e16..0000000 --- a/server.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createServer, IncomingMessage } from "http"; -import next from "next"; -import { WebSocketServer, WebSocket } from "ws"; - -const dev = process.env.NODE_ENV !== "production"; -const app = next({ dev }); -const handle = app.getRequestHandler(); - -/** - * One run = many websocket clients - */ -type LiveRun = { - clients: Set; -}; - -const runs: Record = {}; - -app.prepare().then(() => { - const server = createServer((req, res) => { - handle(req, res); - }); - - const wss = new WebSocketServer({ noServer: true }); - - // WebSocket connections - wss.on("connection", (ws: WebSocket, req: IncomingMessage) => { - ws.on("message", (msg: Buffer) => { - const runId = msg.toString(); - - if (!runs[runId]) { - runs[runId] = { clients: new Set() }; - } - - runs[runId].clients.add(ws); - - ws.once("close", () => { - runs[runId]?.clients.delete(ws); - }); - }); - }); - - // Handle POST requests - server.on("request", (req, res) => { - if (req.method === "POST" && req.url === "/api/run/move") { - let body = ""; - - req.on("data", (chunk: Buffer) => { - body += chunk.toString(); - }); - - req.on("end", () => { - try { - const move: { - runId: string; - moves: unknown; - } = JSON.parse(body); - - runs[move.runId]?.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(move.moves)); - } - }); - - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "ok" })); - } catch (err) { - res.writeHead(400); - res.end("Invalid JSON"); - } - }); - } - }); - - // Upgrade HTTP → WebSocket - server.on("upgrade", (req, socket, head) => { - if (req.url === "/api/view-run") { - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit("connection", ws, req); - }); - } - }); - - server.listen(3000, () => { - console.log("Server running on http://localhost:3000"); - }); -}); diff --git a/server.tsconfig.json b/server.tsconfig.json deleted file mode 100644 index aaced07..0000000 --- a/server.tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "outDir": "./.next", - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2020"], - "types": ["node"], - "skipLibCheck": true, - "incremental": false, - "composite": false - }, - "include": ["server.ts"] -} diff --git a/src/app/api/run/move/route.ts b/src/app/api/run/move/route.ts new file mode 100644 index 0000000..d691e2b --- /dev/null +++ b/src/app/api/run/move/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@/lib/supabase/server"; +import * as Core from "@/modules/live-run/core"; +import { getSession } from "@/modules/account/core"; + +export async function POST(req: NextRequest) { + const { runId, moves } = await req.json(); + + if (!runId || !moves) + return NextResponse.json( + { error: "body requires runId and moves" }, + { status: 400 }, + ); + + const supabase = await createClient(); + + const session = await getSession(supabase); + + if (!session) + return NextResponse.json({ error: "Invalid session" }, { status: 401 }); + + const liveRun = await Core.getLiveRun(runId); + + if (!liveRun) + return NextResponse.json( + { error: "Run is not live or does not exist" }, + { status: 401 }, + ); + + console.log(moves); + + const run = await Core.addLiveRunMoves(liveRun.id, moves); + + return NextResponse.json({ ok: true, run }); +} diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts new file mode 100644 index 0000000..72e0e73 --- /dev/null +++ b/src/app/api/view-run/route.ts @@ -0,0 +1,47 @@ +import { WebSocketServer, WebSocket } from "ws"; +import Redis from "ioredis"; +import { LiveRunMove } from "@/modules/live-run/types"; +import { NextRequest, NextResponse } from "next/server"; + +export function GET(req: NextRequest) { + const headers = new Headers(); + headers.set("Connection", "Upgrade"); + headers.set("Upgrade", "websocket"); + return new Response("Upgrade Required", { status: 426, headers }); +} + +export function UPGRADE(client: WebSocket, server: WebSocketServer) { + console.log("A client connected"); + + const subscriber = new Redis(); + let runId: string; + + client.once("message", (message) => { + runId = message.toString(); + + subscriber.subscribe(`liveRunMoves:${runId}`, (err, count) => { + if (err) { + console.error("Failed to subscribe", err); + client.close(1011, "Redis subscription failed"); + } else { + console.log(`Subscribed to ${count} channels`); + } + }); + + subscriber.on("message", (channel, message) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); + }); + + client.once("close", async () => { + if (runId) await subscriber.unsubscribe(`liveRun:${runId}`); + subscriber.quit(); + console.log("Client disconnected, Redis unsubscribed"); + }); + + client.on("error", (err) => { + console.error("WebSocket error:", err); + }); +} diff --git a/src/app/api/view-run/ws/route.ts b/src/app/api/view-run/ws/route.ts deleted file mode 100644 index 71fc7f3..0000000 --- a/src/app/api/view-run/ws/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WebSocketServer, WebSocket } from "ws"; - -export function UPGRADE(client: WebSocket, server: WebSocketServer) { - console.log("A client connected"); - - client.on("message", (message) => { - console.log("Received message:", message); - client.send(message); - }); - - client.once("close", () => { - console.log("A client disconnected"); - }); -} diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index a90e73d..40784fc 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -4,6 +4,7 @@ import "server-only"; import { LiveRun, LiveRunMove } from "./types"; import { SupabaseClient } from "@supabase/supabase-js"; import { redis } from "@/lib/redis"; +import Redis from "ioredis"; /** -------------------- Live Runs in Redis -------------------- */ @@ -43,6 +44,22 @@ export async function removeLiveRun(id: string) { await redis.del(`liveRun:${id}`, `liveRunMoves:${id}`); } +/** -------------------- Live Run Moves -------------------- */ + +/** + * Add moves to a live run (ephemeral) + */ +export async function addLiveRunMoves(runId: string, moves: LiveRunMove[]) { + const runExists = await redis.exists(`liveRun:${runId}`); + + if (!runExists) throw "Invalid live run"; + + const publisher = new Redis(); + + // Send a message + await publisher.publish(`liveRunMoves:${runId}`, JSON.stringify(moves)); +} + /** -------------------- Submit Run -------------------- */ /** From 33f467cf02504e4f074261a4fe368b63e7817a76 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 14:00:00 +0100 Subject: [PATCH 07/11] Working websocket structure --- src/app/api/view-run/route.ts | 12 +++++++- src/app/runs/[runId]/components/Editor.tsx | 32 ++++++++++++++++------ src/lib/move.ts | 16 +++++++++++ src/modules/live-run/core.ts | 20 +++++++++++--- 4 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 src/lib/move.ts diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts index 72e0e73..3d68064 100644 --- a/src/app/api/view-run/route.ts +++ b/src/app/api/view-run/route.ts @@ -19,6 +19,15 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { client.once("message", (message) => { runId = message.toString(); + const sendText = async () => { + const text = await subscriber.get(`liveRunText:${runId}`); + client.send(JSON.stringify({ moves: [], text })); + }; + + sendText(); + + client.on("message", sendText); + subscriber.subscribe(`liveRunMoves:${runId}`, (err, count) => { if (err) { console.error("Failed to subscribe", err); @@ -29,8 +38,9 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { }); subscriber.on("message", (channel, message) => { + const moves = JSON.parse(message); if (client.readyState === WebSocket.OPEN) { - client.send(message); + client.send(JSON.stringify({ moves, text: null })); } }); }); diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index c82da63..fe16172 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -42,6 +42,7 @@ function getLanguageExtension(lang: Language) { export default function Editor({ run }: { run: LiveRun | null | undefined }) { const [editorView, setEditorView] = useState(); const scheduledTimeouts = useRef([]); + const [text, setText] = useState(""); // WebSocket connection useEffect(() => { @@ -54,17 +55,30 @@ export default function Editor({ run }: { run: LiveRun | null | undefined }) { }; ws.onmessage = (m) => { - const data: LiveRunMove[] = JSON.parse(m.data); + const data: { moves: LiveRunMove[]; text: string | null } = JSON.parse( + m.data, + ); - data.forEach((move) => { + if (data.text) { + setText(data.text); + } + + let cancelled = false; + + data.moves.forEach((move) => { const timeout = setTimeout(() => { - if (!editorView) return; + if (!editorView || cancelled) return; - editorView.dispatch({ - selection: { anchor: move.cursor }, - scrollIntoView: true, - changes: move.changes, - }); + try { + editorView.dispatch({ + selection: { anchor: move.cursor }, + scrollIntoView: true, + changes: move.changes, + }); + } catch { + cancelled = true; + ws.send("getText"); + } }, move.latency); scheduledTimeouts.current.push(timeout); @@ -78,7 +92,7 @@ export default function Editor({ run }: { run: LiveRun | null | undefined }) { { diff --git a/src/lib/move.ts b/src/lib/move.ts new file mode 100644 index 0000000..03f5b29 --- /dev/null +++ b/src/lib/move.ts @@ -0,0 +1,16 @@ +import { LiveRunMove } from "@/modules/live-run/types"; + +export function applyMoves(text: string, moves: LiveRunMove[]) { + let result = text; + + for (const move of moves) { + if (!move.changes) continue; + + result = + result.slice(0, move.changes.from) + + move.changes.insert + + result.slice(move.changes.to); + } + + return result; +} diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 40784fc..1ba4876 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -5,6 +5,7 @@ import { LiveRun, LiveRunMove } from "./types"; import { SupabaseClient } from "@supabase/supabase-js"; import { redis } from "@/lib/redis"; import Redis from "ioredis"; +import { applyMoves } from "@/lib/move"; /** -------------------- Live Runs in Redis -------------------- */ @@ -50,14 +51,25 @@ export async function removeLiveRun(id: string) { * Add moves to a live run (ephemeral) */ export async function addLiveRunMoves(runId: string, moves: LiveRunMove[]) { - const runExists = await redis.exists(`liveRun:${runId}`); + const redis = new Redis(); + // Make sure the live run exists + const runExists = await redis.exists(`liveRun:${runId}`); if (!runExists) throw "Invalid live run"; - const publisher = new Redis(); + // Publish the new moves to subscribers + await redis.publish(`liveRunMoves:${runId}`, JSON.stringify(moves)); - // Send a message - await publisher.publish(`liveRunMoves:${runId}`, JSON.stringify(moves)); + // Get the last text (from Redis) + const lastText = (await redis.get(`liveRunText:${runId}`)) || ""; + + // Apply the new moves to get the updated text + const updatedText = applyMoves(lastText, moves); + + // Store the updated text back in Redis + await redis.set(`liveRunText:${runId}`, updatedText); + + return updatedText; } /** -------------------- Submit Run -------------------- */ From d0027e32d2d09abe48f77b56238f03943b5e9948 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 15:12:34 +0100 Subject: [PATCH 08/11] Improvements --- src/app/api/run/move/route.ts | 10 +++++----- src/app/api/view-run/route.ts | 17 ++++++++--------- src/app/runs/[runId]/components/Editor.tsx | 18 ++++++++++++++---- src/app/runs/[runId]/page.tsx | 11 ++++++----- src/modules/live-run/core.ts | 8 ++++++-- src/modules/live-run/types.ts | 6 ++++++ 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/app/api/run/move/route.ts b/src/app/api/run/move/route.ts index d691e2b..4df1e38 100644 --- a/src/app/api/run/move/route.ts +++ b/src/app/api/run/move/route.ts @@ -4,11 +4,11 @@ import * as Core from "@/modules/live-run/core"; import { getSession } from "@/modules/account/core"; export async function POST(req: NextRequest) { - const { runId, moves } = await req.json(); + const { runId, moves, file } = await req.json(); - if (!runId || !moves) + if (!runId || !moves || file === undefined) return NextResponse.json( - { error: "body requires runId and moves" }, + { error: "body requires runId and moves and file" }, { status: 400 }, ); @@ -27,9 +27,9 @@ export async function POST(req: NextRequest) { { status: 401 }, ); - console.log(moves); + console.log(moves, file); - const run = await Core.addLiveRunMoves(liveRun.id, moves); + const run = await Core.addLiveRunMoves(liveRun.id, file, moves); return NextResponse.json({ ok: true, run }); } diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts index 3d68064..ca8959a 100644 --- a/src/app/api/view-run/route.ts +++ b/src/app/api/view-run/route.ts @@ -1,7 +1,7 @@ import { WebSocketServer, WebSocket } from "ws"; import Redis from "ioredis"; -import { LiveRunMove } from "@/modules/live-run/types"; import { NextRequest, NextResponse } from "next/server"; +import { LiveRunEvent } from "@/modules/live-run/types"; export function GET(req: NextRequest) { const headers = new Headers(); @@ -11,17 +11,18 @@ export function GET(req: NextRequest) { } export function UPGRADE(client: WebSocket, server: WebSocketServer) { - console.log("A client connected"); - const subscriber = new Redis(); let runId: string; client.once("message", (message) => { runId = message.toString(); + let file: string | null; const sendText = async () => { const text = await subscriber.get(`liveRunText:${runId}`); - client.send(JSON.stringify({ moves: [], text })); + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ file, moves: [], text })); + } }; sendText(); @@ -32,15 +33,14 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { if (err) { console.error("Failed to subscribe", err); client.close(1011, "Redis subscription failed"); - } else { - console.log(`Subscribed to ${count} channels`); } }); subscriber.on("message", (channel, message) => { - const moves = JSON.parse(message); + const data: LiveRunEvent = JSON.parse(message); + file = data.file; if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ moves, text: null })); + client.send(JSON.stringify({ moves: data.moves, file, text: null })); } }); }); @@ -48,7 +48,6 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { client.once("close", async () => { if (runId) await subscriber.unsubscribe(`liveRun:${runId}`); subscriber.quit(); - console.log("Client disconnected, Redis unsubscribed"); }); client.on("error", (err) => { diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index fe16172..2036ff9 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -39,7 +39,13 @@ function getLanguageExtension(lang: Language) { } } -export default function Editor({ run }: { run: LiveRun | null | undefined }) { +export default function Editor({ + run, + setFile, +}: { + run: LiveRun | null | undefined; + setFile: (s: string | null) => void; +}) { const [editorView, setEditorView] = useState(); const scheduledTimeouts = useRef([]); const [text, setText] = useState(""); @@ -55,9 +61,13 @@ export default function Editor({ run }: { run: LiveRun | null | undefined }) { }; ws.onmessage = (m) => { - const data: { moves: LiveRunMove[]; text: string | null } = JSON.parse( - m.data, - ); + const data: { + moves: LiveRunMove[]; + file: string | null; + text: string | null; + } = JSON.parse(m.data); + + setFile(data.file); if (data.text) { setText(data.text); diff --git a/src/app/runs/[runId]/page.tsx b/src/app/runs/[runId]/page.tsx index 8c7360a..cf477c5 100644 --- a/src/app/runs/[runId]/page.tsx +++ b/src/app/runs/[runId]/page.tsx @@ -12,19 +12,20 @@ import ProblemCard from "@/app/problems/components/ProblemCard"; import ProfileCard from "./components/ProfileCard"; import { getProblem } from "@/modules/problems/actions"; import { useParams } from "next/navigation"; +import { useState } from "react"; export default function Run() { const params = useParams<{ runId: string }>(); - const [run] = useActionInterval(() => getLiveRun(params.runId), 5000, [ - params, - ]); + const [run] = useAction(() => getLiveRun(params.runId), [params]); const [problem] = useAction( async () => (run?.id ? getProblem(run.problem) : null), [run], ); + const [file, setFile] = useState(null); + const chatMessages: ChatMessage[] = [ { user: "Alice", message: "Nice split!", time: "1:53" }, { user: "Bob", message: "Watch out for the string problem!", time: "1:54" }, @@ -83,10 +84,10 @@ export default function Run() { {/* Code */}
- myfile.js + {file}
- +
diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 1ba4876..d19915a 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -50,7 +50,11 @@ export async function removeLiveRun(id: string) { /** * Add moves to a live run (ephemeral) */ -export async function addLiveRunMoves(runId: string, moves: LiveRunMove[]) { +export async function addLiveRunMoves( + runId: string, + file: string, + moves: LiveRunMove[], +) { const redis = new Redis(); // Make sure the live run exists @@ -58,7 +62,7 @@ export async function addLiveRunMoves(runId: string, moves: LiveRunMove[]) { if (!runExists) throw "Invalid live run"; // Publish the new moves to subscribers - await redis.publish(`liveRunMoves:${runId}`, JSON.stringify(moves)); + await redis.publish(`liveRunMoves:${runId}`, JSON.stringify({ file, moves })); // Get the last text (from Redis) const lastText = (await redis.get(`liveRunText:${runId}`)) || ""; diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts index 99d98e8..75b9268 100644 --- a/src/modules/live-run/types.ts +++ b/src/modules/live-run/types.ts @@ -18,3 +18,9 @@ export interface LiveRunMove { insert: string; }; } + +export interface LiveRunEvent { + moves: LiveRunMove[]; + file: string | null; + text: string | null; +} From c6763fb63f01f25d838230a8a3b549ab93deadd6 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 16:01:28 +0100 Subject: [PATCH 09/11] Improvements --- src/app/api/run/move/route.ts | 10 ++++------ src/app/api/view-run/route.ts | 21 ++++++++++++++------ src/app/runs/[runId]/components/Editor.tsx | 23 +++++++++++----------- src/app/runs/[runId]/page.tsx | 14 ++++++++++--- src/modules/live-run/core.ts | 10 +++++++--- src/modules/live-run/types.ts | 1 + 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/app/api/run/move/route.ts b/src/app/api/run/move/route.ts index 4df1e38..c0bc9cc 100644 --- a/src/app/api/run/move/route.ts +++ b/src/app/api/run/move/route.ts @@ -4,11 +4,11 @@ import * as Core from "@/modules/live-run/core"; import { getSession } from "@/modules/account/core"; export async function POST(req: NextRequest) { - const { runId, moves, file } = await req.json(); + const { runId, moves, file, language } = await req.json(); - if (!runId || !moves || file === undefined) + if (!runId || !moves || file === undefined || language === undefined) return NextResponse.json( - { error: "body requires runId and moves and file" }, + { error: "body requires runId, moves, file and language" }, { status: 400 }, ); @@ -27,9 +27,7 @@ export async function POST(req: NextRequest) { { status: 401 }, ); - console.log(moves, file); - - const run = await Core.addLiveRunMoves(liveRun.id, file, moves); + const run = await Core.addLiveRunEvent(liveRun.id, file, language, moves); return NextResponse.json({ ok: true, run }); } diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts index ca8959a..7dc3097 100644 --- a/src/app/api/view-run/route.ts +++ b/src/app/api/view-run/route.ts @@ -14,14 +14,22 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { const subscriber = new Redis(); let runId: string; - client.once("message", (message) => { + client.once("message", async (message) => { runId = message.toString(); - let file: string | null; + let lastEvent: LiveRunEvent | null = null; const sendText = async () => { const text = await subscriber.get(`liveRunText:${runId}`); + if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ file, moves: [], text })); + client.send( + JSON.stringify({ + file: lastEvent?.file, + language: lastEvent?.language, + moves: [], + text, + }), + ); } }; @@ -29,7 +37,7 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { client.on("message", sendText); - subscriber.subscribe(`liveRunMoves:${runId}`, (err, count) => { + subscriber.subscribe(`liveRunEvent:${runId}`, (err, count) => { if (err) { console.error("Failed to subscribe", err); client.close(1011, "Redis subscription failed"); @@ -38,9 +46,10 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { subscriber.on("message", (channel, message) => { const data: LiveRunEvent = JSON.parse(message); - file = data.file; + + lastEvent = data; if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ moves: data.moves, file, text: null })); + client.send(JSON.stringify({ ...data, text: null })); } }); }); diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index 2036ff9..b3478bf 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -9,7 +9,7 @@ import { json } from "@codemirror/lang-json"; import { python } from "@codemirror/lang-python"; import { java } from "@codemirror/lang-java"; import { useEffect, useRef, useState } from "react"; -import { LiveRun, LiveRunMove } from "@/modules/live-run/types"; +import { LiveRun, LiveRunEvent, LiveRunMove } from "@/modules/live-run/types"; type Language = | "javascript" @@ -20,7 +20,7 @@ type Language = | "java" | "json"; -function getLanguageExtension(lang: Language) { +function getLanguageExtension(lang: Language | null) { switch (lang) { case "javascript": return javascript(); @@ -41,14 +41,15 @@ function getLanguageExtension(lang: Language) { export default function Editor({ run, - setFile, + onEvent, }: { run: LiveRun | null | undefined; - setFile: (s: string | null) => void; + onEvent: (s: LiveRunEvent) => void; }) { const [editorView, setEditorView] = useState(); const scheduledTimeouts = useRef([]); const [text, setText] = useState(""); + const [language, setLanguage] = useState(null); // WebSocket connection useEffect(() => { @@ -61,13 +62,13 @@ export default function Editor({ }; ws.onmessage = (m) => { - const data: { - moves: LiveRunMove[]; - file: string | null; - text: string | null; - } = JSON.parse(m.data); + const data: LiveRunEvent = JSON.parse(m.data); - setFile(data.file); + onEvent(data); + + if (data.language !== language) { + setLanguage(data.language as Language); + } if (data.text) { setText(data.text); @@ -104,7 +105,7 @@ export default function Editor({ className="h-full w-full" value={text} extensions={[ - getLanguageExtension("javascript"), + getLanguageExtension(language), EditorState.transactionFilter.of((tr) => { if (tr.selection && tr.isUserEvent("select")) { return []; diff --git a/src/app/runs/[runId]/page.tsx b/src/app/runs/[runId]/page.tsx index cf477c5..958c5fd 100644 --- a/src/app/runs/[runId]/page.tsx +++ b/src/app/runs/[runId]/page.tsx @@ -24,7 +24,10 @@ export default function Run() { [run], ); - const [file, setFile] = useState(null); + const [file, setFile] = useState<{ + file: string | null; + language: string | null; + }>({ file: null, language: null }); const chatMessages: ChatMessage[] = [ { user: "Alice", message: "Nice split!", time: "1:53" }, @@ -84,10 +87,15 @@ export default function Run() { {/* Code */}
- {file} + {file.file}
- + { + setFile({ file: event.file, language: event.language }); + }} + />
diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index d19915a..55d4a7f 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -42,7 +42,7 @@ export async function getLiveRuns(): Promise { * Remove a live run from Redis */ export async function removeLiveRun(id: string) { - await redis.del(`liveRun:${id}`, `liveRunMoves:${id}`); + await redis.del(`liveRun:${id}`, `liveRunEvent:${id}`); } /** -------------------- Live Run Moves -------------------- */ @@ -50,9 +50,10 @@ export async function removeLiveRun(id: string) { /** * Add moves to a live run (ephemeral) */ -export async function addLiveRunMoves( +export async function addLiveRunEvent( runId: string, file: string, + language: string, moves: LiveRunMove[], ) { const redis = new Redis(); @@ -62,7 +63,10 @@ export async function addLiveRunMoves( if (!runExists) throw "Invalid live run"; // Publish the new moves to subscribers - await redis.publish(`liveRunMoves:${runId}`, JSON.stringify({ file, moves })); + await redis.publish( + `liveRunEvent:${runId}`, + JSON.stringify({ file, language, moves }), + ); // Get the last text (from Redis) const lastText = (await redis.get(`liveRunText:${runId}`)) || ""; diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts index 75b9268..5e3c04a 100644 --- a/src/modules/live-run/types.ts +++ b/src/modules/live-run/types.ts @@ -22,5 +22,6 @@ export interface LiveRunMove { export interface LiveRunEvent { moves: LiveRunMove[]; file: string | null; + language: string | null; text: string | null; } From 0af907c1d4c1410e39215ae8906435bf0445dd10 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 16:31:28 +0100 Subject: [PATCH 10/11] View management --- src/app/api/view-run/route.ts | 5 +++++ src/app/runs/[runId]/components/RunTimer.tsx | 10 +++++++-- src/app/runs/[runId]/page.tsx | 7 ++++-- src/modules/live-run/actions.ts | 4 ++++ src/modules/live-run/core.ts | 23 ++++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/app/api/view-run/route.ts b/src/app/api/view-run/route.ts index 7dc3097..3bc6c7c 100644 --- a/src/app/api/view-run/route.ts +++ b/src/app/api/view-run/route.ts @@ -2,6 +2,7 @@ import { WebSocketServer, WebSocket } from "ws"; import Redis from "ioredis"; import { NextRequest, NextResponse } from "next/server"; import { LiveRunEvent } from "@/modules/live-run/types"; +import { getLiveRun, updateLiveRunViews } from "@/modules/live-run/core"; export function GET(req: NextRequest) { const headers = new Headers(); @@ -16,6 +17,9 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { client.once("message", async (message) => { runId = message.toString(); + + await updateLiveRunViews(runId, 1); + let lastEvent: LiveRunEvent | null = null; const sendText = async () => { @@ -57,6 +61,7 @@ export function UPGRADE(client: WebSocket, server: WebSocketServer) { client.once("close", async () => { if (runId) await subscriber.unsubscribe(`liveRun:${runId}`); subscriber.quit(); + await updateLiveRunViews(runId, -1); }); client.on("error", (err) => { diff --git a/src/app/runs/[runId]/components/RunTimer.tsx b/src/app/runs/[runId]/components/RunTimer.tsx index da520b8..dc5f38d 100644 --- a/src/app/runs/[runId]/components/RunTimer.tsx +++ b/src/app/runs/[runId]/components/RunTimer.tsx @@ -4,7 +4,13 @@ import { LiveRun } from "@/modules/live-run/types"; import { Eye } from "lucide-react"; import { useEffect, useState } from "react"; -export default function RunTimer({ run }: { run: LiveRun }) { +export default function RunTimer({ + run, + views, +}: { + run: LiveRun; + views: number | null; +}) { const [time, setTime] = useState("00:00:00"); useEffect(() => { @@ -29,7 +35,7 @@ export default function RunTimer({ run }: { run: LiveRun }) {

- {run.views} + {views || run.views}

diff --git a/src/app/runs/[runId]/page.tsx b/src/app/runs/[runId]/page.tsx index 958c5fd..c47c258 100644 --- a/src/app/runs/[runId]/page.tsx +++ b/src/app/runs/[runId]/page.tsx @@ -7,7 +7,7 @@ import { FileTree } from "./components/FileTree"; import { File } from "lucide-react"; import Editor from "./components/Editor"; import useAction, { useActionInterval } from "@/hook/use-action"; -import { getLiveRun } from "@/modules/live-run/actions"; +import { getLiveRun, getLiveRunViews } from "@/modules/live-run/actions"; import ProblemCard from "@/app/problems/components/ProblemCard"; import ProfileCard from "./components/ProfileCard"; import { getProblem } from "@/modules/problems/actions"; @@ -18,6 +18,9 @@ export default function Run() { const params = useParams<{ runId: string }>(); const [run] = useAction(() => getLiveRun(params.runId), [params]); + const [views] = useActionInterval(() => getLiveRunViews(params.runId), 2500, [ + params, + ]); const [problem] = useAction( async () => (run?.id ? getProblem(run.problem) : null), @@ -55,7 +58,7 @@ export default function Run() {
- {run && } + {run && }
diff --git a/src/modules/live-run/actions.ts b/src/modules/live-run/actions.ts index 19d4cc5..b878f8d 100644 --- a/src/modules/live-run/actions.ts +++ b/src/modules/live-run/actions.ts @@ -10,3 +10,7 @@ export async function getLiveRuns(): Promise { export async function getLiveRun(id: string): Promise { return Core.getLiveRun(id); } + +export async function getLiveRunViews(id: string): Promise { + return Core.getLiveRunViews(id); +} diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index 55d4a7f..6d90c0a 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -28,6 +28,29 @@ export async function getLiveRun(id: string): Promise { return JSON.parse(data) as LiveRun; } +/** + * Get live run views from Redis + */ +export async function getLiveRunViews(id: string): Promise { + const run = await getLiveRun(id); + if (!run) return null; + return run.views; +} + +/** + * Get live run views from Redis + */ +export async function updateLiveRunViews( + id: string, + increment: number, +): Promise { + const run = await getLiveRun(id); + if (!run) return null; + run.views += increment; + await redis.set(`liveRun:${run.id}`, JSON.stringify(run)); + return run.views; +} + /** * Get all live runs */ From 4727d796a35b9c52165e637c8e0beef8eab2d9e2 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 9 Feb 2026 20:15:50 +0100 Subject: [PATCH 11/11] Fixed invalid origin, for some reason location.href is not it? --- src/app/runs/[runId]/components/Editor.tsx | 9 ++++++--- src/components/auth/ContinueWith.tsx | 2 +- src/lib/supabase/server.ts | 2 ++ src/modules/account/actions.ts | 9 +++++++-- src/modules/getOrigin.ts | 11 +++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/modules/getOrigin.ts diff --git a/src/app/runs/[runId]/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx index b3478bf..31e59d7 100644 --- a/src/app/runs/[runId]/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -10,6 +10,8 @@ import { python } from "@codemirror/lang-python"; import { java } from "@codemirror/lang-java"; import { useEffect, useRef, useState } from "react"; import { LiveRun, LiveRunEvent, LiveRunMove } from "@/modules/live-run/types"; +import useAction from "@/hook/use-action"; +import { getOrigin } from "@/modules/getOrigin"; type Language = | "javascript" @@ -46,6 +48,7 @@ export default function Editor({ run: LiveRun | null | undefined; onEvent: (s: LiveRunEvent) => void; }) { + const [origin] = useAction(getOrigin); const [editorView, setEditorView] = useState(); const scheduledTimeouts = useRef([]); const [text, setText] = useState(""); @@ -53,9 +56,9 @@ export default function Editor({ // WebSocket connection useEffect(() => { - if (!run) return; + if (!run || !origin?.host) return; - const ws = new WebSocket(`ws://${window.location.host}/api/view-run`); + const ws = new WebSocket(`ws://${origin.host}/api/view-run`); ws.onopen = () => { ws.send(run.id); @@ -97,7 +100,7 @@ export default function Editor({ }; return () => ws.close(); - }, [run]); + }, [run, ]); return ( { - signInWithGitHub(window.location.origin); + signInWithGitHub(); }; const continueWithGoogle = () => {}; diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index c9431c0..30a93dc 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,3 +1,5 @@ +"use server"; + import "server-only"; import { createServerClient, type CookieOptions } from "@supabase/ssr"; diff --git a/src/modules/account/actions.ts b/src/modules/account/actions.ts index a364bee..48469f2 100644 --- a/src/modules/account/actions.ts +++ b/src/modules/account/actions.ts @@ -2,15 +2,20 @@ import * as Core from "./core"; import { createClient } from "@/lib/supabase/server"; +import { headers } from "next/headers"; import { redirect } from "next/navigation"; -export async function signInWithGitHub(origin: string) { +export async function signInWithGitHub() { const supabase = await createClient(); + const h = await headers(); + const host = h.get("host"); + const protocol = h.get("x-forwarded-proto") ?? "https"; + const { data, error } = await supabase.auth.signInWithOAuth({ provider: "github", options: { - redirectTo: `${origin}/api/auth/callback`, + redirectTo: `${protocol}://${host}/api/auth/callback`, }, }); diff --git a/src/modules/getOrigin.ts b/src/modules/getOrigin.ts new file mode 100644 index 0000000..82edadb --- /dev/null +++ b/src/modules/getOrigin.ts @@ -0,0 +1,11 @@ +"use server"; + +import { headers } from "next/headers"; + +export async function getOrigin(): Promise<{ protocol: string; host: string }> { + const h = await headers(); + const host = h.get("host") || "localhost:3000"; + const protocol = h.get("x-forwarded-proto") ?? "https"; + + return { protocol, host }; +}