Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ws subscriptions for nuxt and h3 (with doc) #2204

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions grafast/grafserv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
86 changes: 31 additions & 55 deletions grafast/grafserv/src/servers/h3/v1/index.ts
Original file line number Diff line number Diff line change
@@ -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';
benjie marked this conversation as resolved.
Show resolved Hide resolved
import type { App, H3Event } from "h3";
import {
createRouter,
Expand All @@ -20,6 +20,7 @@ import {
import {
convertHandlerResultToResult,
GrafservBase,
makeGraphQLWSConfig,
normalizeRequest,
processHeaders,
} from "../../../index.js";
Expand All @@ -30,7 +31,6 @@ import type {
RequestDigest,
Result,
} from "../../../interfaces.js";
import { makeNodeUpgradeHandler } from "../../node/index.js";

declare global {
namespace Grafast {
Expand Down Expand Up @@ -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<Hooks> {
const graphqlWsServer = makeServer(makeGraphQLWSConfig(this));
interface Client {
handleMessage?: (data: string) => Promise<void>;
closed?: (code?: number, reason?: string) => Promise<void>;
}

const clients = new WeakMap<Peer, Client>();
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),
};
}
}

Expand Down
35 changes: 9 additions & 26 deletions grafast/website/grafserv/servers/h3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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()));
benjie marked this conversation as resolved.
Show resolved Hide resolved

// 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);
});
```
101 changes: 12 additions & 89 deletions grafast/website/grafserv/servers/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Dodobibi marked this conversation as resolved.
Show resolved Hide resolved
```

```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({
Dodobibi marked this conversation as resolved.
Show resolved Hide resolved
// Create and export the `/api/graphql` route handler
handler: (event) => serv.handleGraphQLEvent(event),
// Create and export the `/api/graphql` websocket handler
websocket: serv.makeWsHandler(),
});
```
Loading