@effect-gql/core 0.1.0 → 1.0.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.
Files changed (164) hide show
  1. package/README.md +100 -0
  2. package/builder/index.cjs +1431 -0
  3. package/builder/index.cjs.map +1 -0
  4. package/builder/index.d.cts +259 -0
  5. package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +49 -21
  6. package/builder/index.js +1390 -0
  7. package/builder/index.js.map +1 -0
  8. package/index.cjs +3419 -0
  9. package/index.cjs.map +1 -0
  10. package/index.d.cts +523 -0
  11. package/index.d.ts +523 -0
  12. package/index.js +3242 -0
  13. package/index.js.map +1 -0
  14. package/package.json +19 -28
  15. package/schema-builder-Cvdq7Kz_.d.cts +963 -0
  16. package/schema-builder-Cvdq7Kz_.d.ts +963 -0
  17. package/server/index.cjs +1555 -0
  18. package/server/index.cjs.map +1 -0
  19. package/server/index.d.cts +680 -0
  20. package/server/index.d.ts +680 -0
  21. package/server/index.js +1524 -0
  22. package/server/index.js.map +1 -0
  23. package/dist/analyzer-extension.d.ts +0 -105
  24. package/dist/analyzer-extension.d.ts.map +0 -1
  25. package/dist/analyzer-extension.js +0 -137
  26. package/dist/analyzer-extension.js.map +0 -1
  27. package/dist/builder/execute.d.ts +0 -26
  28. package/dist/builder/execute.d.ts.map +0 -1
  29. package/dist/builder/execute.js +0 -104
  30. package/dist/builder/execute.js.map +0 -1
  31. package/dist/builder/field-builders.d.ts +0 -30
  32. package/dist/builder/field-builders.d.ts.map +0 -1
  33. package/dist/builder/field-builders.js +0 -200
  34. package/dist/builder/field-builders.js.map +0 -1
  35. package/dist/builder/index.d.ts +0 -7
  36. package/dist/builder/index.d.ts.map +0 -1
  37. package/dist/builder/index.js +0 -31
  38. package/dist/builder/index.js.map +0 -1
  39. package/dist/builder/pipe-api.d.ts.map +0 -1
  40. package/dist/builder/pipe-api.js +0 -151
  41. package/dist/builder/pipe-api.js.map +0 -1
  42. package/dist/builder/schema-builder.d.ts +0 -301
  43. package/dist/builder/schema-builder.d.ts.map +0 -1
  44. package/dist/builder/schema-builder.js +0 -566
  45. package/dist/builder/schema-builder.js.map +0 -1
  46. package/dist/builder/type-registry.d.ts +0 -80
  47. package/dist/builder/type-registry.d.ts.map +0 -1
  48. package/dist/builder/type-registry.js +0 -505
  49. package/dist/builder/type-registry.js.map +0 -1
  50. package/dist/builder/types.d.ts +0 -283
  51. package/dist/builder/types.d.ts.map +0 -1
  52. package/dist/builder/types.js +0 -3
  53. package/dist/builder/types.js.map +0 -1
  54. package/dist/cli/generate-schema.d.ts +0 -29
  55. package/dist/cli/generate-schema.d.ts.map +0 -1
  56. package/dist/cli/generate-schema.js +0 -233
  57. package/dist/cli/generate-schema.js.map +0 -1
  58. package/dist/cli/index.d.ts +0 -19
  59. package/dist/cli/index.d.ts.map +0 -1
  60. package/dist/cli/index.js +0 -24
  61. package/dist/cli/index.js.map +0 -1
  62. package/dist/context.d.ts +0 -18
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/context.js +0 -11
  65. package/dist/context.js.map +0 -1
  66. package/dist/error.d.ts +0 -45
  67. package/dist/error.d.ts.map +0 -1
  68. package/dist/error.js +0 -29
  69. package/dist/error.js.map +0 -1
  70. package/dist/extensions.d.ts +0 -130
  71. package/dist/extensions.d.ts.map +0 -1
  72. package/dist/extensions.js +0 -78
  73. package/dist/extensions.js.map +0 -1
  74. package/dist/index.d.ts +0 -12
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js +0 -47
  77. package/dist/index.js.map +0 -1
  78. package/dist/loader.d.ts +0 -169
  79. package/dist/loader.d.ts.map +0 -1
  80. package/dist/loader.js +0 -237
  81. package/dist/loader.js.map +0 -1
  82. package/dist/resolver-context.d.ts +0 -154
  83. package/dist/resolver-context.d.ts.map +0 -1
  84. package/dist/resolver-context.js +0 -184
  85. package/dist/resolver-context.js.map +0 -1
  86. package/dist/schema-mapping.d.ts +0 -30
  87. package/dist/schema-mapping.d.ts.map +0 -1
  88. package/dist/schema-mapping.js +0 -280
  89. package/dist/schema-mapping.js.map +0 -1
  90. package/dist/server/cache-control.d.ts +0 -96
  91. package/dist/server/cache-control.d.ts.map +0 -1
  92. package/dist/server/cache-control.js +0 -308
  93. package/dist/server/cache-control.js.map +0 -1
  94. package/dist/server/complexity.d.ts +0 -165
  95. package/dist/server/complexity.d.ts.map +0 -1
  96. package/dist/server/complexity.js +0 -433
  97. package/dist/server/complexity.js.map +0 -1
  98. package/dist/server/config.d.ts +0 -66
  99. package/dist/server/config.d.ts.map +0 -1
  100. package/dist/server/config.js +0 -104
  101. package/dist/server/config.js.map +0 -1
  102. package/dist/server/graphiql.d.ts +0 -5
  103. package/dist/server/graphiql.d.ts.map +0 -1
  104. package/dist/server/graphiql.js +0 -43
  105. package/dist/server/graphiql.js.map +0 -1
  106. package/dist/server/index.d.ts +0 -18
  107. package/dist/server/index.d.ts.map +0 -1
  108. package/dist/server/index.js +0 -48
  109. package/dist/server/index.js.map +0 -1
  110. package/dist/server/router.d.ts +0 -79
  111. package/dist/server/router.d.ts.map +0 -1
  112. package/dist/server/router.js +0 -232
  113. package/dist/server/router.js.map +0 -1
  114. package/dist/server/schema-builder-extensions.d.ts +0 -42
  115. package/dist/server/schema-builder-extensions.d.ts.map +0 -1
  116. package/dist/server/schema-builder-extensions.js +0 -48
  117. package/dist/server/schema-builder-extensions.js.map +0 -1
  118. package/dist/server/sse-adapter.d.ts +0 -64
  119. package/dist/server/sse-adapter.d.ts.map +0 -1
  120. package/dist/server/sse-adapter.js +0 -227
  121. package/dist/server/sse-adapter.js.map +0 -1
  122. package/dist/server/sse-types.d.ts +0 -192
  123. package/dist/server/sse-types.d.ts.map +0 -1
  124. package/dist/server/sse-types.js +0 -63
  125. package/dist/server/sse-types.js.map +0 -1
  126. package/dist/server/ws-adapter.d.ts +0 -39
  127. package/dist/server/ws-adapter.d.ts.map +0 -1
  128. package/dist/server/ws-adapter.js +0 -247
  129. package/dist/server/ws-adapter.js.map +0 -1
  130. package/dist/server/ws-types.d.ts +0 -169
  131. package/dist/server/ws-types.d.ts.map +0 -1
  132. package/dist/server/ws-types.js +0 -11
  133. package/dist/server/ws-types.js.map +0 -1
  134. package/dist/server/ws-utils.d.ts +0 -42
  135. package/dist/server/ws-utils.d.ts.map +0 -1
  136. package/dist/server/ws-utils.js +0 -99
  137. package/dist/server/ws-utils.js.map +0 -1
  138. package/src/analyzer-extension.ts +0 -254
  139. package/src/builder/execute.ts +0 -153
  140. package/src/builder/field-builders.ts +0 -322
  141. package/src/builder/index.ts +0 -48
  142. package/src/builder/pipe-api.ts +0 -312
  143. package/src/builder/schema-builder.ts +0 -970
  144. package/src/builder/type-registry.ts +0 -670
  145. package/src/builder/types.ts +0 -305
  146. package/src/context.ts +0 -23
  147. package/src/error.ts +0 -32
  148. package/src/extensions.ts +0 -240
  149. package/src/index.ts +0 -32
  150. package/src/loader.ts +0 -363
  151. package/src/resolver-context.ts +0 -253
  152. package/src/schema-mapping.ts +0 -307
  153. package/src/server/cache-control.ts +0 -590
  154. package/src/server/complexity.ts +0 -774
  155. package/src/server/config.ts +0 -174
  156. package/src/server/graphiql.ts +0 -38
  157. package/src/server/index.ts +0 -96
  158. package/src/server/router.ts +0 -432
  159. package/src/server/schema-builder-extensions.ts +0 -51
  160. package/src/server/sse-adapter.ts +0 -327
  161. package/src/server/sse-types.ts +0 -234
  162. package/src/server/ws-adapter.ts +0 -355
  163. package/src/server/ws-types.ts +0 -192
  164. package/src/server/ws-utils.ts +0 -136
