@askrjs/node 0.0.3 → 0.0.5
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 +90 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +87 -2
- package/package.json +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @askrjs/node
|
|
2
|
+
|
|
3
|
+
[](https://github.com/askrjs/askr-node/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@askrjs/node)
|
|
5
|
+
|
|
6
|
+
Run an `@askrjs/server` application on Node.js. The adapter translates Node HTTP messages at the
|
|
7
|
+
boundary while the application continues to use Web `Request` and `Response` objects.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @askrjs/server @askrjs/node
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Create a Node handler
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { createServerApp, json } from "@askrjs/server";
|
|
20
|
+
import { createNodeHandler } from "@askrjs/node";
|
|
21
|
+
|
|
22
|
+
const app = createServerApp({
|
|
23
|
+
routes: [{ path: "/health", handler: () => json({ status: "ok" }) }],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
createServer(createNodeHandler(app)).listen(3000);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`createNodeHandler` also works as Connect middleware because it accepts an optional `next`
|
|
30
|
+
callback. It preserves streaming bodies, repeated `Set-Cookie` headers, aborts, backpressure,
|
|
31
|
+
status text, and HEAD responses.
|
|
32
|
+
|
|
33
|
+
## Listen directly
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { listen } from "@askrjs/node";
|
|
37
|
+
|
|
38
|
+
const server = await listen(app, {
|
|
39
|
+
port: 3000,
|
|
40
|
+
requestTimeout: 120_000,
|
|
41
|
+
headersTimeout: 60_000,
|
|
42
|
+
keepAliveTimeout: 5_000,
|
|
43
|
+
});
|
|
44
|
+
server.close();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pass an `AbortSignal` to integrate shutdown with your process lifecycle.
|
|
48
|
+
|
|
49
|
+
Enable the built-in `ws` transport with `websocket: true`. It defaults to a
|
|
50
|
+
1 MiB maximum message payload with compression disabled; pass
|
|
51
|
+
`websocket: { maxPayload, perMessageDeflate }` to override those settings.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
router.ws("/echo", (socket) => {
|
|
55
|
+
socket.onMessage((message) => socket.send(message));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const server = await listen(createServerApp({ router }), { websocket: true });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Route matching, authentication, and middleware complete before the handshake.
|
|
62
|
+
Rejected upgrades preserve the application response, and shutdown closes active
|
|
63
|
+
sockets.
|
|
64
|
+
|
|
65
|
+
## Serve a production application
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { serve } from "@askrjs/node";
|
|
69
|
+
|
|
70
|
+
const running = await serve(app, {
|
|
71
|
+
port: 3000,
|
|
72
|
+
assets: { root: "./dist/client" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await running.close();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`serve` handles static assets and closes both the HTTP server and the application during shutdown.
|
|
79
|
+
|
|
80
|
+
## MCP over stdio
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { connectMcpStdio } from "@askrjs/node/mcp";
|
|
84
|
+
|
|
85
|
+
const connection = connectMcpStdio(mcp, { dependencies });
|
|
86
|
+
await connection.closed;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Protocol messages use stdin/stdout; diagnostics remain isolated on stderr. Authentication may be
|
|
90
|
+
provided directly or resolved from the process environment for each message.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
2
|
+
import { PerMessageDeflateOptions } from "ws";
|
|
2
3
|
import { ServerApp } from "@askrjs/server";
|
|
3
4
|
import { AddressInfo } from "node:net";
|
|
4
5
|
//#region src/contracts.d.ts
|
|
6
|
+
interface NodeWebSocketOptions {
|
|
7
|
+
readonly maxPayload?: number;
|
|
8
|
+
readonly perMessageDeflate?: boolean | PerMessageDeflateOptions;
|
|
9
|
+
}
|
|
5
10
|
interface NodeHandlerOptions {
|
|
6
11
|
baseUrl?: string;
|
|
7
12
|
}
|
|
@@ -10,6 +15,10 @@ interface ListenOptions {
|
|
|
10
15
|
host?: string;
|
|
11
16
|
backlog?: number;
|
|
12
17
|
signal?: AbortSignal;
|
|
18
|
+
requestTimeout?: number;
|
|
19
|
+
headersTimeout?: number;
|
|
20
|
+
keepAliveTimeout?: number;
|
|
21
|
+
websocket?: boolean | NodeWebSocketOptions;
|
|
13
22
|
}
|
|
14
23
|
interface ServeOptions extends ListenOptions {
|
|
15
24
|
readonly assets?: {
|
|
@@ -39,4 +48,4 @@ declare function serve(app: ServerApp & {
|
|
|
39
48
|
close?: () => void | Promise<void>;
|
|
40
49
|
}, options?: ServeOptions): Promise<ServedApplication>;
|
|
41
50
|
//#endregion
|
|
42
|
-
export { ConnectNext, ListenOptions, ListeningServer, NodeHandler, NodeHandlerOptions, ServeOptions, ServedApplication, createNodeHandler, listen, serve };
|
|
51
|
+
export { ConnectNext, ListenOptions, ListeningServer, NodeHandler, NodeHandlerOptions, NodeWebSocketOptions, ServeOptions, ServedApplication, createNodeHandler, listen, serve };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
3
4
|
import { createReadStream } from "node:fs";
|
|
4
5
|
import { stat } from "node:fs/promises";
|
|
5
6
|
import { extname, resolve, sep } from "node:path";
|
|
@@ -129,9 +130,88 @@ function createNodeHandler(app, options = {}) {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
//#endregion
|
|
133
|
+
//#region src/websocket.ts
|
|
134
|
+
function subscribe(socket, event, listener) {
|
|
135
|
+
socket.on(event, listener);
|
|
136
|
+
return () => socket.off(event, listener);
|
|
137
|
+
}
|
|
138
|
+
function socketLike(socket) {
|
|
139
|
+
return {
|
|
140
|
+
send: (data) => socket.send(data),
|
|
141
|
+
close: (code, reason) => socket.close(code, reason),
|
|
142
|
+
onMessage: (listener) => subscribe(socket, "message", (data, binary) => {
|
|
143
|
+
if (!binary) listener(data.toString());
|
|
144
|
+
else {
|
|
145
|
+
const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
|
146
|
+
listener(new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
onClose: (listener) => subscribe(socket, "close", (code, reason) => listener({
|
|
150
|
+
code,
|
|
151
|
+
reason: reason.toString(),
|
|
152
|
+
wasClean: code === 1e3
|
|
153
|
+
})),
|
|
154
|
+
onError: (listener) => subscribe(socket, "error", listener)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function rejectUpgrade(socket, response) {
|
|
158
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
159
|
+
const lines = [`HTTP/1.1 ${response.status} ${response.statusText || "Rejected"}`];
|
|
160
|
+
response.headers.forEach((value, name) => lines.push(`${name}: ${value}`));
|
|
161
|
+
if (!response.headers.has("content-length")) lines.push(`content-length: ${body.byteLength}`);
|
|
162
|
+
lines.push("connection: close", "", "");
|
|
163
|
+
socket.end(Buffer.concat([Buffer.from(lines.join("\r\n")), body]));
|
|
164
|
+
}
|
|
165
|
+
function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
166
|
+
const webSockets = new WebSocketServer({
|
|
167
|
+
noServer: true,
|
|
168
|
+
maxPayload: options.maxPayload ?? 1048576,
|
|
169
|
+
perMessageDeflate: options.perMessageDeflate ?? false
|
|
170
|
+
});
|
|
171
|
+
server.on("upgrade", (request, socket, head) => {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
let accepted;
|
|
174
|
+
Promise.resolve().then(() => requestFromNode(request, handlerOptions, controller.signal)).then(async (webRequest) => {
|
|
175
|
+
let marker;
|
|
176
|
+
const response = await app.fetch(webRequest, { websocket: { upgrade: (_request, handler, context) => {
|
|
177
|
+
marker = new Response(null, { status: 200 });
|
|
178
|
+
accepted = {
|
|
179
|
+
handler,
|
|
180
|
+
context,
|
|
181
|
+
response: marker
|
|
182
|
+
};
|
|
183
|
+
return marker;
|
|
184
|
+
} } });
|
|
185
|
+
if (!accepted || response !== accepted.response) {
|
|
186
|
+
await rejectUpgrade(socket, response);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
webSockets.handleUpgrade(request, socket, head, (webSocket) => {
|
|
190
|
+
webSocket.once("close", () => controller.abort());
|
|
191
|
+
Promise.resolve(accepted.handler(socketLike(webSocket), accepted.context)).catch(() => webSocket.close(1011, "WebSocket handler failed"));
|
|
192
|
+
});
|
|
193
|
+
}).catch(() => socket.destroy());
|
|
194
|
+
});
|
|
195
|
+
return { close() {
|
|
196
|
+
for (const socket of webSockets.clients) socket.close(1001, "Server shutting down");
|
|
197
|
+
webSockets.close();
|
|
198
|
+
} };
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
132
201
|
//#region src/listen.ts
|
|
133
202
|
function listen(app, options = {}) {
|
|
134
203
|
const server = createServer(createNodeHandler(app));
|
|
204
|
+
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
205
|
+
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
206
|
+
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
207
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
208
|
+
if (webSockets) {
|
|
209
|
+
const nativeClose = server.close.bind(server);
|
|
210
|
+
server.close = ((callback) => {
|
|
211
|
+
webSockets.close();
|
|
212
|
+
return nativeClose(callback);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
135
215
|
const close = () => server.close();
|
|
136
216
|
options.signal?.addEventListener("abort", close, { once: true });
|
|
137
217
|
server.once("close", () => options.signal?.removeEventListener("abort", close));
|
|
@@ -169,8 +249,8 @@ function isAssetPath(pathname) {
|
|
|
169
249
|
}
|
|
170
250
|
async function serve(app, options = {}) {
|
|
171
251
|
const root = options.assets ? resolve(options.assets.root) : void 0;
|
|
172
|
-
const applicationHandler = createNodeHandler({ async fetch(request) {
|
|
173
|
-
const result = await app.fetch(request);
|
|
252
|
+
const applicationHandler = createNodeHandler({ async fetch(request, dispatchOptions) {
|
|
253
|
+
const result = await app.fetch(request, dispatchOptions);
|
|
174
254
|
if (!result.headers.has("cache-control") && result.headers.get("content-type")?.includes("text/html")) {
|
|
175
255
|
const headers = new Headers(result.headers);
|
|
176
256
|
headers.set("cache-control", "no-cache");
|
|
@@ -220,6 +300,10 @@ async function serve(app, options = {}) {
|
|
|
220
300
|
}
|
|
221
301
|
applicationHandler(request, response);
|
|
222
302
|
});
|
|
303
|
+
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
304
|
+
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
305
|
+
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
306
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
223
307
|
let closing;
|
|
224
308
|
const signals = options.signals === false ? [] : options.signals ?? ["SIGINT", "SIGTERM"];
|
|
225
309
|
const removeListeners = () => {
|
|
@@ -228,6 +312,7 @@ async function serve(app, options = {}) {
|
|
|
228
312
|
};
|
|
229
313
|
const close = () => closing ??= new Promise((resolveClose, rejectClose) => {
|
|
230
314
|
removeListeners();
|
|
315
|
+
webSockets?.close();
|
|
231
316
|
const finish = async (serverError) => {
|
|
232
317
|
try {
|
|
233
318
|
await app.close?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Node http adapter for @askrjs/server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -56,10 +56,12 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@askrjs/auth": ">=0.0.1 <0.1.0",
|
|
59
|
-
"@askrjs/server": ">=0.0.
|
|
59
|
+
"@askrjs/server": ">=0.0.5 <0.1.0",
|
|
60
|
+
"ws": "^8.21.1"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/node": "^26.0.0",
|
|
64
|
+
"@types/ws": "^8.18.1",
|
|
63
65
|
"publint": "^0.3.21",
|
|
64
66
|
"typescript": "^6.0.0",
|
|
65
67
|
"vite-plus": "^0.2.4",
|