working live collab
This commit is contained in:
Generated
+272
-4
@@ -10,9 +10,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
@@ -133,6 +135,12 @@
|
|||||||
"@prisma/debug": "5.22.0"
|
"@prisma/debug": "5.22.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -186,7 +194,6 @@
|
|||||||
"version": "2.8.19",
|
"version": "2.8.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -235,7 +242,6 @@
|
|||||||
"version": "24.10.1",
|
"version": "24.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -289,6 +295,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/socket.io": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -356,6 +371,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -620,6 +644,95 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -1456,6 +1569,141 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.3.4",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -1594,7 +1842,6 @@
|
|||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
@@ -1628,6 +1875,27 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yn": {
|
"node_modules/yn": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
|
|||||||
Binary file not shown.
+86
-5
@@ -2,6 +2,8 @@ import express from "express";
|
|||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { createServer } from "http";
|
||||||
|
import { Server } from "socket.io";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { PrismaClient } from "./generated/client";
|
import { PrismaClient } from "./generated/client";
|
||||||
|
|
||||||
@@ -14,12 +16,94 @@ process.env.DATABASE_URL = `file:${dbPath}`;
|
|||||||
console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL);
|
console.log("Resolved DATABASE_URL:", process.env.DATABASE_URL);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const httpServer = createServer(app);
|
||||||
|
const io = new Server(httpServer, {
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
},
|
||||||
|
maxHttpBufferSize: 1e8, // 100 MB
|
||||||
|
});
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
|
|
||||||
|
// Socket.io Logic
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
initials: string;
|
||||||
|
color: string;
|
||||||
|
socketId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomUsers = new Map<string, User[]>();
|
||||||
|
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
socket.on(
|
||||||
|
"join-room",
|
||||||
|
({
|
||||||
|
drawingId,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
drawingId: string;
|
||||||
|
user: Omit<User, "socketId" | "isActive">;
|
||||||
|
}) => {
|
||||||
|
const roomId = `drawing_${drawingId}`;
|
||||||
|
socket.join(roomId);
|
||||||
|
|
||||||
|
const newUser: User = { ...user, socketId: socket.id, isActive: true };
|
||||||
|
|
||||||
|
const currentUsers = roomUsers.get(roomId) || [];
|
||||||
|
const filteredUsers = currentUsers.filter((u) => u.id !== user.id);
|
||||||
|
filteredUsers.push(newUser);
|
||||||
|
roomUsers.set(roomId, filteredUsers);
|
||||||
|
|
||||||
|
io.to(roomId).emit("presence-update", filteredUsers);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on("cursor-move", (data) => {
|
||||||
|
const roomId = `drawing_${data.drawingId}`;
|
||||||
|
// Use volatile for high-frequency, low-importance updates (cursors)
|
||||||
|
// If network is congested, drop these packets
|
||||||
|
socket.volatile.to(roomId).emit("cursor-move", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("element-update", (data) => {
|
||||||
|
const roomId = `drawing_${data.drawingId}`;
|
||||||
|
socket.to(roomId).emit("element-update", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on(
|
||||||
|
"user-activity",
|
||||||
|
({ drawingId, isActive }: { drawingId: string; isActive: boolean }) => {
|
||||||
|
const roomId = `drawing_${drawingId}`;
|
||||||
|
const users = roomUsers.get(roomId);
|
||||||
|
if (users) {
|
||||||
|
const user = users.find((u) => u.socketId === socket.id);
|
||||||
|
if (user) {
|
||||||
|
user.isActive = isActive;
|
||||||
|
io.to(roomId).emit("presence-update", users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
roomUsers.forEach((users, roomId) => {
|
||||||
|
const index = users.findIndex((u) => u.socketId === socket.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
users.splice(index, 1);
|
||||||
|
roomUsers.set(roomId, users);
|
||||||
|
io.to(roomId).emit("presence-update", users);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --- Drawings ---
|
// --- Drawings ---
|
||||||
|
|
||||||
// GET /drawings
|
// GET /drawings
|
||||||
@@ -38,10 +122,7 @@ app.get("/drawings", async (req, res) => {
|
|||||||
where.collectionId = String(collectionId);
|
where.collectionId = String(collectionId);
|
||||||
} else {
|
} else {
|
||||||
// Default: Exclude trash, but include unorganized (null)
|
// Default: Exclude trash, but include unorganized (null)
|
||||||
where.OR = [
|
where.OR = [{ collectionId: { not: "trash" } }, { collectionId: null }];
|
||||||
{ collectionId: { not: "trash" } },
|
|
||||||
{ collectionId: null },
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawings = await prisma.drawing.findMany({
|
const drawings = await prisma.drawing.findMany({
|
||||||
@@ -262,7 +343,7 @@ const ensureTrashCollection = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.listen(PORT, async () => {
|
httpServer.listen(PORT, async () => {
|
||||||
await ensureTrashCollection();
|
await ensureTrashCollection();
|
||||||
console.log(`Server running on port ${PORT}`);
|
console.log(`Server running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+137
@@ -20,6 +20,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.9.6",
|
"react-router-dom": "^7.9.6",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
@@ -2288,6 +2289,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -3871,6 +3878,45 @@
|
|||||||
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==",
|
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==",
|
||||||
"license": "EPL-2.0"
|
"license": "EPL-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -6474,6 +6520,68 @@
|
|||||||
"integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==",
|
"integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sonner": {
|
"node_modules/sonner": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
@@ -7124,6 +7232,35 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.9.6",
|
"react-router-dom": "^7.9.6",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+301
-27
@@ -4,10 +4,34 @@ import { ArrowLeft } from 'lucide-react';
|
|||||||
import { Excalidraw, convertToExcalidrawElements, exportToSvg } from '@excalidraw/excalidraw';
|
import { Excalidraw, convertToExcalidrawElements, exportToSvg } from '@excalidraw/excalidraw';
|
||||||
import '@excalidraw/excalidraw/index.css';
|
import '@excalidraw/excalidraw/index.css';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
import { Toaster, toast } from 'sonner';
|
import { Toaster, toast } from 'sonner';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import { getUserIdentity } from '../utils/identity';
|
||||||
|
import { reconcileElements } from '../utils/sync';
|
||||||
|
import type { UserIdentity } from '../utils/identity';
|
||||||
import * as api from '../api';
|
import * as api from '../api';
|
||||||
import { useTheme } from '../context/ThemeContext';
|
import { useTheme } from '../context/ThemeContext';
|
||||||
|
|
||||||
|
interface Peer extends UserIdentity {
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementVersionInfo {
|
||||||
|
version: number;
|
||||||
|
versionNonce: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move UIOptions outside to prevent re-creation on every render
|
||||||
|
const UIOptions = {
|
||||||
|
canvasActions: {
|
||||||
|
saveToActiveFile: false,
|
||||||
|
loadScene: false,
|
||||||
|
export: { saveFileToDisk: false },
|
||||||
|
toggleTheme: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Editor: React.FC = () => {
|
export const Editor: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -18,8 +42,163 @@ export const Editor: React.FC = () => {
|
|||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [initialData, setInitialData] = useState<any>(null);
|
const [initialData, setInitialData] = useState<any>(null);
|
||||||
|
|
||||||
|
const [peers, setPeers] = useState<Peer[]>([]);
|
||||||
|
const [me] = useState(getUserIdentity());
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
const lastCursorEmit = useRef<number>(0);
|
||||||
|
const elementVersionMap = useRef<Map<string, ElementVersionInfo>>(new Map());
|
||||||
|
const isSyncing = useRef(false);
|
||||||
|
const cursorBuffer = useRef<Map<string, any>>(new Map());
|
||||||
|
const animationFrameId = useRef<number>(0);
|
||||||
|
|
||||||
|
const recordElementVersion = useCallback((element: any) => {
|
||||||
|
elementVersionMap.current.set(element.id, {
|
||||||
|
version: element.version ?? 0,
|
||||||
|
versionNonce: element.versionNonce ?? 0,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasElementChanged = useCallback((element: any) => {
|
||||||
|
const previous = elementVersionMap.current.get(element.id);
|
||||||
|
if (!previous) return true;
|
||||||
|
|
||||||
|
const nextVersion = element.version ?? 0;
|
||||||
|
const nextNonce = element.versionNonce ?? 0;
|
||||||
|
|
||||||
|
return previous.version !== nextVersion || previous.versionNonce !== nextNonce;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id || !isReady) return;
|
||||||
|
|
||||||
|
const socket = io(import.meta.env.VITE_API_URL || 'http://localhost:8000', {
|
||||||
|
transports: ['websocket'],
|
||||||
|
});
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
socket.emit('join-room', { drawingId: id, user: me });
|
||||||
|
|
||||||
|
// Start the render loop for cursors
|
||||||
|
const renderLoop = () => {
|
||||||
|
if (cursorBuffer.current.size > 0 && excalidrawAPI.current) {
|
||||||
|
const collaborators = new Map(excalidrawAPI.current.getAppState().collaborators || []);
|
||||||
|
|
||||||
|
cursorBuffer.current.forEach((data, userId) => {
|
||||||
|
collaborators.set(userId, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
cursorBuffer.current.clear();
|
||||||
|
excalidrawAPI.current.updateScene({ collaborators });
|
||||||
|
}
|
||||||
|
animationFrameId.current = requestAnimationFrame(renderLoop);
|
||||||
|
};
|
||||||
|
renderLoop();
|
||||||
|
|
||||||
|
socket.on('presence-update', (users: Peer[]) => {
|
||||||
|
setPeers(users.filter(u => u.id !== me.id));
|
||||||
|
|
||||||
|
// Update collaborators map to remove inactive users
|
||||||
|
if (excalidrawAPI.current) {
|
||||||
|
const collaborators = new Map(excalidrawAPI.current.getAppState().collaborators || []);
|
||||||
|
users.forEach(user => {
|
||||||
|
if (!user.isActive && user.id !== me.id) {
|
||||||
|
collaborators.delete(user.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
excalidrawAPI.current.updateScene({ collaborators });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('cursor-move', (data: any) => {
|
||||||
|
// Just buffer the data
|
||||||
|
cursorBuffer.current.set(data.userId, {
|
||||||
|
pointer: data.pointer,
|
||||||
|
button: data.button || 'up',
|
||||||
|
selectedElementIds: data.selectedElementIds || {},
|
||||||
|
username: data.username,
|
||||||
|
avatarUrl: data.avatarUrl,
|
||||||
|
color: { background: data.color, stroke: data.color },
|
||||||
|
id: data.userId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('element-update', ({ elements }: { elements: any[] }) => {
|
||||||
|
if (!excalidrawAPI.current) return;
|
||||||
|
|
||||||
|
isSyncing.current = true;
|
||||||
|
|
||||||
|
// 3. THE SELECTION GUARD (Fixes Dragging/Snap-back)
|
||||||
|
// Get IDs of elements YOU are currently holding
|
||||||
|
const currentAppState = excalidrawAPI.current.getAppState();
|
||||||
|
const mySelectedIds = currentAppState.selectedElementIds || {};
|
||||||
|
|
||||||
|
// Filter out updates for elements you are currently dragging
|
||||||
|
// This prevents the server from pulling the object out of your hand
|
||||||
|
const validRemoteElements = elements.filter((el: any) => !mySelectedIds[el.id]);
|
||||||
|
|
||||||
|
const localElements = excalidrawAPI.current.getSceneElementsIncludingDeleted();
|
||||||
|
const mergedElements = reconcileElements(localElements, validRemoteElements);
|
||||||
|
|
||||||
|
// Update version map with remote versions to avoid echoing
|
||||||
|
validRemoteElements.forEach((el: any) => {
|
||||||
|
recordElementVersion(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
excalidrawAPI.current.updateScene({ elements: mergedElements });
|
||||||
|
isSyncing.current = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activity Tracking
|
||||||
|
const handleActivity = (isActive: boolean) => {
|
||||||
|
socket.emit('user-activity', { drawingId: id, isActive });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = () => handleActivity(true);
|
||||||
|
const onBlur = () => handleActivity(false);
|
||||||
|
const onMouseEnter = () => handleActivity(true);
|
||||||
|
const onMouseLeave = () => handleActivity(false);
|
||||||
|
|
||||||
|
window.addEventListener('focus', onFocus);
|
||||||
|
window.addEventListener('blur', onBlur);
|
||||||
|
document.addEventListener('mouseenter', onMouseEnter);
|
||||||
|
document.addEventListener('mouseleave', onMouseLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', onFocus);
|
||||||
|
window.removeEventListener('blur', onBlur);
|
||||||
|
document.removeEventListener('mouseenter', onMouseEnter);
|
||||||
|
document.removeEventListener('mouseleave', onMouseLeave);
|
||||||
|
socket.off('presence-update');
|
||||||
|
socket.off('cursor-move');
|
||||||
|
socket.off('element-update');
|
||||||
|
socket.disconnect();
|
||||||
|
cancelAnimationFrame(animationFrameId.current);
|
||||||
|
};
|
||||||
|
}, [id, me, isReady, recordElementVersion]);
|
||||||
|
|
||||||
|
const onPointerUpdate = useCallback((payload: any) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCursorEmit.current > 50 && socketRef.current) {
|
||||||
|
socketRef.current.emit('cursor-move', {
|
||||||
|
pointer: payload.pointer,
|
||||||
|
button: payload.button,
|
||||||
|
username: me.name,
|
||||||
|
userId: me.id,
|
||||||
|
drawingId: id,
|
||||||
|
color: me.color
|
||||||
|
});
|
||||||
|
lastCursorEmit.current = now;
|
||||||
|
}
|
||||||
|
}, [id, me]);
|
||||||
|
|
||||||
// Refs for API interaction
|
// Refs for API interaction
|
||||||
const excalidrawAPI = useRef<any>(null);
|
const excalidrawAPI = useRef<any>(null);
|
||||||
|
|
||||||
|
const setExcalidrawAPI = useCallback((api: any) => {
|
||||||
|
excalidrawAPI.current = api;
|
||||||
|
setIsReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 1. STABLE SAVE LOGIC (The Fix)
|
// 1. STABLE SAVE LOGIC (The Fix)
|
||||||
@@ -27,6 +206,7 @@ export const Editor: React.FC = () => {
|
|||||||
// doesn't need to be recreated on every render.
|
// doesn't need to be recreated on every render.
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
const saveDataRef = useRef<(elements: any, appState: any) => Promise<void>>(null);
|
const saveDataRef = useRef<(elements: any, appState: any) => Promise<void>>(null);
|
||||||
|
const savePreviewRef = useRef<(elements: any, appState: any, files: any) => Promise<void>>(null);
|
||||||
|
|
||||||
// Update the ref on every render to ensure it has access to the latest props/state
|
// Update the ref on every render to ensure it has access to the latest props/state
|
||||||
saveDataRef.current = async (elements, appState) => {
|
saveDataRef.current = async (elements, appState) => {
|
||||||
@@ -37,9 +217,22 @@ export const Editor: React.FC = () => {
|
|||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await api.updateDrawing(id, {
|
||||||
|
elements,
|
||||||
|
appState: persistableAppState,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save drawing', err);
|
||||||
|
toast.error("Failed to save changes");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
savePreviewRef.current = async (elements, appState, files) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
// Generate preview
|
// Generate preview
|
||||||
const files = excalidrawAPI.current?.getFiles() || null;
|
|
||||||
const svg = await exportToSvg({
|
const svg = await exportToSvg({
|
||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
@@ -50,15 +243,10 @@ export const Editor: React.FC = () => {
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
const preview = svg.outerHTML;
|
const preview = svg.outerHTML;
|
||||||
|
|
||||||
await api.updateDrawing(id, {
|
await api.updateDrawing(id, { preview });
|
||||||
elements,
|
|
||||||
appState: persistableAppState,
|
|
||||||
preview,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to save drawing', err);
|
console.error('Failed to save preview', err);
|
||||||
toast.error("Failed to save changes");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +261,39 @@ export const Editor: React.FC = () => {
|
|||||||
[] // Empty dependency array = Stable across renders
|
[] // Empty dependency array = Stable across renders
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debouncedSavePreview = useCallback(
|
||||||
|
debounce((elements, appState, files) => {
|
||||||
|
if (savePreviewRef.current) {
|
||||||
|
savePreviewRef.current(elements, appState, files);
|
||||||
|
}
|
||||||
|
}, 10000),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const broadcastChanges = useCallback(
|
||||||
|
throttle((elements: readonly any[]) => {
|
||||||
|
if (!socketRef.current || !id) return;
|
||||||
|
|
||||||
|
const changes: any[] = [];
|
||||||
|
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (hasElementChanged(el)) {
|
||||||
|
changes.push(el);
|
||||||
|
recordElementVersion(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changes.length > 0) {
|
||||||
|
socketRef.current.emit('element-update', {
|
||||||
|
drawingId: id,
|
||||||
|
elements: changes,
|
||||||
|
userId: me.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100, { leading: true, trailing: true }),
|
||||||
|
[id, hasElementChanged, recordElementVersion]
|
||||||
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 2. DATA LOADING
|
// 2. DATA LOADING
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -83,8 +304,15 @@ export const Editor: React.FC = () => {
|
|||||||
const data = await api.getDrawing(id);
|
const data = await api.getDrawing(id);
|
||||||
setDrawingName(data.name);
|
setDrawingName(data.name);
|
||||||
|
|
||||||
|
const elements = convertToExcalidrawElements(data.elements || []);
|
||||||
|
|
||||||
|
// Initialize version tracking with loaded data
|
||||||
|
elements.forEach((el: any) => {
|
||||||
|
recordElementVersion(el);
|
||||||
|
});
|
||||||
|
|
||||||
setInitialData({
|
setInitialData({
|
||||||
elements: convertToExcalidrawElements(data.elements || []),
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
...data.appState,
|
...data.appState,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
@@ -96,7 +324,7 @@ export const Editor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadData();
|
loadData();
|
||||||
}, [id]);
|
}, [id, recordElementVersion]);
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// 3. HANDLERS
|
// 3. HANDLERS
|
||||||
@@ -107,11 +335,14 @@ export const Editor: React.FC = () => {
|
|||||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (excalidrawAPI.current && saveDataRef.current) {
|
if (excalidrawAPI.current && saveDataRef.current && savePreviewRef.current) {
|
||||||
const elements = excalidrawAPI.current.getSceneElements();
|
const elements = excalidrawAPI.current.getSceneElements();
|
||||||
const appState = excalidrawAPI.current.getAppState();
|
const appState = excalidrawAPI.current.getAppState();
|
||||||
|
const files = excalidrawAPI.current.getFiles() || null;
|
||||||
// Call save immediately, bypassing debounce
|
// Call save immediately, bypassing debounce
|
||||||
await saveDataRef.current(elements, appState);
|
await saveDataRef.current(elements, appState);
|
||||||
|
// Also update preview
|
||||||
|
savePreviewRef.current(elements, appState, files);
|
||||||
toast.success("Saved changes to server");
|
toast.success("Saved changes to server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +351,26 @@ export const Editor: React.FC = () => {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCanvasChange = (elements: readonly any[], appState: any) => {
|
const handleCanvasChange = useCallback((elements: readonly any[], appState: any) => {
|
||||||
// Trigger the stable debounced save
|
// 4. STOP THE ECHO
|
||||||
debouncedSave(elements, appState);
|
// If this change was caused by a socket update, do NOT broadcast it back
|
||||||
};
|
if (isSyncing.current) return;
|
||||||
|
|
||||||
|
// Get ALL elements including deleted (fixes the "deletion not syncing" bug)
|
||||||
|
const allElements = excalidrawAPI.current
|
||||||
|
? excalidrawAPI.current.getSceneElementsIncludingDeleted()
|
||||||
|
: elements;
|
||||||
|
|
||||||
|
// Trigger Sync (Throttled)
|
||||||
|
broadcastChanges(allElements);
|
||||||
|
|
||||||
|
// Trigger Fast Save
|
||||||
|
debouncedSave(allElements, appState);
|
||||||
|
|
||||||
|
// Trigger Slow Preview Gen
|
||||||
|
const files = excalidrawAPI.current?.getFiles() || null;
|
||||||
|
debouncedSavePreview(allElements, appState, files);
|
||||||
|
}, [debouncedSave, debouncedSavePreview, broadcastChanges]);
|
||||||
|
|
||||||
const handleRenameSubmit = async (e: React.FormEvent) => {
|
const handleRenameSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -139,14 +386,7 @@ export const Editor: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Disable native Excalidraw save dialogs
|
// Disable native Excalidraw save dialogs
|
||||||
const UIOptions = {
|
// UIOptions is now defined outside the component
|
||||||
canvasActions: {
|
|
||||||
saveToActiveFile: false,
|
|
||||||
loadScene: false,
|
|
||||||
export: { saveFileToDisk: false },
|
|
||||||
toggleTheme: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
|
<div className="h-screen flex flex-col bg-white dark:bg-neutral-950 overflow-hidden">
|
||||||
@@ -177,8 +417,41 @@ export const Editor: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
{/* Status indicator removed */}
|
<div className="flex items-center">
|
||||||
|
<div className="relative group">
|
||||||
|
<div
|
||||||
|
className="w-9 h-9 rounded-xl flex items-center justify-center text-sm font-bold text-white shadow-sm"
|
||||||
|
style={{ backgroundColor: me.color }}
|
||||||
|
>
|
||||||
|
{me.initials}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-full mt-2 right-0 bg-gray-900 text-white text-xs py-1 px-2 rounded whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{me.name} (You)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-6 w-px bg-gray-300 dark:bg-gray-700 mx-2" />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{peers.map(peer => (
|
||||||
|
<div
|
||||||
|
key={peer.id}
|
||||||
|
className="relative group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-9 h-9 rounded-xl flex items-center justify-center text-sm font-bold text-white shadow-sm transition-all duration-300 ${!peer.isActive ? 'opacity-30 grayscale' : ''}`}
|
||||||
|
style={{ backgroundColor: peer.color }}
|
||||||
|
>
|
||||||
|
{peer.initials}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-full mt-2 right-0 bg-gray-900 text-white text-xs py-1 px-2 rounded whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{peer.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -187,7 +460,8 @@ export const Editor: React.FC = () => {
|
|||||||
theme={theme === 'dark' ? 'dark' : 'light'}
|
theme={theme === 'dark' ? 'dark' : 'light'}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
onChange={handleCanvasChange}
|
onChange={handleCanvasChange}
|
||||||
excalidrawAPI={(api) => (excalidrawAPI.current = api)}
|
onPointerUpdate={onPointerUpdate}
|
||||||
|
excalidrawAPI={setExcalidrawAPI}
|
||||||
UIOptions={UIOptions}
|
UIOptions={UIOptions}
|
||||||
/>
|
/>
|
||||||
<Toaster position="bottom-center" />
|
<Toaster position="bottom-center" />
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
export interface UserIdentity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
initials: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSFORMERS = [
|
||||||
|
{ name: "Optimus Prime", initials: "OP" },
|
||||||
|
{ name: "Megatron", initials: "ME" },
|
||||||
|
{ name: "Starscream", initials: "ST" },
|
||||||
|
{ name: "Bumblebee", initials: "BB" },
|
||||||
|
{ name: "Ultra Magnus", initials: "UM" },
|
||||||
|
{ name: "Shockwave", initials: "SH" },
|
||||||
|
{ name: "Soundwave", initials: "SW" },
|
||||||
|
{ name: "Ironhide", initials: "IR" },
|
||||||
|
{ name: "Ratchet", initials: "RA" },
|
||||||
|
{ name: "Prowl", initials: "PR" },
|
||||||
|
{ name: "Jazz", initials: "JA" },
|
||||||
|
{ name: "Hot Rod", initials: "HR" },
|
||||||
|
{ name: "Alpha Trion", initials: "AT" },
|
||||||
|
{ name: "Wheeljack", initials: "WH" },
|
||||||
|
{ name: "Sideswipe", initials: "SI" },
|
||||||
|
{ name: "Sunstreaker", initials: "SU" },
|
||||||
|
{ name: "Inferno", initials: "IN" },
|
||||||
|
{ name: "Grapple", initials: "GR" },
|
||||||
|
{ name: "Blaster", initials: "BL" },
|
||||||
|
{ name: "Perceptor", initials: "PE" },
|
||||||
|
{ name: "Trailbreaker", initials: "TR" },
|
||||||
|
{ name: "Cosmos", initials: "CO" },
|
||||||
|
{ name: "Warpath", initials: "WA" },
|
||||||
|
{ name: "Powerglide", initials: "PO" },
|
||||||
|
{ name: "Arcee", initials: "AR" },
|
||||||
|
{ name: "Springer", initials: "SP" },
|
||||||
|
{ name: "Kup", initials: "KU" },
|
||||||
|
{ name: "Blurr", initials: "BU" },
|
||||||
|
{ name: "Grimlock", initials: "GL" },
|
||||||
|
{ name: "Swoop", initials: "WO" },
|
||||||
|
{ name: "Skywarp", initials: "SK" },
|
||||||
|
{ name: "Thundercracker", initials: "TH" },
|
||||||
|
{ name: "Ramjet", initials: "AM" },
|
||||||
|
{ name: "Cyclonus", initials: "CY" },
|
||||||
|
{ name: "Scourge", initials: "SC" },
|
||||||
|
{ name: "Galvatron", initials: "GA" },
|
||||||
|
{ name: "Astrotrain", initials: "AS" },
|
||||||
|
{ name: "Blitzwing", initials: "BZ" },
|
||||||
|
{ name: "Rumble", initials: "RU" },
|
||||||
|
{ name: "Frenzy", initials: "FR" },
|
||||||
|
{ name: "Laserbeak", initials: "LA" },
|
||||||
|
{ name: "Ravage", initials: "RV" },
|
||||||
|
{ name: "Unicron", initials: "UN" },
|
||||||
|
{ name: "Devastator", initials: "DE" },
|
||||||
|
{ name: "Menasor", initials: "MN" },
|
||||||
|
{ name: "Bruticus", initials: "BR" },
|
||||||
|
{ name: "Motormaster", initials: "MO" },
|
||||||
|
{ name: "Scrapper", initials: "CR" },
|
||||||
|
{ name: "Mixmaster", initials: "MA" },
|
||||||
|
{ name: "Bonecrusher", initials: "BO" },
|
||||||
|
{ name: "Hook", initials: "HO" },
|
||||||
|
{ name: "Vortex", initials: "VO" },
|
||||||
|
{ name: "Swindle", initials: "WI" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
"#ef4444", // red-500
|
||||||
|
"#f97316", // orange-500
|
||||||
|
"#f59e0b", // amber-500
|
||||||
|
"#84cc16", // lime-500
|
||||||
|
"#22c55e", // green-500
|
||||||
|
"#10b981", // emerald-500
|
||||||
|
"#14b8a6", // teal-500
|
||||||
|
"#06b6d4", // cyan-500
|
||||||
|
"#0ea5e9", // sky-500
|
||||||
|
"#3b82f6", // blue-500
|
||||||
|
"#6366f1", // indigo-500
|
||||||
|
"#8b5cf6", // violet-500
|
||||||
|
"#a855f7", // purple-500
|
||||||
|
"#d946ef", // fuchsia-500
|
||||||
|
"#ec4899", // pink-500
|
||||||
|
"#f43f5e", // rose-500
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getUserIdentity = (): UserIdentity => {
|
||||||
|
const stored = localStorage.getItem("excalidash-user-id");
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomTransformer =
|
||||||
|
TRANSFORMERS[Math.floor(Math.random() * TRANSFORMERS.length)];
|
||||||
|
const randomColor = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||||
|
|
||||||
|
const identity: UserIdentity = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: randomTransformer.name,
|
||||||
|
initials: randomTransformer.initials,
|
||||||
|
color: randomColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem("excalidash-user-id", JSON.stringify(identity));
|
||||||
|
return identity;
|
||||||
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
export const reconcileElements = (
|
||||||
|
localElements: readonly any[],
|
||||||
|
remoteElements: readonly any[]
|
||||||
|
): any[] => {
|
||||||
|
const localMap = new Map<string, any>();
|
||||||
|
|
||||||
|
// Index local elements
|
||||||
|
localElements.forEach((el) => {
|
||||||
|
localMap.set(el.id, el);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge remote elements
|
||||||
|
// Prefer version + updated timestamp to determine ordering; nonces are random.
|
||||||
|
const getVersion = (element: any) => element?.version ?? 0;
|
||||||
|
const getVersionNonce = (element: any) => element?.versionNonce ?? 0;
|
||||||
|
const getUpdated = (element: any) => {
|
||||||
|
const value = element?.updated;
|
||||||
|
return typeof value === 'number' ? value : Number(value) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteElements.forEach((remoteEl) => {
|
||||||
|
const localEl = localMap.get(remoteEl.id);
|
||||||
|
|
||||||
|
if (!localEl) {
|
||||||
|
localMap.set(remoteEl.id, remoteEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteVersion = getVersion(remoteEl);
|
||||||
|
const localVersion = getVersion(localEl);
|
||||||
|
|
||||||
|
if (remoteVersion > localVersion) {
|
||||||
|
localMap.set(remoteEl.id, remoteEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteVersion < localVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteUpdated = getUpdated(remoteEl);
|
||||||
|
const localUpdated = getUpdated(localEl);
|
||||||
|
|
||||||
|
if (remoteUpdated > localUpdated) {
|
||||||
|
localMap.set(remoteEl.id, remoteEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
remoteUpdated === localUpdated &&
|
||||||
|
getVersionNonce(remoteEl) !== getVersionNonce(localEl)
|
||||||
|
) {
|
||||||
|
localMap.set(remoteEl.id, remoteEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(localMap.values());
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user