@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.
- package/README.md +100 -0
- package/builder/index.cjs +1446 -0
- package/builder/index.cjs.map +1 -0
- package/builder/index.d.cts +260 -0
- package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +50 -21
- package/builder/index.js +1405 -0
- package/builder/index.js.map +1 -0
- package/index.cjs +3469 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +529 -0
- package/index.d.ts +529 -0
- package/index.js +3292 -0
- package/index.js.map +1 -0
- package/package.json +19 -28
- package/schema-builder-DKvkzU_M.d.cts +965 -0
- package/schema-builder-DKvkzU_M.d.ts +965 -0
- package/server/index.cjs +1579 -0
- package/server/index.cjs.map +1 -0
- package/server/index.d.cts +682 -0
- package/server/index.d.ts +682 -0
- package/server/index.js +1548 -0
- package/server/index.js.map +1 -0
- package/dist/analyzer-extension.d.ts +0 -105
- package/dist/analyzer-extension.d.ts.map +0 -1
- package/dist/analyzer-extension.js +0 -137
- package/dist/analyzer-extension.js.map +0 -1
- package/dist/builder/execute.d.ts +0 -26
- package/dist/builder/execute.d.ts.map +0 -1
- package/dist/builder/execute.js +0 -104
- package/dist/builder/execute.js.map +0 -1
- package/dist/builder/field-builders.d.ts +0 -30
- package/dist/builder/field-builders.d.ts.map +0 -1
- package/dist/builder/field-builders.js +0 -200
- package/dist/builder/field-builders.js.map +0 -1
- package/dist/builder/index.d.ts +0 -7
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/index.js +0 -31
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/pipe-api.d.ts.map +0 -1
- package/dist/builder/pipe-api.js +0 -151
- package/dist/builder/pipe-api.js.map +0 -1
- package/dist/builder/schema-builder.d.ts +0 -301
- package/dist/builder/schema-builder.d.ts.map +0 -1
- package/dist/builder/schema-builder.js +0 -566
- package/dist/builder/schema-builder.js.map +0 -1
- package/dist/builder/type-registry.d.ts +0 -80
- package/dist/builder/type-registry.d.ts.map +0 -1
- package/dist/builder/type-registry.js +0 -505
- package/dist/builder/type-registry.js.map +0 -1
- package/dist/builder/types.d.ts +0 -283
- package/dist/builder/types.d.ts.map +0 -1
- package/dist/builder/types.js +0 -3
- package/dist/builder/types.js.map +0 -1
- package/dist/cli/generate-schema.d.ts +0 -29
- package/dist/cli/generate-schema.d.ts.map +0 -1
- package/dist/cli/generate-schema.js +0 -233
- package/dist/cli/generate-schema.js.map +0 -1
- package/dist/cli/index.d.ts +0 -19
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -24
- package/dist/cli/index.js.map +0 -1
- package/dist/context.d.ts +0 -18
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -11
- package/dist/context.js.map +0 -1
- package/dist/error.d.ts +0 -45
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -29
- package/dist/error.js.map +0 -1
- package/dist/extensions.d.ts +0 -130
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -78
- package/dist/extensions.js.map +0 -1
- package/dist/index.d.ts +0 -12
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -47
- package/dist/index.js.map +0 -1
- package/dist/loader.d.ts +0 -169
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -237
- package/dist/loader.js.map +0 -1
- package/dist/resolver-context.d.ts +0 -154
- package/dist/resolver-context.d.ts.map +0 -1
- package/dist/resolver-context.js +0 -184
- package/dist/resolver-context.js.map +0 -1
- package/dist/schema-mapping.d.ts +0 -30
- package/dist/schema-mapping.d.ts.map +0 -1
- package/dist/schema-mapping.js +0 -280
- package/dist/schema-mapping.js.map +0 -1
- package/dist/server/cache-control.d.ts +0 -96
- package/dist/server/cache-control.d.ts.map +0 -1
- package/dist/server/cache-control.js +0 -308
- package/dist/server/cache-control.js.map +0 -1
- package/dist/server/complexity.d.ts +0 -165
- package/dist/server/complexity.d.ts.map +0 -1
- package/dist/server/complexity.js +0 -433
- package/dist/server/complexity.js.map +0 -1
- package/dist/server/config.d.ts +0 -66
- package/dist/server/config.d.ts.map +0 -1
- package/dist/server/config.js +0 -104
- package/dist/server/config.js.map +0 -1
- package/dist/server/graphiql.d.ts +0 -5
- package/dist/server/graphiql.d.ts.map +0 -1
- package/dist/server/graphiql.js +0 -43
- package/dist/server/graphiql.js.map +0 -1
- package/dist/server/index.d.ts +0 -18
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -48
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.d.ts +0 -79
- package/dist/server/router.d.ts.map +0 -1
- package/dist/server/router.js +0 -232
- package/dist/server/router.js.map +0 -1
- package/dist/server/schema-builder-extensions.d.ts +0 -42
- package/dist/server/schema-builder-extensions.d.ts.map +0 -1
- package/dist/server/schema-builder-extensions.js +0 -48
- package/dist/server/schema-builder-extensions.js.map +0 -1
- package/dist/server/sse-adapter.d.ts +0 -64
- package/dist/server/sse-adapter.d.ts.map +0 -1
- package/dist/server/sse-adapter.js +0 -227
- package/dist/server/sse-adapter.js.map +0 -1
- package/dist/server/sse-types.d.ts +0 -192
- package/dist/server/sse-types.d.ts.map +0 -1
- package/dist/server/sse-types.js +0 -63
- package/dist/server/sse-types.js.map +0 -1
- package/dist/server/ws-adapter.d.ts +0 -39
- package/dist/server/ws-adapter.d.ts.map +0 -1
- package/dist/server/ws-adapter.js +0 -247
- package/dist/server/ws-adapter.js.map +0 -1
- package/dist/server/ws-types.d.ts +0 -169
- package/dist/server/ws-types.d.ts.map +0 -1
- package/dist/server/ws-types.js +0 -11
- package/dist/server/ws-types.js.map +0 -1
- package/dist/server/ws-utils.d.ts +0 -42
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/ws-utils.js +0 -99
- package/dist/server/ws-utils.js.map +0 -1
- package/src/analyzer-extension.ts +0 -254
- package/src/builder/execute.ts +0 -153
- package/src/builder/field-builders.ts +0 -322
- package/src/builder/index.ts +0 -48
- package/src/builder/pipe-api.ts +0 -312
- package/src/builder/schema-builder.ts +0 -970
- package/src/builder/type-registry.ts +0 -670
- package/src/builder/types.ts +0 -305
- package/src/context.ts +0 -23
- package/src/error.ts +0 -32
- package/src/extensions.ts +0 -240
- package/src/index.ts +0 -32
- package/src/loader.ts +0 -363
- package/src/resolver-context.ts +0 -253
- package/src/schema-mapping.ts +0 -307
- package/src/server/cache-control.ts +0 -590
- package/src/server/complexity.ts +0 -774
- package/src/server/config.ts +0 -174
- package/src/server/graphiql.ts +0 -38
- package/src/server/index.ts +0 -96
- package/src/server/router.ts +0 -432
- package/src/server/schema-builder-extensions.ts +0 -51
- package/src/server/sse-adapter.ts +0 -327
- package/src/server/sse-types.ts +0 -234
- package/src/server/ws-adapter.ts +0 -355
- package/src/server/ws-types.ts +0 -192
- package/src/server/ws-utils.ts +0 -136
package/src/server/ws-adapter.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/ws-types.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/ws-utils.ts
DELETED
|
@@ -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
|
-
}
|