diff --git a/package-lock.json b/package-lock.json index 6089aee..1dbb25c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@fastify/sse": "^0.4.0", "@fastify/swagger": "^9.6.1", "@fastify/swagger-ui": "^5.2.3", + "@fastify/websocket": "^11.2.0", "dotenv": "^17.2.2", "fastify": "^5.6.2", "fastify-axios": "^1.3.0", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/node": "^24.10.1", + "@types/ws": "^8.18.1", "tsx": "^4.20.6", "typescript": "^5.9.3" } @@ -736,6 +738,27 @@ "yaml": "^2.4.1" } }, + "node_modules/@fastify/websocket": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.2.0.tgz", + "integrity": "sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^5.0.0", + "ws": "^8.16.0" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -782,6 +805,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -990,6 +1023,27 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1630,6 +1684,15 @@ "node": ">=14.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openapi-types": { "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", @@ -1724,6 +1787,20 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -1796,6 +1873,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-regex2": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", @@ -1891,6 +1988,21 @@ "node": ">= 0.8" } }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/swagger-themes": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/swagger-themes/-/swagger-themes-1.4.3.tgz", @@ -1968,6 +2080,39 @@ "dev": true, "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", diff --git a/package.json b/package.json index 4a02725..0fc72b9 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,34 @@ { - "name": "@dpu/api", - "version": "1.0.0", - "description": "", - "scripts": { - "clean": "rimraf dist", - "build": "npm run clean && tsc", - "start": "node dist/index.js", - "dev": "tsx watch src/index.ts" - }, - "keywords": [], - "author": "Darius", - "license": "", - "type": "module", - "dependencies": { - "@dpu/shared": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git", - "@fastify/cors": "^11.2.0", - "@fastify/sse": "^0.4.0", - "@fastify/swagger": "^9.6.1", - "@fastify/swagger-ui": "^5.2.3", - "dotenv": "^17.2.2", - "fastify": "^5.6.2", - "fastify-axios": "^1.3.0", - "fastify-type-provider-zod": "^6.1.0", - "swagger-themes": "^1.4.3", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/node": "^24.10.1", - "tsx": "^4.20.6", - "typescript": "^5.9.3" - } + "name": "@dpu/api", + "version": "1.0.0", + "description": "", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts" + }, + "keywords": [], + "author": "Darius", + "license": "", + "type": "module", + "dependencies": { + "@dpu/shared": "git+https://git.dariusbag.dev/DarDarBinks/dpu-shared.git", + "@fastify/cors": "^11.2.0", + "@fastify/swagger": "^9.6.1", + "@fastify/swagger-ui": "^5.2.3", + "@fastify/websocket": "^11.2.0", + "dotenv": "^17.2.2", + "fastify": "^5.6.2", + "fastify-axios": "^1.3.0", + "fastify-type-provider-zod": "^6.1.0", + "swagger-themes": "^1.4.3", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/ws": "^8.18.1", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } } diff --git a/src/index.ts b/src/index.ts index 3d8f8f2..d395d01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,12 +19,12 @@ import { HomeAssistantService } from "./homeassistant/service.js"; import { TidalClient } from "./tidal/client.js"; import { TidalService } from "./tidal/service.js"; import { tidalRoutes } from "./tidal/routes.js"; -import { sseRoutes } from "./sse/routes.js"; +import { sseRoutes } from "./websocket/routes.js"; import { SseService } from "@dpu/shared"; import { HomepageService } from "./homepage/service.js"; import { homepageRoutes } from "./homepage/routes.js"; -import fastifySSE from "@fastify/sse"; import fastifyCors from "@fastify/cors"; +import fastifyWebsocket from "@fastify/websocket"; const fastify = Fastify().withTypeProvider(); @@ -81,7 +81,7 @@ await fastify.register(fastifyAxios, { }, }); -await fastify.register(fastifySSE); +await fastify.register(fastifyWebsocket); const haClient = new HomeAssistantClient(fastify.axios.homeassistant); const haService = new HomeAssistantService(haClient); diff --git a/src/sse/routes.ts b/src/websocket/routes.ts similarity index 66% rename from src/sse/routes.ts rename to src/websocket/routes.ts index a42c9d2..e936954 100644 --- a/src/sse/routes.ts +++ b/src/websocket/routes.ts @@ -14,29 +14,26 @@ export async function sseRoutes( "/dpu/events", { schema: { - description: "Register for SSE", - tags: ["sse"], + description: "Register for WebSocket events", + tags: ["ws"], hide: true, }, - sse: true, + websocket: true, }, - async (_request, reply) => { - reply.sse.keepAlive(); - + (socket, _request) => { const clientId = randomUUID(); const sendEvent = (event: SseEvent) => { - reply.sse.send({ - event: event.type, - data: JSON.stringify(event.data), - }); + if (socket.readyState === socket.OPEN) { + socket.send(JSON.stringify({ event: event.type, data: event.data })); + } }; sseService.addClient({ id: clientId, send: sendEvent }); - await reply.sse.send({ data: "Connected" }); + socket.send(JSON.stringify({ event: "connected", data: "Connected" })); logInfo(`Connection for client ${clientId} established`); - reply.sse.onClose(() => { + socket.on("close", () => { sseService.removeClient(clientId); logInfo(`Connection for client ${clientId} closed`); });