@effect-gql/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ws.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { Effect, Layer } from "effect"
2
+ import type { IncomingMessage, Server } from "node:http"
3
+ import type { Duplex } from "node:stream"
4
+ import { WebSocket, WebSocketServer } from "ws"
5
+ import { GraphQLSchema } from "graphql"
6
+ import {
7
+ makeGraphQLWSHandler,
8
+ toEffectWebSocketFromWs,
9
+ type EffectWebSocket,
10
+ type GraphQLWSOptions,
11
+ } from "@effect-gql/core"
12
+
13
+ /**
14
+ * Options for Node.js WebSocket server
15
+ */
16
+ export interface NodeWSOptions<R> extends GraphQLWSOptions<R> {
17
+ /**
18
+ * Path for WebSocket connections.
19
+ * @default "/graphql"
20
+ */
21
+ readonly path?: string
22
+ }
23
+
24
+ /**
25
+ * Convert a Node.js WebSocket (from 'ws' library) to an EffectWebSocket.
26
+ *
27
+ * This creates an Effect-based wrapper around the ws WebSocket instance,
28
+ * providing a Stream for incoming messages and Effect-based send/close operations.
29
+ *
30
+ * @param ws - The WebSocket instance from the 'ws' library
31
+ * @returns An EffectWebSocket that can be used with makeGraphQLWSHandler
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * wss.on("connection", (ws, req) => {
36
+ * const effectSocket = toEffectWebSocket(ws)
37
+ * Effect.runPromise(handler(effectSocket))
38
+ * })
39
+ * ```
40
+ */
41
+ export const toEffectWebSocket = (ws: WebSocket): EffectWebSocket => toEffectWebSocketFromWs(ws)
42
+
43
+ /**
44
+ * Create a WebSocket server that handles GraphQL subscriptions.
45
+ *
46
+ * This function creates a WebSocketServer and returns utilities for
47
+ * integrating it with an HTTP server via the upgrade event.
48
+ *
49
+ * @param schema - The GraphQL schema with subscription definitions
50
+ * @param layer - Effect layer providing services required by resolvers
51
+ * @param options - Optional configuration and lifecycle hooks
52
+ * @returns Object containing the WebSocketServer and handlers
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const httpServer = createServer(requestHandler)
57
+ * const { wss, handleUpgrade } = createGraphQLWSServer(schema, serviceLayer)
58
+ *
59
+ * httpServer.on("upgrade", (request, socket, head) => {
60
+ * if (request.url === "/graphql") {
61
+ * handleUpgrade(request, socket, head)
62
+ * }
63
+ * })
64
+ *
65
+ * httpServer.listen(4000)
66
+ * ```
67
+ */
68
+ export const createGraphQLWSServer = <R>(
69
+ schema: GraphQLSchema,
70
+ layer: Layer.Layer<R>,
71
+ options?: NodeWSOptions<R>
72
+ ): {
73
+ /** The underlying WebSocketServer instance */
74
+ wss: WebSocketServer
75
+ /** Handle HTTP upgrade requests */
76
+ handleUpgrade: (request: IncomingMessage, socket: Duplex, head: Buffer) => void
77
+ /** Close the WebSocket server */
78
+ close: () => Promise<void>
79
+ } => {
80
+ const wss = new WebSocketServer({ noServer: true })
81
+ const path = options?.path ?? "/graphql"
82
+
83
+ // Create the handler from core
84
+ const handler = makeGraphQLWSHandler(schema, layer, options)
85
+
86
+ // Track active connections for cleanup
87
+ const activeConnections = new Set<WebSocket>()
88
+
89
+ wss.on("connection", (ws, _request) => {
90
+ activeConnections.add(ws)
91
+
92
+ const effectSocket = toEffectWebSocket(ws)
93
+
94
+ // Run the handler
95
+ Effect.runPromise(
96
+ handler(effectSocket).pipe(
97
+ Effect.catchAll((error) => Effect.logError("GraphQL WebSocket handler error", error))
98
+ )
99
+ ).finally(() => {
100
+ activeConnections.delete(ws)
101
+ })
102
+ })
103
+
104
+ const handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => {
105
+ // Check if this is the GraphQL WebSocket path
106
+ const url = new URL(request.url ?? "/", `http://${request.headers.host}`)
107
+ if (url.pathname !== path) {
108
+ socket.destroy()
109
+ return
110
+ }
111
+
112
+ // Check for correct WebSocket subprotocol
113
+ const protocol = request.headers["sec-websocket-protocol"]
114
+ if (!protocol?.includes("graphql-transport-ws")) {
115
+ socket.write("HTTP/1.1 400 Bad Request\r\n\r\n")
116
+ socket.destroy()
117
+ return
118
+ }
119
+
120
+ wss.handleUpgrade(request, socket, head, (ws) => {
121
+ wss.emit("connection", ws, request)
122
+ })
123
+ }
124
+
125
+ const close = async () => {
126
+ // Close all active connections
127
+ for (const ws of activeConnections) {
128
+ ws.close(1001, "Server shutting down")
129
+ }
130
+ activeConnections.clear()
131
+
132
+ // Close the WebSocket server
133
+ return new Promise<void>((resolve, reject) => {
134
+ wss.close((error) => {
135
+ if (error) reject(error)
136
+ else resolve()
137
+ })
138
+ })
139
+ }
140
+
141
+ return { wss, handleUpgrade, close }
142
+ }
143
+
144
+ /**
145
+ * Attach WebSocket subscription support to an existing HTTP server.
146
+ *
147
+ * This is a convenience function that creates a GraphQL WebSocket server
148
+ * and attaches it to an HTTP server's upgrade event.
149
+ *
150
+ * @param server - The HTTP server to attach to
151
+ * @param schema - The GraphQL schema with subscription definitions
152
+ * @param layer - Effect layer providing services required by resolvers
153
+ * @param options - Optional configuration and lifecycle hooks
154
+ * @returns Cleanup function to close the WebSocket server
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const httpServer = createServer(requestHandler)
159
+ *
160
+ * const cleanup = attachWebSocketToServer(httpServer, schema, serviceLayer, {
161
+ * path: "/graphql",
162
+ * })
163
+ *
164
+ * httpServer.listen(4000)
165
+ *
166
+ * // Later, to cleanup:
167
+ * await cleanup()
168
+ * ```
169
+ */
170
+ export const attachWebSocketToServer = <R>(
171
+ server: Server,
172
+ schema: GraphQLSchema,
173
+ layer: Layer.Layer<R>,
174
+ options?: NodeWSOptions<R>
175
+ ): { close: () => Promise<void> } => {
176
+ const { handleUpgrade, close } = createGraphQLWSServer(schema, layer, options)
177
+
178
+ server.on("upgrade", (request, socket, head) => {
179
+ handleUpgrade(request, socket as Duplex, head)
180
+ })
181
+
182
+ return { close }
183
+ }