@effect-gql/bun 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/src/serve.ts DELETED
@@ -1,182 +0,0 @@
1
- import { Effect, Layer } from "effect"
2
- import { HttpApp, HttpRouter, HttpServer } from "@effect/platform"
3
- import { BunHttpServer, BunRuntime } from "@effect/platform-bun"
4
- import type { GraphQLSchema } from "graphql"
5
- import type { GraphQLWSOptions } from "@effect-gql/core"
6
-
7
- /**
8
- * Configuration for WebSocket subscriptions
9
- */
10
- export interface SubscriptionsConfig<R> extends GraphQLWSOptions<R> {
11
- /**
12
- * The GraphQL schema (required for subscriptions).
13
- * Must be the same schema used to create the router.
14
- */
15
- readonly schema: GraphQLSchema
16
- /**
17
- * Path for WebSocket connections.
18
- * @default "/graphql"
19
- */
20
- readonly path?: string
21
- }
22
-
23
- /**
24
- * Options for the Bun GraphQL server
25
- */
26
- export interface ServeOptions<R = never> {
27
- /** Port to listen on (default: 4000) */
28
- readonly port?: number
29
- /** Hostname to bind to (default: "0.0.0.0") */
30
- readonly host?: string
31
- /** Callback when server starts */
32
- readonly onStart?: (url: string) => void
33
- /**
34
- * Enable WebSocket subscriptions.
35
- * When provided, the server will handle WebSocket upgrade requests
36
- * for GraphQL subscriptions using the graphql-ws protocol.
37
- */
38
- readonly subscriptions?: SubscriptionsConfig<R>
39
- }
40
-
41
- /**
42
- * Start a Bun HTTP server with the given router.
43
- *
44
- * This is the main entry point for running a GraphQL server on Bun.
45
- * It handles all the Effect runtime setup and server lifecycle.
46
- *
47
- * @param router - The HttpRouter to serve (typically from makeGraphQLRouter or toRouter)
48
- * @param layer - Layer providing the router's service dependencies
49
- * @param options - Server configuration options
50
- *
51
- * @example
52
- * ```typescript
53
- * import { makeGraphQLRouter } from "@effect-gql/core"
54
- * import { serve } from "@effect-gql/bun"
55
- *
56
- * const schema = GraphQLSchemaBuilder.empty
57
- * .query("hello", { type: S.String, resolve: () => Effect.succeed("world") })
58
- * .buildSchema()
59
- *
60
- * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
61
- *
62
- * // Without subscriptions
63
- * serve(router, serviceLayer, {
64
- * port: 4000,
65
- * onStart: (url) => console.log(`Server running at ${url}`)
66
- * })
67
- *
68
- * // With subscriptions
69
- * serve(router, serviceLayer, {
70
- * port: 4000,
71
- * subscriptions: { schema },
72
- * onStart: (url) => console.log(`Server running at ${url}`)
73
- * })
74
- * ```
75
- */
76
- export const serve = <E, R, RE>(
77
- router: HttpRouter.HttpRouter<E, R>,
78
- layer: Layer.Layer<R, RE>,
79
- options: ServeOptions<R> = {}
80
- ): void => {
81
- const { port = 4000, host = "0.0.0.0", onStart, subscriptions } = options
82
-
83
- if (subscriptions) {
84
- // With WebSocket subscriptions - use Bun.serve() directly
85
- serveWithSubscriptions(router, layer, port, host, subscriptions, onStart)
86
- } else {
87
- // Without subscriptions - use the standard Effect approach
88
- const app = router.pipe(
89
- Effect.catchAllCause((cause) => Effect.die(cause)),
90
- HttpServer.serve()
91
- )
92
-
93
- const serverLayer = BunHttpServer.layer({ port })
94
- const fullLayer = Layer.merge(serverLayer, layer)
95
-
96
- if (onStart) {
97
- onStart(`http://${host === "0.0.0.0" ? "localhost" : host}:${port}`)
98
- }
99
-
100
- BunRuntime.runMain(Layer.launch(Layer.provide(app, fullLayer)))
101
- }
102
- }
103
-
104
- /**
105
- * Internal implementation for serving with WebSocket subscriptions.
106
- * Uses Bun.serve() directly to enable WebSocket support.
107
- */
108
- function serveWithSubscriptions<E, R, RE>(
109
- router: HttpRouter.HttpRouter<E, R>,
110
- layer: Layer.Layer<R, RE>,
111
- port: number,
112
- host: string,
113
- subscriptions: SubscriptionsConfig<R>,
114
- onStart?: (url: string) => void
115
- ): void {
116
- // Dynamically import ws module to keep it optional
117
- const importWs = Effect.tryPromise({
118
- try: () => import("./ws"),
119
- catch: (error) => error as Error,
120
- })
121
-
122
- Effect.runPromise(
123
- importWs.pipe(
124
- Effect.catchAll((error) =>
125
- Effect.logError("Failed to load WebSocket support", error).pipe(
126
- Effect.andThen(Effect.sync(() => process.exit(1))),
127
- Effect.andThen(Effect.fail(error))
128
- )
129
- )
130
- )
131
- ).then(({ createBunWSHandlers }) => {
132
- // Create the web handler from the Effect router
133
- const { handler } = HttpApp.toWebHandlerLayer(router, layer)
134
-
135
- // Create WebSocket handlers
136
- const { upgrade, websocket } = createBunWSHandlers(
137
- subscriptions.schema,
138
- layer as Layer.Layer<R>,
139
- {
140
- path: subscriptions.path,
141
- complexity: subscriptions.complexity,
142
- fieldComplexities: subscriptions.fieldComplexities,
143
- onConnect: subscriptions.onConnect,
144
- onDisconnect: subscriptions.onDisconnect,
145
- onSubscribe: subscriptions.onSubscribe,
146
- onComplete: subscriptions.onComplete,
147
- onError: subscriptions.onError,
148
- }
149
- )
150
-
151
- // Start Bun server with WebSocket support
152
- const server = Bun.serve({
153
- port,
154
- hostname: host,
155
- fetch: async (request, server) => {
156
- // Try WebSocket upgrade first
157
- if (upgrade(request, server)) {
158
- return new Response(null, { status: 101 })
159
- }
160
-
161
- // Handle HTTP requests
162
- return handler(request)
163
- },
164
- websocket,
165
- })
166
-
167
- if (onStart) {
168
- onStart(`http://${host === "0.0.0.0" ? "localhost" : host}:${port}`)
169
- }
170
-
171
- // Handle shutdown
172
- process.on("SIGINT", () => {
173
- server.stop()
174
- process.exit(0)
175
- })
176
-
177
- process.on("SIGTERM", () => {
178
- server.stop()
179
- process.exit(0)
180
- })
181
- })
182
- }
package/src/sse.ts DELETED
@@ -1,182 +0,0 @@
1
- import { Effect, Layer, Stream } from "effect"
2
- import { GraphQLSchema } from "graphql"
3
- import {
4
- makeGraphQLSSEHandler,
5
- formatSSEMessage,
6
- SSE_HEADERS,
7
- type GraphQLSSEOptions,
8
- type SSESubscriptionRequest,
9
- } from "@effect-gql/core"
10
-
11
- /**
12
- * Options for Bun SSE handler
13
- */
14
- export interface BunSSEOptions<R> extends GraphQLSSEOptions<R> {
15
- /**
16
- * Path for SSE connections.
17
- * @default "/graphql/stream"
18
- */
19
- readonly path?: string
20
- }
21
-
22
- /**
23
- * Create an SSE handler for Bun.serve().
24
- *
25
- * This function creates a handler that returns a streaming Response for SSE
26
- * subscription requests. It's designed to integrate with Bun.serve()'s fetch handler.
27
- *
28
- * @param schema - The GraphQL schema with subscription definitions
29
- * @param layer - Effect layer providing services required by resolvers
30
- * @param options - Optional lifecycle hooks and configuration
31
- * @returns A function that handles SSE requests and returns a Response
32
- *
33
- * @example
34
- * ```typescript
35
- * const sseHandler = createBunSSEHandler(schema, serviceLayer, {
36
- * path: "/graphql/stream",
37
- * })
38
- *
39
- * Bun.serve({
40
- * port: 4000,
41
- * fetch(req, server) {
42
- * const url = new URL(req.url)
43
- *
44
- * // Handle SSE subscriptions
45
- * if (url.pathname === "/graphql/stream" && req.method === "POST") {
46
- * return sseHandler(req)
47
- * }
48
- *
49
- * // Handle other requests...
50
- * },
51
- * })
52
- * ```
53
- */
54
- export const createBunSSEHandler = <R>(
55
- schema: GraphQLSchema,
56
- layer: Layer.Layer<R>,
57
- options?: BunSSEOptions<R>
58
- ): ((request: Request) => Promise<Response>) => {
59
- const sseHandler = makeGraphQLSSEHandler(schema, layer, options)
60
-
61
- return async (request: Request): Promise<Response> => {
62
- // Check Accept header for SSE support
63
- const accept = request.headers.get("accept") ?? ""
64
- if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
65
- return new Response(
66
- JSON.stringify({
67
- errors: [{ message: "Client must accept text/event-stream" }],
68
- }),
69
- { status: 406, headers: { "Content-Type": "application/json" } }
70
- )
71
- }
72
-
73
- // Read and parse the request body
74
- let subscriptionRequest: SSESubscriptionRequest
75
- try {
76
- const body = (await request.json()) as Record<string, unknown>
77
- if (typeof body.query !== "string") {
78
- throw new Error("Missing query")
79
- }
80
- subscriptionRequest = {
81
- query: body.query,
82
- variables: body.variables as Record<string, unknown> | undefined,
83
- operationName: body.operationName as string | undefined,
84
- extensions: body.extensions as Record<string, unknown> | undefined,
85
- }
86
- } catch {
87
- return new Response(
88
- JSON.stringify({
89
- errors: [{ message: "Invalid GraphQL request body" }],
90
- }),
91
- { status: 400, headers: { "Content-Type": "application/json" } }
92
- )
93
- }
94
-
95
- // Get the event stream
96
- const eventStream = sseHandler(subscriptionRequest, request.headers)
97
-
98
- // Create a ReadableStream from the Effect Stream
99
- const readableStream = new ReadableStream({
100
- async start(controller) {
101
- const encoder = new TextEncoder()
102
-
103
- await Effect.runPromise(
104
- Stream.runForEach(eventStream, (event) =>
105
- Effect.sync(() => {
106
- const message = formatSSEMessage(event)
107
- controller.enqueue(encoder.encode(message))
108
- })
109
- ).pipe(
110
- Effect.catchAll((error) => Effect.logWarning("SSE stream error", error)),
111
- Effect.ensuring(Effect.sync(() => controller.close()))
112
- )
113
- )
114
- },
115
- })
116
-
117
- return new Response(readableStream, {
118
- status: 200,
119
- headers: SSE_HEADERS,
120
- })
121
- }
122
- }
123
-
124
- /**
125
- * Create SSE handlers that integrate with Bun.serve().
126
- *
127
- * This returns an object with methods to check if a request should be
128
- * handled as SSE and to handle it.
129
- *
130
- * @param schema - The GraphQL schema with subscription definitions
131
- * @param layer - Effect layer providing services required by resolvers
132
- * @param options - Optional lifecycle hooks and configuration
133
- *
134
- * @example
135
- * ```typescript
136
- * const { upgrade: wsUpgrade, websocket } = createBunWSHandlers(schema, layer)
137
- * const sse = createBunSSEHandlers(schema, layer)
138
- *
139
- * Bun.serve({
140
- * port: 4000,
141
- * fetch(req, server) {
142
- * // Try WebSocket upgrade first
143
- * if (wsUpgrade(req, server)) {
144
- * return
145
- * }
146
- *
147
- * // Try SSE subscriptions
148
- * if (sse.shouldHandle(req)) {
149
- * return sse.handle(req)
150
- * }
151
- *
152
- * // Handle other requests...
153
- * },
154
- * websocket,
155
- * })
156
- * ```
157
- */
158
- export const createBunSSEHandlers = <R>(
159
- schema: GraphQLSchema,
160
- layer: Layer.Layer<R>,
161
- options?: BunSSEOptions<R>
162
- ): {
163
- /** Path this SSE handler responds to */
164
- readonly path: string
165
- /** Check if a request should be handled as SSE */
166
- shouldHandle: (request: Request) => boolean
167
- /** Handle an SSE request */
168
- handle: (request: Request) => Promise<Response>
169
- } => {
170
- const path = options?.path ?? "/graphql/stream"
171
- const handler = createBunSSEHandler(schema, layer, options)
172
-
173
- return {
174
- path,
175
- shouldHandle: (request: Request) => {
176
- if (request.method !== "POST") return false
177
- const url = new URL(request.url)
178
- return url.pathname === path
179
- },
180
- handle: handler,
181
- }
182
- }
package/src/ws.ts DELETED
@@ -1,247 +0,0 @@
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
- })