@effect-gql/web 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 ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Nick Fisher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,56 @@
1
+ import { Context, Layer } from "effect";
2
+ import { HttpRouter } from "@effect/platform";
3
+ /**
4
+ * Result of creating a web handler
5
+ */
6
+ export interface WebHandler {
7
+ /**
8
+ * Handle a web standard Request and return a Response.
9
+ * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.
10
+ */
11
+ readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
12
+ /**
13
+ * Dispose of the handler and clean up resources.
14
+ * Call this when shutting down the worker.
15
+ */
16
+ readonly dispose: () => Promise<void>;
17
+ }
18
+ /**
19
+ * Create a web standard Request/Response handler from an HttpRouter.
20
+ *
21
+ * This is designed for Cloudflare Workers, Deno, and other WASM-based runtimes
22
+ * that use the Web standard fetch API.
23
+ *
24
+ * @param router - The HttpRouter to handle (typically from makeGraphQLRouter or toRouter)
25
+ * @param layer - Layer providing any services required by the router
26
+ * @returns A handler object with handler() and dispose() methods
27
+ *
28
+ * @example Cloudflare Workers
29
+ * ```typescript
30
+ * import { makeGraphQLRouter } from "@effect-gql/core"
31
+ * import { toHandler } from "@effect-gql/web"
32
+ * import { Layer } from "effect"
33
+ *
34
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
35
+ * const { handler } = toHandler(router, Layer.empty)
36
+ *
37
+ * export default {
38
+ * async fetch(request: Request) {
39
+ * return await handler(request)
40
+ * }
41
+ * }
42
+ * ```
43
+ *
44
+ * @example Deno
45
+ * ```typescript
46
+ * import { makeGraphQLRouter } from "@effect-gql/core"
47
+ * import { toHandler } from "@effect-gql/web"
48
+ *
49
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
50
+ * const { handler } = toHandler(router, Layer.empty)
51
+ *
52
+ * Deno.serve((request) => handler(request))
53
+ * ```
54
+ */
55
+ export declare const toHandler: <E, R, RE>(router: HttpRouter.HttpRouter<E, R>, layer: Layer.Layer<R, RE>) => WebHandler;
56
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,EAAW,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAEtD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE3F;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,EAAE,EAAE,EAChC,QAAQ,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EACnC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KACxB,UAEF,CAAA"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toHandler = void 0;
4
+ const platform_1 = require("@effect/platform");
5
+ /**
6
+ * Create a web standard Request/Response handler from an HttpRouter.
7
+ *
8
+ * This is designed for Cloudflare Workers, Deno, and other WASM-based runtimes
9
+ * that use the Web standard fetch API.
10
+ *
11
+ * @param router - The HttpRouter to handle (typically from makeGraphQLRouter or toRouter)
12
+ * @param layer - Layer providing any services required by the router
13
+ * @returns A handler object with handler() and dispose() methods
14
+ *
15
+ * @example Cloudflare Workers
16
+ * ```typescript
17
+ * import { makeGraphQLRouter } from "@effect-gql/core"
18
+ * import { toHandler } from "@effect-gql/web"
19
+ * import { Layer } from "effect"
20
+ *
21
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
22
+ * const { handler } = toHandler(router, Layer.empty)
23
+ *
24
+ * export default {
25
+ * async fetch(request: Request) {
26
+ * return await handler(request)
27
+ * }
28
+ * }
29
+ * ```
30
+ *
31
+ * @example Deno
32
+ * ```typescript
33
+ * import { makeGraphQLRouter } from "@effect-gql/core"
34
+ * import { toHandler } from "@effect-gql/web"
35
+ *
36
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
37
+ * const { handler } = toHandler(router, Layer.empty)
38
+ *
39
+ * Deno.serve((request) => handler(request))
40
+ * ```
41
+ */
42
+ const toHandler = (router, layer) => {
43
+ return platform_1.HttpApp.toWebHandlerLayer(router, layer);
44
+ };
45
+ exports.toHandler = toHandler;
46
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":";;;AACA,+CAAsD;AAmBtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACI,MAAM,SAAS,GAAG,CACvB,MAAmC,EACnC,KAAyB,EACb,EAAE;IACd,OAAO,kBAAO,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AACjD,CAAC,CAAA;AALY,QAAA,SAAS,aAKrB"}
@@ -0,0 +1,3 @@
1
+ export { toHandler, type WebHandler } from "./handler";
2
+ export { createSSEHandler, createSSEHandlers, type WebSSEOptions } from "./sse";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSSEHandlers = exports.createSSEHandler = exports.toHandler = void 0;
4
+ var handler_1 = require("./handler");
5
+ Object.defineProperty(exports, "toHandler", { enumerable: true, get: function () { return handler_1.toHandler; } });
6
+ // SSE (Server-Sent Events) subscription support
7
+ var sse_1 = require("./sse");
8
+ Object.defineProperty(exports, "createSSEHandler", { enumerable: true, get: function () { return sse_1.createSSEHandler; } });
9
+ Object.defineProperty(exports, "createSSEHandlers", { enumerable: true, get: function () { return sse_1.createSSEHandlers; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qCAAsD;AAA7C,oGAAA,SAAS,OAAA;AAElB,gDAAgD;AAChD,6BAA+E;AAAtE,uGAAA,gBAAgB,OAAA;AAAE,wGAAA,iBAAiB,OAAA"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { Layer } from "effect";
2
+ import { GraphQLSchema } from "graphql";
3
+ import { type GraphQLSSEOptions } from "@effect-gql/core";
4
+ /**
5
+ * Options for Web SSE handler
6
+ */
7
+ export interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {
8
+ /**
9
+ * Path for SSE connections.
10
+ * @default "/graphql/stream"
11
+ */
12
+ readonly path?: string;
13
+ }
14
+ /**
15
+ * Create an SSE handler for web standard environments.
16
+ *
17
+ * This handler is designed for Cloudflare Workers, Deno, and other runtimes
18
+ * that use the Web standard fetch API. It returns a streaming Response for
19
+ * SSE subscription requests.
20
+ *
21
+ * @param schema - The GraphQL schema with subscription definitions
22
+ * @param layer - Effect layer providing services required by resolvers
23
+ * @param options - Optional lifecycle hooks and configuration
24
+ * @returns A function that handles SSE requests and returns a Response
25
+ *
26
+ * @example Cloudflare Workers
27
+ * ```typescript
28
+ * import { toHandler } from "@effect-gql/web"
29
+ * import { createSSEHandler } from "@effect-gql/web"
30
+ *
31
+ * const graphqlHandler = toHandler(router, Layer.empty)
32
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
33
+ *
34
+ * export default {
35
+ * async fetch(request: Request) {
36
+ * const url = new URL(request.url)
37
+ *
38
+ * // Handle SSE subscriptions
39
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
40
+ * return await sseHandler(request)
41
+ * }
42
+ *
43
+ * // Handle regular GraphQL requests
44
+ * return await graphqlHandler.handler(request)
45
+ * }
46
+ * }
47
+ * ```
48
+ *
49
+ * @example Deno
50
+ * ```typescript
51
+ * import { toHandler, createSSEHandler } from "@effect-gql/web"
52
+ *
53
+ * const graphqlHandler = toHandler(router, Layer.empty)
54
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
55
+ *
56
+ * Deno.serve((request) => {
57
+ * const url = new URL(request.url)
58
+ *
59
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
60
+ * return sseHandler(request)
61
+ * }
62
+ *
63
+ * return graphqlHandler.handler(request)
64
+ * })
65
+ * ```
66
+ */
67
+ export declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => ((request: Request) => Promise<Response>);
68
+ /**
69
+ * Create SSE handlers with path matching for web standard environments.
70
+ *
71
+ * This returns an object with methods to check if a request should be
72
+ * handled as SSE and to handle it.
73
+ *
74
+ * @param schema - The GraphQL schema with subscription definitions
75
+ * @param layer - Effect layer providing services required by resolvers
76
+ * @param options - Optional lifecycle hooks and configuration
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const sse = createSSEHandlers(schema, Layer.empty)
81
+ *
82
+ * export default {
83
+ * async fetch(request: Request) {
84
+ * if (sse.shouldHandle(request)) {
85
+ * return sse.handle(request)
86
+ * }
87
+ * // Handle other requests...
88
+ * }
89
+ * }
90
+ * ```
91
+ */
92
+ export declare const createSSEHandlers: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => {
93
+ /** Path this SSE handler responds to */
94
+ readonly path: string;
95
+ /** Check if a request should be handled as SSE */
96
+ shouldHandle: (request: Request) => boolean;
97
+ /** Handle an SSE request */
98
+ handle: (request: Request) => Promise<Response>;
99
+ };
100
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAU,MAAM,QAAQ,CAAA;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAIL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAA;AAEzB;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,CAAE,SAAQ,iBAAiB,CAAC,CAAC,CAAC;IAC5D;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAChC,QAAQ,aAAa,EACrB,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,UAAU,aAAa,CAAC,CAAC,CAAC,KACzB,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAgE1C,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,QAAQ,aAAa,EACrB,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,UAAU,aAAa,CAAC,CAAC,CAAC,KACzB;IACD,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;IAC3C,4BAA4B;IAC5B,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAchD,CAAA"}
package/dist/sse.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSSEHandlers = exports.createSSEHandler = void 0;
4
+ const effect_1 = require("effect");
5
+ const core_1 = require("@effect-gql/core");
6
+ /**
7
+ * Create an SSE handler for web standard environments.
8
+ *
9
+ * This handler is designed for Cloudflare Workers, Deno, and other runtimes
10
+ * that use the Web standard fetch API. It returns a streaming Response for
11
+ * SSE subscription requests.
12
+ *
13
+ * @param schema - The GraphQL schema with subscription definitions
14
+ * @param layer - Effect layer providing services required by resolvers
15
+ * @param options - Optional lifecycle hooks and configuration
16
+ * @returns A function that handles SSE requests and returns a Response
17
+ *
18
+ * @example Cloudflare Workers
19
+ * ```typescript
20
+ * import { toHandler } from "@effect-gql/web"
21
+ * import { createSSEHandler } from "@effect-gql/web"
22
+ *
23
+ * const graphqlHandler = toHandler(router, Layer.empty)
24
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
25
+ *
26
+ * export default {
27
+ * async fetch(request: Request) {
28
+ * const url = new URL(request.url)
29
+ *
30
+ * // Handle SSE subscriptions
31
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
32
+ * return await sseHandler(request)
33
+ * }
34
+ *
35
+ * // Handle regular GraphQL requests
36
+ * return await graphqlHandler.handler(request)
37
+ * }
38
+ * }
39
+ * ```
40
+ *
41
+ * @example Deno
42
+ * ```typescript
43
+ * import { toHandler, createSSEHandler } from "@effect-gql/web"
44
+ *
45
+ * const graphqlHandler = toHandler(router, Layer.empty)
46
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
47
+ *
48
+ * Deno.serve((request) => {
49
+ * const url = new URL(request.url)
50
+ *
51
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
52
+ * return sseHandler(request)
53
+ * }
54
+ *
55
+ * return graphqlHandler.handler(request)
56
+ * })
57
+ * ```
58
+ */
59
+ const createSSEHandler = (schema, layer, options) => {
60
+ const sseHandler = (0, core_1.makeGraphQLSSEHandler)(schema, layer, options);
61
+ return async (request) => {
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(JSON.stringify({
66
+ errors: [{ message: "Client must accept text/event-stream" }],
67
+ }), { status: 406, headers: { "Content-Type": "application/json" } });
68
+ }
69
+ // Read and parse the request body
70
+ let subscriptionRequest;
71
+ try {
72
+ const body = (await request.json());
73
+ if (typeof body.query !== "string") {
74
+ throw new Error("Missing query");
75
+ }
76
+ subscriptionRequest = {
77
+ query: body.query,
78
+ variables: body.variables,
79
+ operationName: body.operationName,
80
+ extensions: body.extensions,
81
+ };
82
+ }
83
+ catch {
84
+ return new Response(JSON.stringify({
85
+ errors: [{ message: "Invalid GraphQL request body" }],
86
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
87
+ }
88
+ // Get the event stream
89
+ const eventStream = sseHandler(subscriptionRequest, request.headers);
90
+ // Create a ReadableStream from the Effect Stream
91
+ const readableStream = new ReadableStream({
92
+ async start(controller) {
93
+ const encoder = new TextEncoder();
94
+ await effect_1.Effect.runPromise(effect_1.Stream.runForEach(eventStream, (event) => effect_1.Effect.sync(() => {
95
+ const message = (0, core_1.formatSSEMessage)(event);
96
+ controller.enqueue(encoder.encode(message));
97
+ })).pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.logWarning("SSE stream error", error)), effect_1.Effect.ensuring(effect_1.Effect.sync(() => controller.close()))));
98
+ },
99
+ });
100
+ return new Response(readableStream, {
101
+ status: 200,
102
+ headers: core_1.SSE_HEADERS,
103
+ });
104
+ };
105
+ };
106
+ exports.createSSEHandler = createSSEHandler;
107
+ /**
108
+ * Create SSE handlers with path matching for web standard environments.
109
+ *
110
+ * This returns an object with methods to check if a request should be
111
+ * handled as SSE and to handle it.
112
+ *
113
+ * @param schema - The GraphQL schema with subscription definitions
114
+ * @param layer - Effect layer providing services required by resolvers
115
+ * @param options - Optional lifecycle hooks and configuration
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const sse = createSSEHandlers(schema, Layer.empty)
120
+ *
121
+ * export default {
122
+ * async fetch(request: Request) {
123
+ * if (sse.shouldHandle(request)) {
124
+ * return sse.handle(request)
125
+ * }
126
+ * // Handle other requests...
127
+ * }
128
+ * }
129
+ * ```
130
+ */
131
+ const createSSEHandlers = (schema, layer, options) => {
132
+ const path = options?.path ?? "/graphql/stream";
133
+ const handler = (0, exports.createSSEHandler)(schema, layer, options);
134
+ return {
135
+ path,
136
+ shouldHandle: (request) => {
137
+ if (request.method !== "POST")
138
+ return false;
139
+ const url = new URL(request.url);
140
+ return url.pathname === path;
141
+ },
142
+ handle: handler,
143
+ };
144
+ };
145
+ exports.createSSEHandlers = createSSEHandlers;
146
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@effect-gql/web",
3
+ "version": "0.1.0",
4
+ "description": "Web standard handler for @effect-gql/core - works with Cloudflare Workers, Deno, and other WASM runtimes",
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": "^3.19.0"
21
+ },
22
+ "devDependencies": {
23
+ "@effect-gql/core": "*",
24
+ "@effect/platform": "^0.94.0",
25
+ "effect": "^3.19.13",
26
+ "graphql": "^16.0.0"
27
+ },
28
+ "keywords": [
29
+ "effect",
30
+ "graphql",
31
+ "cloudflare",
32
+ "workers",
33
+ "wasm",
34
+ "web",
35
+ "deno"
36
+ ],
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "dev": "tsc --watch",
41
+ "clean": "rm -rf dist",
42
+ "test": "vitest run",
43
+ "test:unit": "vitest run test/unit",
44
+ "test:watch": "vitest"
45
+ }
46
+ }
package/src/handler.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { Context, Layer } from "effect"
2
+ import { HttpApp, HttpRouter } from "@effect/platform"
3
+
4
+ /**
5
+ * Result of creating a web handler
6
+ */
7
+ export interface WebHandler {
8
+ /**
9
+ * Handle a web standard Request and return a Response.
10
+ * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.
11
+ */
12
+ readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>
13
+
14
+ /**
15
+ * Dispose of the handler and clean up resources.
16
+ * Call this when shutting down the worker.
17
+ */
18
+ readonly dispose: () => Promise<void>
19
+ }
20
+
21
+ /**
22
+ * Create a web standard Request/Response handler from an HttpRouter.
23
+ *
24
+ * This is designed for Cloudflare Workers, Deno, and other WASM-based runtimes
25
+ * that use the Web standard fetch API.
26
+ *
27
+ * @param router - The HttpRouter to handle (typically from makeGraphQLRouter or toRouter)
28
+ * @param layer - Layer providing any services required by the router
29
+ * @returns A handler object with handler() and dispose() methods
30
+ *
31
+ * @example Cloudflare Workers
32
+ * ```typescript
33
+ * import { makeGraphQLRouter } from "@effect-gql/core"
34
+ * import { toHandler } from "@effect-gql/web"
35
+ * import { Layer } from "effect"
36
+ *
37
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
38
+ * const { handler } = toHandler(router, Layer.empty)
39
+ *
40
+ * export default {
41
+ * async fetch(request: Request) {
42
+ * return await handler(request)
43
+ * }
44
+ * }
45
+ * ```
46
+ *
47
+ * @example Deno
48
+ * ```typescript
49
+ * import { makeGraphQLRouter } from "@effect-gql/core"
50
+ * import { toHandler } from "@effect-gql/web"
51
+ *
52
+ * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
53
+ * const { handler } = toHandler(router, Layer.empty)
54
+ *
55
+ * Deno.serve((request) => handler(request))
56
+ * ```
57
+ */
58
+ export const toHandler = <E, R, RE>(
59
+ router: HttpRouter.HttpRouter<E, R>,
60
+ layer: Layer.Layer<R, RE>
61
+ ): WebHandler => {
62
+ return HttpApp.toWebHandlerLayer(router, layer)
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { toHandler, type WebHandler } from "./handler"
2
+
3
+ // SSE (Server-Sent Events) subscription support
4
+ export { createSSEHandler, createSSEHandlers, type WebSSEOptions } from "./sse"
package/src/sse.ts ADDED
@@ -0,0 +1,193 @@
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 Web SSE handler
13
+ */
14
+ export interface WebSSEOptions<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 web standard environments.
24
+ *
25
+ * This handler is designed for Cloudflare Workers, Deno, and other runtimes
26
+ * that use the Web standard fetch API. It returns a streaming Response for
27
+ * SSE subscription requests.
28
+ *
29
+ * @param schema - The GraphQL schema with subscription definitions
30
+ * @param layer - Effect layer providing services required by resolvers
31
+ * @param options - Optional lifecycle hooks and configuration
32
+ * @returns A function that handles SSE requests and returns a Response
33
+ *
34
+ * @example Cloudflare Workers
35
+ * ```typescript
36
+ * import { toHandler } from "@effect-gql/web"
37
+ * import { createSSEHandler } from "@effect-gql/web"
38
+ *
39
+ * const graphqlHandler = toHandler(router, Layer.empty)
40
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
41
+ *
42
+ * export default {
43
+ * async fetch(request: Request) {
44
+ * const url = new URL(request.url)
45
+ *
46
+ * // Handle SSE subscriptions
47
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
48
+ * return await sseHandler(request)
49
+ * }
50
+ *
51
+ * // Handle regular GraphQL requests
52
+ * return await graphqlHandler.handler(request)
53
+ * }
54
+ * }
55
+ * ```
56
+ *
57
+ * @example Deno
58
+ * ```typescript
59
+ * import { toHandler, createSSEHandler } from "@effect-gql/web"
60
+ *
61
+ * const graphqlHandler = toHandler(router, Layer.empty)
62
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
63
+ *
64
+ * Deno.serve((request) => {
65
+ * const url = new URL(request.url)
66
+ *
67
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
68
+ * return sseHandler(request)
69
+ * }
70
+ *
71
+ * return graphqlHandler.handler(request)
72
+ * })
73
+ * ```
74
+ */
75
+ export const createSSEHandler = <R>(
76
+ schema: GraphQLSchema,
77
+ layer: Layer.Layer<R>,
78
+ options?: WebSSEOptions<R>
79
+ ): ((request: Request) => Promise<Response>) => {
80
+ const sseHandler = makeGraphQLSSEHandler(schema, layer, options)
81
+
82
+ return async (request: Request): Promise<Response> => {
83
+ // Check Accept header for SSE support
84
+ const accept = request.headers.get("accept") ?? ""
85
+ if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
86
+ return new Response(
87
+ JSON.stringify({
88
+ errors: [{ message: "Client must accept text/event-stream" }],
89
+ }),
90
+ { status: 406, headers: { "Content-Type": "application/json" } }
91
+ )
92
+ }
93
+
94
+ // Read and parse the request body
95
+ let subscriptionRequest: SSESubscriptionRequest
96
+ try {
97
+ const body = (await request.json()) as Record<string, unknown>
98
+ if (typeof body.query !== "string") {
99
+ throw new Error("Missing query")
100
+ }
101
+ subscriptionRequest = {
102
+ query: body.query,
103
+ variables: body.variables as Record<string, unknown> | undefined,
104
+ operationName: body.operationName as string | undefined,
105
+ extensions: body.extensions as Record<string, unknown> | undefined,
106
+ }
107
+ } catch {
108
+ return new Response(
109
+ JSON.stringify({
110
+ errors: [{ message: "Invalid GraphQL request body" }],
111
+ }),
112
+ { status: 400, headers: { "Content-Type": "application/json" } }
113
+ )
114
+ }
115
+
116
+ // Get the event stream
117
+ const eventStream = sseHandler(subscriptionRequest, request.headers)
118
+
119
+ // Create a ReadableStream from the Effect Stream
120
+ const readableStream = new ReadableStream({
121
+ async start(controller) {
122
+ const encoder = new TextEncoder()
123
+
124
+ await Effect.runPromise(
125
+ Stream.runForEach(eventStream, (event) =>
126
+ Effect.sync(() => {
127
+ const message = formatSSEMessage(event)
128
+ controller.enqueue(encoder.encode(message))
129
+ })
130
+ ).pipe(
131
+ Effect.catchAll((error) => Effect.logWarning("SSE stream error", error)),
132
+ Effect.ensuring(Effect.sync(() => controller.close()))
133
+ )
134
+ )
135
+ },
136
+ })
137
+
138
+ return new Response(readableStream, {
139
+ status: 200,
140
+ headers: SSE_HEADERS,
141
+ })
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create SSE handlers with path matching for web standard environments.
147
+ *
148
+ * This returns an object with methods to check if a request should be
149
+ * handled as SSE and to handle it.
150
+ *
151
+ * @param schema - The GraphQL schema with subscription definitions
152
+ * @param layer - Effect layer providing services required by resolvers
153
+ * @param options - Optional lifecycle hooks and configuration
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const sse = createSSEHandlers(schema, Layer.empty)
158
+ *
159
+ * export default {
160
+ * async fetch(request: Request) {
161
+ * if (sse.shouldHandle(request)) {
162
+ * return sse.handle(request)
163
+ * }
164
+ * // Handle other requests...
165
+ * }
166
+ * }
167
+ * ```
168
+ */
169
+ export const createSSEHandlers = <R>(
170
+ schema: GraphQLSchema,
171
+ layer: Layer.Layer<R>,
172
+ options?: WebSSEOptions<R>
173
+ ): {
174
+ /** Path this SSE handler responds to */
175
+ readonly path: string
176
+ /** Check if a request should be handled as SSE */
177
+ shouldHandle: (request: Request) => boolean
178
+ /** Handle an SSE request */
179
+ handle: (request: Request) => Promise<Response>
180
+ } => {
181
+ const path = options?.path ?? "/graphql/stream"
182
+ const handler = createSSEHandler(schema, layer, options)
183
+
184
+ return {
185
+ path,
186
+ shouldHandle: (request: Request) => {
187
+ if (request.method !== "POST") return false
188
+ const url = new URL(request.url)
189
+ return url.pathname === path
190
+ },
191
+ handle: handler,
192
+ }
193
+ }