From 29a80f8efd4cf51b329cf70d47a1fdefa95b9758 Mon Sep 17 00:00:00 2001 From: Nilesh <19304+nileshtrivedi@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:36:22 +0530 Subject: [PATCH 1/3] Feature: Add collaborative text editing using Yjs, y-webrtc and y-monaco --- client/src/App.tsx | 99 +++++++++++++++++- client/src/css/index.css | 20 ++++ package-lock.json | 211 +++++++++++++++++++++++++++++++++++++-- package.json | 7 +- 4 files changed, 326 insertions(+), 11 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 333a3cbf..15ce35ed 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -8,8 +8,11 @@ import { useAtom } from 'jotai/react' import { LeanMonaco, LeanMonacoEditor, LeanMonacoOptions } from 'lean4monaco' import * as monaco from 'monaco-editor' import * as path from 'path' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState, useMemo } from 'react' import Split from 'react-split' +import * as Y from 'yjs' +import { WebrtcProvider } from 'y-webrtc' +import { MonacoBinding } from 'y-monaco' import LeanLogo from './assets/logo.svg' import { codeAtom } from './editor/code-atoms' @@ -38,6 +41,13 @@ function App() { const [project] = useAtom(projectAtom) const [code, setCode] = useAtom(codeAtom) const [freshlyImportedCode] = useAtom(freshlyImportedCodeAtom) + const ydoc = useMemo(() => new Y.Doc(), []) + const [provider, setProvider] = useState(null) + const [binding, setBinding] = useState(null) + const [collabDialogVisible, setCollabDialogVisible] = useState(false) + const [collabRoomName, setCollabRoomName] = useState('') + const [collabDisplayName, setCollabDisplayName] = useState('') + const [isCollaborating, setIsCollaborating] = useState(false) const model = editor?.getModel() @@ -66,6 +76,53 @@ function App() { return () => window.removeEventListener('resize', handleResize) }, [setScreenWidth]) + // clean up ydoc on unmount + useEffect(() => { + return () => ydoc.destroy() + }, [ydoc]) + + // this effect manages the lifetime of the Yjs document and the provider + useEffect(() => { + // const provider = new WebsocketProvider('wss://demos.yjs.dev/ws', roomname, ydoc) + // See https://github.com/yjs/y-webrtc for options + if (!isCollaborating || !collabRoomName) { + setProvider(null) + return + } + const provider = new WebrtcProvider( + collabRoomName, // roomname + ydoc, + { + maxConns: 50, + password: undefined, + signaling: [ + 'wss://wide-robin-20.snowmountain.deno.net/' + ], + filterBcConns: true, + } + ) + if (collabDisplayName) { + provider.awareness.setLocalStateField('user', { name: collabDisplayName }) + } + setProvider(provider) + return () => { + provider?.destroy() + } + }, [ydoc, isCollaborating, collabRoomName, collabDisplayName]) + + // this effect manages the lifetime of the editor binding + useEffect(() => { + if (provider == null || editor == null) { + return + } + console.log('reached', provider) + const binding = new MonacoBinding(ydoc.getText(), editor.getModel()!, new Set([editor]), provider?.awareness) + setBinding(binding) + return () => { + binding.destroy() + } + }, [ydoc, provider, editor]) + // Update LeanMonaco options when preferences are loaded or change useEffect(() => { if (!project) return @@ -269,7 +326,47 @@ function App() { codeMirror={codeMirror} setCodeMirror={setCodeMirror} /> + + {collabDialogVisible && ( +
+
{ + e.preventDefault(); + if (collabRoomName) { + setIsCollaborating(true); + setCollabDialogVisible(false); + } + }} + > +

Join Collaboration

