diff --git a/server/hocuspocus/bun.lock b/server/hocuspocus/bun.lock index 24cb6402..e527287d 100644 --- a/server/hocuspocus/bun.lock +++ b/server/hocuspocus/bun.lock @@ -5,18 +5,15 @@ "": { "name": "zedi-hocuspocus", "dependencies": { - "@hocuspocus/extension-redis": "^3.4.4", - "@hocuspocus/server": "^3.4.4", + "@hocuspocus/extension-redis": "^4.0.0", + "@hocuspocus/server": "^4.0.0", "aws-jwt-verify": "^5.1.1", - "ioredis": "^5.9.3", "pg": "^8.19.0", - "ws": "^8.19.0", "yjs": "^13.6.29", }, "devDependencies": { "@types/node": "^25.3.2", "@types/pg": "^8.16.0", - "@types/ws": "^8.18.1", "tsx": "^4.19.0", "typescript": "^6.0.2", "vitest": "^4.0.16", @@ -82,11 +79,11 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - "@hocuspocus/common": ["@hocuspocus/common@3.4.4", "", { "dependencies": { "lib0": "^0.2.87" } }, "sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA=="], + "@hocuspocus/common": ["@hocuspocus/common@4.0.0", "", { "dependencies": { "lib0": "^0.2.87" } }, "sha512-7BE8TsKBkdiOZO6tfm3ny6bIHPbxkIZb3hsYdVn/X5xbXI8n8w9pnE6pXgEMKQhJm6zsWsa9IDRJIp/c9u+DmA=="], - "@hocuspocus/extension-redis": ["@hocuspocus/extension-redis@3.4.4", "", { "dependencies": { "@hocuspocus/server": "^3.4.4", "@sesamecare-oss/redlock": "^1.4.0", "ioredis": "^5.6.1", "kleur": "^4.1.4", "lodash.debounce": "^4.0.8" }, "peerDependencies": { "y-protocols": "^1.0.6", "yjs": "^13.6.8" } }, "sha512-ZgzqTisse2jmtqep7xkTwCa9Bnl7+lGKrHgs1MiCyEza61cOZFSPBVYuFiLOEuCAzJnAM7WlvRj6KEEmWZnSXQ=="], + "@hocuspocus/extension-redis": ["@hocuspocus/extension-redis@4.0.0", "", { "dependencies": { "@hocuspocus/common": "^4.0.0", "@hocuspocus/server": "^4.0.0", "@sesamecare-oss/redlock": "^1.4.0", "ioredis": "~5.6.1", "kleur": "^4.1.4" }, "peerDependencies": { "y-protocols": "^1.0.6", "yjs": "^13.6.8" } }, "sha512-3IHjPKjxxspPn8fVg0Se1tas6sVJyVXJ1rV9VWHOkH10yvz19V/OYRbcysKyUclylHnZGrrKDOFxft2a7/Utdw=="], - "@hocuspocus/server": ["@hocuspocus/server@3.4.4", "", { "dependencies": { "@hocuspocus/common": "^3.4.4", "async-lock": "^1.3.1", "async-mutex": "^0.5.0", "kleur": "^4.1.4", "lib0": "^0.2.47", "ws": "^8.5.0" }, "peerDependencies": { "y-protocols": "^1.0.6", "yjs": "^13.6.8" } }, "sha512-UV+oaONAejOzeYgUygNcgsc8RdZvSokVvAxluZJIisLACpRO/VsseQ5lWKDRwLd7Fn6+rHWDH3hGuQ1fdX1Ycg=="], + "@hocuspocus/server": ["@hocuspocus/server@4.0.0", "", { "dependencies": { "@hocuspocus/common": "^4.0.0", "async-mutex": "^0.5.0", "crossws": "^0.4.4", "kleur": "^4.1.4", "lib0": "^0.2.47" }, "peerDependencies": { "y-protocols": "^1.0.6", "yjs": "^13.6.8" } }, "sha512-Pgm+kVtTrVvybIJUVot5XumSzD8rPLQglUV0QAAiN6J64dkYc2wWNdGcMwJz3q3yJPV0dO+eRU9JEck+iVdgzQ=="], "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], @@ -144,8 +141,6 @@ "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="], "@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="], @@ -162,8 +157,6 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], - "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], "aws-jwt-verify": ["aws-jwt-verify@5.1.1", "", {}, "sha512-j6whGdGJmQ27agk4ijY8RPv6itb8JLb7SCJ86fEnneTcSBrpxuwL8kLq6y5WVH95aIknyAloEqAsaOLS1J8ITQ=="], @@ -174,6 +167,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "crossws": ["crossws@0.4.5", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], @@ -194,7 +189,7 @@ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - "ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="], + "ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="], "isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="], @@ -226,8 +221,6 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], @@ -314,8 +307,6 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y-protocols": ["y-protocols@1.0.7", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw=="], diff --git a/server/hocuspocus/package.json b/server/hocuspocus/package.json index 81765dd5..9d51f96c 100644 --- a/server/hocuspocus/package.json +++ b/server/hocuspocus/package.json @@ -12,15 +12,12 @@ "@hocuspocus/extension-redis": "^4.0.0", "@hocuspocus/server": "^4.0.0", "aws-jwt-verify": "^5.1.1", - "ioredis": "^5.9.3", "pg": "^8.19.0", - "ws": "^8.19.0", "yjs": "^13.6.29" }, "devDependencies": { "@types/node": "^25.3.2", "@types/pg": "^8.16.0", - "@types/ws": "^8.18.1", "tsx": "^4.19.0", "typescript": "^6.0.2", "vitest": "^4.0.16" diff --git a/server/hocuspocus/src/index.ts b/server/hocuspocus/src/index.ts index b5ce328b..cdf425ba 100644 --- a/server/hocuspocus/src/index.ts +++ b/server/hocuspocus/src/index.ts @@ -1,6 +1,5 @@ -import { Hocuspocus } from "@hocuspocus/server"; -import { createServer, IncomingMessage, ServerResponse } from "http"; -import { WebSocketServer } from "ws"; +import { Server as HocuspocusServer } from "@hocuspocus/server"; +import { IncomingMessage, ServerResponse } from "http"; import { Redis } from "@hocuspocus/extension-redis"; import { Pool, PoolClient } from "pg"; import * as Y from "yjs"; @@ -285,7 +284,9 @@ if (REDIS_URL) { } } -const hocuspocus = new Hocuspocus({ +const hocuspocusServer = new HocuspocusServer({ + port: PORT, + stopOnSignals: false, name: "zedi-hocuspocus", extensions, @@ -384,6 +385,7 @@ const hocuspocus = new Hocuspocus({ // ドキュメント変更時(デバウンス前) }, }); +const hocuspocus = hocuspocusServer.hocuspocus; async function invalidateLiveDocument(documentName: string): Promise { if (!hocuspocus.documents.has(documentName)) { @@ -422,8 +424,10 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro await handleHttpRequestFallback(requestUrl, res); } -// カスタムHTTPサーバー(ヘルスチェック用) -const httpServer = createServer((req: IncomingMessage, res: ServerResponse) => { +// Hocuspocus v4 owns WebSocket upgrades; keep our health/internal HTTP routes. +// WebSocket upgrade は Hocuspocus v4 に任せ、health/internal の HTTP ルートだけ差し替える。 +hocuspocusServer.httpServer.removeAllListeners("request"); +hocuspocusServer.httpServer.on("request", (req: IncomingMessage, res: ServerResponse) => { void handleHttpRequest(req, res).catch((error) => { console.error("[HTTP] Request handling failed:", error); if (!res.headersSent) { @@ -454,14 +458,6 @@ async function handleHttpRequestFallback(requestUrl: URL, res: ServerResponse): res.end(JSON.stringify({ error: "Not Found" })); } -// WebSocketサーバーをHTTPサーバーにアタッチ -const wss = new WebSocketServer({ server: httpServer }); - -// WebSocket接続をHocuspocusに渡す -wss.on("connection", (socket, request) => { - hocuspocus.handleConnection(socket, request); -}); - if (NODE_ENV === "production" && !API_INTERNAL_URL) { console.error( "[Auth] CRITICAL: API_INTERNAL_URL is unset in production. Refusing to start. / " + @@ -471,52 +467,55 @@ if (NODE_ENV === "production" && !API_INTERNAL_URL) { } // サーバー起動 -httpServer.listen(PORT, () => { - console.log("========================================"); - console.log(" Zedi Hocuspocus Server Started"); - console.log("========================================"); - console.log(` Port: ${PORT}`); - console.log(` Health: http://localhost:${PORT}/health`); - console.log(` WebSocket: ws://localhost:${PORT}`); - console.log(` Redis: ${REDIS_URL ? "Enabled" : "Disabled"}`); - console.log(` Environment: ${NODE_ENV || "development"}`); - if (!API_INTERNAL_URL && NODE_ENV !== "production") { - if (isTruthyEnvFlag(HOCUSPOCUS_DEV_MODE)) { - console.warn( - "[Auth] API_INTERNAL_URL is unset; HOCUSPOCUS_DEV_MODE allows unauthenticated collaboration. / " + - "内部 API URL 未設定のため開発バイパスが有効です。", - ); - } else { - console.warn( - "[Auth] API_INTERNAL_URL is unset; WebSocket auth will fail until it is set or HOCUSPOCUS_DEV_MODE=true (local dev only). / " + - "内部 API URL 未設定のため接続は拒否されます(ローカル検証のみ HOCUSPOCUS_DEV_MODE=true)。", - ); +hocuspocusServer + .listen(PORT, () => { + console.log("========================================"); + console.log(" Zedi Hocuspocus Server Started"); + console.log("========================================"); + console.log(` Port: ${PORT}`); + console.log(` Health: http://localhost:${PORT}/health`); + console.log(` WebSocket: ws://localhost:${PORT}`); + console.log(` Redis: ${REDIS_URL ? "Enabled" : "Disabled"}`); + console.log(` Environment: ${NODE_ENV || "development"}`); + if (!API_INTERNAL_URL && NODE_ENV !== "production") { + if (isTruthyEnvFlag(HOCUSPOCUS_DEV_MODE)) { + console.warn( + "[Auth] API_INTERNAL_URL is unset; HOCUSPOCUS_DEV_MODE allows unauthenticated collaboration. / " + + "内部 API URL 未設定のため開発バイパスが有効です。", + ); + } else { + console.warn( + "[Auth] API_INTERNAL_URL is unset; WebSocket auth will fail until it is set or HOCUSPOCUS_DEV_MODE=true (local dev only). / " + + "内部 API URL 未設定のため接続は拒否されます(ローカル検証のみ HOCUSPOCUS_DEV_MODE=true)。", + ); + } } - } - console.log("========================================"); -}); + console.log("========================================"); + }) + .catch((error) => { + // Fail loudly on startup errors (port in use, onListen hook reject, etc.) + // 起動時エラー(ポート競合・onListen フック失敗など)を握り潰さず即座に終了する + console.error("[Startup] Failed to start hocuspocus server:", error); + process.exit(1); + }); // Graceful shutdown process.on("SIGTERM", async () => { console.log("[Shutdown] SIGTERM received, closing server..."); - hocuspocus.closeConnections(); + await hocuspocusServer.destroy(); if (pgPool) { await pgPool.end(); } - httpServer.close(() => { - console.log("[Shutdown] Server closed"); - process.exit(0); - }); + console.log("[Shutdown] Server closed"); + process.exit(0); }); process.on("SIGINT", async () => { console.log("[Shutdown] SIGINT received, closing server..."); - hocuspocus.closeConnections(); + await hocuspocusServer.destroy(); if (pgPool) { await pgPool.end(); } - httpServer.close(() => { - console.log("[Shutdown] Server closed"); - process.exit(0); - }); + console.log("[Shutdown] Server closed"); + process.exit(0); });