@@ -1,355 +0,0 @@
1
- import { Effect, Layer, Runtime, Stream, Queue, Fiber, Deferred } from "effect"
2
- import { GraphQLSchema, subscribe, GraphQLError } from "graphql"
3
- import { makeServer, type ServerOptions } from "graphql-ws"
4
- import type { GraphQLEffectContext } from "../builder/types"
5
- import type {
6
- EffectWebSocket,
7
- GraphQLWSOptions,
8
- ConnectionContext,
9
- CloseEvent,
10
- WebSocketError,
11
- } from "./ws-types"
12
- import { validateComplexity, type FieldComplexityMap } from "./complexity"
13
-
14
- /**
15
- * Extra context passed through graphql-ws.
16
- * This is the `extra` field in graphql-ws Context.
17
- */
18
- interface WSExtra<R> {
19
- socket: EffectWebSocket
20
- runtime: Runtime.Runtime<R>
21
- connectionParams: Record<string, unknown>
22
- }
23
-
24
- /**
25
- * Create a ConnectionContext from WSExtra for use in lifecycle hooks.
26
- */
27
- const createConnectionContext = <R>(extra: WSExtra<R>): ConnectionContext<R> => ({
28
- runtime: extra.runtime,
29
- connectionParams: extra.connectionParams,
30
- socket: extra.socket,
31
- })
32
-
33
- /**
34
- * Create the onConnect handler for graphql-ws.
35
- */
36
- const makeOnConnectHandler = <R>(
37
- options: GraphQLWSOptions<R> | undefined
38
- ): ServerOptions<Record<string, unknown>, WSExtra<R>>["onConnect"] => {
39
- if (!options?.onConnect) return undefined
40
-
41
- return async (ctx) => {
42
- const extra = ctx.extra as WSExtra<R>
43
- try {
44
- const result = await Runtime.runPromise(extra.runtime)(
45
- options.onConnect!(ctx.connectionParams ?? {})
46
- )
47
- if (typeof result === "object" && result !== null) {
48
- Object.assign(extra.connectionParams, result)
49
- }
50
- return result !== false
51
- } catch {
52
- return false
53
- }
54
- }
55
- }
56
-
57
- /**
58
- * Create the onDisconnect handler for graphql-ws.
59
- */
60
- const makeOnDisconnectHandler = <R>(
61
- options: GraphQLWSOptions<R> | undefined
62
- ): ServerOptions<Record<string, unknown>, WSExtra<R>>["onDisconnect"] => {
63
- if (!options?.onDisconnect) return undefined
64
-
65
- return async (ctx) => {
66
- const extra = ctx.extra as WSExtra<R>
67
- await Runtime.runPromise(extra.runtime)(
68
- options.onDisconnect!(createConnectionContext(extra))
69
- ).catch(() => {
70
- // Ignore cleanup errors
71
- })
72
- }
73
- }
74
-
75
- /**
76
- * Create the onSubscribe handler for graphql-ws with complexity validation.
77
- */
78
- const makeOnSubscribeHandler = <R>(
79
- options: GraphQLWSOptions<R> | undefined,
80
- schema: GraphQLSchema,
81
- complexityConfig: GraphQLWSOptions<R>["complexity"],
82
- fieldComplexities: FieldComplexityMap
83
- ): ServerOptions<Record<string, unknown>, WSExtra<R>>["onSubscribe"] => {
84
- // graphql-ws 6.0: signature changed from (ctx, msg) to (ctx, id, payload)
85
- return async (ctx, id, payload) => {
86
- const extra = ctx.extra as WSExtra<R>
87
- const connectionCtx = createConnectionContext(extra)
88
-
89
- // Validate complexity if configured
90
- if (complexityConfig) {
91
- const validationEffect = validateComplexity(
92
- payload.query,
93
- payload.operationName ?? undefined,
94
- payload.variables ?? undefined,
95
- schema,
96
- fieldComplexities,
97
- complexityConfig
98
- ).pipe(
99
- Effect.catchAll((error) => {
100
- if (error._tag === "ComplexityLimitExceededError") {
101
- throw new GraphQLError(error.message, {
102
- extensions: {
103
- code: "COMPLEXITY_LIMIT_EXCEEDED",
104
- limitType: error.limitType,
105
- limit: error.limit,
106
- actual: error.actual,
107
- },
108
- })
109
- }
110
- return Effect.logWarning("Complexity analysis failed for subscription", error)
111
- })
112
- )
113
-
114
- await Effect.runPromise(validationEffect)
115
- }
116
-
117
- // Call user's onSubscribe hook if provided
118
- if (options?.onSubscribe) {
119
- await Runtime.runPromise(extra.runtime)(
120
- options.onSubscribe(connectionCtx, {
121
- id,
122
- payload: {
123
- query: payload.query,
124
- variables: payload.variables ?? undefined,
125
- operationName: payload.operationName ?? undefined,
126
- extensions: payload.extensions ?? undefined,
127
- },
128
- })
129
- )
130
- }
131
- }
132
- }
133
-
134
- /**
135
- * Create the onComplete handler for graphql-ws.
136
- */
137
- const makeOnCompleteHandler = <R>(
138
- options: GraphQLWSOptions<R> | undefined
139
- ): ServerOptions<Record<string, unknown>, WSExtra<R>>["onComplete"] => {
140
- if (!options?.onComplete) return undefined
141
-
142
- // graphql-ws 6.0: signature changed from (ctx, msg) to (ctx, id, payload)
143
- return async (ctx, id, _payload) => {
144
- const extra = ctx.extra as WSExtra<R>
145
- await Runtime.runPromise(extra.runtime)(
146
- options.onComplete!(createConnectionContext(extra), { id })
147
- ).catch(() => {
148
- // Ignore cleanup errors
149
- })
150
- }
151
- }
152
-
153
- /**
154
- * Create the onError handler for graphql-ws.
155
- */
156
- const makeOnErrorHandler = <R>(
157
- options: GraphQLWSOptions<R> | undefined
158
- ): ServerOptions<Record<string, unknown>, WSExtra<R>>["onError"] => {
159
- if (!options?.onError) return undefined
160
-
161
- // graphql-ws 6.0: signature changed from (ctx, msg, errors) to (ctx, id, payload, errors)
162
- return async (ctx, _id, _payload, errors) => {
163
- const extra = ctx.extra as WSExtra<R>
164
- await Runtime.runPromise(extra.runtime)(
165
- options.onError!(createConnectionContext(extra), errors)
166
- ).catch(() => {
167
- // Ignore error handler errors
168
- })
169
- }
170
- }
171
-
172
- /**
173
- * Create a graphql-ws compatible socket adapter from an EffectWebSocket.
174
- */
175
- const createGraphqlWsSocketAdapter = <R>(socket: EffectWebSocket, runtime: Runtime.Runtime<R>) => {
176
- let messageCallback: ((message: string) => Promise<void>) | null = null
177
-
178
- return {
179
- adapter: {
180
- protocol: socket.protocol,
181
-
182
- send: (data: string) =>
183
- Runtime.runPromise(runtime)(
184
- socket
185
- .send(data)
186
- .pipe(Effect.catchAll((error) => Effect.logError("WebSocket send error", error)))
187
- ),
188
-
189
- close: (code?: number, reason?: string) => {
190
- Runtime.runPromise(runtime)(socket.close(code, reason)).catch(() => {
191
- // Ignore close errors
192
- })
193
- },
194
-
195
- onMessage: (cb: (message: string) => Promise<void>) => {
196
- messageCallback = cb
197
- },
198
-
199
- onPong: (_payload: Record<string, unknown> | undefined) => {
200
- // Pong handling - can be used for keepalive
201
- },
202
- },
203
- dispatchMessage: async (message: string) => {
204
- if (messageCallback) {
205
- await messageCallback(message)
206
- }
207
- },
208
- }
209
- }
210
-
211
- /**
212
- * Run the connection lifecycle - manages message queue, fibers, and cleanup.
213
- */
214
- const runConnectionLifecycle = <R>(
215
- socket: EffectWebSocket,
216
- wsServer: ReturnType<typeof makeServer<Record<string, unknown>, WSExtra<R>>>,
217
- extra: WSExtra<R>
218
- ): Effect.Effect<void, never, never> =>
219
- Effect.gen(function* () {
220
- // Create message queue for bridging Stream to callback
221
- const messageQueue = yield* Queue.unbounded<string>()
222
- const closedDeferred = yield* Deferred.make<CloseEvent, WebSocketError>()
223
-
224
- // Fork fiber to consume socket messages and push to queue
225
- const messageFiber = yield* Effect.fork(
226
- Stream.runForEach(socket.messages, (msg) => Queue.offer(messageQueue, msg)).pipe(
227
- Effect.catchAll((error) => Deferred.fail(closedDeferred, error))
228
- )
229
- )
230
-
231
- // Fork fiber to handle socket close
232
- const closeFiber = yield* Effect.fork(
233
- socket.closed.pipe(
234
- Effect.tap((event) => Deferred.succeed(closedDeferred, event)),
235
- Effect.catchAll((error) => Deferred.fail(closedDeferred, error))
236
- )
237
- )
238
-
239
- // Create the graphql-ws socket adapter
240
- const { adapter, dispatchMessage } = createGraphqlWsSocketAdapter(socket, extra.runtime)
241
-
242
- // Open the connection with graphql-ws
243
- const closedHandler = wsServer.opened(adapter, extra)
244
-
245
- // Fork fiber to process messages from queue
246
- const processMessagesFiber = yield* Effect.fork(
247
- Effect.gen(function* () {
248
- while (true) {
249
- const message = yield* Queue.take(messageQueue)
250
- yield* Effect.tryPromise({
251
- try: () => dispatchMessage(message),
252
- catch: (error) => error,
253
- }).pipe(Effect.catchAll(() => Effect.void))
254
- }
255
- })
256
- )
257
-
258
- // Wait for connection to close
259
- yield* Deferred.await(closedDeferred).pipe(
260
- Effect.catchAll(() => Effect.succeed({ code: 1000, reason: "Error" }))
261
- )
262
-
263
- // Cleanup
264
- closedHandler(1000, "Connection closed")
265
- yield* Fiber.interrupt(messageFiber)
266
- yield* Fiber.interrupt(closeFiber)
267
- yield* Fiber.interrupt(processMessagesFiber)
268
- yield* Queue.shutdown(messageQueue)
269
- }).pipe(
270
- Effect.catchAllCause(() => Effect.void),
271
- Effect.scoped
272
- )
273
-
274
- /**
275
- * Create a WebSocket handler for GraphQL subscriptions using the graphql-ws protocol.
276
- *
277
- * This function creates a handler that can be used with any WebSocket implementation
278
- * that conforms to the EffectWebSocket interface. Platform packages (node, bun, express)
279
- * provide adapters that convert their native WebSocket to EffectWebSocket.
280
- *
281
- * The handler:
282
- * - Uses the graphql-ws protocol for client communication
283
- * - Creates an Effect runtime from the provided layer for each connection
284
- * - Executes subscriptions using GraphQL's subscribe() function
285
- * - Properly cleans up resources when connections close
286
- *
287
- * @param schema - The GraphQL schema with subscription definitions
288
- * @param layer - Effect layer providing services required by resolvers
289
- * @param options - Optional lifecycle hooks for connection/subscription events
290
- * @returns A function that handles individual WebSocket connections
291
- *
292
- * @example
293
- * ```typescript
294
- * import { makeGraphQLWSHandler } from "@effect-gql/core"
295
- *
296
- * const handler = makeGraphQLWSHandler(schema, serviceLayer, {
297
- * onConnect: (params) => Effect.gen(function* () {
298
- * const user = yield* AuthService.validateToken(params.authToken)
299
- * return { user }
300
- * }),
301
- * })
302
- *
303
- * // In platform-specific code:
304
- * const effectSocket = toEffectWebSocket(rawWebSocket)
305
- * await Effect.runPromise(handler(effectSocket))
306
- * ```
307
- */
308
- export const makeGraphQLWSHandler = <R>(
309
- schema: GraphQLSchema,
310
- layer: Layer.Layer<R>,
311
- options?: GraphQLWSOptions<R>
312
- ): ((socket: EffectWebSocket) => Effect.Effect<void, never, never>) => {
313
- const complexityConfig = options?.complexity
314
- const fieldComplexities: FieldComplexityMap = options?.fieldComplexities ?? new Map()
315
-
316
- // Build server options using extracted handler factories
317
- const serverOptions: ServerOptions<Record<string, unknown>, WSExtra<R>> = {
318
- schema,
319
-
320
- context: async (ctx): Promise<GraphQLEffectContext<R> & Record<string, unknown>> => {
321
- const extra = ctx.extra as WSExtra<R>
322
- return {
323
- runtime: extra.runtime,
324
- ...extra.connectionParams,
325
- }
326
- },
327
-
328
- subscribe: async (args) => subscribe(args),
329
-
330
- onConnect: makeOnConnectHandler(options),
331
- onDisconnect: makeOnDisconnectHandler(options),
332
- onSubscribe: makeOnSubscribeHandler(options, schema, complexityConfig, fieldComplexities),
333
- onComplete: makeOnCompleteHandler(options),
334
- onError: makeOnErrorHandler(options),
335
- }
336
-
337
- const wsServer = makeServer(serverOptions)
338
-
339
- // Return the connection handler
340
- return (socket: EffectWebSocket): Effect.Effect<void, never, never> =>
341
- Effect.gen(function* () {
342
- const runtime = yield* Effect.provide(Effect.runtime<R>(), layer)
343
-
344
- const extra: WSExtra<R> = {
345
- socket,
346
- runtime,
347
- connectionParams: {},
348
- }
349
-
350
- yield* runConnectionLifecycle(socket, wsServer, extra)
351
- }).pipe(
352
- Effect.catchAllCause(() => Effect.void),
353
- Effect.scoped
354
- )
355
- }
@@ -1,192 +0,0 @@
1
- import { Data, Effect, Runtime, Stream } from "effect"
2
- import type { ComplexityConfig, FieldComplexityMap } from "./complexity"
3
-
4
- /**
5
- * Error type for WebSocket operations
6
- */
7
- export class WebSocketError extends Data.TaggedError("WebSocketError")<{
8
- readonly cause: unknown
9
- }> {}
10
-
11
- /**
12
- * WebSocket close event information
13
- */
14
- export interface CloseEvent {
15
- readonly code: number
16
- readonly reason: string
17
- }
18
-
19
- /**
20
- * Platform-neutral WebSocket interface using Effect types.
21
- *
22
- * This interface abstracts WebSocket operations across different platforms
23
- * (Node.js ws, Bun built-in, browser WebSocket). Platform packages implement
24
- * this interface to bridge their specific WebSocket implementations.
25
- */
26
- export interface EffectWebSocket {
27
- /**
28
- * Send a message to the client.
29
- * Returns an Effect that completes when the message is sent.
30
- */
31
- readonly send: (data: string) => Effect.Effect<void, WebSocketError>
32
-
33
- /**
34
- * Close the WebSocket connection.
35
- * @param code - Optional close code (default: 1000)
36
- * @param reason - Optional close reason
37
- */
38
- readonly close: (code?: number, reason?: string) => Effect.Effect<void, WebSocketError>
39
-
40
- /**
41
- * Stream of incoming messages from the client.
42
- * The stream completes when the connection closes.
43
- */
44
- readonly messages: Stream.Stream<string, WebSocketError>
45
-
46
- /**
47
- * Effect that completes with CloseEvent when the connection closes.
48
- * Use this to detect client disconnection.
49
- */
50
- readonly closed: Effect.Effect<CloseEvent, WebSocketError>
51
-
52
- /**
53
- * The WebSocket subprotocol negotiated during handshake.
54
- * For GraphQL subscriptions, this should be "graphql-transport-ws".
55
- */
56
- readonly protocol: string
57
- }
58
-
59
- /**
60
- * Context available during a WebSocket connection.
61
- * This is passed to lifecycle hooks.
62
- */
63
- export interface ConnectionContext<R> {
64
- /**
65
- * The Effect runtime for this connection.
66
- * Use this to run Effects within the connection scope.
67
- */
68
- readonly runtime: Runtime.Runtime<R>
69
-
70
- /**
71
- * Connection parameters sent by the client during CONNECTION_INIT.
72
- * Often used for authentication tokens.
73
- */
74
- readonly connectionParams: Record<string, unknown>
75
-
76
- /**
77
- * The underlying WebSocket for this connection.
78
- */
79
- readonly socket: EffectWebSocket
80
- }
81
-
82
- /**
83
- * Options for configuring the GraphQL WebSocket handler.
84
- *
85
- * @template R - Service requirements for lifecycle hooks
86
- */
87
- export interface GraphQLWSOptions<R> {
88
- /**
89
- * Query complexity limiting configuration.
90
- * When provided, subscriptions are validated against complexity limits
91
- * before execution begins.
92
- */
93
- readonly complexity?: ComplexityConfig
94
-
95
- /**
96
- * Field complexity definitions from the schema builder.
97
- * If using the platform serve() functions with subscriptions config,
98
- * this is typically passed automatically.
99
- */
100
- readonly fieldComplexities?: FieldComplexityMap
101
-
102
- /**
103
- * Called when a client initiates a connection (CONNECTION_INIT message).
104
- *
105
- * Use this for authentication. Return:
106
- * - `true` to accept the connection
107
- * - `false` to reject the connection
108
- * - An object to accept and provide additional context
109
- *
110
- * The returned object (or true) is merged into the GraphQL context.
111
- *
112
- * @example
113
- * ```typescript
114
- * onConnect: (params) => Effect.gen(function* () {
115
- * const token = params.authToken as string
116
- * const user = yield* AuthService.validateToken(token)
117
- * return { user } // Available in GraphQL context
118
- * })
119
- * ```
120
- */
121
- readonly onConnect?: (
122
- params: Record<string, unknown>
123
- ) => Effect.Effect<boolean | Record<string, unknown>, unknown, R>
124
-
125
- /**
126
- * Called when a client disconnects.
127
- * Use this for cleanup (e.g., removing user from active connections).
128
- */
129
- readonly onDisconnect?: (ctx: ConnectionContext<R>) => Effect.Effect<void, never, R>
130
-
131
- /**
132
- * Called when a client starts a subscription (SUBSCRIBE message).
133
- * Use this for per-subscription authorization or logging.
134
- *
135
- * Note: If complexity validation is enabled, it runs before this hook.
136
- * Throw an error to reject the subscription.
137
- */
138
- readonly onSubscribe?: (
139
- ctx: ConnectionContext<R>,
140
- message: SubscribeMessage
141
- ) => Effect.Effect<void, unknown, R>
142
-
143
- /**
144
- * Called when a subscription completes or is stopped.
145
- */
146
- readonly onComplete?: (
147
- ctx: ConnectionContext<R>,
148
- message: CompleteMessage
149
- ) => Effect.Effect<void, never, R>
150
-
151
- /**
152
- * Called when an error occurs during subscription execution.
153
- */
154
- readonly onError?: (ctx: ConnectionContext<R>, error: unknown) => Effect.Effect<void, never, R>
155
- }
156
-
157
- /**
158
- * GraphQL WebSocket SUBSCRIBE message payload
159
- */
160
- export interface SubscribeMessage {
161
- readonly id: string
162
- readonly payload: {
163
- readonly query: string
164
- readonly variables?: Record<string, unknown>
165
- readonly operationName?: string
166
- readonly extensions?: Record<string, unknown>
167
- }
168
- }
169
-
170
- /**
171
- * GraphQL WebSocket COMPLETE message payload
172
- */
173
- export interface CompleteMessage {
174
- readonly id: string
175
- }
176
-
177
- /**
178
- * Configuration for the WebSocket endpoint
179
- */
180
- export interface GraphQLWSConfig {
181
- /**
182
- * Path for WebSocket connections.
183
- * @default "/graphql"
184
- */
185
- readonly path?: string
186
-
187
- /**
188
- * How long to wait for CONNECTION_INIT message before closing.
189
- * @default 5000 (5 seconds)
190
- */
191
- readonly connectionInitWaitTimeout?: number
192
- }
@@ -1,136 +0,0 @@
1
- import { Effect, Stream, Queue, Deferred } from "effect"
2
- import type { EffectWebSocket, CloseEvent } from "./ws-types"
3
- import { WebSocketError } from "./ws-types"
4
-
5
- /**
6
- * Interface for the 'ws' library WebSocket.
7
- * This allows type-safe usage without requiring core to depend on 'ws'.
8
- */
9
- export interface WsWebSocket {
10
- readonly protocol: string
11
- readonly readyState: number
12
- send(data: string, callback?: (error?: Error) => void): void
13
- close(code?: number, reason?: string): void
14
- on(event: "message", listener: (data: Buffer | string) => void): void
15
- on(event: "error", listener: (error: Error) => void): void
16
- on(event: "close", listener: (code: number, reason: Buffer) => void): void
17
- removeListener(event: string, listener: (...args: any[]) => void): void
18
- }
19
-
20
- /** WebSocket.CLOSED constant from 'ws' library */
21
- export const WS_CLOSED = 3
22
-
23
- /**
24
- * Convert a WebSocket from the 'ws' library to an EffectWebSocket.
25
- *
26
- * This creates an Effect-based wrapper around the ws WebSocket instance,
27
- * providing a Stream for incoming messages and Effect-based send/close operations.
28
- *
29
- * This utility is used by platform packages (node, express) that integrate
30
- * with the 'ws' library for WebSocket support.
31
- *
32
- * @param ws - The WebSocket instance from the 'ws' library
33
- * @returns An EffectWebSocket that can be used with makeGraphQLWSHandler
34
- *
35
- * @example
36
- * ```typescript
37
- * import { toEffectWebSocketFromWs } from "@effect-gql/core"
38
- * import { WebSocket } from "ws"
39
- *
40
- * wss.on("connection", (ws: WebSocket) => {
41
- * const effectSocket = toEffectWebSocketFromWs(ws)
42
- * Effect.runPromise(handler(effectSocket))
43
- * })
44
- * ```
45
- */
46
- export const toEffectWebSocketFromWs = (ws: WsWebSocket): EffectWebSocket => {
47
- // Create the message stream using a queue
48
- const messagesEffect = Effect.gen(function* () {
49
- const queue = yield* Queue.unbounded<string>()
50
- const closed = yield* Deferred.make<CloseEvent, WebSocketError>()
51
-
52
- // Set up message listener
53
- ws.on("message", (data) => {
54
- const message = data.toString()
55
- Effect.runPromise(Queue.offer(queue, message)).catch(() => {
56
- // Queue might be shutdown
57
- })
58
- })
59
-
60
- // Set up error listener
61
- ws.on("error", (error) => {
62
- Effect.runPromise(Deferred.fail(closed, new WebSocketError({ cause: error }))).catch(() => {
63
- // Already completed
64
- })
65
- })
66
-
67
- // Set up close listener
68
- ws.on("close", (code, reason) => {
69
- Effect.runPromise(
70
- Queue.shutdown(queue).pipe(
71
- Effect.andThen(Deferred.succeed(closed, { code, reason: reason.toString() }))
72
- )
73
- ).catch(() => {
74
- // Already completed
75
- })
76
- })
77
-
78
- return { queue, closed }
79
- })
80
-
81
- // Create the message stream
82
- const messages: Stream.Stream<string, WebSocketError> = Stream.unwrap(
83
- messagesEffect.pipe(
84
- Effect.map(({ queue }) => Stream.fromQueue(queue).pipe(Stream.catchAll(() => Stream.empty)))
85
- )
86
- )
87
-
88
- return {
89
- protocol: ws.protocol || "graphql-transport-ws",
90
-
91
- send: (data: string) =>
92
- Effect.async<void, WebSocketError>((resume) => {
93
- ws.send(data, (error) => {
94
- if (error) {
95
- resume(Effect.fail(new WebSocketError({ cause: error })))
96
- } else {
97
- resume(Effect.succeed(undefined))
98
- }
99
- })
100
- }),
101
-
102
- close: (code?: number, reason?: string) =>
103
- Effect.sync(() => {
104
- ws.close(code ?? 1000, reason ?? "")
105
- }),
106
-
107
- messages,
108
-
109
- closed: Effect.async<CloseEvent, WebSocketError>((resume) => {
110
- if (ws.readyState === WS_CLOSED) {
111
- resume(Effect.succeed({ code: 1000, reason: "" }))
112
- return
113
- }
114
-
115
- const onClose = (code: number, reason: Buffer) => {
116
- cleanup()
117
- resume(Effect.succeed({ code, reason: reason.toString() }))
118
- }
119
-
120
- const onError = (error: Error) => {
121
- cleanup()
122
- resume(Effect.fail(new WebSocketError({ cause: error })))
123
- }
124
-
125
- const cleanup = () => {
126
- ws.removeListener("close", onClose)
127
- ws.removeListener("error", onError)
128
- }
129
-
130
- ws.on("close", onClose)
131
- ws.on("error", onError)
132
-
133
- return Effect.sync(cleanup)
134
- }),
135
- }
136
- }