@effect-gql/web 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 ADDED
@@ -0,0 +1,100 @@
1
+ # Effect GraphQL
2
+
3
+ A GraphQL framework for Effect-TS that brings full type safety, composability, and functional programming to your GraphQL servers.
4
+
5
+ > **Note:** This is an experimental prototype exploring the integration between Effect Schema, Effect's service system, and GraphQL.
6
+
7
+ ## Features
8
+
9
+ - **Type-Safe End-to-End** - Define schemas once with Effect Schema, get TypeScript types and GraphQL types automatically
10
+ - **Effect-Powered Resolvers** - Resolvers are Effect programs with built-in error handling and service injection
11
+ - **Immutable Builder** - Fluent, pipe-able API for composing schemas from reusable parts
12
+ - **Service Integration** - Use Effect's Layer system for dependency injection
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @effect-gql/core effect graphql
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { Effect, Layer } from "effect"
24
+ import * as S from "effect/Schema"
25
+ import { GraphQLSchemaBuilder, execute } from "@effect-gql/core"
26
+
27
+ // Define your schema with Effect Schema
28
+ const UserSchema = S.Struct({
29
+ id: S.String,
30
+ name: S.String,
31
+ email: S.String,
32
+ })
33
+
34
+ // Build your GraphQL schema
35
+ const schema = GraphQLSchemaBuilder.empty
36
+ .objectType({ name: "User", schema: UserSchema })
37
+ .query("users", {
38
+ type: S.Array(UserSchema),
39
+ resolve: () => Effect.succeed([
40
+ { id: "1", name: "Alice", email: "alice@example.com" },
41
+ { id: "2", name: "Bob", email: "bob@example.com" },
42
+ ]),
43
+ })
44
+ .query("user", {
45
+ type: UserSchema,
46
+ args: S.Struct({ id: S.String }),
47
+ resolve: (args) => Effect.succeed({
48
+ id: args.id,
49
+ name: "Alice",
50
+ email: "alice@example.com",
51
+ }),
52
+ })
53
+ .buildSchema()
54
+
55
+ // Execute a query
56
+ const result = await Effect.runPromise(
57
+ execute(schema, Layer.empty)(`
58
+ query {
59
+ users { id name email }
60
+ }
61
+ `)
62
+ )
63
+ ```
64
+
65
+ ## Documentation
66
+
67
+ For full documentation, guides, and API reference, visit the [documentation site](https://nrf110.github.io/effect-gql/).
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ # Install dependencies
73
+ npm install
74
+
75
+ # Build the project
76
+ npm run build
77
+
78
+ # Run tests
79
+ npm test
80
+
81
+ # Development mode with watch
82
+ npm run dev
83
+ ```
84
+
85
+ ## Contributing
86
+
87
+ Contributions are welcome! Here's how you can help:
88
+
89
+ 1. **Report bugs** - Open an issue describing the problem and steps to reproduce
90
+ 2. **Suggest features** - Open an issue describing your idea
91
+ 3. **Submit PRs** - Fork the repo, make your changes, and open a pull request
92
+
93
+ Please ensure your code:
94
+ - Passes all existing tests (`npm test`)
95
+ - Includes tests for new functionality
96
+ - Follows the existing code style
97
+
98
+ ## License
99
+
100
+ MIT
package/index.cjs ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ var platform = require('@effect/platform');
4
+ var effect = require('effect');
5
+ var core = require('@effect-gql/core');
6
+
7
+ // src/handler.ts
8
+ var toHandler = (router, layer) => {
9
+ return platform.HttpApp.toWebHandlerLayer(router, layer);
10
+ };
11
+ var createSSEHandler = (schema, layer, options) => {
12
+ const sseHandler = core.makeGraphQLSSEHandler(schema, layer, options);
13
+ return async (request) => {
14
+ const accept = request.headers.get("accept") ?? "";
15
+ if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
16
+ return new Response(
17
+ JSON.stringify({
18
+ errors: [{ message: "Client must accept text/event-stream" }]
19
+ }),
20
+ { status: 406, headers: { "Content-Type": "application/json" } }
21
+ );
22
+ }
23
+ let subscriptionRequest;
24
+ try {
25
+ const body = await request.json();
26
+ if (typeof body.query !== "string") {
27
+ throw new Error("Missing query");
28
+ }
29
+ subscriptionRequest = {
30
+ query: body.query,
31
+ variables: body.variables,
32
+ operationName: body.operationName,
33
+ extensions: body.extensions
34
+ };
35
+ } catch {
36
+ return new Response(
37
+ JSON.stringify({
38
+ errors: [{ message: "Invalid GraphQL request body" }]
39
+ }),
40
+ { status: 400, headers: { "Content-Type": "application/json" } }
41
+ );
42
+ }
43
+ const eventStream = sseHandler(subscriptionRequest, request.headers);
44
+ const readableStream = new ReadableStream({
45
+ async start(controller) {
46
+ const encoder = new TextEncoder();
47
+ await effect.Effect.runPromise(
48
+ effect.Stream.runForEach(
49
+ eventStream,
50
+ (event) => effect.Effect.sync(() => {
51
+ const message = core.formatSSEMessage(event);
52
+ controller.enqueue(encoder.encode(message));
53
+ })
54
+ ).pipe(
55
+ effect.Effect.catchAll((error) => effect.Effect.logWarning("SSE stream error", error)),
56
+ effect.Effect.ensuring(effect.Effect.sync(() => controller.close()))
57
+ )
58
+ );
59
+ }
60
+ });
61
+ return new Response(readableStream, {
62
+ status: 200,
63
+ headers: core.SSE_HEADERS
64
+ });
65
+ };
66
+ };
67
+ var createSSEHandlers = (schema, layer, options) => {
68
+ const path = options?.path ?? "/graphql/stream";
69
+ const handler = createSSEHandler(schema, layer, options);
70
+ return {
71
+ path,
72
+ shouldHandle: (request) => {
73
+ if (request.method !== "POST") return false;
74
+ const url = new URL(request.url);
75
+ return url.pathname === path;
76
+ },
77
+ handle: handler
78
+ };
79
+ };
80
+
81
+ exports.createSSEHandler = createSSEHandler;
82
+ exports.createSSEHandlers = createSSEHandlers;
83
+ exports.toHandler = toHandler;
84
+ //# sourceMappingURL=index.cjs.map
85
+ //# sourceMappingURL=index.cjs.map
package/index.cjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handler.ts","../src/sse.ts"],"names":["HttpApp","makeGraphQLSSEHandler","Effect","Stream","formatSSEMessage","SSE_HEADERS"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Context, Layer } from \"effect\"\nimport { HttpApp, HttpRouter } from \"@effect/platform\"\n\n/**\n * Result of creating a web handler\n */\nexport interface WebHandler {\n /**\n * Handle a web standard Request and return a Response.\n * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.\n */\n readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>\n\n /**\n * Dispose of the handler and clean up resources.\n * Call this when shutting down the worker.\n */\n readonly dispose: () => Promise<void>\n}\n\n/**\n * Create a web standard Request/Response handler from an HttpRouter.\n *\n * This is designed for Cloudflare Workers, Deno, and other WASM-based runtimes\n * that use the Web standard fetch API.\n *\n * @param router - The HttpRouter to handle (typically from makeGraphQLRouter or toRouter)\n * @param layer - Layer providing any services required by the router\n * @returns A handler object with handler() and dispose() methods\n *\n * @example Cloudflare Workers\n * ```typescript\n * import { makeGraphQLRouter } from \"@effect-gql/core\"\n * import { toHandler } from \"@effect-gql/web\"\n * import { Layer } from \"effect\"\n *\n * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })\n * const { handler } = toHandler(router, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * return await handler(request)\n * }\n * }\n * ```\n *\n * @example Deno\n * ```typescript\n * import { makeGraphQLRouter } from \"@effect-gql/core\"\n * import { toHandler } from \"@effect-gql/web\"\n *\n * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })\n * const { handler } = toHandler(router, Layer.empty)\n *\n * Deno.serve((request) => handler(request))\n * ```\n */\nexport const toHandler = <E, R, RE>(\n router: HttpRouter.HttpRouter<E, R>,\n layer: Layer.Layer<R, RE>\n): WebHandler => {\n return HttpApp.toWebHandlerLayer(router, layer)\n}\n","import { Effect, Layer, Stream } from \"effect\"\nimport { GraphQLSchema } from \"graphql\"\nimport {\n makeGraphQLSSEHandler,\n formatSSEMessage,\n SSE_HEADERS,\n type GraphQLSSEOptions,\n type SSESubscriptionRequest,\n} from \"@effect-gql/core\"\n\n/**\n * Options for Web SSE handler\n */\nexport interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {\n /**\n * Path for SSE connections.\n * @default \"/graphql/stream\"\n */\n readonly path?: string\n}\n\n/**\n * Create an SSE handler for web standard environments.\n *\n * This handler is designed for Cloudflare Workers, Deno, and other runtimes\n * that use the Web standard fetch API. It returns a streaming Response for\n * SSE subscription requests.\n *\n * @param schema - The GraphQL schema with subscription definitions\n * @param layer - Effect layer providing services required by resolvers\n * @param options - Optional lifecycle hooks and configuration\n * @returns A function that handles SSE requests and returns a Response\n *\n * @example Cloudflare Workers\n * ```typescript\n * import { toHandler } from \"@effect-gql/web\"\n * import { createSSEHandler } from \"@effect-gql/web\"\n *\n * const graphqlHandler = toHandler(router, Layer.empty)\n * const sseHandler = createSSEHandler(schema, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * const url = new URL(request.url)\n *\n * // Handle SSE subscriptions\n * if (url.pathname === \"/graphql/stream\" && request.method === \"POST\") {\n * return await sseHandler(request)\n * }\n *\n * // Handle regular GraphQL requests\n * return await graphqlHandler.handler(request)\n * }\n * }\n * ```\n *\n * @example Deno\n * ```typescript\n * import { toHandler, createSSEHandler } from \"@effect-gql/web\"\n *\n * const graphqlHandler = toHandler(router, Layer.empty)\n * const sseHandler = createSSEHandler(schema, Layer.empty)\n *\n * Deno.serve((request) => {\n * const url = new URL(request.url)\n *\n * if (url.pathname === \"/graphql/stream\" && request.method === \"POST\") {\n * return sseHandler(request)\n * }\n *\n * return graphqlHandler.handler(request)\n * })\n * ```\n */\nexport const createSSEHandler = <R>(\n schema: GraphQLSchema,\n layer: Layer.Layer<R>,\n options?: WebSSEOptions<R>\n): ((request: Request) => Promise<Response>) => {\n const sseHandler = makeGraphQLSSEHandler(schema, layer, options)\n\n return async (request: Request): Promise<Response> => {\n // Check Accept header for SSE support\n const accept = request.headers.get(\"accept\") ?? \"\"\n if (!accept.includes(\"text/event-stream\") && !accept.includes(\"*/*\")) {\n return new Response(\n JSON.stringify({\n errors: [{ message: \"Client must accept text/event-stream\" }],\n }),\n { status: 406, headers: { \"Content-Type\": \"application/json\" } }\n )\n }\n\n // Read and parse the request body\n let subscriptionRequest: SSESubscriptionRequest\n try {\n const body = (await request.json()) as Record<string, unknown>\n if (typeof body.query !== \"string\") {\n throw new Error(\"Missing query\")\n }\n subscriptionRequest = {\n query: body.query,\n variables: body.variables as Record<string, unknown> | undefined,\n operationName: body.operationName as string | undefined,\n extensions: body.extensions as Record<string, unknown> | undefined,\n }\n } catch {\n return new Response(\n JSON.stringify({\n errors: [{ message: \"Invalid GraphQL request body\" }],\n }),\n { status: 400, headers: { \"Content-Type\": \"application/json\" } }\n )\n }\n\n // Get the event stream\n const eventStream = sseHandler(subscriptionRequest, request.headers)\n\n // Create a ReadableStream from the Effect Stream\n const readableStream = new ReadableStream({\n async start(controller) {\n const encoder = new TextEncoder()\n\n await Effect.runPromise(\n Stream.runForEach(eventStream, (event) =>\n Effect.sync(() => {\n const message = formatSSEMessage(event)\n controller.enqueue(encoder.encode(message))\n })\n ).pipe(\n Effect.catchAll((error) => Effect.logWarning(\"SSE stream error\", error)),\n Effect.ensuring(Effect.sync(() => controller.close()))\n )\n )\n },\n })\n\n return new Response(readableStream, {\n status: 200,\n headers: SSE_HEADERS,\n })\n }\n}\n\n/**\n * Create SSE handlers with path matching for web standard environments.\n *\n * This returns an object with methods to check if a request should be\n * handled as SSE and to handle it.\n *\n * @param schema - The GraphQL schema with subscription definitions\n * @param layer - Effect layer providing services required by resolvers\n * @param options - Optional lifecycle hooks and configuration\n *\n * @example\n * ```typescript\n * const sse = createSSEHandlers(schema, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * if (sse.shouldHandle(request)) {\n * return sse.handle(request)\n * }\n * // Handle other requests...\n * }\n * }\n * ```\n */\nexport const createSSEHandlers = <R>(\n schema: GraphQLSchema,\n layer: Layer.Layer<R>,\n options?: WebSSEOptions<R>\n): {\n /** Path this SSE handler responds to */\n readonly path: string\n /** Check if a request should be handled as SSE */\n shouldHandle: (request: Request) => boolean\n /** Handle an SSE request */\n handle: (request: Request) => Promise<Response>\n} => {\n const path = options?.path ?? \"/graphql/stream\"\n const handler = createSSEHandler(schema, layer, options)\n\n return {\n path,\n shouldHandle: (request: Request) => {\n if (request.method !== \"POST\") return false\n const url = new URL(request.url)\n return url.pathname === path\n },\n handle: handler,\n }\n}\n"]}
@@ -1,10 +1,66 @@
1
- import { Layer } from "effect";
2
- import { GraphQLSchema } from "graphql";
3
- import { type GraphQLSSEOptions } from "@effect-gql/core";
1
+ import { Layer, Context } from 'effect';
2
+ import { HttpRouter } from '@effect/platform';
3
+ import { GraphQLSchema } from 'graphql';
4
+ import { GraphQLSSEOptions } from '@effect-gql/core';
5
+
6
+ /**
7
+ * Result of creating a web handler
8
+ */
9
+ interface WebHandler {
10
+ /**
11
+ * Handle a web standard Request and return a Response.
12
+ * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.
13
+ */
14
+ readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
15
+ /**
16
+ * Dispose of the handler and clean up resources.
17
+ * Call this when shutting down the worker.
18
+ */
19
+ readonly dispose: () => Promise<void>;
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
+ declare const toHandler: <E, R, RE>(router: HttpRouter.HttpRouter<E, R>, layer: Layer.Layer<R, RE>) => WebHandler;
59
+
4
60
  /**
5
61
  * Options for Web SSE handler
6
62
  */
7
- export interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {
63
+ interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {
8
64
  /**
9
65
  * Path for SSE connections.
10
66
  * @default "/graphql/stream"
@@ -64,7 +120,7 @@ export interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {
64
120
  * })
65
121
  * ```
66
122
  */
67
- export declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => ((request: Request) => Promise<Response>);
123
+ declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => ((request: Request) => Promise<Response>);
68
124
  /**
69
125
  * Create SSE handlers with path matching for web standard environments.
70
126
  *
@@ -89,7 +145,7 @@ export declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.L
89
145
  * }
90
146
  * ```
91
147
  */
92
- export declare const createSSEHandlers: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => {
148
+ declare const createSSEHandlers: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => {
93
149
  /** Path this SSE handler responds to */
94
150
  readonly path: string;
95
151
  /** Check if a request should be handled as SSE */
@@ -97,4 +153,5 @@ export declare const createSSEHandlers: <R>(schema: GraphQLSchema, layer: Layer.
97
153
  /** Handle an SSE request */
98
154
  handle: (request: Request) => Promise<Response>;
99
155
  };
100
- //# sourceMappingURL=sse.d.ts.map
156
+
157
+ export { type WebHandler, type WebSSEOptions, createSSEHandler, createSSEHandlers, toHandler };
package/index.d.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Layer, Context } from 'effect';
2
+ import { HttpRouter } from '@effect/platform';
3
+ import { GraphQLSchema } from 'graphql';
4
+ import { GraphQLSSEOptions } from '@effect-gql/core';
5
+
6
+ /**
7
+ * Result of creating a web handler
8
+ */
9
+ interface WebHandler {
10
+ /**
11
+ * Handle a web standard Request and return a Response.
12
+ * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.
13
+ */
14
+ readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>;
15
+ /**
16
+ * Dispose of the handler and clean up resources.
17
+ * Call this when shutting down the worker.
18
+ */
19
+ readonly dispose: () => Promise<void>;
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
+ declare const toHandler: <E, R, RE>(router: HttpRouter.HttpRouter<E, R>, layer: Layer.Layer<R, RE>) => WebHandler;
59
+
60
+ /**
61
+ * Options for Web SSE handler
62
+ */
63
+ interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {
64
+ /**
65
+ * Path for SSE connections.
66
+ * @default "/graphql/stream"
67
+ */
68
+ readonly path?: string;
69
+ }
70
+ /**
71
+ * Create an SSE handler for web standard environments.
72
+ *
73
+ * This handler is designed for Cloudflare Workers, Deno, and other runtimes
74
+ * that use the Web standard fetch API. It returns a streaming Response for
75
+ * SSE subscription requests.
76
+ *
77
+ * @param schema - The GraphQL schema with subscription definitions
78
+ * @param layer - Effect layer providing services required by resolvers
79
+ * @param options - Optional lifecycle hooks and configuration
80
+ * @returns A function that handles SSE requests and returns a Response
81
+ *
82
+ * @example Cloudflare Workers
83
+ * ```typescript
84
+ * import { toHandler } from "@effect-gql/web"
85
+ * import { createSSEHandler } from "@effect-gql/web"
86
+ *
87
+ * const graphqlHandler = toHandler(router, Layer.empty)
88
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
89
+ *
90
+ * export default {
91
+ * async fetch(request: Request) {
92
+ * const url = new URL(request.url)
93
+ *
94
+ * // Handle SSE subscriptions
95
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
96
+ * return await sseHandler(request)
97
+ * }
98
+ *
99
+ * // Handle regular GraphQL requests
100
+ * return await graphqlHandler.handler(request)
101
+ * }
102
+ * }
103
+ * ```
104
+ *
105
+ * @example Deno
106
+ * ```typescript
107
+ * import { toHandler, createSSEHandler } from "@effect-gql/web"
108
+ *
109
+ * const graphqlHandler = toHandler(router, Layer.empty)
110
+ * const sseHandler = createSSEHandler(schema, Layer.empty)
111
+ *
112
+ * Deno.serve((request) => {
113
+ * const url = new URL(request.url)
114
+ *
115
+ * if (url.pathname === "/graphql/stream" && request.method === "POST") {
116
+ * return sseHandler(request)
117
+ * }
118
+ *
119
+ * return graphqlHandler.handler(request)
120
+ * })
121
+ * ```
122
+ */
123
+ declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => ((request: Request) => Promise<Response>);
124
+ /**
125
+ * Create SSE handlers with path matching for web standard environments.
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 sse = createSSEHandlers(schema, Layer.empty)
137
+ *
138
+ * export default {
139
+ * async fetch(request: Request) {
140
+ * if (sse.shouldHandle(request)) {
141
+ * return sse.handle(request)
142
+ * }
143
+ * // Handle other requests...
144
+ * }
145
+ * }
146
+ * ```
147
+ */
148
+ declare const createSSEHandlers: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: WebSSEOptions<R>) => {
149
+ /** Path this SSE handler responds to */
150
+ readonly path: string;
151
+ /** Check if a request should be handled as SSE */
152
+ shouldHandle: (request: Request) => boolean;
153
+ /** Handle an SSE request */
154
+ handle: (request: Request) => Promise<Response>;
155
+ };
156
+
157
+ export { type WebHandler, type WebSSEOptions, createSSEHandler, createSSEHandlers, toHandler };
package/index.js ADDED
@@ -0,0 +1,81 @@
1
+ import { HttpApp } from '@effect/platform';
2
+ import { Effect, Stream } from 'effect';
3
+ import { makeGraphQLSSEHandler, formatSSEMessage, SSE_HEADERS } from '@effect-gql/core';
4
+
5
+ // src/handler.ts
6
+ var toHandler = (router, layer) => {
7
+ return HttpApp.toWebHandlerLayer(router, layer);
8
+ };
9
+ var createSSEHandler = (schema, layer, options) => {
10
+ const sseHandler = makeGraphQLSSEHandler(schema, layer, options);
11
+ return async (request) => {
12
+ const accept = request.headers.get("accept") ?? "";
13
+ if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
14
+ return new Response(
15
+ JSON.stringify({
16
+ errors: [{ message: "Client must accept text/event-stream" }]
17
+ }),
18
+ { status: 406, headers: { "Content-Type": "application/json" } }
19
+ );
20
+ }
21
+ let subscriptionRequest;
22
+ try {
23
+ const body = await request.json();
24
+ if (typeof body.query !== "string") {
25
+ throw new Error("Missing query");
26
+ }
27
+ subscriptionRequest = {
28
+ query: body.query,
29
+ variables: body.variables,
30
+ operationName: body.operationName,
31
+ extensions: body.extensions
32
+ };
33
+ } catch {
34
+ return new Response(
35
+ JSON.stringify({
36
+ errors: [{ message: "Invalid GraphQL request body" }]
37
+ }),
38
+ { status: 400, headers: { "Content-Type": "application/json" } }
39
+ );
40
+ }
41
+ const eventStream = sseHandler(subscriptionRequest, request.headers);
42
+ const readableStream = new ReadableStream({
43
+ async start(controller) {
44
+ const encoder = new TextEncoder();
45
+ await Effect.runPromise(
46
+ Stream.runForEach(
47
+ eventStream,
48
+ (event) => Effect.sync(() => {
49
+ const message = formatSSEMessage(event);
50
+ controller.enqueue(encoder.encode(message));
51
+ })
52
+ ).pipe(
53
+ Effect.catchAll((error) => Effect.logWarning("SSE stream error", error)),
54
+ Effect.ensuring(Effect.sync(() => controller.close()))
55
+ )
56
+ );
57
+ }
58
+ });
59
+ return new Response(readableStream, {
60
+ status: 200,
61
+ headers: SSE_HEADERS
62
+ });
63
+ };
64
+ };
65
+ var createSSEHandlers = (schema, layer, options) => {
66
+ const path = options?.path ?? "/graphql/stream";
67
+ const handler = createSSEHandler(schema, layer, options);
68
+ return {
69
+ path,
70
+ shouldHandle: (request) => {
71
+ if (request.method !== "POST") return false;
72
+ const url = new URL(request.url);
73
+ return url.pathname === path;
74
+ },
75
+ handle: handler
76
+ };
77
+ };
78
+
79
+ export { createSSEHandler, createSSEHandlers, toHandler };
80
+ //# sourceMappingURL=index.js.map
81
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handler.ts","../src/sse.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Context, Layer } from \"effect\"\nimport { HttpApp, HttpRouter } from \"@effect/platform\"\n\n/**\n * Result of creating a web handler\n */\nexport interface WebHandler {\n /**\n * Handle a web standard Request and return a Response.\n * This is the main entry point for Cloudflare Workers, Deno, and other WASM runtimes.\n */\n readonly handler: (request: Request, context?: Context.Context<never>) => Promise<Response>\n\n /**\n * Dispose of the handler and clean up resources.\n * Call this when shutting down the worker.\n */\n readonly dispose: () => Promise<void>\n}\n\n/**\n * Create a web standard Request/Response handler from an HttpRouter.\n *\n * This is designed for Cloudflare Workers, Deno, and other WASM-based runtimes\n * that use the Web standard fetch API.\n *\n * @param router - The HttpRouter to handle (typically from makeGraphQLRouter or toRouter)\n * @param layer - Layer providing any services required by the router\n * @returns A handler object with handler() and dispose() methods\n *\n * @example Cloudflare Workers\n * ```typescript\n * import { makeGraphQLRouter } from \"@effect-gql/core\"\n * import { toHandler } from \"@effect-gql/web\"\n * import { Layer } from \"effect\"\n *\n * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })\n * const { handler } = toHandler(router, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * return await handler(request)\n * }\n * }\n * ```\n *\n * @example Deno\n * ```typescript\n * import { makeGraphQLRouter } from \"@effect-gql/core\"\n * import { toHandler } from \"@effect-gql/web\"\n *\n * const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })\n * const { handler } = toHandler(router, Layer.empty)\n *\n * Deno.serve((request) => handler(request))\n * ```\n */\nexport const toHandler = <E, R, RE>(\n router: HttpRouter.HttpRouter<E, R>,\n layer: Layer.Layer<R, RE>\n): WebHandler => {\n return HttpApp.toWebHandlerLayer(router, layer)\n}\n","import { Effect, Layer, Stream } from \"effect\"\nimport { GraphQLSchema } from \"graphql\"\nimport {\n makeGraphQLSSEHandler,\n formatSSEMessage,\n SSE_HEADERS,\n type GraphQLSSEOptions,\n type SSESubscriptionRequest,\n} from \"@effect-gql/core\"\n\n/**\n * Options for Web SSE handler\n */\nexport interface WebSSEOptions<R> extends GraphQLSSEOptions<R> {\n /**\n * Path for SSE connections.\n * @default \"/graphql/stream\"\n */\n readonly path?: string\n}\n\n/**\n * Create an SSE handler for web standard environments.\n *\n * This handler is designed for Cloudflare Workers, Deno, and other runtimes\n * that use the Web standard fetch API. It returns a streaming Response for\n * SSE subscription requests.\n *\n * @param schema - The GraphQL schema with subscription definitions\n * @param layer - Effect layer providing services required by resolvers\n * @param options - Optional lifecycle hooks and configuration\n * @returns A function that handles SSE requests and returns a Response\n *\n * @example Cloudflare Workers\n * ```typescript\n * import { toHandler } from \"@effect-gql/web\"\n * import { createSSEHandler } from \"@effect-gql/web\"\n *\n * const graphqlHandler = toHandler(router, Layer.empty)\n * const sseHandler = createSSEHandler(schema, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * const url = new URL(request.url)\n *\n * // Handle SSE subscriptions\n * if (url.pathname === \"/graphql/stream\" && request.method === \"POST\") {\n * return await sseHandler(request)\n * }\n *\n * // Handle regular GraphQL requests\n * return await graphqlHandler.handler(request)\n * }\n * }\n * ```\n *\n * @example Deno\n * ```typescript\n * import { toHandler, createSSEHandler } from \"@effect-gql/web\"\n *\n * const graphqlHandler = toHandler(router, Layer.empty)\n * const sseHandler = createSSEHandler(schema, Layer.empty)\n *\n * Deno.serve((request) => {\n * const url = new URL(request.url)\n *\n * if (url.pathname === \"/graphql/stream\" && request.method === \"POST\") {\n * return sseHandler(request)\n * }\n *\n * return graphqlHandler.handler(request)\n * })\n * ```\n */\nexport const createSSEHandler = <R>(\n schema: GraphQLSchema,\n layer: Layer.Layer<R>,\n options?: WebSSEOptions<R>\n): ((request: Request) => Promise<Response>) => {\n const sseHandler = makeGraphQLSSEHandler(schema, layer, options)\n\n return async (request: Request): Promise<Response> => {\n // Check Accept header for SSE support\n const accept = request.headers.get(\"accept\") ?? \"\"\n if (!accept.includes(\"text/event-stream\") && !accept.includes(\"*/*\")) {\n return new Response(\n JSON.stringify({\n errors: [{ message: \"Client must accept text/event-stream\" }],\n }),\n { status: 406, headers: { \"Content-Type\": \"application/json\" } }\n )\n }\n\n // Read and parse the request body\n let subscriptionRequest: SSESubscriptionRequest\n try {\n const body = (await request.json()) as Record<string, unknown>\n if (typeof body.query !== \"string\") {\n throw new Error(\"Missing query\")\n }\n subscriptionRequest = {\n query: body.query,\n variables: body.variables as Record<string, unknown> | undefined,\n operationName: body.operationName as string | undefined,\n extensions: body.extensions as Record<string, unknown> | undefined,\n }\n } catch {\n return new Response(\n JSON.stringify({\n errors: [{ message: \"Invalid GraphQL request body\" }],\n }),\n { status: 400, headers: { \"Content-Type\": \"application/json\" } }\n )\n }\n\n // Get the event stream\n const eventStream = sseHandler(subscriptionRequest, request.headers)\n\n // Create a ReadableStream from the Effect Stream\n const readableStream = new ReadableStream({\n async start(controller) {\n const encoder = new TextEncoder()\n\n await Effect.runPromise(\n Stream.runForEach(eventStream, (event) =>\n Effect.sync(() => {\n const message = formatSSEMessage(event)\n controller.enqueue(encoder.encode(message))\n })\n ).pipe(\n Effect.catchAll((error) => Effect.logWarning(\"SSE stream error\", error)),\n Effect.ensuring(Effect.sync(() => controller.close()))\n )\n )\n },\n })\n\n return new Response(readableStream, {\n status: 200,\n headers: SSE_HEADERS,\n })\n }\n}\n\n/**\n * Create SSE handlers with path matching for web standard environments.\n *\n * This returns an object with methods to check if a request should be\n * handled as SSE and to handle it.\n *\n * @param schema - The GraphQL schema with subscription definitions\n * @param layer - Effect layer providing services required by resolvers\n * @param options - Optional lifecycle hooks and configuration\n *\n * @example\n * ```typescript\n * const sse = createSSEHandlers(schema, Layer.empty)\n *\n * export default {\n * async fetch(request: Request) {\n * if (sse.shouldHandle(request)) {\n * return sse.handle(request)\n * }\n * // Handle other requests...\n * }\n * }\n * ```\n */\nexport const createSSEHandlers = <R>(\n schema: GraphQLSchema,\n layer: Layer.Layer<R>,\n options?: WebSSEOptions<R>\n): {\n /** Path this SSE handler responds to */\n readonly path: string\n /** Check if a request should be handled as SSE */\n shouldHandle: (request: Request) => boolean\n /** Handle an SSE request */\n handle: (request: Request) => Promise<Response>\n} => {\n const path = options?.path ?? \"/graphql/stream\"\n const handler = createSSEHandler(schema, layer, options)\n\n return {\n path,\n shouldHandle: (request: Request) => {\n if (request.method !== \"POST\") return false\n const url = new URL(request.url)\n return url.pathname === path\n },\n handle: handler,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,30 +1,26 @@
1
1
  {
2
2
  "name": "@effect-gql/web",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
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",
5
+ "repository": {
6
+ "url": "https://github.com/nrf110/effect-gql"
7
+ },
8
+ "type": "module",
9
+ "main": "./index.cjs",
10
+ "module": "dist/index.js",
11
+ "types": "./index.d.ts",
7
12
  "exports": {
8
13
  ".": {
9
- "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
14
+ "types": "./index.d.ts",
15
+ "import": "./index.js",
16
+ "require": "./index.cjs"
11
17
  }
12
18
  },
13
- "files": [
14
- "dist",
15
- "src"
16
- ],
17
19
  "peerDependencies": {
18
- "@effect-gql/core": "^0.1.0",
20
+ "@effect-gql/core": "^1.1.0",
19
21
  "@effect/platform": "^0.94.0",
20
22
  "effect": "^3.19.0"
21
23
  },
22
- "devDependencies": {
23
- "@effect-gql/core": "*",
24
- "@effect/platform": "^0.94.0",
25
- "effect": "^3.19.13",
26
- "graphql": "^16.0.0"
27
- },
28
24
  "keywords": [
29
25
  "effect",
30
26
  "graphql",
@@ -34,13 +30,5 @@
34
30
  "web",
35
31
  "deno"
36
32
  ],
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
- }
33
+ "license": "MIT"
34
+ }
package/dist/handler.d.ts DELETED
@@ -1,56 +0,0 @@
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
@@ -1 +0,0 @@
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"}
package/dist/handler.js DELETED
@@ -1,46 +0,0 @@
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
@@ -1 +0,0 @@
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"}
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export { toHandler, type WebHandler } from "./handler";
2
- export { createSSEHandler, createSSEHandlers, type WebSSEOptions } from "./sse";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
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 DELETED
@@ -1,10 +0,0 @@
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
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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.map DELETED
@@ -1 +0,0 @@
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 DELETED
@@ -1,146 +0,0 @@
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
package/dist/sse.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}
package/src/handler.ts DELETED
@@ -1,63 +0,0 @@
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 DELETED
@@ -1,4 +0,0 @@
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 DELETED
@@ -1,193 +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 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
- }