diff options
Diffstat (limited to 'source/ws.ts')
| -rw-r--r-- | source/ws.ts | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/source/ws.ts b/source/ws.ts new file mode 100644 index 0000000..fdd9fb1 --- /dev/null +++ b/source/ws.ts @@ -0,0 +1,99 @@ + + + +/*** IMPORT ------------------------------------------- ***/ + +import { makeServer, type ServerOptions } from "graphql-ws"; + +/*** UTILITY ------------------------------------------ ***/ + +import type { GraphQLSchema } from "graphql"; + +/*** EXPORT ------------------------------------------- ***/ + +/** + * Configuration accepted by {@link GraphQLWS}. + * + * Wraps `graphql-ws`'s {@link ServerOptions} and requires a `schema`. Anything + * else accepted by `graphql-ws` (custom `execute`, `subscribe`, `onConnect`, + * `onSubscribe`, etc.) can be passed through. + */ +export type WSOptions<Ctx = unknown> = { + /** Builds the context value passed to resolvers. Runs per WS connection. */ + context?: (ctx: { connectionParams?: Record<string, unknown> }) => Ctx | Promise<Ctx>; + /** The executable schema to subscribe against. */ + schema: GraphQLSchema; +} & Partial<Omit<ServerOptions, "schema" | "context">>; + +/** + * Builds a WebSocket subscription handler that speaks the + * `graphql-transport-ws` protocol via `graphql-ws`. + * + * The returned function expects a Fetch `Request` with an `Upgrade: websocket` + * header. It performs the upgrade with `Deno.upgradeWebSocket` and hands the + * socket to `graphql-ws`. Pass the result as `subscriptions` to {@link GraphQLHTTP} + * so HTTP and WS share one endpoint. + * + * @example + * ```ts + * import { executeSchema, GraphQLHTTP, GraphQLWS, PubSub, gql } from "@eol/gq"; + * + * const pubsub = new PubSub(); + * + * const schema = executeSchema({ + * resolvers: { + * Subscription: { + * pinged: { subscribe: () => pubsub.asyncIterator(["PING"]) } + * } + * }, + * typeDefs: gql`type Subscription { pinged: String }` + * }); + * + * const subscriptions = GraphQLWS({ schema }); + * Deno.serve(GraphQLHTTP({ schema, subscriptions })); + * ``` + */ +export function GraphQLWS<Ctx = unknown>(options: WSOptions<Ctx>) { + const server = makeServer({ + ...options, + context: options.context as ServerOptions["context"], + schema: options.schema + }); + + return (request: Request): Response => { + const { response, socket } = Deno.upgradeWebSocket(request, { + protocol: "graphql-transport-ws" + }); + + socket.onopen = () => { + const closed = server.opened( + { + close: (code, reason) => socket.close(code, reason), + onMessage: (cb) => { + socket.onmessage = async (event) => { + try { + await cb( + typeof event.data === "string" ? + event.data : + await (event.data as Blob).text() + ); + } catch (err) { + socket.close(1011, (err as Error).message); + } + }; + }, + protocol: socket.protocol, + send: (data) => Promise.resolve(socket.send(data)) + }, + { + request, + socket + } + ); + + socket.onclose = (event) => closed(event.code, event.reason); + }; + + return response; + }; +} |