@effect-gql/core 0.1.0 → 1.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.
Files changed (164) hide show
  1. package/README.md +100 -0
  2. package/builder/index.cjs +1446 -0
  3. package/builder/index.cjs.map +1 -0
  4. package/builder/index.d.cts +260 -0
  5. package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +50 -21
  6. package/builder/index.js +1405 -0
  7. package/builder/index.js.map +1 -0
  8. package/index.cjs +3469 -0
  9. package/index.cjs.map +1 -0
  10. package/index.d.cts +529 -0
  11. package/index.d.ts +529 -0
  12. package/index.js +3292 -0
  13. package/index.js.map +1 -0
  14. package/package.json +19 -28
  15. package/schema-builder-DKvkzU_M.d.cts +965 -0
  16. package/schema-builder-DKvkzU_M.d.ts +965 -0
  17. package/server/index.cjs +1579 -0
  18. package/server/index.cjs.map +1 -0
  19. package/server/index.d.cts +682 -0
  20. package/server/index.d.ts +682 -0
  21. package/server/index.js +1548 -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,327 +0,0 @@
1
- import { Effect, Layer, Stream } from "effect"
2
- import {
3
- GraphQLSchema,
4
- parse,
5
- validate,
6
- subscribe,
7
- GraphQLError,
8
- Kind,
9
- type ExecutionResult,
10
- type DocumentNode,
11
- type OperationDefinitionNode,
12
- } from "graphql"
13
- import type { GraphQLEffectContext } from "../builder/types"
14
- import {
15
- SSEError,
16
- type GraphQLSSEOptions,
17
- type SSEConnectionContext,
18
- type SSESubscriptionRequest,
19
- type SSEEvent,
20
- formatNextEvent,
21
- formatErrorEvent,
22
- formatCompleteEvent,
23
- } from "./sse-types"
24
- import { validateComplexity, type FieldComplexityMap } from "./complexity"
25
-
26
- /**
27
- * Create a subscription event stream for SSE.
28
- *
29
- * This function handles the GraphQL subscription lifecycle:
30
- * 1. Parse and validate the query
31
- * 2. Check complexity limits if configured
32
- * 3. Execute the subscription
33
- * 4. Stream results as SSE events
34
- *
35
- * @param schema - The GraphQL schema with subscription definitions
36
- * @param layer - Effect layer providing services required by resolvers
37
- * @param request - The subscription request (query, variables, operationName)
38
- * @param headers - HTTP headers from the request (for auth)
39
- * @param options - Optional lifecycle hooks and configuration
40
- * @returns A Stream of SSE events to send to the client
41
- *
42
- * @example
43
- * ```typescript
44
- * const eventStream = makeSSESubscriptionStream(
45
- * schema,
46
- * serviceLayer,
47
- * { query: "subscription { tick { count } }" },
48
- * new Headers(),
49
- * { onConnect: (req, headers) => Effect.succeed({ user: "alice" }) }
50
- * )
51
- *
52
- * // In platform-specific code, consume and send events:
53
- * Stream.runForEach(eventStream, (event) =>
54
- * Effect.sync(() => res.write(formatSSEMessage(event)))
55
- * )
56
- * ```
57
- */
58
- export const makeSSESubscriptionStream = <R>(
59
- schema: GraphQLSchema,
60
- layer: Layer.Layer<R>,
61
- request: SSESubscriptionRequest,
62
- headers: Headers,
63
- options?: GraphQLSSEOptions<R>
64
- ): Stream.Stream<SSEEvent, SSEError> => {
65
- const complexityConfig = options?.complexity
66
- const fieldComplexities: FieldComplexityMap = options?.fieldComplexities ?? new Map()
67
-
68
- return Stream.unwrap(
69
- Effect.gen(function* () {
70
- // Create a runtime from the layer
71
- const runtime = yield* Effect.provide(Effect.runtime<R>(), layer)
72
-
73
- // Run onConnect hook if provided
74
- let connectionContext: Record<string, unknown> = {}
75
- if (options?.onConnect) {
76
- try {
77
- connectionContext = yield* Effect.provide(options.onConnect(request, headers), layer)
78
- } catch {
79
- // Connection rejected
80
- return Stream.make(
81
- formatErrorEvent([
82
- new GraphQLError("Subscription connection rejected", {
83
- extensions: { code: "CONNECTION_REJECTED" },
84
- }),
85
- ]),
86
- formatCompleteEvent()
87
- )
88
- }
89
- }
90
-
91
- // Parse the query
92
- let document: DocumentNode
93
- try {
94
- document = parse(request.query)
95
- } catch (syntaxError) {
96
- return Stream.make(formatErrorEvent([syntaxError]), formatCompleteEvent())
97
- }
98
-
99
- // Validate the query
100
- const validationErrors = validate(schema, document)
101
- if (validationErrors.length > 0) {
102
- return Stream.make(formatErrorEvent(validationErrors), formatCompleteEvent())
103
- }
104
-
105
- // Find the subscription operation
106
- const operations = document.definitions.filter(
107
- (d): d is OperationDefinitionNode => d.kind === Kind.OPERATION_DEFINITION
108
- )
109
-
110
- const operation = request.operationName
111
- ? operations.find((o) => o.name?.value === request.operationName)
112
- : operations[0]
113
-
114
- if (!operation) {
115
- return Stream.make(
116
- formatErrorEvent([new GraphQLError("No operation found in query")]),
117
- formatCompleteEvent()
118
- )
119
- }
120
-
121
- if (operation.operation !== "subscription") {
122
- return Stream.make(
123
- formatErrorEvent([
124
- new GraphQLError(
125
- `SSE endpoint only supports subscriptions, received: ${operation.operation}`,
126
- { extensions: { code: "OPERATION_NOT_SUPPORTED" } }
127
- ),
128
- ]),
129
- formatCompleteEvent()
130
- )
131
- }
132
-
133
- // Validate complexity if configured
134
- if (complexityConfig) {
135
- const complexityResult = yield* validateComplexity(
136
- request.query,
137
- request.operationName,
138
- request.variables,
139
- schema,
140
- fieldComplexities,
141
- complexityConfig
142
- ).pipe(
143
- Effect.map(() => null),
144
- Effect.catchAll((error) => {
145
- if (error._tag === "ComplexityLimitExceededError") {
146
- return Effect.succeed(
147
- new GraphQLError(error.message, {
148
- extensions: {
149
- code: "COMPLEXITY_LIMIT_EXCEEDED",
150
- limitType: error.limitType,
151
- limit: error.limit,
152
- actual: error.actual,
153
- },
154
- })
155
- )
156
- }
157
- // Log analysis errors but don't block (fail open)
158
- return Effect.logWarning("Complexity analysis failed for SSE subscription", error).pipe(
159
- Effect.map(() => null)
160
- )
161
- })
162
- )
163
-
164
- if (complexityResult) {
165
- return Stream.make(formatErrorEvent([complexityResult]), formatCompleteEvent())
166
- }
167
- }
168
-
169
- // Build the context for the subscription
170
- const ctx: SSEConnectionContext<R> = {
171
- runtime,
172
- request,
173
- connectionContext,
174
- }
175
-
176
- // Call onSubscribe hook if provided
177
- if (options?.onSubscribe) {
178
- yield* Effect.provide(options.onSubscribe(ctx), layer).pipe(
179
- Effect.catchAll(() => Effect.void)
180
- )
181
- }
182
-
183
- // Execute the subscription
184
- const graphqlContext: GraphQLEffectContext<R> & Record<string, unknown> = {
185
- runtime,
186
- ...connectionContext,
187
- }
188
-
189
- const subscriptionResult = yield* Effect.tryPromise({
190
- try: () =>
191
- subscribe({
192
- schema,
193
- document,
194
- variableValues: request.variables,
195
- operationName: request.operationName ?? undefined,
196
- contextValue: graphqlContext,
197
- }),
198
- catch: (error) => new SSEError({ cause: error }),
199
- })
200
-
201
- // Check if subscribe returned an error result instead of async iterator
202
- if (!isAsyncIterable(subscriptionResult)) {
203
- // It's an ExecutionResult with errors
204
- const result = subscriptionResult as ExecutionResult
205
- if (result.errors) {
206
- return Stream.make(formatErrorEvent(result.errors), formatCompleteEvent())
207
- }
208
- // Shouldn't happen, but handle gracefully
209
- return Stream.make(formatNextEvent(result), formatCompleteEvent())
210
- }
211
-
212
- // Create a stream from the async iterator
213
- const asyncIterator = subscriptionResult[Symbol.asyncIterator]()
214
-
215
- const eventStream = Stream.async<SSEEvent, SSEError>((emit) => {
216
- let done = false
217
-
218
- const iterate = async () => {
219
- try {
220
- while (!done) {
221
- const result = await asyncIterator.next()
222
- if (result.done) {
223
- emit.end()
224
- break
225
- }
226
- emit.single(formatNextEvent(result.value))
227
- }
228
- } catch (error) {
229
- if (!done) {
230
- emit.single(
231
- formatErrorEvent([
232
- error instanceof GraphQLError
233
- ? error
234
- : new GraphQLError(
235
- error instanceof Error ? error.message : "Subscription error",
236
- { extensions: { code: "SUBSCRIPTION_ERROR" } }
237
- ),
238
- ])
239
- )
240
- emit.end()
241
- }
242
- }
243
- }
244
-
245
- iterate()
246
-
247
- // Return cleanup function
248
- return Effect.sync(() => {
249
- done = true
250
- asyncIterator.return?.()
251
- })
252
- })
253
-
254
- // Add complete event at the end and handle cleanup
255
- return eventStream.pipe(
256
- Stream.onDone(() =>
257
- Effect.gen(function* () {
258
- yield* Effect.sync(() => {})
259
- }).pipe(Effect.asVoid)
260
- ),
261
- Stream.concat(Stream.make(formatCompleteEvent())),
262
- Stream.onDone(() => {
263
- if (options?.onComplete) {
264
- return Effect.provide(options.onComplete(ctx), layer).pipe(
265
- Effect.catchAll(() => Effect.void)
266
- )
267
- }
268
- return Effect.void
269
- })
270
- )
271
- }).pipe(
272
- Effect.catchAll((error) =>
273
- Effect.succeed(
274
- Stream.make(
275
- formatErrorEvent([
276
- new GraphQLError(error instanceof Error ? error.message : "Internal error", {
277
- extensions: { code: "INTERNAL_ERROR" },
278
- }),
279
- ]),
280
- formatCompleteEvent()
281
- )
282
- )
283
- )
284
- )
285
- )
286
- }
287
-
288
- /**
289
- * Create an SSE subscription handler that can be used with platform-specific servers.
290
- *
291
- * This is a higher-level API that returns a handler function. The handler
292
- * takes a request and headers, and returns a Stream of SSE events.
293
- *
294
- * @param schema - The GraphQL schema with subscription definitions
295
- * @param layer - Effect layer providing services required by resolvers
296
- * @param options - Optional lifecycle hooks and configuration
297
- * @returns A handler function for SSE subscription requests
298
- *
299
- * @example
300
- * ```typescript
301
- * const handler = makeGraphQLSSEHandler(schema, serviceLayer, {
302
- * onConnect: (request, headers) => Effect.gen(function* () {
303
- * const token = headers.get("authorization")
304
- * const user = yield* AuthService.validateToken(token)
305
- * return { user }
306
- * }),
307
- * })
308
- *
309
- * // In platform-specific code:
310
- * const events = handler(request, headers)
311
- * // Stream events to client...
312
- * ```
313
- */
314
- export const makeGraphQLSSEHandler = <R>(
315
- schema: GraphQLSchema,
316
- layer: Layer.Layer<R>,
317
- options?: GraphQLSSEOptions<R>
318
- ): ((request: SSESubscriptionRequest, headers: Headers) => Stream.Stream<SSEEvent, SSEError>) => {
319
- return (request, headers) => makeSSESubscriptionStream(schema, layer, request, headers, options)
320
- }
321
-
322
- /**
323
- * Type guard to check if a value is an AsyncIterable (subscription result)
324
- */
325
- function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {
326
- return typeof value === "object" && value !== null && Symbol.asyncIterator in value
327
- }
@@ -1,234 +0,0 @@
1
- import { Data, Effect, Runtime, Stream } from "effect"
2
- import type { ExecutionResult } from "graphql"
3
- import type { ComplexityConfig, FieldComplexityMap } from "./complexity"
4
-
5
- /**
6
- * Standard SSE response headers following the graphql-sse protocol.
7
- * Use these headers when writing SSE responses in platform adapters.
8
- */
9
- export const SSE_HEADERS: Record<string, string> = {
10
- "Content-Type": "text/event-stream",
11
- "Cache-Control": "no-cache",
12
- Connection: "keep-alive",
13
- "X-Accel-Buffering": "no", // Disable nginx buffering
14
- } as const
15
-
16
- /**
17
- * Error type for SSE operations
18
- */
19
- export class SSEError extends Data.TaggedError("SSEError")<{
20
- readonly cause: unknown
21
- }> {}
22
-
23
- /**
24
- * SSE event types following the graphql-sse protocol (distinct connections mode).
25
- * @see https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md
26
- */
27
- export type SSEEventType = "next" | "error" | "complete"
28
-
29
- /**
30
- * An SSE event to be sent to the client.
31
- */
32
- export interface SSEEvent {
33
- readonly event: SSEEventType
34
- readonly data: string
35
- }
36
-
37
- /**
38
- * Platform-neutral SSE response interface using Effect types.
39
- *
40
- * This interface abstracts SSE operations across different platforms
41
- * (Node.js, Bun, Deno, Workers). Platform packages implement this
42
- * interface to bridge their specific HTTP response implementations.
43
- *
44
- * Unlike WebSocket which is bidirectional, SSE is unidirectional
45
- * (server to client only). The subscription query is provided
46
- * upfront when creating the SSE connection.
47
- */
48
- export interface EffectSSE {
49
- /**
50
- * Send an SSE event to the client.
51
- * The platform adapter formats this as proper SSE format:
52
- * ```
53
- * event: next
54
- * data: {"data":{"field":"value"}}
55
- *
56
- * ```
57
- */
58
- readonly sendEvent: (event: SSEEvent) => Effect.Effect<void, SSEError>
59
-
60
- /**
61
- * Effect that completes when the client disconnects.
62
- * Use this to detect client disconnection and cleanup.
63
- */
64
- readonly closed: Effect.Effect<void, SSEError>
65
- }
66
-
67
- /**
68
- * The GraphQL request payload for SSE subscriptions.
69
- * Same as a regular GraphQL HTTP request.
70
- */
71
- export interface SSESubscriptionRequest {
72
- readonly query: string
73
- readonly variables?: Record<string, unknown>
74
- readonly operationName?: string
75
- readonly extensions?: Record<string, unknown>
76
- }
77
-
78
- /**
79
- * Context available during an SSE subscription.
80
- * This is passed to lifecycle hooks.
81
- */
82
- export interface SSEConnectionContext<R> {
83
- /**
84
- * The Effect runtime for this connection.
85
- * Use this to run Effects within the connection scope.
86
- */
87
- readonly runtime: Runtime.Runtime<R>
88
-
89
- /**
90
- * The original subscription request.
91
- */
92
- readonly request: SSESubscriptionRequest
93
-
94
- /**
95
- * Optional authentication/authorization context.
96
- * Populated by the onConnect hook.
97
- */
98
- readonly connectionContext: Record<string, unknown>
99
- }
100
-
101
- /**
102
- * Options for configuring the GraphQL SSE handler.
103
- *
104
- * @template R - Service requirements for lifecycle hooks
105
- */
106
- export interface GraphQLSSEOptions<R> {
107
- /**
108
- * Query complexity limiting configuration.
109
- * When provided, subscriptions are validated against complexity limits
110
- * before execution begins.
111
- */
112
- readonly complexity?: ComplexityConfig
113
-
114
- /**
115
- * Field complexity definitions from the schema builder.
116
- * If using the platform serve() functions, this is typically
117
- * passed automatically.
118
- */
119
- readonly fieldComplexities?: FieldComplexityMap
120
-
121
- /**
122
- * Called before a subscription starts.
123
- *
124
- * Use this for authentication/authorization. Return:
125
- * - A context object to accept the subscription
126
- * - Throw/fail to reject the subscription
127
- *
128
- * The returned object is available in the GraphQL context.
129
- *
130
- * @example
131
- * ```typescript
132
- * onConnect: (request, headers) => Effect.gen(function* () {
133
- * const token = headers.get("authorization")
134
- * const user = yield* AuthService.validateToken(token)
135
- * return { user } // Available in GraphQL context
136
- * })
137
- * ```
138
- */
139
- readonly onConnect?: (
140
- request: SSESubscriptionRequest,
141
- headers: Headers
142
- ) => Effect.Effect<Record<string, unknown>, unknown, R>
143
-
144
- /**
145
- * Called when the subscription starts streaming.
146
- */
147
- readonly onSubscribe?: (ctx: SSEConnectionContext<R>) => Effect.Effect<void, never, R>
148
-
149
- /**
150
- * Called when the subscription completes (normally or due to error).
151
- */
152
- readonly onComplete?: (ctx: SSEConnectionContext<R>) => Effect.Effect<void, never, R>
153
-
154
- /**
155
- * Called when the client disconnects.
156
- */
157
- readonly onDisconnect?: (ctx: SSEConnectionContext<R>) => Effect.Effect<void, never, R>
158
-
159
- /**
160
- * Called when an error occurs during subscription execution.
161
- */
162
- readonly onError?: (ctx: SSEConnectionContext<R>, error: unknown) => Effect.Effect<void, never, R>
163
- }
164
-
165
- /**
166
- * Configuration for the SSE endpoint
167
- */
168
- export interface GraphQLSSEConfig {
169
- /**
170
- * Path for SSE connections.
171
- * @default "/graphql/stream"
172
- */
173
- readonly path?: string
174
- }
175
-
176
- /**
177
- * Result of SSE subscription handler creation.
178
- * This is used by platform packages to implement their SSE response.
179
- */
180
- export interface SSESubscriptionResult {
181
- /**
182
- * Stream of SSE events to send to the client.
183
- * The platform adapter should consume this stream and send events.
184
- */
185
- readonly events: Stream.Stream<SSEEvent, SSEError>
186
-
187
- /**
188
- * Effect that should be run when client disconnects.
189
- * This allows cleanup of resources.
190
- */
191
- readonly cleanup: Effect.Effect<void, never, never>
192
- }
193
-
194
- /**
195
- * Format an ExecutionResult as an SSE "next" event.
196
- */
197
- export const formatNextEvent = (result: ExecutionResult): SSEEvent => ({
198
- event: "next",
199
- data: JSON.stringify(result),
200
- })
201
-
202
- /**
203
- * Format errors as an SSE "error" event.
204
- */
205
- export const formatErrorEvent = (errors: readonly unknown[]): SSEEvent => ({
206
- event: "error",
207
- data: JSON.stringify({ errors }),
208
- })
209
-
210
- /**
211
- * Format a "complete" event.
212
- */
213
- export const formatCompleteEvent = (): SSEEvent => ({
214
- event: "complete",
215
- data: "",
216
- })
217
-
218
- /**
219
- * Format an SSE event to the wire format.
220
- * Each event is formatted as:
221
- * ```
222
- * event: <type>
223
- * data: <json>
224
- *
225
- * ```
226
- */
227
- export const formatSSEMessage = (event: SSEEvent): string => {
228
- const lines = [`event: ${event.event}`]
229
- if (event.data) {
230
- lines.push(`data: ${event.data}`)
231
- }
232
- lines.push("", "") // Two newlines to end the event
233
- return lines.join("\n")
234
- }