From ae7e5351ab878288c11d20fddc882ebb076fe0b3 Mon Sep 17 00:00:00 2001 From: Dominique BILLET Date: Fri, 4 Oct 2024 11:15:49 +0200 Subject: [PATCH 01/11] Support ws subscriptions for nuxt and h3 (with doc) --- grafast/grafserv/package.json | 4 +- grafast/grafserv/src/servers/h3/v1/index.ts | 86 ++++++----------- grafast/website/grafserv/servers/h3.md | 35 ++----- grafast/website/grafserv/servers/nuxt.md | 101 +++----------------- 4 files changed, 54 insertions(+), 172 deletions(-) diff --git a/grafast/grafserv/package.json b/grafast/grafserv/package.json index bc5efc178c..9e0e8ab25b 100644 --- a/grafast/grafserv/package.json +++ b/grafast/grafserv/package.json @@ -93,7 +93,7 @@ "grafast": "workspace:^", "graphile-config": "workspace:^", "graphql": "^16.1.0-experimental-stream-defer.6", - "h3": "^1.7.1", + "h3": "^1.13.0", "ws": "^8.12.1" }, "peerDependenciesMeta": { @@ -119,7 +119,7 @@ "fastify": "^4.22.1", "grafast": "workspace:^", "graphql-http": "^1.22.0", - "h3": "^1.8.1", + "h3": "^1.13.0", "jest": "^29.6.4", "jest-serializer-graphql-schema": "workspace:^", "koa": "^2.14.2", diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index f2c795388c..7e4e4e9be9 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -1,8 +1,8 @@ -import type { IncomingMessage, Server as HTTPServer } from "node:http"; -import type { Server as HTTPSServer } from "node:https"; -import type { Duplex } from "node:stream"; import { PassThrough } from "node:stream"; +//@ts-expect-error type imports. +import type { Hooks, Peer } from 'crossws'; +import { GRAPHQL_TRANSPORT_WS_PROTOCOL, makeServer } from 'graphql-ws'; import type { App, H3Event } from "h3"; import { createRouter, @@ -20,6 +20,7 @@ import { import { convertHandlerResultToResult, GrafservBase, + makeGraphQLWSConfig, normalizeRequest, processHeaders, } from "../../../index.js"; @@ -30,7 +31,6 @@ import type { RequestDigest, Result, } from "../../../interfaces.js"; -import { makeNodeUpgradeHandler } from "../../node/index.js"; declare global { namespace Grafast { @@ -242,58 +242,34 @@ export class H3Grafserv extends GrafservBase { } } - async getUpgradeHandler_experimental() { - if (this.resolvedPreset.grafserv?.websockets) { - return makeNodeUpgradeHandler(this); - } else { - return null; - } - } - - shouldHandleUpgrade_experimental( - req: IncomingMessage, - _socket: Duplex, - _head: Buffer, - ) { - const fullUrl = req.url; - if (!fullUrl) { - return false; - } - const q = fullUrl.indexOf("?"); - const url = q >= 0 ? fullUrl.substring(0, q) : fullUrl; - const graphqlPath = this.dynamicOptions.graphqlPath; - return url === graphqlPath; - } - - public async addTo_experimental( - app: App, - server: HTTPServer | HTTPSServer | undefined, - addExclusiveWebsocketHandler = true, - ) { - this.addTo(app); - - if (addExclusiveWebsocketHandler && server) { - await this.attachWebsocketsToServer_experimental(server); - } - } - - public async attachWebsocketsToServer_experimental( - server: HTTPServer | HTTPSServer, - ) { - const grafservUpgradeHandler = await this.getUpgradeHandler_experimental(); - if (grafservUpgradeHandler) { - const upgrade = (req: IncomingMessage, socket: Duplex, head: Buffer) => { - if (this.shouldHandleUpgrade_experimental(req, socket, head)) { - grafservUpgradeHandler(req, socket, head); - } else { - socket.destroy(); - } - }; - server.on("upgrade", upgrade); - this.onRelease(() => { - server.off("upgrade", upgrade); - }); + public makeWsHandler(): Partial { + const graphqlWsServer = makeServer(makeGraphQLWSConfig(this)); + interface Client { + handleMessage?: (data: string) => Promise; + closed?: (code?: number, reason?: string) => Promise; } + + const clients = new WeakMap(); + return { + open: async (peer) => { + const client: Client = {}; + //@ts-expect-error Close code and reason are optional for close (https://github.com/enisdenjo/graphql-ws/pull/573) + client.closed = graphqlWsServer.opened( + { + protocol: peer.websocket.protocol ?? GRAPHQL_TRANSPORT_WS_PROTOCOL, // will be validated + send: (data) => { + peer.send(data); + }, + close: (code, reason) => peer.close(code, reason), // there are protocol standard closures + onMessage: (cb) => (client.handleMessage = cb), + }, + { socket: peer.websocket, request: peer.request } + ); + clients.set(peer, client); + }, + message: (peer, message) => clients.get(peer)?.handleMessage?.(message.text()), + close: (peer, details) => clients.get(peer)?.closed?.(details.code, details.reason), + }; } } diff --git a/grafast/website/grafserv/servers/h3.md b/grafast/website/grafserv/servers/h3.md index e88a463452..06c8304dc2 100644 --- a/grafast/website/grafserv/servers/h3.md +++ b/grafast/website/grafserv/servers/h3.md @@ -2,7 +2,12 @@ ```ts import { createServer } from "node:http"; -import { createApp, eventHandler, toNodeListener } from "h3"; +import { + createApp, + defineWebSocketHandler, + eventHandler, + toNodeListener, +} from "h3"; import { grafserv } from "grafserv/h3/v1"; import preset from "./graphile.config"; import schema from "./schema.mjs"; @@ -26,31 +31,9 @@ serv.addTo(app).catch((e) => { process.exit(1); }); +// If you need websocket subscriptions (need h3@^1.13.0); +app.use("/path_to_ws_handler", defineWebSocketHandler(serv.makeWsHandler())); + // Start the server server.listen(preset.grafserv?.port ?? 5678); ``` - -# Experimental - -## Websocket support - -h3 does not yet support WebSocket. - -An unofficial and experimental workaround consists to replace - -```ts -serv.addTo(app).catch((e) => { - console.error(e); - process.exit(1); -}); -``` - -with - -```ts -// this method register directly `server.on('upgrade', ...)` for handling websockets by postgraphile -serv.addTo_experimental(app, server).catch((e) => { - console.error(e); - process.exit(1); -}); -``` diff --git a/grafast/website/grafserv/servers/nuxt.md b/grafast/website/grafserv/servers/nuxt.md index 6e90a08bb5..464c545378 100644 --- a/grafast/website/grafserv/servers/nuxt.md +++ b/grafast/website/grafserv/servers/nuxt.md @@ -40,101 +40,24 @@ import { serv } from "@/server/grafserv/serv"; export default eventHandler((event) => serv.handleEventStreamEvent(event)); ``` -# Experimental - -## Websocket support - -Nitro and h3 does not yet support WebSocket. - -An unofficial and experimental workaround consists to create a nuxt module: - -```ts title="modules/grafserv/index.ts" -// nuxt auto-register modules located in `modules/*.ts` or `modules/*/index.ts` - -import { defineNuxtModule, addServerPlugin } from "@nuxt/kit"; - -import httpProxy from "http-proxy"; - -export default defineNuxtModule({ - async setup(options, nuxt) { - /** - * Register websockets in DEVELOPMENT mode. - */ - if (nuxt.options.dev) { - // hook called in development only - nuxt.hook("listen", (devServer) => { - // create a proxy for routing ws to runtime server created in dev plugin - const proxy = httpProxy.createProxy({ - target: { - host: "localhost", - port: 3100, - }, - }); - // registering ws on devServer - devServer.on("upgrade", (req, socket, head) => { - // routing ws by path - switch (req.url) { - case "/api/graphql": - // proxy websocket to runtime server created in dev plugin - proxy.ws(req, socket, head); - break; - default: - socket.destroy(); - } - }); - }); - // Registering runtime plugin for dev - addServerPlugin("@/modules/grafserv/ws-dev"); - } - - /** - * Register websockets in PRODUCTION mode. - */ - if (!nuxt.options.dev) - // Registering runtime plugin for production - addServerPlugin("@/modules/grafserv/ws"); - }, -}); -``` - -and two Nitro plugins (one for dev, and one for prod) +## Websocket support (need h3@^1.13.0) -```ts title="modules/grafserv/ws-dev.ts" -import { Server } from "http"; +```ts title="server/api/graphql-ws.ts" import { serv } from "@/server/grafserv/serv"; -// plugin running in DEVELOPMENT (runtime) -export default defineNitroPlugin(async (nitroApp) => { - // Create a server for handling websockets - const server = new Server().listen({ port: 3100 }, () => - console.log("Runtime server listening on port 3100"), - ); - // Cleanly close server (on leave or HMR before plugin reload) - nitroApp.hooks.hookOnce("close", () => { - server.closeAllConnections(); - server.close((err) => - err - ? console.warn("Runtime server wrongly closed", err) - : console.log("Runtime server stopped"), - ); - }); - // Attaching ws to server - serv.attachWebsocketsToServer_experimental(server); -}); +// Create and export the `/api/graphql` websocket handler +export default defineWebSocketHandler(serv.makeWsHandler()); ``` -```ts title="modules/grafserv/ws.ts" +If the ws endpoint and the graphql endpoint share the same path: + +```ts title="server/api/graphql.ts" import { serv } from "@/server/grafserv/serv"; -// plugin running in PRODUCTION (runtime) -export default defineNitroPlugin(async (nitroApp) => { - // this hook will be called only once at first http request - nitroApp.hooks.hookOnce("request", (event: H3Event) => { - const server = event.node.req.socket.server; - if (server) { - // Attaching ws to server - serv.attachWebsocketsToServer_experimental(server); - } - }); +export default eventHandler({ + // Create and export the `/api/graphql` route handler + handler: (event) => serv.handleGraphQLEvent(event), + // Create and export the `/api/graphql` websocket handler + websocket: serv.makeWsHandler(), }); ``` From d9ca7f723e9fc7ac6c6812917b56346fcfda3622 Mon Sep 17 00:00:00 2001 From: Dominique BILLET Date: Fri, 4 Oct 2024 15:10:55 +0200 Subject: [PATCH 02/11] delegate the ws mount in `addTo` function for h3 server. --- grafast/grafserv/src/servers/h3/v1/index.ts | 20 +++- grafast/website/grafserv/servers/h3.md | 3 - yarn.lock | 124 +++++++++++--------- 3 files changed, 85 insertions(+), 62 deletions(-) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index 7e4e4e9be9..0860be9261 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -1,11 +1,12 @@ import { PassThrough } from "node:stream"; //@ts-expect-error type imports. -import type { Hooks, Peer } from 'crossws'; -import { GRAPHQL_TRANSPORT_WS_PROTOCOL, makeServer } from 'graphql-ws'; +import type { Hooks, Peer } from "crossws"; +import { GRAPHQL_TRANSPORT_WS_PROTOCOL, makeServer } from "graphql-ws"; import type { App, H3Event } from "h3"; import { createRouter, + defineWebSocketHandler, eventHandler, getQuery, getRequestHeaders, @@ -227,6 +228,11 @@ export class H3Grafserv extends GrafservBase { : ["post"], ); + app.use( + this.dynamicOptions.graphqlPath, + defineWebSocketHandler(this.makeWsHandler()), + ); + if (dynamicOptions.graphiql) { router.get( this.dynamicOptions.graphiqlPath, @@ -248,7 +254,7 @@ export class H3Grafserv extends GrafservBase { handleMessage?: (data: string) => Promise; closed?: (code?: number, reason?: string) => Promise; } - + const clients = new WeakMap(); return { open: async (peer) => { @@ -263,12 +269,14 @@ export class H3Grafserv extends GrafservBase { close: (code, reason) => peer.close(code, reason), // there are protocol standard closures onMessage: (cb) => (client.handleMessage = cb), }, - { socket: peer.websocket, request: peer.request } + { socket: peer.websocket, request: peer.request }, ); clients.set(peer, client); }, - message: (peer, message) => clients.get(peer)?.handleMessage?.(message.text()), - close: (peer, details) => clients.get(peer)?.closed?.(details.code, details.reason), + message: (peer, message) => + clients.get(peer)?.handleMessage?.(message.text()), + close: (peer, details) => + clients.get(peer)?.closed?.(details.code, details.reason), }; } } diff --git a/grafast/website/grafserv/servers/h3.md b/grafast/website/grafserv/servers/h3.md index 06c8304dc2..9d18a0ba80 100644 --- a/grafast/website/grafserv/servers/h3.md +++ b/grafast/website/grafserv/servers/h3.md @@ -31,9 +31,6 @@ serv.addTo(app).catch((e) => { process.exit(1); }); -// If you need websocket subscriptions (need h3@^1.13.0); -app.use("/path_to_ws_handler", defineWebSocketHandler(serv.makeWsHandler())); - // Start the server server.listen(preset.grafserv?.port ?? 5678); ``` diff --git a/yarn.lock b/yarn.lock index 58d8e78677..565cee09bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8430,10 +8430,10 @@ __metadata: languageName: node linkType: hard -"cookie-es@npm:^1.0.0": - version: 1.0.0 - resolution: "cookie-es@npm:1.0.0" - checksum: 65be48df23a6286c73aac8e8b8e2c8a2bca6c85ba32b42215ca447bebe8ffe752a18d1c46c79700f5cb057ebb4b7bcb7f3114893329dfac484cb52a7551e4c8a +"cookie-es@npm:^1.2.2": + version: 1.2.2 + resolution: "cookie-es@npm:1.2.2" + checksum: 8318f7251c43835744cf5421fe5cdb6f92695d0a6234d39af616ff2b692d3c05019517c8b56afe726ff8d6a2462c35247d6c6d1db18dbf2816cba2150036ce6e languageName: node linkType: hard @@ -8699,6 +8699,15 @@ __metadata: languageName: node linkType: hard +"crossws@npm:>=0.2.0 <0.4.0": + version: 0.3.1 + resolution: "crossws@npm:0.3.1" + dependencies: + uncrypto: "npm:^0.1.3" + checksum: d6551e6037ca337e1ee32331d13c793ff28ef54080402e081dc7461f5f9dcb4eb5143d2729b847cebbefc477578bc433798aa83a133d81d8c551390a1c3fab89 + languageName: node + linkType: hard + "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0" @@ -9591,10 +9600,10 @@ __metadata: languageName: node linkType: hard -"defu@npm:^6.1.2": - version: 6.1.2 - resolution: "defu@npm:6.1.2" - checksum: edaea3395b6d39c6ec724600d257c7f49176044c5a14dbe3870197347079ca162e1155425b3612d1162966db3bad1e453c4f4ca5cf259efaf29ccb5045829d2e +"defu@npm:^6.1.4": + version: 6.1.4 + resolution: "defu@npm:6.1.4" + checksum: 81c57b057389c0a8f99267c84a4e93d296a266a72e6ef0ac8288b5947444bdf3e450672a56e767d91d6be7efd7f2412b278087cc88f936a6ef9fdffd76d12f34 languageName: node linkType: hard @@ -9658,10 +9667,10 @@ __metadata: languageName: node linkType: hard -"destr@npm:^2.0.1": - version: 2.0.1 - resolution: "destr@npm:2.0.1" - checksum: 115d03532278d70350591e31bda4a6ca3031782d2c92d665e11b3f3d22602c8c0e7a4caa76f624f7124b3f9f76b75adabe7b87c75b7192e8af6a6166b52a26f4 +"destr@npm:^2.0.3": + version: 2.0.3 + resolution: "destr@npm:2.0.3" + checksum: 7f6367774e3d0364551d325f21f4a04c784a62a870ba80ee06b8cdf72a593972dc26439ddfe56c76000d3360f3a508e1e7f027b70e6400863b03ab9a81e3713d languageName: node linkType: hard @@ -11973,7 +11982,7 @@ __metadata: graphile-config: "workspace:^" graphql-http: "npm:^1.22.0" graphql-ws: "npm:^5.14.0" - h3: "npm:^1.8.1" + h3: "npm:^1.13.0" jest: "npm:^29.6.4" jest-serializer-graphql-schema: "workspace:^" koa: "npm:^2.14.2" @@ -11988,7 +11997,7 @@ __metadata: grafast: "workspace:^" graphile-config: "workspace:^" graphql: ^16.1.0-experimental-stream-defer.6 - h3: ^1.7.1 + h3: ^1.13.0 ws: ^8.12.1 peerDependenciesMeta: "@envelop/core": @@ -12327,19 +12336,21 @@ __metadata: languageName: node linkType: hard -"h3@npm:^1.8.1": - version: 1.8.1 - resolution: "h3@npm:1.8.1" - dependencies: - cookie-es: "npm:^1.0.0" - defu: "npm:^6.1.2" - destr: "npm:^2.0.1" - iron-webcrypto: "npm:^0.8.0" - radix3: "npm:^1.1.0" - ufo: "npm:^1.3.0" +"h3@npm:^1.13.0": + version: 1.13.0 + resolution: "h3@npm:1.13.0" + dependencies: + cookie-es: "npm:^1.2.2" + crossws: "npm:>=0.2.0 <0.4.0" + defu: "npm:^6.1.4" + destr: "npm:^2.0.3" + iron-webcrypto: "npm:^1.2.1" + ohash: "npm:^1.1.4" + radix3: "npm:^1.1.2" + ufo: "npm:^1.5.4" uncrypto: "npm:^0.1.3" - unenv: "npm:^1.7.4" - checksum: 79339b4f1e3adb239a9e75c2534d49848c69575cdb64e7794e5fdc95f14c4869515b618032cc78687e0693cbae874e1d86cad096142a25764aad6a1805e55115 + unenv: "npm:^1.10.0" + checksum: 87bc9d10eff6c1fece20f1f3616828be429e2a51e14855a066093000b6557f08c698768d39a68ac15e869a2f214f7a708c7cad1f8fdc70551b4ee48c70757e5d languageName: node linkType: hard @@ -13097,10 +13108,10 @@ __metadata: languageName: node linkType: hard -"iron-webcrypto@npm:^0.8.0": - version: 0.8.2 - resolution: "iron-webcrypto@npm:0.8.2" - checksum: 8b345cf37e80574abbe48cdeb96ae3949fb757adbe862e9feade36ee3aa45cb9a5cc1b8a462fd9366250dacdadd223c5a1d57b32603afaf8775534e617f5e377 +"iron-webcrypto@npm:^1.2.1": + version: 1.2.1 + resolution: "iron-webcrypto@npm:1.2.1" + checksum: 3eef80d46c3d9bf51bdd267b98f3b65fbd3e69c4da1d0811fe9332497e9053e5a5cf7965e3c8cf9e008487833aed219e919aa377b0122686d32685640f64f39b languageName: node linkType: hard @@ -16143,10 +16154,10 @@ __metadata: languageName: node linkType: hard -"node-fetch-native@npm:^1.4.0": - version: 1.4.0 - resolution: "node-fetch-native@npm:1.4.0" - checksum: 1cb840bd1341060aa8fcd0a3b9d445b539b045451886f99e1ed5a0ab8acfafa88d9cc885db0b1363bb8818dea11dcbe2a10a9bc286bf00f0dcdff44773f3f92c +"node-fetch-native@npm:^1.6.4": + version: 1.6.4 + resolution: "node-fetch-native@npm:1.6.4" + checksum: d37c8fbb30f31eac6fa941082bcaacc4047c79114d32ff2242e1c1ff3c7f622cd314d9a69a91065c45259f3d86d089047b0ee78cdc132898b7b23b2f52b2d5de languageName: node linkType: hard @@ -16513,6 +16524,13 @@ __metadata: languageName: node linkType: hard +"ohash@npm:^1.1.4": + version: 1.1.4 + resolution: "ohash@npm:1.1.4" + checksum: f9c2b2dd9498e41f79e170bc6b6d630f31888239941d6057364327b045f390b91ef37651fbcc1e6213461d0abfd0274d15602a97104508db89672e5014d86dd7 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.0 resolution: "on-exit-leak-free@npm:2.1.0" @@ -16908,10 +16926,10 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.1": - version: 1.1.1 - resolution: "pathe@npm:1.1.1" - checksum: 8bca7eccd68b0076cbcffdc74490cce9515ec88e6d9ba94860a7766a03345170d3d1b36ca43083960dfbd2aa59f9dba0a07e2a27075818da7f19b1cce2985f47 +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 3ee799ed8a7515cbb180a619e5533443a7428607804e5bb06a5494b241dd38ae4c0b32df35bb69dce014e3d4bc31f62ff12f2d7506ad12e3c497762ffbfd0cca languageName: node linkType: hard @@ -18164,10 +18182,10 @@ __metadata: languageName: node linkType: hard -"radix3@npm:^1.1.0": - version: 1.1.0 - resolution: "radix3@npm:1.1.0" - checksum: e0427bcfed1adc041b5244a6260cb10d17c78fe174ed6acc92925f010e799fb2135c9bc7d15d31fb3df309be705d6334576a43e7e3d4c777eaddcd3bbc27ce89 +"radix3@npm:^1.1.2": + version: 1.1.2 + resolution: "radix3@npm:1.1.2" + checksum: 9644282fb1549548e28fb475bfcdd366c38ab6231f319715108c7dbe056540b0c95c0eadbd8cf4ba11e9466d236a41588c40f5b776524c60ac36a17d9c9bb83f languageName: node linkType: hard @@ -21173,10 +21191,10 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.3.0": - version: 1.3.0 - resolution: "ufo@npm:1.3.0" - checksum: fdc3368988816f6727060281eb0a01dbc341f031e4d52f9f43add6f54237cc6fcdddc03a958dbba75057f2d21847f56023dcce58c994b88e0a68f05610132d19 +"ufo@npm:^1.5.4": + version: 1.5.4 + resolution: "ufo@npm:1.5.4" + checksum: af0457d95f443bb92f0be1118b7831c3c18f59fb56aeb3873fc914f70b65981a18bb3834b54f2879cae254dd40cc287dc63e2a0c3f9ea37b55c734b3614e9358 languageName: node linkType: hard @@ -21216,16 +21234,16 @@ __metadata: languageName: node linkType: hard -"unenv@npm:^1.7.4": - version: 1.7.4 - resolution: "unenv@npm:1.7.4" +"unenv@npm:^1.10.0": + version: 1.10.0 + resolution: "unenv@npm:1.10.0" dependencies: consola: "npm:^3.2.3" - defu: "npm:^6.1.2" + defu: "npm:^6.1.4" mime: "npm:^3.0.0" - node-fetch-native: "npm:^1.4.0" - pathe: "npm:^1.1.1" - checksum: 09d2d5bad1dbeefb4a4acd368ef607928bd81d11b405129cae8f2cf185877b311c3f4eb44f609534071df40958ed81cc57635d63a69bb7bb133d2ffaa703bc62 + node-fetch-native: "npm:^1.6.4" + pathe: "npm:^1.1.2" + checksum: 4980e55fb92e7309deceeba81a6e2ced2360cc53803e58821248f40a9776b467fd8a81c305535356b1b1f6c68bcde32835058c091eaee5e9b9037a6cb69cc8bc languageName: node linkType: hard From 485b95c9b8681fafaacff52bb4715447fd2d64ae Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 16:58:34 +0100 Subject: [PATCH 03/11] Only expose websockets if enabled --- grafast/grafserv/src/servers/h3/v1/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index 0860be9261..2110aea69f 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -228,10 +228,12 @@ export class H3Grafserv extends GrafservBase { : ["post"], ); - app.use( - this.dynamicOptions.graphqlPath, - defineWebSocketHandler(this.makeWsHandler()), - ); + if (this.resolvedPreset.grafserv?.websockets) { + app.use( + this.dynamicOptions.graphqlPath, + defineWebSocketHandler(this.makeWsHandler()), + ); + } if (dynamicOptions.graphiql) { router.get( From e1d530da13ef16d4af00aea1709b4473eef63cff Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 16:59:47 +0100 Subject: [PATCH 04/11] Refactor syntax --- grafast/grafserv/src/servers/h3/v1/index.ts | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index 2110aea69f..dc465113c8 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -259,26 +259,32 @@ export class H3Grafserv extends GrafservBase { const clients = new WeakMap(); return { - open: async (peer) => { + open(peer) { const client: Client = {}; //@ts-expect-error Close code and reason are optional for close (https://github.com/enisdenjo/graphql-ws/pull/573) client.closed = graphqlWsServer.opened( { protocol: peer.websocket.protocol ?? GRAPHQL_TRANSPORT_WS_PROTOCOL, // will be validated - send: (data) => { + send(data) { peer.send(data); }, - close: (code, reason) => peer.close(code, reason), // there are protocol standard closures - onMessage: (cb) => (client.handleMessage = cb), + close(code, reason) { + peer.close(code, reason); // there are protocol standard closures + }, + onMessage(cb) { + client.handleMessage = cb; + }, }, { socket: peer.websocket, request: peer.request }, ); clients.set(peer, client); }, - message: (peer, message) => - clients.get(peer)?.handleMessage?.(message.text()), - close: (peer, details) => - clients.get(peer)?.closed?.(details.code, details.reason), + message(peer, message) { + clients.get(peer)?.handleMessage?.(message.text()); + }, + close(peer, details) { + clients.get(peer)?.closed?.(details.code, details.reason); + }, }; } } From aad0d9a7863179ab5333425928103e30b7897f5d Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 17:01:29 +0100 Subject: [PATCH 05/11] Refactor --- grafast/grafserv/src/servers/h3/v1/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index dc465113c8..fda0a86617 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -261,8 +261,8 @@ export class H3Grafserv extends GrafservBase { return { open(peer) { const client: Client = {}; - //@ts-expect-error Close code and reason are optional for close (https://github.com/enisdenjo/graphql-ws/pull/573) - client.closed = graphqlWsServer.opened( + clients.set(peer, client); + const onClose = graphqlWsServer.opened( { protocol: peer.websocket.protocol ?? GRAPHQL_TRANSPORT_WS_PROTOCOL, // will be validated send(data) { @@ -277,7 +277,10 @@ export class H3Grafserv extends GrafservBase { }, { socket: peer.websocket, request: peer.request }, ); - clients.set(peer, client); + client.closed = async (code, reason) => { + // @ts-expect-error fixed in unreleased https://github.com/enisdenjo/graphql-ws/pull/573 + onClose(code, reason); + }; }, message(peer, message) { clients.get(peer)?.handleMessage?.(message.text()); From faadf96a163f1c24b40bb2cce2b2845e5198b331 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 17:01:45 +0100 Subject: [PATCH 06/11] Avoid memory leak? --- grafast/grafserv/src/servers/h3/v1/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index fda0a86617..814540285a 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -287,6 +287,7 @@ export class H3Grafserv extends GrafservBase { }, close(peer, details) { clients.get(peer)?.closed?.(details.code, details.reason); + clients.delete(peer); }, }; } From ac29090263d179751d91507a9faeb8a8aebefd2f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 17:01:55 +0100 Subject: [PATCH 07/11] Do we need an error handler? --- grafast/grafserv/src/servers/h3/v1/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index 814540285a..68c8c7a8a8 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -289,6 +289,9 @@ export class H3Grafserv extends GrafservBase { clients.get(peer)?.closed?.(details.code, details.reason); clients.delete(peer); }, + error(peer, error) { + // clients.delete(peer)? + }, }; } } From ec1324f6ba74fe922b7df9e984b5e84e75cded79 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 17:02:05 +0100 Subject: [PATCH 08/11] Remove unused import --- grafast/website/grafserv/servers/h3.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/grafast/website/grafserv/servers/h3.md b/grafast/website/grafserv/servers/h3.md index 9d18a0ba80..2e32b1779d 100644 --- a/grafast/website/grafserv/servers/h3.md +++ b/grafast/website/grafserv/servers/h3.md @@ -2,12 +2,7 @@ ```ts import { createServer } from "node:http"; -import { - createApp, - defineWebSocketHandler, - eventHandler, - toNodeListener, -} from "h3"; +import { createApp, eventHandler, toNodeListener } from "h3"; import { grafserv } from "grafserv/h3/v1"; import preset from "./graphile.config"; import schema from "./schema.mjs"; From 468da1f13380e77ca0fc32d220314e1d3daf564b Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 16 Oct 2024 17:02:12 +0100 Subject: [PATCH 09/11] Add missing import --- grafast/website/grafserv/servers/nuxt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/grafast/website/grafserv/servers/nuxt.md b/grafast/website/grafserv/servers/nuxt.md index 464c545378..57bfc061cf 100644 --- a/grafast/website/grafserv/servers/nuxt.md +++ b/grafast/website/grafserv/servers/nuxt.md @@ -43,6 +43,7 @@ export default eventHandler((event) => serv.handleEventStreamEvent(event)); ## Websocket support (need h3@^1.13.0) ```ts title="server/api/graphql-ws.ts" +import { defineWebSocketHandler } from "h3"; import { serv } from "@/server/grafserv/serv"; // Create and export the `/api/graphql` websocket handler From 9c96c2637330dcfee3af92ee73e588abe5d57cb0 Mon Sep 17 00:00:00 2001 From: Dominique BILLET Date: Thu, 17 Oct 2024 14:41:44 +0200 Subject: [PATCH 10/11] Add missing import & reordering (doc) --- grafast/website/grafserv/servers/nuxt.md | 48 ++++++++++++------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/grafast/website/grafserv/servers/nuxt.md b/grafast/website/grafserv/servers/nuxt.md index 57bfc061cf..5f57c25fdd 100644 --- a/grafast/website/grafserv/servers/nuxt.md +++ b/grafast/website/grafserv/servers/nuxt.md @@ -17,48 +17,50 @@ import schema from "./schema.mjs"; export const serv = grafserv({ schema, preset }); ``` -and the API routes : +## API routes + +### Graphql endpoint + +_without websockets_ ```ts title="server/api/graphql.ts" +import { eventHandler } from "h3"; import { serv } from "@/server/grafserv/serv"; // Create and export the `/api/graphql` route handler export default eventHandler((event) => serv.handleGraphQLEvent(event)); ``` -```ts title="pages/routes/ruru.ts" -import { serv } from "@/server/grafserv/serv"; - -// Create and export the `/ruru` route handler -export default eventHandler((event) => serv.handleGraphiqlEvent(event)); -``` +_or with websockets enabled_ (need h3@^1.13.0): -```ts title="pages/api/graphql/stream.ts" +```ts title="server/api/graphql.ts" +import { eventHandler } from "h3"; import { serv } from "@/server/grafserv/serv"; -// Create and export the `/api/graphql/stream` route handler -export default eventHandler((event) => serv.handleEventStreamEvent(event)); +export default eventHandler({ + // Create and export the `/api/graphql` route handler + handler: (event) => serv.handleGraphQLEvent(event), + // Create and export the `/api/graphql` websocket handler + websocket: serv.makeWsHandler(), +}); ``` -## Websocket support (need h3@^1.13.0) +### Ruru endpoint -```ts title="server/api/graphql-ws.ts" -import { defineWebSocketHandler } from "h3"; +```ts title="pages/routes/ruru.ts" +import { eventHandler } from "h3"; import { serv } from "@/server/grafserv/serv"; -// Create and export the `/api/graphql` websocket handler -export default defineWebSocketHandler(serv.makeWsHandler()); +// Create and export the `/ruru` route handler +export default eventHandler((event) => serv.handleGraphiqlEvent(event)); ``` -If the ws endpoint and the graphql endpoint share the same path: +### EventStream endpoint -```ts title="server/api/graphql.ts" +```ts title="pages/api/graphql/stream.ts" +import { eventHandler } from "h3"; import { serv } from "@/server/grafserv/serv"; -export default eventHandler({ - // Create and export the `/api/graphql` route handler - handler: (event) => serv.handleGraphQLEvent(event), - // Create and export the `/api/graphql` websocket handler - websocket: serv.makeWsHandler(), -}); +// Create and export the `/api/graphql/stream` route handler +export default eventHandler((event) => serv.handleEventStreamEvent(event)); ``` From 6f8e921d6797e1bde29103f05e4302d62988c653 Mon Sep 17 00:00:00 2001 From: Dominique BILLET Date: Thu, 17 Oct 2024 14:43:48 +0200 Subject: [PATCH 11/11] Use Map for peer & explicitly delete peer --- grafast/grafserv/src/servers/h3/v1/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grafast/grafserv/src/servers/h3/v1/index.ts b/grafast/grafserv/src/servers/h3/v1/index.ts index 68c8c7a8a8..7ccd5738a7 100644 --- a/grafast/grafserv/src/servers/h3/v1/index.ts +++ b/grafast/grafserv/src/servers/h3/v1/index.ts @@ -257,7 +257,7 @@ export class H3Grafserv extends GrafservBase { closed?: (code?: number, reason?: string) => Promise; } - const clients = new WeakMap(); + const clients = new Map(); return { open(peer) { const client: Client = {}; @@ -289,8 +289,8 @@ export class H3Grafserv extends GrafservBase { clients.get(peer)?.closed?.(details.code, details.reason); clients.delete(peer); }, - error(peer, error) { - // clients.delete(peer)? + error(peer, _error) { + clients.delete(peer); }, }; }