@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/dist/ws.js ADDED
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toBunEffectWebSocket = exports.createBunWSHandlers = void 0;
4
+ const effect_1 = require("effect");
5
+ const core_1 = require("@effect-gql/core");
6
+ /**
7
+ * Create WebSocket handlers for Bun.serve().
8
+ *
9
+ * Bun has built-in WebSocket support that's configured as part of Bun.serve().
10
+ * This function returns the handlers needed to integrate GraphQL subscriptions.
11
+ *
12
+ * @param schema - The GraphQL schema with subscription definitions
13
+ * @param layer - Effect layer providing services required by resolvers
14
+ * @param options - Optional configuration and lifecycle hooks
15
+ * @returns Object containing upgrade check and WebSocket handlers
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const { upgrade, websocket } = createBunWSHandlers(schema, serviceLayer)
20
+ *
21
+ * Bun.serve({
22
+ * port: 4000,
23
+ * fetch(req, server) {
24
+ * // Try WebSocket upgrade first
25
+ * if (upgrade(req, server)) {
26
+ * return // Upgraded to WebSocket
27
+ * }
28
+ * // Handle HTTP requests...
29
+ * },
30
+ * websocket,
31
+ * })
32
+ * ```
33
+ */
34
+ const createBunWSHandlers = (schema, layer, options) => {
35
+ const path = options?.path ?? "/graphql";
36
+ const handler = (0, core_1.makeGraphQLWSHandler)(schema, layer, options);
37
+ // Track active connection handlers for cleanup
38
+ const activeHandlers = new Map();
39
+ const upgrade = (request, server) => {
40
+ const url = new URL(request.url);
41
+ // Check if this is a WebSocket upgrade request for the GraphQL path
42
+ if (url.pathname !== path) {
43
+ return false;
44
+ }
45
+ const upgradeHeader = request.headers.get("upgrade");
46
+ if (upgradeHeader?.toLowerCase() !== "websocket") {
47
+ return false;
48
+ }
49
+ // Check for correct subprotocol
50
+ const protocol = request.headers.get("sec-websocket-protocol");
51
+ if (!protocol?.includes("graphql-transport-ws")) {
52
+ return false;
53
+ }
54
+ // Perform upgrade - data will be set in open handler
55
+ const success = server.upgrade(request, {
56
+ data: {}, // Will be populated in open handler
57
+ });
58
+ return success;
59
+ };
60
+ const websocket = {
61
+ open: (ws) => {
62
+ // Create Effect-based socket wrapper
63
+ const setupEffect = effect_1.Effect.gen(function* () {
64
+ const messageQueue = yield* effect_1.Queue.unbounded();
65
+ const closedDeferred = yield* effect_1.Deferred.make();
66
+ const effectSocket = {
67
+ protocol: ws.data?.effectSocket?.protocol || "graphql-transport-ws",
68
+ send: (data) => effect_1.Effect.try({
69
+ try: () => {
70
+ ws.send(data);
71
+ },
72
+ catch: (error) => new core_1.WebSocketError({ cause: error }),
73
+ }),
74
+ close: (code, reason) => effect_1.Effect.sync(() => {
75
+ ws.close(code ?? 1000, reason ?? "");
76
+ }),
77
+ messages: effect_1.Stream.fromQueue(messageQueue).pipe(effect_1.Stream.catchAll(() => effect_1.Stream.empty)),
78
+ closed: effect_1.Deferred.await(closedDeferred),
79
+ };
80
+ // Store in WebSocket data
81
+ ws.data = {
82
+ messageQueue,
83
+ closedDeferred,
84
+ effectSocket,
85
+ };
86
+ return effectSocket;
87
+ });
88
+ // Run setup and handler
89
+ const handlerPromise = effect_1.Effect.runPromise(setupEffect.pipe(effect_1.Effect.flatMap((effectSocket) => handler(effectSocket)), effect_1.Effect.catchAllCause(() => effect_1.Effect.void)));
90
+ activeHandlers.set(ws, handlerPromise);
91
+ },
92
+ message: (ws, message) => {
93
+ const data = ws.data;
94
+ if (data?.messageQueue) {
95
+ const messageStr = typeof message === "string" ? message : message.toString();
96
+ effect_1.Effect.runPromise(effect_1.Queue.offer(data.messageQueue, messageStr)).catch(() => {
97
+ // Queue might be shutdown
98
+ });
99
+ }
100
+ },
101
+ close: (ws, code, reason) => {
102
+ const data = ws.data;
103
+ if (data) {
104
+ effect_1.Effect.runPromise(effect_1.Effect.all([
105
+ effect_1.Queue.shutdown(data.messageQueue),
106
+ effect_1.Deferred.succeed(data.closedDeferred, { code, reason }),
107
+ ])).catch(() => {
108
+ // Already completed
109
+ });
110
+ }
111
+ activeHandlers.delete(ws);
112
+ },
113
+ error: (ws, error) => {
114
+ const data = ws.data;
115
+ if (data) {
116
+ effect_1.Effect.runPromise(effect_1.Deferred.fail(data.closedDeferred, new core_1.WebSocketError({ cause: error }))).catch(() => {
117
+ // Already completed
118
+ });
119
+ }
120
+ },
121
+ };
122
+ return { upgrade, websocket };
123
+ };
124
+ exports.createBunWSHandlers = createBunWSHandlers;
125
+ /**
126
+ * Convert a Bun ServerWebSocket to an EffectWebSocket.
127
+ *
128
+ * This is a lower-level utility for custom WebSocket handling.
129
+ * Most users should use createBunWSHandlers() instead.
130
+ *
131
+ * @param ws - The Bun ServerWebSocket instance
132
+ * @returns An EffectWebSocket that can be used with makeGraphQLWSHandler
133
+ */
134
+ const toBunEffectWebSocket = (ws) => effect_1.Effect.gen(function* () {
135
+ const messageQueue = yield* effect_1.Queue.unbounded();
136
+ const closedDeferred = yield* effect_1.Deferred.make();
137
+ const effectSocket = {
138
+ protocol: "graphql-transport-ws",
139
+ send: (data) => effect_1.Effect.try({
140
+ try: () => {
141
+ ws.send(data);
142
+ },
143
+ catch: (error) => new core_1.WebSocketError({ cause: error }),
144
+ }),
145
+ close: (code, reason) => effect_1.Effect.sync(() => {
146
+ ws.close(code ?? 1000, reason ?? "");
147
+ }),
148
+ messages: effect_1.Stream.fromQueue(messageQueue).pipe(effect_1.Stream.catchAll(() => effect_1.Stream.empty)),
149
+ closed: effect_1.Deferred.await(closedDeferred),
150
+ };
151
+ // Store in WebSocket data for event handlers
152
+ ws.data = {
153
+ messageQueue,
154
+ closedDeferred,
155
+ effectSocket,
156
+ };
157
+ return effectSocket;
158
+ });
159
+ exports.toBunEffectWebSocket = toBunEffectWebSocket;
160
+ //# sourceMappingURL=ws.js.map
package/dist/ws.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@effect-gql/bun",
3
+ "version": "0.1.0",
4
+ "description": "Bun HTTP server integration for @effect-gql/core",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
17
+ "peerDependencies": {
18
+ "@effect-gql/core": "^0.1.0",
19
+ "@effect/platform": "^0.94.0",
20
+ "@effect/platform-bun": "^0.87.0",
21
+ "effect": "^3.19.0",
22
+ "graphql": "^16.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@effect-gql/core": "*",
26
+ "@effect/platform": "^0.94.0",
27
+ "@effect/platform-bun": "^0.87.0",
28
+ "@types/bun": "latest",
29
+ "effect": "^3.19.13",
30
+ "graphql": "^16.0.0",
31
+ "graphql-ws": "^6.0.6",
32
+ "ws": "^8.18.0",
33
+ "@types/ws": "^8.5.0"
34
+ },
35
+ "keywords": [
36
+ "effect",
37
+ "graphql",
38
+ "bun",
39
+ "http",
40
+ "server"
41
+ ],
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "dev": "tsc --watch",
46
+ "clean": "rm -rf dist",
47
+ "test": "vitest run",
48
+ "test:unit": "vitest run test/unit",
49
+ "test:integration": "vitest run test/integration",
50
+ "test:watch": "vitest"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { serve, type ServeOptions, type SubscriptionsConfig } from "./serve"
2
+
3
+ // WebSocket subscription support
4
+ export { createBunWSHandlers, toBunEffectWebSocket, type BunWSOptions } from "./ws"
5
+
6
+ // SSE (Server-Sent Events) subscription support
7
+ export { createBunSSEHandler, createBunSSEHandlers, type BunSSEOptions } from "./sse"
package/src/serve.ts ADDED
@@ -0,0 +1,182 @@
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 ADDED
@@ -0,0 +1,182 @@
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
+ }