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 */}
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 */}
-
+ {
+ 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 };
+}