/*** 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 = { /** Builds the context value passed to resolvers. Runs per WS connection. */ context?: (ctx: { connectionParams?: Record }) => Ctx | Promise; /** The executable schema to subscribe against. */ schema: GraphQLSchema; } & Partial>; /** * 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(options: WSOptions): (request: Request) => Response { 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; }; }