@effect-gql/express 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/http-utils.d.ts +12 -0
- package/dist/http-utils.d.ts.map +1 -0
- package/dist/http-utils.js +28 -0
- package/dist/http-utils.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +30 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +61 -0
- package/dist/middleware.js.map +1 -0
- package/dist/sse.d.ts +84 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +217 -0
- package/dist/sse.js.map +1 -0
- package/dist/ws.d.ts +73 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +118 -0
- package/dist/ws.js.map +1 -0
- package/package.json +61 -0
- package/src/http-utils.ts +24 -0
- package/src/index.ts +7 -0
- package/src/middleware.ts +65 -0
- package/src/sse.ts +285 -0
- package/src/ws.ts +152 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Nick Fisher
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Convert Node.js/Express IncomingHttpHeaders to web standard Headers.
|
|
4
|
+
*
|
|
5
|
+
* This handles the difference between Node.js headers (which can be
|
|
6
|
+
* string | string[] | undefined) and web Headers (which are always strings).
|
|
7
|
+
*
|
|
8
|
+
* @param nodeHeaders - Headers from req.headers
|
|
9
|
+
* @returns A web standard Headers object
|
|
10
|
+
*/
|
|
11
|
+
export declare const toWebHeaders: (nodeHeaders: IncomingHttpHeaders) => Headers;
|
|
12
|
+
//# sourceMappingURL=http-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-utils.d.ts","sourceRoot":"","sources":["../src/http-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAEpD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,aAAa,mBAAmB,KAAG,OAY/D,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toWebHeaders = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Convert Node.js/Express IncomingHttpHeaders to web standard Headers.
|
|
6
|
+
*
|
|
7
|
+
* This handles the difference between Node.js headers (which can be
|
|
8
|
+
* string | string[] | undefined) and web Headers (which are always strings).
|
|
9
|
+
*
|
|
10
|
+
* @param nodeHeaders - Headers from req.headers
|
|
11
|
+
* @returns A web standard Headers object
|
|
12
|
+
*/
|
|
13
|
+
const toWebHeaders = (nodeHeaders) => {
|
|
14
|
+
const headers = new Headers();
|
|
15
|
+
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
16
|
+
if (value) {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
value.forEach((v) => headers.append(key, v));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
headers.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return headers;
|
|
26
|
+
};
|
|
27
|
+
exports.toWebHeaders = toWebHeaders;
|
|
28
|
+
//# sourceMappingURL=http-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-utils.js","sourceRoot":"","sources":["../src/http-utils.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;GAQG;AACI,MAAM,YAAY,GAAG,CAAC,WAAgC,EAAW,EAAE;IACxE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAA;YAC9C,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAZY,QAAA,YAAY,gBAYxB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAG3C,OAAO,EAAE,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,MAAM,CAAA;AAG7D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSSEHandler = exports.sseMiddleware = exports.attachWebSocket = exports.toMiddleware = void 0;
|
|
4
|
+
var middleware_1 = require("./middleware");
|
|
5
|
+
Object.defineProperty(exports, "toMiddleware", { enumerable: true, get: function () { return middleware_1.toMiddleware; } });
|
|
6
|
+
// WebSocket subscription support
|
|
7
|
+
var ws_1 = require("./ws");
|
|
8
|
+
Object.defineProperty(exports, "attachWebSocket", { enumerable: true, get: function () { return ws_1.attachWebSocket; } });
|
|
9
|
+
// SSE (Server-Sent Events) subscription support
|
|
10
|
+
var sse_1 = require("./sse");
|
|
11
|
+
Object.defineProperty(exports, "sseMiddleware", { enumerable: true, get: function () { return sse_1.sseMiddleware; } });
|
|
12
|
+
Object.defineProperty(exports, "createSSEHandler", { enumerable: true, get: function () { return sse_1.createSSEHandler; } });
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA2C;AAAlC,0GAAA,YAAY,OAAA;AAErB,iCAAiC;AACjC,2BAA6D;AAApD,qGAAA,eAAe,OAAA;AAExB,gDAAgD;AAChD,6BAA+E;AAAtE,oGAAA,aAAa,OAAA;AAAE,uGAAA,gBAAgB,OAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { HttpRouter } from "@effect/platform";
|
|
3
|
+
import type { RequestHandler } from "express";
|
|
4
|
+
/**
|
|
5
|
+
* Convert an HttpRouter to Express middleware.
|
|
6
|
+
*
|
|
7
|
+
* This creates Express-compatible middleware that can be mounted on any Express app.
|
|
8
|
+
* The middleware converts Express requests to web standard Requests, processes them
|
|
9
|
+
* through the Effect router, and writes the response back to Express.
|
|
10
|
+
*
|
|
11
|
+
* @param router - The HttpRouter to convert (typically from makeGraphQLRouter or toRouter)
|
|
12
|
+
* @param layer - Layer providing any services required by the router
|
|
13
|
+
* @returns Express middleware function
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import express from "express"
|
|
18
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
19
|
+
* import { toMiddleware } from "@effect-gql/express"
|
|
20
|
+
* import { Layer } from "effect"
|
|
21
|
+
*
|
|
22
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
23
|
+
*
|
|
24
|
+
* const app = express()
|
|
25
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
26
|
+
* app.listen(4000, () => console.log("Server running on http://localhost:4000"))
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const toMiddleware: <E, R, RE>(router: HttpRouter.HttpRouter<E, R>, layer: Layer.Layer<R, RE>) => RequestHandler;
|
|
30
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAAW,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAA;AAG9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,CAAC,EAAE,EAAE,EACnC,QAAQ,UAAU,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EACnC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KACxB,cA+BF,CAAA"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toMiddleware = void 0;
|
|
4
|
+
const platform_1 = require("@effect/platform");
|
|
5
|
+
const http_utils_1 = require("./http-utils");
|
|
6
|
+
/**
|
|
7
|
+
* Convert an HttpRouter to Express middleware.
|
|
8
|
+
*
|
|
9
|
+
* This creates Express-compatible middleware that can be mounted on any Express app.
|
|
10
|
+
* The middleware converts Express requests to web standard Requests, processes them
|
|
11
|
+
* through the Effect router, and writes the response back to Express.
|
|
12
|
+
*
|
|
13
|
+
* @param router - The HttpRouter to convert (typically from makeGraphQLRouter or toRouter)
|
|
14
|
+
* @param layer - Layer providing any services required by the router
|
|
15
|
+
* @returns Express middleware function
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import express from "express"
|
|
20
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
21
|
+
* import { toMiddleware } from "@effect-gql/express"
|
|
22
|
+
* import { Layer } from "effect"
|
|
23
|
+
*
|
|
24
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
25
|
+
*
|
|
26
|
+
* const app = express()
|
|
27
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
28
|
+
* app.listen(4000, () => console.log("Server running on http://localhost:4000"))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
const toMiddleware = (router, layer) => {
|
|
32
|
+
const { handler } = platform_1.HttpApp.toWebHandlerLayer(router, layer);
|
|
33
|
+
return async (req, res, next) => {
|
|
34
|
+
try {
|
|
35
|
+
// Convert Express request to web standard Request
|
|
36
|
+
// Use URL constructor for safe URL parsing (avoids Host header injection)
|
|
37
|
+
const baseUrl = `${req.protocol}://${req.hostname}`;
|
|
38
|
+
const url = new URL(req.originalUrl || "/", baseUrl).href;
|
|
39
|
+
const headers = (0, http_utils_1.toWebHeaders)(req.headers);
|
|
40
|
+
const webRequest = new Request(url, {
|
|
41
|
+
method: req.method,
|
|
42
|
+
headers,
|
|
43
|
+
body: ["GET", "HEAD"].includes(req.method) ? undefined : JSON.stringify(req.body),
|
|
44
|
+
});
|
|
45
|
+
// Process through Effect handler
|
|
46
|
+
const webResponse = await handler(webRequest);
|
|
47
|
+
// Write response back to Express
|
|
48
|
+
res.status(webResponse.status);
|
|
49
|
+
webResponse.headers.forEach((value, key) => {
|
|
50
|
+
res.setHeader(key, value);
|
|
51
|
+
});
|
|
52
|
+
const body = await webResponse.text();
|
|
53
|
+
res.send(body);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
next(error);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
exports.toMiddleware = toMiddleware;
|
|
61
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":";;;AACA,+CAAsD;AAEtD,6CAA2C;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACI,MAAM,YAAY,GAAG,CAC1B,MAAmC,EACnC,KAAyB,EACT,EAAE;IAClB,MAAM,EAAE,OAAO,EAAE,GAAG,kBAAO,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAE5D,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,IAAI,CAAC;YACH,kDAAkD;YAClD,0EAA0E;YAC1E,MAAM,OAAO,GAAG,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAA;YACnD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,CAAA;YACzD,MAAM,OAAO,GAAG,IAAA,yBAAY,EAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAEzC,MAAM,UAAU,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;gBAClC,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,OAAO;gBACP,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;aAClF,CAAC,CAAA;YAEF,iCAAiC;YACjC,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAA;YAE7C,iCAAiC;YACjC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YAC9B,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;gBACzC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAC3B,CAAC,CAAC,CAAA;YACF,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,CAAA;YACrC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,CAAA;QACb,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA;AAlCY,QAAA,YAAY,gBAkCxB"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type { RequestHandler } from "express";
|
|
3
|
+
import { GraphQLSchema } from "graphql";
|
|
4
|
+
import { type GraphQLSSEOptions } from "@effect-gql/core";
|
|
5
|
+
/**
|
|
6
|
+
* Options for Express SSE middleware
|
|
7
|
+
*/
|
|
8
|
+
export interface ExpressSSEOptions<R> extends GraphQLSSEOptions<R> {
|
|
9
|
+
/**
|
|
10
|
+
* Path for SSE connections.
|
|
11
|
+
* @default "/graphql/stream"
|
|
12
|
+
*/
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an Express middleware for SSE subscriptions.
|
|
17
|
+
*
|
|
18
|
+
* This middleware handles POST requests to the configured path and streams
|
|
19
|
+
* GraphQL subscription events as Server-Sent Events.
|
|
20
|
+
*
|
|
21
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
22
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
23
|
+
* @param options - Optional lifecycle hooks and configuration
|
|
24
|
+
* @returns An Express middleware function
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import express from "express"
|
|
29
|
+
* import { createServer } from "node:http"
|
|
30
|
+
* import { toMiddleware, sseMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
31
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
32
|
+
*
|
|
33
|
+
* const app = express()
|
|
34
|
+
* app.use(express.json())
|
|
35
|
+
*
|
|
36
|
+
* // Regular GraphQL endpoint
|
|
37
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
38
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
39
|
+
*
|
|
40
|
+
* // SSE subscriptions endpoint
|
|
41
|
+
* app.use(sseMiddleware(schema, Layer.empty, {
|
|
42
|
+
* path: "/graphql/stream",
|
|
43
|
+
* onConnect: (request, headers) => Effect.gen(function* () {
|
|
44
|
+
* const token = headers.get("authorization")
|
|
45
|
+
* const user = yield* AuthService.validateToken(token)
|
|
46
|
+
* return { user }
|
|
47
|
+
* }),
|
|
48
|
+
* }))
|
|
49
|
+
*
|
|
50
|
+
* const server = createServer(app)
|
|
51
|
+
*
|
|
52
|
+
* // Optional: Also attach WebSocket subscriptions
|
|
53
|
+
* attachWebSocket(server, schema, Layer.empty)
|
|
54
|
+
*
|
|
55
|
+
* server.listen(4000)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare const sseMiddleware: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: ExpressSSEOptions<R>) => RequestHandler;
|
|
59
|
+
/**
|
|
60
|
+
* Create a standalone Express route handler for SSE subscriptions.
|
|
61
|
+
*
|
|
62
|
+
* Use this if you want more control over routing than the middleware provides.
|
|
63
|
+
*
|
|
64
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
65
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
66
|
+
* @param options - Optional lifecycle hooks and configuration
|
|
67
|
+
* @returns An Express request handler
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* import express from "express"
|
|
72
|
+
* import { createSSEHandler } from "@effect-gql/express"
|
|
73
|
+
*
|
|
74
|
+
* const app = express()
|
|
75
|
+
* app.use(express.json())
|
|
76
|
+
*
|
|
77
|
+
* const sseHandler = createSSEHandler(schema, Layer.empty)
|
|
78
|
+
* app.post("/graphql/stream", sseHandler)
|
|
79
|
+
*
|
|
80
|
+
* app.listen(4000)
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare const createSSEHandler: <R>(schema: GraphQLSchema, layer: Layer.Layer<R>, options?: Omit<ExpressSSEOptions<R>, "path">) => RequestHandler;
|
|
84
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAoB,MAAM,QAAQ,CAAA;AACxD,OAAO,KAAK,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAIL,KAAK,iBAAiB,EAGvB,MAAM,kBAAkB,CAAA;AAGzB;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,CAAE,SAAQ,iBAAiB,CAAC,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,QAAQ,aAAa,EACrB,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,UAAU,iBAAiB,CAAC,CAAC,CAAC,KAC7B,cAkGF,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAChC,QAAQ,aAAa,EACrB,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,UAAU,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAC3C,cAqFF,CAAA"}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSSEHandler = exports.sseMiddleware = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const core_1 = require("@effect-gql/core");
|
|
6
|
+
const http_utils_1 = require("./http-utils");
|
|
7
|
+
/**
|
|
8
|
+
* Create an Express middleware for SSE subscriptions.
|
|
9
|
+
*
|
|
10
|
+
* This middleware handles POST requests to the configured path and streams
|
|
11
|
+
* GraphQL subscription events as Server-Sent Events.
|
|
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 An Express middleware function
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import express from "express"
|
|
21
|
+
* import { createServer } from "node:http"
|
|
22
|
+
* import { toMiddleware, sseMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
23
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
24
|
+
*
|
|
25
|
+
* const app = express()
|
|
26
|
+
* app.use(express.json())
|
|
27
|
+
*
|
|
28
|
+
* // Regular GraphQL endpoint
|
|
29
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
30
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
31
|
+
*
|
|
32
|
+
* // SSE subscriptions endpoint
|
|
33
|
+
* app.use(sseMiddleware(schema, Layer.empty, {
|
|
34
|
+
* path: "/graphql/stream",
|
|
35
|
+
* onConnect: (request, headers) => Effect.gen(function* () {
|
|
36
|
+
* const token = headers.get("authorization")
|
|
37
|
+
* const user = yield* AuthService.validateToken(token)
|
|
38
|
+
* return { user }
|
|
39
|
+
* }),
|
|
40
|
+
* }))
|
|
41
|
+
*
|
|
42
|
+
* const server = createServer(app)
|
|
43
|
+
*
|
|
44
|
+
* // Optional: Also attach WebSocket subscriptions
|
|
45
|
+
* attachWebSocket(server, schema, Layer.empty)
|
|
46
|
+
*
|
|
47
|
+
* server.listen(4000)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
const sseMiddleware = (schema, layer, options) => {
|
|
51
|
+
const path = options?.path ?? "/graphql/stream";
|
|
52
|
+
const sseHandler = (0, core_1.makeGraphQLSSEHandler)(schema, layer, options);
|
|
53
|
+
return async (req, res, next) => {
|
|
54
|
+
// Check if this request is for our path
|
|
55
|
+
if (req.path !== path) {
|
|
56
|
+
next();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Only handle POST requests
|
|
60
|
+
if (req.method !== "POST") {
|
|
61
|
+
next();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Check Accept header for SSE support
|
|
65
|
+
const accept = req.headers.accept ?? "";
|
|
66
|
+
if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
|
|
67
|
+
res.status(406).json({
|
|
68
|
+
errors: [{ message: "Client must accept text/event-stream" }],
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Parse the GraphQL request from the body
|
|
73
|
+
let subscriptionRequest;
|
|
74
|
+
try {
|
|
75
|
+
const body = req.body;
|
|
76
|
+
if (typeof body.query !== "string") {
|
|
77
|
+
throw new Error("Missing query");
|
|
78
|
+
}
|
|
79
|
+
subscriptionRequest = {
|
|
80
|
+
query: body.query,
|
|
81
|
+
variables: body.variables,
|
|
82
|
+
operationName: body.operationName,
|
|
83
|
+
extensions: body.extensions,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
res.status(400).json({
|
|
88
|
+
errors: [{ message: "Invalid GraphQL request body" }],
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Convert Express headers to web Headers
|
|
93
|
+
const headers = (0, http_utils_1.toWebHeaders)(req.headers);
|
|
94
|
+
// Set SSE headers
|
|
95
|
+
res.writeHead(200, core_1.SSE_HEADERS);
|
|
96
|
+
// Get the event stream
|
|
97
|
+
const eventStream = sseHandler(subscriptionRequest, headers);
|
|
98
|
+
// Create the streaming effect
|
|
99
|
+
const streamEffect = effect_1.Effect.gen(function* () {
|
|
100
|
+
// Track client disconnection
|
|
101
|
+
const clientDisconnected = yield* effect_1.Deferred.make();
|
|
102
|
+
req.on("close", () => {
|
|
103
|
+
effect_1.Effect.runPromise(effect_1.Deferred.succeed(clientDisconnected, undefined)).catch(() => { });
|
|
104
|
+
});
|
|
105
|
+
req.on("error", (error) => {
|
|
106
|
+
effect_1.Effect.runPromise(effect_1.Deferred.fail(clientDisconnected, new core_1.SSEError({ cause: error }))).catch(() => { });
|
|
107
|
+
});
|
|
108
|
+
// Stream events to the client
|
|
109
|
+
const runStream = effect_1.Stream.runForEach(eventStream, (event) => effect_1.Effect.async((resume) => {
|
|
110
|
+
const message = (0, core_1.formatSSEMessage)(event);
|
|
111
|
+
res.write(message, (error) => {
|
|
112
|
+
if (error) {
|
|
113
|
+
resume(effect_1.Effect.fail(new core_1.SSEError({ cause: error })));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
resume(effect_1.Effect.succeed(undefined));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
// Race between stream completion and client disconnection
|
|
121
|
+
yield* effect_1.Effect.race(runStream.pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.logWarning("SSE stream error", error))), effect_1.Deferred.await(clientDisconnected));
|
|
122
|
+
});
|
|
123
|
+
await effect_1.Effect.runPromise(streamEffect.pipe(effect_1.Effect.ensuring(effect_1.Effect.sync(() => res.end())), effect_1.Effect.catchAll(() => effect_1.Effect.void)));
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
exports.sseMiddleware = sseMiddleware;
|
|
127
|
+
/**
|
|
128
|
+
* Create a standalone Express route handler for SSE subscriptions.
|
|
129
|
+
*
|
|
130
|
+
* Use this if you want more control over routing than the middleware provides.
|
|
131
|
+
*
|
|
132
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
133
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
134
|
+
* @param options - Optional lifecycle hooks and configuration
|
|
135
|
+
* @returns An Express request handler
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* import express from "express"
|
|
140
|
+
* import { createSSEHandler } from "@effect-gql/express"
|
|
141
|
+
*
|
|
142
|
+
* const app = express()
|
|
143
|
+
* app.use(express.json())
|
|
144
|
+
*
|
|
145
|
+
* const sseHandler = createSSEHandler(schema, Layer.empty)
|
|
146
|
+
* app.post("/graphql/stream", sseHandler)
|
|
147
|
+
*
|
|
148
|
+
* app.listen(4000)
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
const createSSEHandler = (schema, layer, options) => {
|
|
152
|
+
const sseHandler = (0, core_1.makeGraphQLSSEHandler)(schema, layer, options);
|
|
153
|
+
return async (req, res, _next) => {
|
|
154
|
+
// Check Accept header for SSE support
|
|
155
|
+
const accept = req.headers.accept ?? "";
|
|
156
|
+
if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
|
|
157
|
+
res.status(406).json({
|
|
158
|
+
errors: [{ message: "Client must accept text/event-stream" }],
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Parse the GraphQL request from the body
|
|
163
|
+
let subscriptionRequest;
|
|
164
|
+
try {
|
|
165
|
+
const body = req.body;
|
|
166
|
+
if (typeof body.query !== "string") {
|
|
167
|
+
throw new Error("Missing query");
|
|
168
|
+
}
|
|
169
|
+
subscriptionRequest = {
|
|
170
|
+
query: body.query,
|
|
171
|
+
variables: body.variables,
|
|
172
|
+
operationName: body.operationName,
|
|
173
|
+
extensions: body.extensions,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
res.status(400).json({
|
|
178
|
+
errors: [{ message: "Invalid GraphQL request body" }],
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Convert Express headers to web Headers
|
|
183
|
+
const headers = (0, http_utils_1.toWebHeaders)(req.headers);
|
|
184
|
+
// Set SSE headers
|
|
185
|
+
res.writeHead(200, core_1.SSE_HEADERS);
|
|
186
|
+
// Get the event stream
|
|
187
|
+
const eventStream = sseHandler(subscriptionRequest, headers);
|
|
188
|
+
// Create the streaming effect
|
|
189
|
+
const streamEffect = effect_1.Effect.gen(function* () {
|
|
190
|
+
// Track client disconnection
|
|
191
|
+
const clientDisconnected = yield* effect_1.Deferred.make();
|
|
192
|
+
req.on("close", () => {
|
|
193
|
+
effect_1.Effect.runPromise(effect_1.Deferred.succeed(clientDisconnected, undefined)).catch(() => { });
|
|
194
|
+
});
|
|
195
|
+
req.on("error", (error) => {
|
|
196
|
+
effect_1.Effect.runPromise(effect_1.Deferred.fail(clientDisconnected, new core_1.SSEError({ cause: error }))).catch(() => { });
|
|
197
|
+
});
|
|
198
|
+
// Stream events to the client
|
|
199
|
+
const runStream = effect_1.Stream.runForEach(eventStream, (event) => effect_1.Effect.async((resume) => {
|
|
200
|
+
const message = (0, core_1.formatSSEMessage)(event);
|
|
201
|
+
res.write(message, (error) => {
|
|
202
|
+
if (error) {
|
|
203
|
+
resume(effect_1.Effect.fail(new core_1.SSEError({ cause: error })));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
resume(effect_1.Effect.succeed(undefined));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}));
|
|
210
|
+
// Race between stream completion and client disconnection
|
|
211
|
+
yield* effect_1.Effect.race(runStream.pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.logWarning("SSE stream error", error))), effect_1.Deferred.await(clientDisconnected));
|
|
212
|
+
});
|
|
213
|
+
await effect_1.Effect.runPromise(streamEffect.pipe(effect_1.Effect.ensuring(effect_1.Effect.sync(() => res.end())), effect_1.Effect.catchAll(() => effect_1.Effect.void)));
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
exports.createSSEHandler = createSSEHandler;
|
|
217
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}
|
package/dist/ws.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type { Server } from "node:http";
|
|
3
|
+
import { GraphQLSchema } from "graphql";
|
|
4
|
+
import { type GraphQLWSOptions } from "@effect-gql/core";
|
|
5
|
+
/**
|
|
6
|
+
* Options for Express WebSocket server
|
|
7
|
+
*/
|
|
8
|
+
export interface ExpressWSOptions<R> extends GraphQLWSOptions<R> {
|
|
9
|
+
/**
|
|
10
|
+
* Path for WebSocket connections.
|
|
11
|
+
* @default "/graphql"
|
|
12
|
+
*/
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attach WebSocket subscription support to an Express HTTP server.
|
|
17
|
+
*
|
|
18
|
+
* Since Express middleware doesn't own the HTTP server, this function
|
|
19
|
+
* must be called separately with the HTTP server instance to enable
|
|
20
|
+
* WebSocket subscriptions.
|
|
21
|
+
*
|
|
22
|
+
* @param server - The HTTP server running the Express app
|
|
23
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
24
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
25
|
+
* @param options - Optional configuration and lifecycle hooks
|
|
26
|
+
* @returns Object with cleanup function
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import express from "express"
|
|
31
|
+
* import { createServer } from "node:http"
|
|
32
|
+
* import { toMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
33
|
+
* import { makeGraphQLRouter, GraphQLSchemaBuilder } from "@effect-gql/core"
|
|
34
|
+
* import { Layer, Effect, Stream } from "effect"
|
|
35
|
+
* import * as S from "effect/Schema"
|
|
36
|
+
*
|
|
37
|
+
* // Build schema with subscriptions
|
|
38
|
+
* const schema = GraphQLSchemaBuilder.empty
|
|
39
|
+
* .query("hello", { type: S.String, resolve: () => Effect.succeed("world") })
|
|
40
|
+
* .subscription("counter", {
|
|
41
|
+
* type: S.Int,
|
|
42
|
+
* subscribe: () => Effect.succeed(Stream.fromIterable([1, 2, 3]))
|
|
43
|
+
* })
|
|
44
|
+
* .buildSchema()
|
|
45
|
+
*
|
|
46
|
+
* // Create Express app with middleware
|
|
47
|
+
* const app = express()
|
|
48
|
+
* app.use(express.json())
|
|
49
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
50
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
51
|
+
*
|
|
52
|
+
* // Create HTTP server and attach WebSocket support
|
|
53
|
+
* const server = createServer(app)
|
|
54
|
+
* const ws = attachWebSocket(server, schema, Layer.empty, {
|
|
55
|
+
* path: "/graphql"
|
|
56
|
+
* })
|
|
57
|
+
*
|
|
58
|
+
* server.listen(4000, () => {
|
|
59
|
+
* console.log("Server running on http://localhost:4000")
|
|
60
|
+
* console.log("WebSocket subscriptions available at ws://localhost:4000/graphql")
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* // Cleanup on shutdown
|
|
64
|
+
* process.on("SIGINT", async () => {
|
|
65
|
+
* await ws.close()
|
|
66
|
+
* server.close()
|
|
67
|
+
* })
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare const attachWebSocket: <R>(server: Server, schema: GraphQLSchema, layer: Layer.Layer<R>, options?: ExpressWSOptions<R>) => {
|
|
71
|
+
close: () => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=ws.d.ts.map
|
package/dist/ws.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.d.ts","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAIvC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAA;AAEzB;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,CAAE,SAAQ,gBAAgB,CAAC,CAAC,CAAC;IAC9D;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAC/B,QAAQ,MAAM,EACd,QAAQ,aAAa,EACrB,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,UAAU,gBAAgB,CAAC,CAAC,CAAC,KAC5B;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAoE9B,CAAA"}
|
package/dist/ws.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachWebSocket = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const ws_1 = require("ws");
|
|
6
|
+
const core_1 = require("@effect-gql/core");
|
|
7
|
+
/**
|
|
8
|
+
* Attach WebSocket subscription support to an Express HTTP server.
|
|
9
|
+
*
|
|
10
|
+
* Since Express middleware doesn't own the HTTP server, this function
|
|
11
|
+
* must be called separately with the HTTP server instance to enable
|
|
12
|
+
* WebSocket subscriptions.
|
|
13
|
+
*
|
|
14
|
+
* @param server - The HTTP server running the Express app
|
|
15
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
16
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
17
|
+
* @param options - Optional configuration and lifecycle hooks
|
|
18
|
+
* @returns Object with cleanup function
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import express from "express"
|
|
23
|
+
* import { createServer } from "node:http"
|
|
24
|
+
* import { toMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
25
|
+
* import { makeGraphQLRouter, GraphQLSchemaBuilder } from "@effect-gql/core"
|
|
26
|
+
* import { Layer, Effect, Stream } from "effect"
|
|
27
|
+
* import * as S from "effect/Schema"
|
|
28
|
+
*
|
|
29
|
+
* // Build schema with subscriptions
|
|
30
|
+
* const schema = GraphQLSchemaBuilder.empty
|
|
31
|
+
* .query("hello", { type: S.String, resolve: () => Effect.succeed("world") })
|
|
32
|
+
* .subscription("counter", {
|
|
33
|
+
* type: S.Int,
|
|
34
|
+
* subscribe: () => Effect.succeed(Stream.fromIterable([1, 2, 3]))
|
|
35
|
+
* })
|
|
36
|
+
* .buildSchema()
|
|
37
|
+
*
|
|
38
|
+
* // Create Express app with middleware
|
|
39
|
+
* const app = express()
|
|
40
|
+
* app.use(express.json())
|
|
41
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
42
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
43
|
+
*
|
|
44
|
+
* // Create HTTP server and attach WebSocket support
|
|
45
|
+
* const server = createServer(app)
|
|
46
|
+
* const ws = attachWebSocket(server, schema, Layer.empty, {
|
|
47
|
+
* path: "/graphql"
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* server.listen(4000, () => {
|
|
51
|
+
* console.log("Server running on http://localhost:4000")
|
|
52
|
+
* console.log("WebSocket subscriptions available at ws://localhost:4000/graphql")
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* // Cleanup on shutdown
|
|
56
|
+
* process.on("SIGINT", async () => {
|
|
57
|
+
* await ws.close()
|
|
58
|
+
* server.close()
|
|
59
|
+
* })
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
const attachWebSocket = (server, schema, layer, options) => {
|
|
63
|
+
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
64
|
+
const path = options?.path ?? "/graphql";
|
|
65
|
+
// Create the handler from core
|
|
66
|
+
const handler = (0, core_1.makeGraphQLWSHandler)(schema, layer, options);
|
|
67
|
+
// Track active connections for cleanup
|
|
68
|
+
const activeConnections = new Set();
|
|
69
|
+
wss.on("connection", (ws) => {
|
|
70
|
+
activeConnections.add(ws);
|
|
71
|
+
const effectSocket = (0, core_1.toEffectWebSocketFromWs)(ws);
|
|
72
|
+
// Run the handler
|
|
73
|
+
effect_1.Effect.runPromise(handler(effectSocket).pipe(effect_1.Effect.catchAll((error) => effect_1.Effect.logError("GraphQL WebSocket handler error", error)))).finally(() => {
|
|
74
|
+
activeConnections.delete(ws);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
const handleUpgrade = (request, socket, head) => {
|
|
78
|
+
// Check if this is the GraphQL WebSocket path
|
|
79
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
|
|
80
|
+
if (url.pathname !== path) {
|
|
81
|
+
socket.destroy();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Check for correct WebSocket subprotocol
|
|
85
|
+
const protocol = request.headers["sec-websocket-protocol"];
|
|
86
|
+
if (!protocol?.includes("graphql-transport-ws")) {
|
|
87
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
88
|
+
socket.destroy();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
92
|
+
wss.emit("connection", ws, request);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
// Attach upgrade handler to server
|
|
96
|
+
server.on("upgrade", (request, socket, head) => {
|
|
97
|
+
handleUpgrade(request, socket, head);
|
|
98
|
+
});
|
|
99
|
+
const close = async () => {
|
|
100
|
+
// Close all active connections
|
|
101
|
+
for (const ws of activeConnections) {
|
|
102
|
+
ws.close(1001, "Server shutting down");
|
|
103
|
+
}
|
|
104
|
+
activeConnections.clear();
|
|
105
|
+
// Close the WebSocket server
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
wss.close((error) => {
|
|
108
|
+
if (error)
|
|
109
|
+
reject(error);
|
|
110
|
+
else
|
|
111
|
+
resolve();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
return { close };
|
|
116
|
+
};
|
|
117
|
+
exports.attachWebSocket = attachWebSocket;
|
|
118
|
+
//# sourceMappingURL=ws.js.map
|
package/dist/ws.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-gql/express",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Express middleware adapter for @effect-gql/core",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@effect-gql/core": "^0.1.0",
|
|
19
|
+
"@effect/platform": "^0.94.0",
|
|
20
|
+
"effect": "^3.19.0",
|
|
21
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
22
|
+
"graphql-ws": "^6.0.0",
|
|
23
|
+
"ws": "^8.14.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"graphql-ws": {
|
|
27
|
+
"optional": true
|
|
28
|
+
},
|
|
29
|
+
"ws": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@effect-gql/core": "*",
|
|
35
|
+
"@effect/platform": "^0.94.0",
|
|
36
|
+
"@types/express": "^5.0.6",
|
|
37
|
+
"@types/ws": "^8.5.0",
|
|
38
|
+
"effect": "^3.19.13",
|
|
39
|
+
"express": "^5.2.1",
|
|
40
|
+
"graphql-ws": "^6.0.6",
|
|
41
|
+
"ws": "^8.18.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"effect",
|
|
45
|
+
"graphql",
|
|
46
|
+
"express",
|
|
47
|
+
"middleware",
|
|
48
|
+
"http",
|
|
49
|
+
"server"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"dev": "tsc --watch",
|
|
55
|
+
"clean": "rm -rf dist",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:unit": "vitest run test/unit",
|
|
58
|
+
"test:integration": "vitest run test/integration",
|
|
59
|
+
"test:watch": "vitest"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert Node.js/Express IncomingHttpHeaders to web standard Headers.
|
|
5
|
+
*
|
|
6
|
+
* This handles the difference between Node.js headers (which can be
|
|
7
|
+
* string | string[] | undefined) and web Headers (which are always strings).
|
|
8
|
+
*
|
|
9
|
+
* @param nodeHeaders - Headers from req.headers
|
|
10
|
+
* @returns A web standard Headers object
|
|
11
|
+
*/
|
|
12
|
+
export const toWebHeaders = (nodeHeaders: IncomingHttpHeaders): Headers => {
|
|
13
|
+
const headers = new Headers()
|
|
14
|
+
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
15
|
+
if (value) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
value.forEach((v) => headers.append(key, v))
|
|
18
|
+
} else {
|
|
19
|
+
headers.set(key, value)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return headers
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { toMiddleware } from "./middleware"
|
|
2
|
+
|
|
3
|
+
// WebSocket subscription support
|
|
4
|
+
export { attachWebSocket, type ExpressWSOptions } from "./ws"
|
|
5
|
+
|
|
6
|
+
// SSE (Server-Sent Events) subscription support
|
|
7
|
+
export { sseMiddleware, createSSEHandler, type ExpressSSEOptions } from "./sse"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Layer } from "effect"
|
|
2
|
+
import { HttpApp, HttpRouter } from "@effect/platform"
|
|
3
|
+
import type { Request, Response, NextFunction, RequestHandler } from "express"
|
|
4
|
+
import { toWebHeaders } from "./http-utils"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert an HttpRouter to Express middleware.
|
|
8
|
+
*
|
|
9
|
+
* This creates Express-compatible middleware that can be mounted on any Express app.
|
|
10
|
+
* The middleware converts Express requests to web standard Requests, processes them
|
|
11
|
+
* through the Effect router, and writes the response back to Express.
|
|
12
|
+
*
|
|
13
|
+
* @param router - The HttpRouter to convert (typically from makeGraphQLRouter or toRouter)
|
|
14
|
+
* @param layer - Layer providing any services required by the router
|
|
15
|
+
* @returns Express middleware function
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import express from "express"
|
|
20
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
21
|
+
* import { toMiddleware } from "@effect-gql/express"
|
|
22
|
+
* import { Layer } from "effect"
|
|
23
|
+
*
|
|
24
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
25
|
+
*
|
|
26
|
+
* const app = express()
|
|
27
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
28
|
+
* app.listen(4000, () => console.log("Server running on http://localhost:4000"))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const toMiddleware = <E, R, RE>(
|
|
32
|
+
router: HttpRouter.HttpRouter<E, R>,
|
|
33
|
+
layer: Layer.Layer<R, RE>
|
|
34
|
+
): RequestHandler => {
|
|
35
|
+
const { handler } = HttpApp.toWebHandlerLayer(router, layer)
|
|
36
|
+
|
|
37
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
38
|
+
try {
|
|
39
|
+
// Convert Express request to web standard Request
|
|
40
|
+
// Use URL constructor for safe URL parsing (avoids Host header injection)
|
|
41
|
+
const baseUrl = `${req.protocol}://${req.hostname}`
|
|
42
|
+
const url = new URL(req.originalUrl || "/", baseUrl).href
|
|
43
|
+
const headers = toWebHeaders(req.headers)
|
|
44
|
+
|
|
45
|
+
const webRequest = new Request(url, {
|
|
46
|
+
method: req.method,
|
|
47
|
+
headers,
|
|
48
|
+
body: ["GET", "HEAD"].includes(req.method) ? undefined : JSON.stringify(req.body),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Process through Effect handler
|
|
52
|
+
const webResponse = await handler(webRequest)
|
|
53
|
+
|
|
54
|
+
// Write response back to Express
|
|
55
|
+
res.status(webResponse.status)
|
|
56
|
+
webResponse.headers.forEach((value, key) => {
|
|
57
|
+
res.setHeader(key, value)
|
|
58
|
+
})
|
|
59
|
+
const body = await webResponse.text()
|
|
60
|
+
res.send(body)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
next(error)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/sse.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { Effect, Layer, Stream, Deferred } from "effect"
|
|
2
|
+
import type { Request, Response, NextFunction, RequestHandler } from "express"
|
|
3
|
+
import { GraphQLSchema } from "graphql"
|
|
4
|
+
import {
|
|
5
|
+
makeGraphQLSSEHandler,
|
|
6
|
+
formatSSEMessage,
|
|
7
|
+
SSE_HEADERS,
|
|
8
|
+
type GraphQLSSEOptions,
|
|
9
|
+
type SSESubscriptionRequest,
|
|
10
|
+
SSEError,
|
|
11
|
+
} from "@effect-gql/core"
|
|
12
|
+
import { toWebHeaders } from "./http-utils"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for Express SSE middleware
|
|
16
|
+
*/
|
|
17
|
+
export interface ExpressSSEOptions<R> extends GraphQLSSEOptions<R> {
|
|
18
|
+
/**
|
|
19
|
+
* Path for SSE connections.
|
|
20
|
+
* @default "/graphql/stream"
|
|
21
|
+
*/
|
|
22
|
+
readonly path?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an Express middleware for SSE subscriptions.
|
|
27
|
+
*
|
|
28
|
+
* This middleware handles POST requests to the configured path and streams
|
|
29
|
+
* GraphQL subscription events as Server-Sent Events.
|
|
30
|
+
*
|
|
31
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
32
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
33
|
+
* @param options - Optional lifecycle hooks and configuration
|
|
34
|
+
* @returns An Express middleware function
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import express from "express"
|
|
39
|
+
* import { createServer } from "node:http"
|
|
40
|
+
* import { toMiddleware, sseMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
41
|
+
* import { makeGraphQLRouter } from "@effect-gql/core"
|
|
42
|
+
*
|
|
43
|
+
* const app = express()
|
|
44
|
+
* app.use(express.json())
|
|
45
|
+
*
|
|
46
|
+
* // Regular GraphQL endpoint
|
|
47
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
48
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
49
|
+
*
|
|
50
|
+
* // SSE subscriptions endpoint
|
|
51
|
+
* app.use(sseMiddleware(schema, Layer.empty, {
|
|
52
|
+
* path: "/graphql/stream",
|
|
53
|
+
* onConnect: (request, headers) => Effect.gen(function* () {
|
|
54
|
+
* const token = headers.get("authorization")
|
|
55
|
+
* const user = yield* AuthService.validateToken(token)
|
|
56
|
+
* return { user }
|
|
57
|
+
* }),
|
|
58
|
+
* }))
|
|
59
|
+
*
|
|
60
|
+
* const server = createServer(app)
|
|
61
|
+
*
|
|
62
|
+
* // Optional: Also attach WebSocket subscriptions
|
|
63
|
+
* attachWebSocket(server, schema, Layer.empty)
|
|
64
|
+
*
|
|
65
|
+
* server.listen(4000)
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export const sseMiddleware = <R>(
|
|
69
|
+
schema: GraphQLSchema,
|
|
70
|
+
layer: Layer.Layer<R>,
|
|
71
|
+
options?: ExpressSSEOptions<R>
|
|
72
|
+
): RequestHandler => {
|
|
73
|
+
const path = options?.path ?? "/graphql/stream"
|
|
74
|
+
const sseHandler = makeGraphQLSSEHandler(schema, layer, options)
|
|
75
|
+
|
|
76
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
77
|
+
// Check if this request is for our path
|
|
78
|
+
if (req.path !== path) {
|
|
79
|
+
next()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only handle POST requests
|
|
84
|
+
if (req.method !== "POST") {
|
|
85
|
+
next()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check Accept header for SSE support
|
|
90
|
+
const accept = req.headers.accept ?? ""
|
|
91
|
+
if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
|
|
92
|
+
res.status(406).json({
|
|
93
|
+
errors: [{ message: "Client must accept text/event-stream" }],
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parse the GraphQL request from the body
|
|
99
|
+
let subscriptionRequest: SSESubscriptionRequest
|
|
100
|
+
try {
|
|
101
|
+
const body = req.body as Record<string, unknown>
|
|
102
|
+
if (typeof body.query !== "string") {
|
|
103
|
+
throw new Error("Missing query")
|
|
104
|
+
}
|
|
105
|
+
subscriptionRequest = {
|
|
106
|
+
query: body.query,
|
|
107
|
+
variables: body.variables as Record<string, unknown> | undefined,
|
|
108
|
+
operationName: body.operationName as string | undefined,
|
|
109
|
+
extensions: body.extensions as Record<string, unknown> | undefined,
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
res.status(400).json({
|
|
113
|
+
errors: [{ message: "Invalid GraphQL request body" }],
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Convert Express headers to web Headers
|
|
119
|
+
const headers = toWebHeaders(req.headers)
|
|
120
|
+
|
|
121
|
+
// Set SSE headers
|
|
122
|
+
res.writeHead(200, SSE_HEADERS)
|
|
123
|
+
|
|
124
|
+
// Get the event stream
|
|
125
|
+
const eventStream = sseHandler(subscriptionRequest, headers)
|
|
126
|
+
|
|
127
|
+
// Create the streaming effect
|
|
128
|
+
const streamEffect = Effect.gen(function* () {
|
|
129
|
+
// Track client disconnection
|
|
130
|
+
const clientDisconnected = yield* Deferred.make<void, SSEError>()
|
|
131
|
+
|
|
132
|
+
req.on("close", () => {
|
|
133
|
+
Effect.runPromise(Deferred.succeed(clientDisconnected, undefined)).catch(() => {})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
req.on("error", (error) => {
|
|
137
|
+
Effect.runPromise(Deferred.fail(clientDisconnected, new SSEError({ cause: error }))).catch(
|
|
138
|
+
() => {}
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Stream events to the client
|
|
143
|
+
const runStream = Stream.runForEach(eventStream, (event) =>
|
|
144
|
+
Effect.async<void, SSEError>((resume) => {
|
|
145
|
+
const message = formatSSEMessage(event)
|
|
146
|
+
res.write(message, (error) => {
|
|
147
|
+
if (error) {
|
|
148
|
+
resume(Effect.fail(new SSEError({ cause: error })))
|
|
149
|
+
} else {
|
|
150
|
+
resume(Effect.succeed(undefined))
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Race between stream completion and client disconnection
|
|
157
|
+
yield* Effect.race(
|
|
158
|
+
runStream.pipe(Effect.catchAll((error) => Effect.logWarning("SSE stream error", error))),
|
|
159
|
+
Deferred.await(clientDisconnected)
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
await Effect.runPromise(
|
|
164
|
+
streamEffect.pipe(
|
|
165
|
+
Effect.ensuring(Effect.sync(() => res.end())),
|
|
166
|
+
Effect.catchAll(() => Effect.void)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a standalone Express route handler for SSE subscriptions.
|
|
174
|
+
*
|
|
175
|
+
* Use this if you want more control over routing than the middleware provides.
|
|
176
|
+
*
|
|
177
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
178
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
179
|
+
* @param options - Optional lifecycle hooks and configuration
|
|
180
|
+
* @returns An Express request handler
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* import express from "express"
|
|
185
|
+
* import { createSSEHandler } from "@effect-gql/express"
|
|
186
|
+
*
|
|
187
|
+
* const app = express()
|
|
188
|
+
* app.use(express.json())
|
|
189
|
+
*
|
|
190
|
+
* const sseHandler = createSSEHandler(schema, Layer.empty)
|
|
191
|
+
* app.post("/graphql/stream", sseHandler)
|
|
192
|
+
*
|
|
193
|
+
* app.listen(4000)
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export const createSSEHandler = <R>(
|
|
197
|
+
schema: GraphQLSchema,
|
|
198
|
+
layer: Layer.Layer<R>,
|
|
199
|
+
options?: Omit<ExpressSSEOptions<R>, "path">
|
|
200
|
+
): RequestHandler => {
|
|
201
|
+
const sseHandler = makeGraphQLSSEHandler(schema, layer, options)
|
|
202
|
+
|
|
203
|
+
return async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
|
204
|
+
// Check Accept header for SSE support
|
|
205
|
+
const accept = req.headers.accept ?? ""
|
|
206
|
+
if (!accept.includes("text/event-stream") && !accept.includes("*/*")) {
|
|
207
|
+
res.status(406).json({
|
|
208
|
+
errors: [{ message: "Client must accept text/event-stream" }],
|
|
209
|
+
})
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Parse the GraphQL request from the body
|
|
214
|
+
let subscriptionRequest: SSESubscriptionRequest
|
|
215
|
+
try {
|
|
216
|
+
const body = req.body as Record<string, unknown>
|
|
217
|
+
if (typeof body.query !== "string") {
|
|
218
|
+
throw new Error("Missing query")
|
|
219
|
+
}
|
|
220
|
+
subscriptionRequest = {
|
|
221
|
+
query: body.query,
|
|
222
|
+
variables: body.variables as Record<string, unknown> | undefined,
|
|
223
|
+
operationName: body.operationName as string | undefined,
|
|
224
|
+
extensions: body.extensions as Record<string, unknown> | undefined,
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
res.status(400).json({
|
|
228
|
+
errors: [{ message: "Invalid GraphQL request body" }],
|
|
229
|
+
})
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Convert Express headers to web Headers
|
|
234
|
+
const headers = toWebHeaders(req.headers)
|
|
235
|
+
|
|
236
|
+
// Set SSE headers
|
|
237
|
+
res.writeHead(200, SSE_HEADERS)
|
|
238
|
+
|
|
239
|
+
// Get the event stream
|
|
240
|
+
const eventStream = sseHandler(subscriptionRequest, headers)
|
|
241
|
+
|
|
242
|
+
// Create the streaming effect
|
|
243
|
+
const streamEffect = Effect.gen(function* () {
|
|
244
|
+
// Track client disconnection
|
|
245
|
+
const clientDisconnected = yield* Deferred.make<void, SSEError>()
|
|
246
|
+
|
|
247
|
+
req.on("close", () => {
|
|
248
|
+
Effect.runPromise(Deferred.succeed(clientDisconnected, undefined)).catch(() => {})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
req.on("error", (error) => {
|
|
252
|
+
Effect.runPromise(Deferred.fail(clientDisconnected, new SSEError({ cause: error }))).catch(
|
|
253
|
+
() => {}
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Stream events to the client
|
|
258
|
+
const runStream = Stream.runForEach(eventStream, (event) =>
|
|
259
|
+
Effect.async<void, SSEError>((resume) => {
|
|
260
|
+
const message = formatSSEMessage(event)
|
|
261
|
+
res.write(message, (error) => {
|
|
262
|
+
if (error) {
|
|
263
|
+
resume(Effect.fail(new SSEError({ cause: error })))
|
|
264
|
+
} else {
|
|
265
|
+
resume(Effect.succeed(undefined))
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Race between stream completion and client disconnection
|
|
272
|
+
yield* Effect.race(
|
|
273
|
+
runStream.pipe(Effect.catchAll((error) => Effect.logWarning("SSE stream error", error))),
|
|
274
|
+
Deferred.await(clientDisconnected)
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
await Effect.runPromise(
|
|
279
|
+
streamEffect.pipe(
|
|
280
|
+
Effect.ensuring(Effect.sync(() => res.end())),
|
|
281
|
+
Effect.catchAll(() => Effect.void)
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect"
|
|
2
|
+
import type { Server } from "node:http"
|
|
3
|
+
import type { IncomingMessage } from "node:http"
|
|
4
|
+
import type { Duplex } from "node:stream"
|
|
5
|
+
import { WebSocket, WebSocketServer } from "ws"
|
|
6
|
+
import { GraphQLSchema } from "graphql"
|
|
7
|
+
import {
|
|
8
|
+
makeGraphQLWSHandler,
|
|
9
|
+
toEffectWebSocketFromWs,
|
|
10
|
+
type GraphQLWSOptions,
|
|
11
|
+
} from "@effect-gql/core"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for Express WebSocket server
|
|
15
|
+
*/
|
|
16
|
+
export interface ExpressWSOptions<R> extends GraphQLWSOptions<R> {
|
|
17
|
+
/**
|
|
18
|
+
* Path for WebSocket connections.
|
|
19
|
+
* @default "/graphql"
|
|
20
|
+
*/
|
|
21
|
+
readonly path?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Attach WebSocket subscription support to an Express HTTP server.
|
|
26
|
+
*
|
|
27
|
+
* Since Express middleware doesn't own the HTTP server, this function
|
|
28
|
+
* must be called separately with the HTTP server instance to enable
|
|
29
|
+
* WebSocket subscriptions.
|
|
30
|
+
*
|
|
31
|
+
* @param server - The HTTP server running the Express app
|
|
32
|
+
* @param schema - The GraphQL schema with subscription definitions
|
|
33
|
+
* @param layer - Effect layer providing services required by resolvers
|
|
34
|
+
* @param options - Optional configuration and lifecycle hooks
|
|
35
|
+
* @returns Object with cleanup function
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import express from "express"
|
|
40
|
+
* import { createServer } from "node:http"
|
|
41
|
+
* import { toMiddleware, attachWebSocket } from "@effect-gql/express"
|
|
42
|
+
* import { makeGraphQLRouter, GraphQLSchemaBuilder } from "@effect-gql/core"
|
|
43
|
+
* import { Layer, Effect, Stream } from "effect"
|
|
44
|
+
* import * as S from "effect/Schema"
|
|
45
|
+
*
|
|
46
|
+
* // Build schema with subscriptions
|
|
47
|
+
* const schema = GraphQLSchemaBuilder.empty
|
|
48
|
+
* .query("hello", { type: S.String, resolve: () => Effect.succeed("world") })
|
|
49
|
+
* .subscription("counter", {
|
|
50
|
+
* type: S.Int,
|
|
51
|
+
* subscribe: () => Effect.succeed(Stream.fromIterable([1, 2, 3]))
|
|
52
|
+
* })
|
|
53
|
+
* .buildSchema()
|
|
54
|
+
*
|
|
55
|
+
* // Create Express app with middleware
|
|
56
|
+
* const app = express()
|
|
57
|
+
* app.use(express.json())
|
|
58
|
+
* const router = makeGraphQLRouter(schema, Layer.empty, { graphiql: true })
|
|
59
|
+
* app.use(toMiddleware(router, Layer.empty))
|
|
60
|
+
*
|
|
61
|
+
* // Create HTTP server and attach WebSocket support
|
|
62
|
+
* const server = createServer(app)
|
|
63
|
+
* const ws = attachWebSocket(server, schema, Layer.empty, {
|
|
64
|
+
* path: "/graphql"
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* server.listen(4000, () => {
|
|
68
|
+
* console.log("Server running on http://localhost:4000")
|
|
69
|
+
* console.log("WebSocket subscriptions available at ws://localhost:4000/graphql")
|
|
70
|
+
* })
|
|
71
|
+
*
|
|
72
|
+
* // Cleanup on shutdown
|
|
73
|
+
* process.on("SIGINT", async () => {
|
|
74
|
+
* await ws.close()
|
|
75
|
+
* server.close()
|
|
76
|
+
* })
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export const attachWebSocket = <R>(
|
|
80
|
+
server: Server,
|
|
81
|
+
schema: GraphQLSchema,
|
|
82
|
+
layer: Layer.Layer<R>,
|
|
83
|
+
options?: ExpressWSOptions<R>
|
|
84
|
+
): { close: () => Promise<void> } => {
|
|
85
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
86
|
+
const path = options?.path ?? "/graphql"
|
|
87
|
+
|
|
88
|
+
// Create the handler from core
|
|
89
|
+
const handler = makeGraphQLWSHandler(schema, layer, options)
|
|
90
|
+
|
|
91
|
+
// Track active connections for cleanup
|
|
92
|
+
const activeConnections = new Set<WebSocket>()
|
|
93
|
+
|
|
94
|
+
wss.on("connection", (ws) => {
|
|
95
|
+
activeConnections.add(ws)
|
|
96
|
+
|
|
97
|
+
const effectSocket = toEffectWebSocketFromWs(ws)
|
|
98
|
+
|
|
99
|
+
// Run the handler
|
|
100
|
+
Effect.runPromise(
|
|
101
|
+
handler(effectSocket).pipe(
|
|
102
|
+
Effect.catchAll((error) => Effect.logError("GraphQL WebSocket handler error", error))
|
|
103
|
+
)
|
|
104
|
+
).finally(() => {
|
|
105
|
+
activeConnections.delete(ws)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
110
|
+
// Check if this is the GraphQL WebSocket path
|
|
111
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host}`)
|
|
112
|
+
if (url.pathname !== path) {
|
|
113
|
+
socket.destroy()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check for correct WebSocket subprotocol
|
|
118
|
+
const protocol = request.headers["sec-websocket-protocol"]
|
|
119
|
+
if (!protocol?.includes("graphql-transport-ws")) {
|
|
120
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n")
|
|
121
|
+
socket.destroy()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
126
|
+
wss.emit("connection", ws, request)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Attach upgrade handler to server
|
|
131
|
+
server.on("upgrade", (request, socket, head) => {
|
|
132
|
+
handleUpgrade(request, socket as Duplex, head)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const close = async () => {
|
|
136
|
+
// Close all active connections
|
|
137
|
+
for (const ws of activeConnections) {
|
|
138
|
+
ws.close(1001, "Server shutting down")
|
|
139
|
+
}
|
|
140
|
+
activeConnections.clear()
|
|
141
|
+
|
|
142
|
+
// Close the WebSocket server
|
|
143
|
+
return new Promise<void>((resolve, reject) => {
|
|
144
|
+
wss.close((error) => {
|
|
145
|
+
if (error) reject(error)
|
|
146
|
+
else resolve()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { close }
|
|
152
|
+
}
|