@effect-gql/bun 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/LICENSE +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/serve.d.ts +73 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +145 -0
- package/dist/serve.js.map +1 -0
- package/dist/sse.d.ts +89 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +135 -0
- package/dist/sse.js.map +1 -0
- package/dist/ws.d.ts +78 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +160 -0
- package/dist/ws.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +7 -0
- package/src/serve.ts +182 -0
- package/src/sse.ts +182 -0
- package/src/ws.ts +247 -0
package/src/ws.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Effect, Stream, Queue, Deferred, Layer } from "effect"
|
|
2
|
+
import { GraphQLSchema } from "graphql"
|
|
3
|
+
import {
|
|
4
|
+
makeGraphQLWSHandler,
|
|
5
|
+
type EffectWebSocket,
|
|
6
|
+
type GraphQLWSOptions,
|
|
7
|
+
WebSocketError,
|
|
8
|
+
type CloseEvent,
|
|
9
|
+
} from "@effect-gql/core"
|
|
10
|
+
import type { Server, ServerWebSocket } from "bun"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Data attached to each WebSocket connection
|
|
14
|
+
*/
|
|
15
|
+
interface WebSocketData {
|
|
16
|
+
messageQueue: Queue.Queue<string>
|
|
17
|
+
closedDeferred: Deferred.Deferred<CloseEvent, WebSocketError>
|
|
18
|
+
effectSocket: EffectWebSocket
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for Bun WebSocket server
|
|
23
|
+
*/
|
|
24
|
+
export interface BunWSOptions<R> extends GraphQLWSOptions<R> {
|
|
25
|
+
/**
|
|
26
|
+
* Path for WebSocket connections.
|
|
27
|
+
* @default "/graphql"
|
|
28
|
+
*/
|
|
29
|
+
readonly path?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create WebSocket handlers for Bun.serve().
|
|
34
|
+
*
|
|
35
|
+
* Bun has built-in WebSocket support that's configured as part of Bun.serve().
|
|
36
|
+
* This function returns the handlers needed to integrate GraphQL subscriptions.
|
|
37
|
+
*
|
|
38
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
39
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
40
|
+
* @param options - Optional configuration and lifecycle hooks
|
|
41
|
+
* @returns Object containing upgrade check and WebSocket handlers
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const { upgrade, websocket } = createBunWSHandlers(schema, serviceLayer)
|
|
46
|
+
*
|
|
47
|
+
* Bun.serve({
|
|
48
|
+
* port: 4000,
|
|
49
|
+
* fetch(req, server) {
|
|
50
|
+
* // Try WebSocket upgrade first
|
|
51
|
+
* if (upgrade(req, server)) {
|
|
52
|
+
* return // Upgraded to WebSocket
|
|
53
|
+
* }
|
|
54
|
+
* // Handle HTTP requests...
|
|
55
|
+
* },
|
|
56
|
+
* websocket,
|
|
57
|
+
* })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const createBunWSHandlers = <R>(
|
|
61
|
+
schema: GraphQLSchema,
|
|
62
|
+
layer: Layer.Layer<R>,
|
|
63
|
+
options?: BunWSOptions<R>
|
|
64
|
+
): {
|
|
65
|
+
/**
|
|
66
|
+
* Check if request should upgrade to WebSocket and perform upgrade.
|
|
67
|
+
* Returns true if upgraded, false otherwise.
|
|
68
|
+
*/
|
|
69
|
+
upgrade: (request: Request, server: Server<WebSocketData>) => boolean
|
|
70
|
+
/**
|
|
71
|
+
* WebSocket event handlers for Bun.serve()
|
|
72
|
+
*/
|
|
73
|
+
websocket: {
|
|
74
|
+
open: (ws: ServerWebSocket<WebSocketData>) => void
|
|
75
|
+
message: (ws: ServerWebSocket<WebSocketData>, message: string | Buffer) => void
|
|
76
|
+
close: (ws: ServerWebSocket<WebSocketData>, code: number, reason: string) => void
|
|
77
|
+
error: (ws: ServerWebSocket<WebSocketData>, error: Error) => void
|
|
78
|
+
}
|
|
79
|
+
} => {
|
|
80
|
+
const path = options?.path ?? "/graphql"
|
|
81
|
+
const handler = makeGraphQLWSHandler(schema, layer, options)
|
|
82
|
+
|
|
83
|
+
// Track active connection handlers for cleanup
|
|
84
|
+
const activeHandlers = new Map<ServerWebSocket<WebSocketData>, Promise<void>>()
|
|
85
|
+
|
|
86
|
+
const upgrade = (request: Request, server: Server<WebSocketData>): boolean => {
|
|
87
|
+
const url = new URL(request.url)
|
|
88
|
+
|
|
89
|
+
// Check if this is a WebSocket upgrade request for the GraphQL path
|
|
90
|
+
if (url.pathname !== path) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const upgradeHeader = request.headers.get("upgrade")
|
|
95
|
+
if (upgradeHeader?.toLowerCase() !== "websocket") {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for correct subprotocol
|
|
100
|
+
const protocol = request.headers.get("sec-websocket-protocol")
|
|
101
|
+
if (!protocol?.includes("graphql-transport-ws")) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Perform upgrade - data will be set in open handler
|
|
106
|
+
const success = server.upgrade(request, {
|
|
107
|
+
data: {} as WebSocketData, // Will be populated in open handler
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return success
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const websocket = {
|
|
114
|
+
open: (ws: ServerWebSocket<WebSocketData>) => {
|
|
115
|
+
// Create Effect-based socket wrapper
|
|
116
|
+
const setupEffect = Effect.gen(function* () {
|
|
117
|
+
const messageQueue = yield* Queue.unbounded<string>()
|
|
118
|
+
const closedDeferred = yield* Deferred.make<CloseEvent, WebSocketError>()
|
|
119
|
+
|
|
120
|
+
const effectSocket: EffectWebSocket = {
|
|
121
|
+
protocol: ws.data?.effectSocket?.protocol || "graphql-transport-ws",
|
|
122
|
+
|
|
123
|
+
send: (data: string) =>
|
|
124
|
+
Effect.try({
|
|
125
|
+
try: () => {
|
|
126
|
+
ws.send(data)
|
|
127
|
+
},
|
|
128
|
+
catch: (error) => new WebSocketError({ cause: error }),
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
close: (code?: number, reason?: string) =>
|
|
132
|
+
Effect.sync(() => {
|
|
133
|
+
ws.close(code ?? 1000, reason ?? "")
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
messages: Stream.fromQueue(messageQueue).pipe(Stream.catchAll(() => Stream.empty)),
|
|
137
|
+
|
|
138
|
+
closed: Deferred.await(closedDeferred),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Store in WebSocket data
|
|
142
|
+
ws.data = {
|
|
143
|
+
messageQueue,
|
|
144
|
+
closedDeferred,
|
|
145
|
+
effectSocket,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return effectSocket
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Run setup and handler
|
|
152
|
+
const handlerPromise = Effect.runPromise(
|
|
153
|
+
setupEffect.pipe(
|
|
154
|
+
Effect.flatMap((effectSocket) => handler(effectSocket)),
|
|
155
|
+
Effect.catchAllCause(() => Effect.void)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
activeHandlers.set(ws, handlerPromise)
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
message: (ws: ServerWebSocket<WebSocketData>, message: string | Buffer) => {
|
|
163
|
+
const data = ws.data as WebSocketData | undefined
|
|
164
|
+
if (data?.messageQueue) {
|
|
165
|
+
const messageStr = typeof message === "string" ? message : message.toString()
|
|
166
|
+
Effect.runPromise(Queue.offer(data.messageQueue, messageStr)).catch(() => {
|
|
167
|
+
// Queue might be shutdown
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
close: (ws: ServerWebSocket<WebSocketData>, code: number, reason: string) => {
|
|
173
|
+
const data = ws.data as WebSocketData | undefined
|
|
174
|
+
if (data) {
|
|
175
|
+
Effect.runPromise(
|
|
176
|
+
Effect.all([
|
|
177
|
+
Queue.shutdown(data.messageQueue),
|
|
178
|
+
Deferred.succeed(data.closedDeferred, { code, reason }),
|
|
179
|
+
])
|
|
180
|
+
).catch(() => {
|
|
181
|
+
// Already completed
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
activeHandlers.delete(ws)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
error: (ws: ServerWebSocket<WebSocketData>, error: Error) => {
|
|
188
|
+
const data = ws.data as WebSocketData | undefined
|
|
189
|
+
if (data) {
|
|
190
|
+
Effect.runPromise(
|
|
191
|
+
Deferred.fail(data.closedDeferred, new WebSocketError({ cause: error }))
|
|
192
|
+
).catch(() => {
|
|
193
|
+
// Already completed
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { upgrade, websocket }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Convert a Bun ServerWebSocket to an EffectWebSocket.
|
|
204
|
+
*
|
|
205
|
+
* This is a lower-level utility for custom WebSocket handling.
|
|
206
|
+
* Most users should use createBunWSHandlers() instead.
|
|
207
|
+
*
|
|
208
|
+
* @param ws - The Bun ServerWebSocket instance
|
|
209
|
+
* @returns An EffectWebSocket that can be used with makeGraphQLWSHandler
|
|
210
|
+
*/
|
|
211
|
+
export const toBunEffectWebSocket = (
|
|
212
|
+
ws: ServerWebSocket<WebSocketData>
|
|
213
|
+
): Effect.Effect<EffectWebSocket, never, never> =>
|
|
214
|
+
Effect.gen(function* () {
|
|
215
|
+
const messageQueue = yield* Queue.unbounded<string>()
|
|
216
|
+
const closedDeferred = yield* Deferred.make<CloseEvent, WebSocketError>()
|
|
217
|
+
|
|
218
|
+
const effectSocket: EffectWebSocket = {
|
|
219
|
+
protocol: "graphql-transport-ws",
|
|
220
|
+
|
|
221
|
+
send: (data: string) =>
|
|
222
|
+
Effect.try({
|
|
223
|
+
try: () => {
|
|
224
|
+
ws.send(data)
|
|
225
|
+
},
|
|
226
|
+
catch: (error) => new WebSocketError({ cause: error }),
|
|
227
|
+
}),
|
|
228
|
+
|
|
229
|
+
close: (code?: number, reason?: string) =>
|
|
230
|
+
Effect.sync(() => {
|
|
231
|
+
ws.close(code ?? 1000, reason ?? "")
|
|
232
|
+
}),
|
|
233
|
+
|
|
234
|
+
messages: Stream.fromQueue(messageQueue).pipe(Stream.catchAll(() => Stream.empty)),
|
|
235
|
+
|
|
236
|
+
closed: Deferred.await(closedDeferred),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Store in WebSocket data for event handlers
|
|
240
|
+
ws.data = {
|
|
241
|
+
messageQueue,
|
|
242
|
+
closedDeferred,
|
|
243
|
+
effectSocket,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return effectSocket
|
|
247
|
+
})
|