+
+ + setCollabRoomName(e.target.value)} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} /> +
+
+ + setCollabDisplayName(e.target.value)} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} /> +
+
+ + +
+
+
+ )} { diff --git a/client/src/css/index.css b/client/src/css/index.css index dceb0cc6..9eecaf2e 100644 --- a/client/src/css/index.css +++ b/client/src/css/index.css @@ -22,3 +22,23 @@ body { font-family: 'JuliaMono'; src: local('JuliaMono'), url('/fonts/JuliaMono-Regular.ttf'); } + +.yRemoteSelection { + background-color: rgb(250, 129, 0, .5) +} +.yRemoteSelectionHead { + position: absolute; + border-left: orange solid 2px; + border-top: orange solid 2px; + border-bottom: orange solid 2px; + height: 100%; + box-sizing: border-box; +} +.yRemoteSelectionHead::after { + position: absolute; + content: ' '; + border: 3px solid orange; + border-radius: 4px; + left: -4px; + top: -5px; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f10c58d2..fae8ecd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,10 @@ "react-split": "^2.0.14", "tailwindcss": "^4.1.18", "vscode-ws-jsonrpc": "^3.5.0", - "ws": "^8.18.3" + "ws": "^8.18.3", + "y-monaco": "^0.1.6", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.29" }, "bin": { "server": "server/index.mjs" @@ -4288,7 +4291,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5692,6 +5694,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6878,6 +6886,12 @@ "node": ">=6.9.0" } }, + "node_modules/get-browser-rtc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", + "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==", + "license": "MIT" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7329,7 +7343,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7992,6 +8005,16 @@ "node": ">=10" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8344,6 +8367,27 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -8973,6 +9017,7 @@ "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-8.0.4.tgz", "integrity": "sha512-rKUdFRsnzTV1dj6FL/MD+cBIUAGv9aJNz8bTx4BA5sDK3lDWuifhdCBvk4WZEj6glfXe3sEfGXKy1Vp5btDOJQ==", "license": "MIT", + "peer": true, "dependencies": { "vscode": "npm:@codingame/monaco-vscode-api@8.0.4" } @@ -9930,11 +9975,30 @@ "node": ">=0.4.x" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -10042,7 +10106,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10346,7 +10409,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10688,6 +10750,59 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-peer": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", + "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "debug": "^4.3.2", + "err-code": "^3.0.1", + "get-browser-rtc": "^1.1.0", + "queue-microtask": "^1.2.3", + "randombytes": "^2.1.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/simple-peer/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -10832,7 +10947,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11715,7 +11829,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -12242,6 +12355,70 @@ "node": ">=0.4" } }, + "node_modules/y-monaco": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz", + "integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.43" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "monaco-editor": ">=0.20.0", + "yjs": "^13.3.1" + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-webrtc": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", + "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.42", + "simple-peer": "^9.11.0", + "y-protocols": "^1.0.6" + }, + "bin": { + "y-webrtc-signaling": "bin/server.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^8.14.2" + }, + "peerDependencies": { + "yjs": "^13.6.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12296,6 +12473,24 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yjs": { + "version": "13.6.29", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", + "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 5998330d..6397eede 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,10 @@ "react-split": "^2.0.14", "tailwindcss": "^4.1.18", "vscode-ws-jsonrpc": "^3.5.0", - "ws": "^8.18.3" + "ws": "^8.18.3", + "y-monaco": "^0.1.6", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.29" }, "devDependencies": { "@codingame/esbuild-import-meta-url-plugin": "^1.0.3", @@ -93,4 +96,4 @@ "engines": { "node": "25.x" } -} +} \ No newline at end of file From c1c84754f9c7341505a2b4e448d5c7b71ac62b38 Mon Sep 17 00:00:00 2001 From: Nilesh <19304+nileshtrivedi@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:37:38 +0530 Subject: [PATCH 2/3] implement yjs signaling server within index.mjs --- client/package.json | 5 ++- client/src/App.tsx | 26 ++++++++--- server/index.mjs | 107 ++++++++++++++++++++++++++++++++++++++++++++ server/package.json | 4 +- 4 files changed, 133 insertions(+), 9 deletions(-) diff --git a/client/package.json b/client/package.json index 3bd4bc8b..b5e6952e 100644 --- a/client/package.json +++ b/client/package.json @@ -26,7 +26,10 @@ "react-split": "^2.0.14", "react": "^19.2.0", "tailwindcss": "^4.1.18", - "vscode-ws-jsonrpc": "^3.5.0" + "vscode-ws-jsonrpc": "^3.5.0", + "y-monaco": "^0.1.6", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.30" }, "devDependencies": { "@codingame/esbuild-import-meta-url-plugin": "^1.0.3", diff --git a/client/src/App.tsx b/client/src/App.tsx index c8d06ed7..dbcbaee6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -48,6 +48,7 @@ function App() { const [collabRoomName, setCollabRoomName] = useState('') const [collabDisplayName, setCollabDisplayName] = useState('') const [isCollaborating, setIsCollaborating] = useState(false) + const [collabError, setCollabError] = useState('') const model = editor?.getModel() @@ -89,15 +90,17 @@ function App() { setProvider(null) return } + + const signalingUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host.replace(':3000', ':8080') + '/yjs-signaling' + console.log('COLLAB: Signaling URL:', signalingUrl); + const provider = new WebrtcProvider( collabRoomName, // roomname ydoc, { maxConns: 50, password: undefined, - signaling: [ - 'wss://wide-robin-20.snowmountain.deno.net/' - ], + signaling: [signalingUrl], filterBcConns: true, } ) @@ -345,6 +348,16 @@ function App() { style={{background: 'var(--vscode-editor-background, white)', color: 'var(--vscode-editor-foreground, black)', padding: '20px', borderRadius: '8px', display: 'flex', flexDirection: 'column', gap: '15px', minWidth: '300px', border: '1px solid var(--vscode-dropdown-border, #ccc)'}} onSubmit={(e) => { e.preventDefault(); + const isValid = /^[a-z0-9]{3,20}$/; + if (!isValid.test(collabRoomName)) { + setCollabError("Room name must be 3-20 lowercase alphanumeric characters."); + return; + } + if (!isValid.test(collabDisplayName)) { + setCollabError("Display name must be 3-20 lowercase alphanumeric characters."); + return; + } + setCollabError(""); if (collabRoomName) { setIsCollaborating(true); setCollabDialogVisible(false); @@ -352,16 +365,17 @@ function App() { }} >

Join Collaboration

+ {collabError &&
{collabError}
}
- setCollabRoomName(e.target.value)} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} /> + {setCollabRoomName(e.target.value); setCollabError('');}} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} />
- setCollabDisplayName(e.target.value)} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} /> + {setCollabDisplayName(e.target.value); setCollabError('');}} style={{padding: '6px', backgroundColor: 'var(--vscode-input-background, white)', color: 'var(--vscode-input-foreground, black)', border: '1px solid var(--vscode-input-border, #ccc)'}} />
- +
diff --git a/server/index.mjs b/server/index.mjs index 62d193ac..a44f6ecc 100755 --- a/server/index.mjs +++ b/server/index.mjs @@ -228,10 +228,117 @@ function FilenamesToUri(prefix, obj) { return obj; } +const enableCollab = process.argv.includes('--collab=yes'); +if (enableCollab) { + console.log("COLLAB: Enabling signaling server for collaboration."); +} + +const yjsTopics = new Map(); // roomName -> Set + +const sendYjs = (conn, message) => { + if (conn.readyState !== 0 && conn.readyState !== 1) { + conn.close(); + } + try { + conn.send(JSON.stringify(message)); + } catch (e) { + conn.close(); + } +}; + +const setupYjsConnection = (conn, req) => { + const ip = req ? (req.headers["x-forwarded-for"] || req.socket.remoteAddress) : "unknown"; + console.log(`COLLAB: New connection established from ${ip}`); + const subscribedTopics = new Set(); + let closed = false; + // Check if connection is still alive + let pongReceived = true; + const pingInterval = setInterval(() => { + if (!pongReceived) { + conn.close(); + clearInterval(pingInterval); + } else { + pongReceived = false; + try { + conn.ping(); + } catch (e) { + conn.close(); + } + } + }, 30000); + conn.on("pong", () => { + pongReceived = true; + }); + conn.on("close", () => { + console.log(`COLLAB: Connection closed from ${ip}`); + subscribedTopics.forEach((topicName) => { + const subs = yjsTopics.get(topicName) || new Set(); + subs.delete(conn); + if (subs.size === 0) { + yjsTopics.delete(topicName); + } + }); + subscribedTopics.clear(); + closed = true; + }); + conn.on("message", (message) => { + if (typeof message === "string" || message instanceof Buffer) { + try { + message = JSON.parse(message); + } catch (e) { + return; + } + } + if (message && message.type && !closed) { + switch (message.type) { + case "subscribe": + (message.topics || []).forEach((topicName) => { + if (typeof topicName === "string") { + let topic = yjsTopics.get(topicName); + if (!topic) { + topic = new Set(); + yjsTopics.set(topicName, topic); + } + topic.add(conn); + subscribedTopics.add(topicName); + console.log(`COLLAB: Client subscribed to topic: ${topicName}`); + } + }); + break; + case "unsubscribe": + (message.topics || []).forEach((topicName) => { + const subs = yjsTopics.get(topicName); + if (subs) { + subs.delete(conn); + console.log(`COLLAB: Client unsubscribed from topic: ${topicName}`); + } + }); + break; + case "publish": + if (message.topic) { + const receivers = yjsTopics.get(message.topic); + console.log(`COLLAB: Client published to topic ${message.topic} (receivers: ${receivers ? receivers.size : 0})`); + if (receivers) { + message.clients = receivers.size; + receivers.forEach((receiver) => sendYjs(receiver, message)); + } + } + break; + case "ping": + sendYjs(conn, { type: "pong" }); + } + } + }); +}; + wss.addListener("connection", async function (ws, req) { const urlRegEx = /^\/websocket\/([\w.-]+)$/; const reRes = urlRegEx.exec(req.url); if (!reRes) { + if (enableCollab && (req.url === "/yjs-signaling" || req.url === "/yjs-signaling/")) { + setupYjsConnection(ws, req); + return; + } console.error(`Connection refused because of invalid URL: ${req.url}`); return; } diff --git a/server/package.json b/server/package.json index 480d71e4..6d6641f1 100644 --- a/server/package.json +++ b/server/package.json @@ -6,8 +6,8 @@ "server": "index.mjs" }, "scripts": { - "dev": "NODE_ENV=development nodemon index.mjs", - "prod": "NODE_ENV=production nodemon index.mjs", + "dev": "NODE_ENV=development nodemon index.mjs --collab=yes", + "prod": "NODE_ENV=production nodemon index.mjs --collab=yes", "build": "./build.sh" }, "dependencies": { From 568015236ab798441498468fa04162c98ae42f46 Mon Sep 17 00:00:00 2001 From: Nilesh <19304+nileshtrivedi@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:43:37 +0530 Subject: [PATCH 3/3] restore top-level package.json --- package-lock.json | 206 ++-------------------------------------------- package.json | 2 +- 2 files changed, 8 insertions(+), 200 deletions(-) diff --git a/package-lock.json b/package-lock.json index 186d5751..c391de3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4371,6 +4371,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -5775,12 +5776,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/err-code": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", - "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", - "license": "MIT" - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6968,12 +6963,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-browser-rtc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", - "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==", - "license": "MIT" - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7427,6 +7416,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -8095,16 +8085,6 @@ "node": ">=10" } }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "license": "MIT", - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8439,27 +8419,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lib0": { - "version": "0.2.117", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", - "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", - "license": "MIT", - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -9090,7 +9049,6 @@ "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-8.0.4.tgz", "integrity": "sha512-rKUdFRsnzTV1dj6FL/MD+cBIUAGv9aJNz8bTx4BA5sDK3lDWuifhdCBvk4WZEj6glfXe3sEfGXKy1Vp5btDOJQ==", "license": "MIT", - "peer": true, "dependencies": { "vscode": "npm:@codingame/monaco-vscode-api@8.0.4" } @@ -10053,30 +10011,11 @@ "node": ">=0.4.x" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" @@ -10182,6 +10121,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -10490,6 +10430,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -10832,59 +10773,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-peer": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", - "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "buffer": "^6.0.3", - "debug": "^4.3.2", - "err-code": "^3.0.1", - "get-browser-rtc": "^1.1.0", - "queue-microtask": "^1.2.3", - "randombytes": "^2.1.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/simple-peer/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -11025,6 +10913,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -11911,6 +11800,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -12436,70 +12326,6 @@ "node": ">=0.4" } }, - "node_modules/y-monaco": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz", - "integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==", - "license": "MIT", - "dependencies": { - "lib0": "^0.2.43" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - }, - "peerDependencies": { - "monaco-editor": ">=0.20.0", - "yjs": "^13.3.1" - } - }, - "node_modules/y-protocols": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", - "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", - "license": "MIT", - "dependencies": { - "lib0": "^0.2.85" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - }, - "peerDependencies": { - "yjs": "^13.0.0" - } - }, - "node_modules/y-webrtc": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", - "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", - "license": "MIT", - "dependencies": { - "lib0": "^0.2.42", - "simple-peer": "^9.11.0", - "y-protocols": "^1.0.6" - }, - "bin": { - "y-webrtc-signaling": "bin/server.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - }, - "optionalDependencies": { - "ws": "^8.14.2" - }, - "peerDependencies": { - "yjs": "^13.6.8" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -12557,24 +12383,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yjs": { - "version": "13.6.29", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", - "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "lib0": "^0.2.99" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 3e69e18a..101248aa 100644 --- a/package.json +++ b/package.json @@ -50,4 +50,4 @@ "engines": { "node": ">=24.x" } -} \ No newline at end of file +}