@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 +100 -0
- package/index.cjs +85 -0
- package/index.cjs.map +1 -0
- package/{dist/sse.d.ts → index.d.cts} +64 -7
- package/index.d.ts +157 -0
- package/index.js +81 -0
- package/index.js.map +1 -0
- package/package.json +14 -26
- package/dist/handler.d.ts +0 -56
- package/dist/handler.d.ts.map +0 -1
- package/dist/handler.js +0 -46
- package/dist/handler.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -10
- package/dist/index.js.map +0 -1
- package/dist/sse.d.ts.map +0 -1
- package/dist/sse.js +0 -146
- package/dist/sse.js.map +0 -1
- package/src/handler.ts +0 -63
- package/src/index.ts +0 -4
- package/src/sse.ts +0 -193
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
6
|
-
|
|
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": "./
|
|
10
|
-
"
|
|
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": "^
|
|
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
|
-
|
|
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
|
package/dist/handler.d.ts.map
DELETED
|
@@ -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
|
package/dist/handler.js.map
DELETED
|
@@ -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
package/dist/index.d.ts.map
DELETED
|
@@ -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
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
|
-
}
|