@orkestrel/router 0.0.12 → 0.0.13

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.
@@ -3,8 +3,8 @@ import { IncomingMessage } from 'node:http';
3
3
  import { ServerResponse } from 'node:http';
4
4
 
5
5
  /**
6
- * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` — the
7
- * server-adapter half of the §5.3 conversion seam.
6
+ * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the
7
+ * server-adapter half of the fetch/node conversion seam.
8
8
  *
9
9
  * @remarks
10
10
  * - `method` is carried over verbatim (defaulting to `GET` when absent).
@@ -29,7 +29,7 @@ import { ServerResponse } from 'node:http';
29
29
  *
30
30
  * @param message - The raw `node:http` request
31
31
  * @param options - Optional `origin` override and paired `response` for
32
- * response-side disconnect tracking (§5.3 {@link RequestOptions})
32
+ * response-side disconnect tracking ({@link RequestOptions})
33
33
  * @returns A fetch `Request` whose `signal` fires on an incomplete request, or
34
34
  * on a response-side client disconnect when `options.response` is provided
35
35
  *
@@ -47,20 +47,20 @@ import { ServerResponse } from 'node:http';
47
47
  export declare function buildRequest(message: IncomingMessage, options?: RequestOptions): Request;
48
48
 
49
49
  /**
50
- * Create a `node:http` request listener over a core {@link DispatcherInterface} —
51
- * the whole server face's entry point (§5.3): convert the incoming message to
52
- * a fetch `Request`, hand it to the dispatcher with the consumer's per-request
53
- * `state`, and write the resulting `Response` back.
50
+ * Creates a `node:http` request listener over a core {@link DispatcherInterface} —
51
+ * the whole server face's entry point: converts the incoming message to
52
+ * a fetch `Request`, hands it to the dispatcher with the consumer's per-request
53
+ * `state`, and writes the resulting `Response` back.
54
54
  *
55
55
  * @remarks
56
56
  * A rejected `dispatcher.handle` (a route handler throw — the dispatcher
57
- * never invents an error boundary, §5.1) is this listener's transport-level
57
+ * never invents an error boundary) is this listener's transport-level
58
58
  * LAST RESORT, distinct from an application error boundary: when nothing has
59
59
  * been sent yet, it destroys the connection with a bare `500` head (never
60
- * leaking a hanging socket); once headers are already sent, it destroys the
60
+ * leaking a hanging socket); after headers are already sent, it destroys the
61
61
  * connection outright. The router still owns no error POLICY — a consumer
62
62
  * that wants mapped error responses installs its own boundary around
63
- * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).
63
+ * `dispatcher.handle` (the future `@orkestrel/server` seam).
64
64
  *
65
65
  * @typeParam TState - The consumer's opaque per-request state type
66
66
  * @param dispatcher - The core dispatcher to run each converted request through
@@ -82,7 +82,7 @@ export declare function buildRequest(message: IncomingMessage, options?: Request
82
82
  export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
83
83
 
84
84
  /**
85
- * Handle one `node:http` request through a core dispatcher and write its
85
+ * Handles one `node:http` request through a core dispatcher and writes its
86
86
  * fetch-standard response.
87
87
  *
88
88
  * @remarks
@@ -108,13 +108,13 @@ export declare function createListener<TState>(dispatcher: DispatcherInterface<T
108
108
  export declare function handleListenerRequest<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>, request: IncomingMessage, response: ServerResponse): Promise<void>;
109
109
 
110
110
  /**
111
- * Determine whether a `node:http` connection socket is TLS-encrypted — the
112
- * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
111
+ * Determines whether a `node:http` connection socket is TLS-encrypted — the
112
+ * total, never-throwing narrow `buildRequest` uses to pick the
113
113
  * derived scheme (`https` vs `http`).
114
114
  *
115
115
  * @param socket - The connection value to test (typically `message.socket`)
116
- * @returns `true` when `socket` carries a truthy `encrypted` property (a
117
- * `tls.TLSSocket`), `false` for anything else (including `undefined`)
116
+ * @returns True if `socket` carries a truthy `encrypted` property (a
117
+ * `tls.TLSSocket`); false otherwise, including for `undefined`
118
118
  *
119
119
  * @example
120
120
  * ```ts
@@ -129,7 +129,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
129
129
  };
130
130
 
131
131
  /**
132
- * A `node:http` request handler — the function `createListener` returns,
132
+ * Represents a `node:http` request handler — the function `createListener` returns,
133
133
  * matching `http.createServer`'s handler signature.
134
134
  *
135
135
  * @remarks
@@ -140,7 +140,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
140
140
  export declare type ListenerFunction = (request: IncomingMessage, response: ServerResponse) => void;
141
141
 
142
142
  /**
143
- * Options for `buildRequest` — URL origin and response-side disconnect tracking.
143
+ * Represents the options for `buildRequest` — URL origin and response-side disconnect tracking.
144
144
  *
145
145
  * @remarks
146
146
  * - `origin` — an explicit scheme + host to build the request URL against
@@ -156,12 +156,12 @@ export declare interface RequestOptions {
156
156
  }
157
157
 
158
158
  /**
159
- * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
160
- * the reverse half of the §5.3 conversion seam.
159
+ * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —
160
+ * the reverse half of the fetch/node conversion seam.
161
161
  *
162
162
  * @remarks
163
163
  * Writes `status`/`statusText`, then every response header (`set-cookie`
164
- * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
164
+ * written through {@link Headers.getSetCookie} so multiple cookies stay distinct
165
165
  * instead of collapsing into one comma-joined header), then streams the web
166
166
  * body to `target` chunk by chunk (`for await` over `response.body`), ending
167
167
  * `target` when the stream completes. When a write reports backpressure, the
@@ -174,7 +174,7 @@ export declare interface RequestOptions {
174
174
  *
175
175
  * @param response - The fetch `Response` to write
176
176
  * @param target - The `node:http` response to write it to
177
- * @returns A promise that resolves once `target` has been ended (or the
177
+ * @returns A promise that resolves after `target` has been ended (or the
178
178
  * stream stopped because `target` was destroyed)
179
179
  *
180
180
  * @example
@@ -1,15 +1,15 @@
1
+ import { isRecord } from "@orkestrel/contract";
1
2
  import { once } from "node:events";
2
3
  import { createAbort } from "@orkestrel/abort";
3
- import { isRecord } from "@orkestrel/contract";
4
- //#region src/server/helpers.ts
4
+ //#region src/server/validators.ts
5
5
  /**
6
- * Determine whether a `node:http` connection socket is TLS-encrypted — the
7
- * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
6
+ * Determines whether a `node:http` connection socket is TLS-encrypted — the
7
+ * total, never-throwing narrow `buildRequest` uses to pick the
8
8
  * derived scheme (`https` vs `http`).
9
9
  *
10
10
  * @param socket - The connection value to test (typically `message.socket`)
11
- * @returns `true` when `socket` carries a truthy `encrypted` property (a
12
- * `tls.TLSSocket`), `false` for anything else (including `undefined`)
11
+ * @returns True if `socket` carries a truthy `encrypted` property (a
12
+ * `tls.TLSSocket`); false otherwise, including for `undefined`
13
13
  *
14
14
  * @example
15
15
  * ```ts
@@ -22,9 +22,11 @@ import { isRecord } from "@orkestrel/contract";
22
22
  function isEncryptedSocket(socket) {
23
23
  return isRecord(socket) && socket.encrypted === true;
24
24
  }
25
+ //#endregion
26
+ //#region src/server/helpers.ts
25
27
  /**
26
- * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` — the
27
- * server-adapter half of the §5.3 conversion seam.
28
+ * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the
29
+ * server-adapter half of the fetch/node conversion seam.
28
30
  *
29
31
  * @remarks
30
32
  * - `method` is carried over verbatim (defaulting to `GET` when absent).
@@ -49,7 +51,7 @@ function isEncryptedSocket(socket) {
49
51
  *
50
52
  * @param message - The raw `node:http` request
51
53
  * @param options - Optional `origin` override and paired `response` for
52
- * response-side disconnect tracking (§5.3 {@link RequestOptions})
54
+ * response-side disconnect tracking ({@link RequestOptions})
53
55
  * @returns A fetch `Request` whose `signal` fires on an incomplete request, or
54
56
  * on a response-side client disconnect when `options.response` is provided
55
57
  *
@@ -110,12 +112,12 @@ function buildRequest(message, options) {
110
112
  return new Request(url, streamed);
111
113
  }
112
114
  /**
113
- * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
114
- * the reverse half of the §5.3 conversion seam.
115
+ * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —
116
+ * the reverse half of the fetch/node conversion seam.
115
117
  *
116
118
  * @remarks
117
119
  * Writes `status`/`statusText`, then every response header (`set-cookie`
118
- * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
120
+ * written through {@link Headers.getSetCookie} so multiple cookies stay distinct
119
121
  * instead of collapsing into one comma-joined header), then streams the web
120
122
  * body to `target` chunk by chunk (`for await` over `response.body`), ending
121
123
  * `target` when the stream completes. When a write reports backpressure, the
@@ -128,7 +130,7 @@ function buildRequest(message, options) {
128
130
  *
129
131
  * @param response - The fetch `Response` to write
130
132
  * @param target - The `node:http` response to write it to
131
- * @returns A promise that resolves once `target` has been ended (or the
133
+ * @returns A promise that resolves after `target` has been ended (or the
132
134
  * stream stopped because `target` was destroyed)
133
135
  *
134
136
  * @example
@@ -173,8 +175,10 @@ async function sendResponse(response, target) {
173
175
  if (!target.destroyed) target.end();
174
176
  }
175
177
  }
178
+ //#endregion
179
+ //#region src/server/handlers.ts
176
180
  /**
177
- * Handle one `node:http` request through a core dispatcher and write its
181
+ * Handles one `node:http` request through a core dispatcher and writes its
178
182
  * fetch-standard response.
179
183
  *
180
184
  * @remarks
@@ -209,20 +213,20 @@ async function handleListenerRequest(dispatcher, state, request, response) {
209
213
  }
210
214
  }
211
215
  /**
212
- * Create a `node:http` request listener over a core {@link DispatcherInterface} —
213
- * the whole server face's entry point (§5.3): convert the incoming message to
214
- * a fetch `Request`, hand it to the dispatcher with the consumer's per-request
215
- * `state`, and write the resulting `Response` back.
216
+ * Creates a `node:http` request listener over a core {@link DispatcherInterface} —
217
+ * the whole server face's entry point: converts the incoming message to
218
+ * a fetch `Request`, hands it to the dispatcher with the consumer's per-request
219
+ * `state`, and writes the resulting `Response` back.
216
220
  *
217
221
  * @remarks
218
222
  * A rejected `dispatcher.handle` (a route handler throw — the dispatcher
219
- * never invents an error boundary, §5.1) is this listener's transport-level
223
+ * never invents an error boundary) is this listener's transport-level
220
224
  * LAST RESORT, distinct from an application error boundary: when nothing has
221
225
  * been sent yet, it destroys the connection with a bare `500` head (never
222
- * leaking a hanging socket); once headers are already sent, it destroys the
226
+ * leaking a hanging socket); after headers are already sent, it destroys the
223
227
  * connection outright. The router still owns no error POLICY — a consumer
224
228
  * that wants mapped error responses installs its own boundary around
225
- * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).
229
+ * `dispatcher.handle` (the future `@orkestrel/server` seam).
226
230
  *
227
231
  * @typeParam TState - The consumer's opaque per-request state type
228
232
  * @param dispatcher - The core dispatcher to run each converted request through
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/helpers.ts"],"sourcesContent":["// ============================================================================\n// Pure conversion + glue between `node:http` and the fetch vocabulary the\n// core `Dispatcher` speaks — no lifecycle, no listener ownership beyond the\n// handler function `createListener` returns (§5.3). Every function is\n// exported per AGENTS §5.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, RequestOptions, StateFunction } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determine whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns `true` when `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`), `false` for anything else (including `undefined`)\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n\n/**\n * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the §5.3 conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking (§5.3 {@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of Array.isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, Array.isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the §5.3 conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written via {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves once `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n\n/**\n * Handle one `node:http` request through a core dispatcher and write its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(error instanceof Error ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Create a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point (§5.3): convert the incoming message to\n * a fetch `Request`, hand it to the dispatcher with the consumer's per-request\n * `state`, and write the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary, §5.1) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); once headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@src/server'\n * import { createDispatcher } from '@src/core'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher()\n * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })\n * http.createServer(createListener(dispatcher, () => undefined)).listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/validators.ts","../../../src/server/helpers.ts","../../../src/server/handlers.ts"],"sourcesContent":["// ============================================================================\n// Server guards — the centralized-file rule's home for the total `is*` narrows the\n// `node:http` conversion seam applies to raw connection values. Every\n// declaration here is total and `export`ed per the centralized-file rule.\n// ============================================================================\n\nimport { isRecord } from '@orkestrel/contract'\n\n/**\n * Determines whether a `node:http` connection socket is TLS-encrypted — the\n * total, never-throwing narrow `buildRequest` uses to pick the\n * derived scheme (`https` vs `http`).\n *\n * @param socket - The connection value to test (typically `message.socket`)\n * @returns True if `socket` carries a truthy `encrypted` property (a\n * `tls.TLSSocket`); false otherwise, including for `undefined`\n *\n * @example\n * ```ts\n * import { isEncryptedSocket } from '@src/server'\n *\n * isEncryptedSocket({ encrypted: true }) // true\n * isEncryptedSocket({}) // false\n * ```\n */\nexport function isEncryptedSocket(socket: unknown): socket is { readonly encrypted: true } {\n\treturn isRecord(socket) && socket.encrypted === true\n}\n","// ============================================================================\n// Pure conversion between `node:http` and the fetch vocabulary the core\n// `Dispatcher` speaks — no lifecycle and no listener ownership. Every\n// function is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RequestOptions } from './types.js'\nimport { once } from 'node:events'\nimport { createAbort } from '@orkestrel/abort'\nimport { isEncryptedSocket } from './validators.js'\n\n/**\n * Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the\n * server-adapter half of the fetch/node conversion seam.\n *\n * @remarks\n * - `method` is carried over verbatim (defaulting to `GET` when absent).\n * - The URL is built against `options.origin` when given, otherwise a scheme\n * derived from the connection (`https` when {@link isEncryptedSocket}, else\n * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).\n * - Every request header is copied; multi-value headers are joined per fetch\n * semantics (`', '`-joined), except `set-cookie`, whose values are each\n * appended individually (fetch `Headers` preserves multiple `set-cookie`\n * entries distinctly).\n * - For a method that carries a body (anything but `GET`/`HEAD`), the message\n * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`\n * (reconciling the DOM + node type worlds under the root config), with\n * `duplex: 'half'` set as Node's fetch implementation requires for a\n * streamed request body.\n * - A fresh `@orkestrel/abort` handle backs `request.signal`. It aborts when\n * the request connection closes before the message finished\n * (`!message.complete`), or when the paired `options.response` closes before\n * its response finished (`!response.writableEnded`). A handler awaiting the\n * signal therefore observes either side of a client disconnect the\n * fetch-standard way, with zero router-specific API.\n *\n * @param message - The raw `node:http` request\n * @param options - Optional `origin` override and paired `response` for\n * response-side disconnect tracking ({@link RequestOptions})\n * @returns A fetch `Request` whose `signal` fires on an incomplete request, or\n * on a response-side client disconnect when `options.response` is provided\n *\n * @example\n * ```ts\n * import { buildRequest } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer((incoming, response) => {\n * \tconst request = buildRequest(incoming, { response })\n * \tconsole.log(request.method, request.url)\n * })\n * ```\n */\nexport function buildRequest(message: IncomingMessage, options?: RequestOptions): Request {\n\tconst method = message.method ?? 'GET'\n\tconst host = message.headers.host ?? 'localhost'\n\tconst scheme = isEncryptedSocket(message.socket) ? 'https' : 'http'\n\tconst origin = options?.origin ?? `${scheme}://${host}`\n\tconst url = new URL(message.url ?? '/', origin)\n\n\tconst headers = new Headers()\n\tfor (const [name, value] of Object.entries(message.headers)) {\n\t\tif (value === undefined) continue\n\t\tif (name === 'set-cookie') {\n\t\t\tfor (const cookie of Array.isArray(value) ? value : [value]) headers.append(name, cookie)\n\t\t\tcontinue\n\t\t}\n\t\theaders.set(name, Array.isArray(value) ? value.join(', ') : value)\n\t}\n\n\tconst abort = createAbort()\n\tmessage.once('close', () => {\n\t\tif (!message.complete)\n\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before completion`))\n\t})\n\tconst response = options?.response\n\tif (response !== undefined)\n\t\tresponse.once('close', () => {\n\t\t\tif (!response.writableEnded)\n\t\t\t\tabort.abort(new Error(`request to ${url.pathname} disconnected before response completed`))\n\t\t})\n\n\tconst carriesBody = method !== 'GET' && method !== 'HEAD'\n\tconst init: RequestInit = { method, headers, signal: abort.signal }\n\tif (!carriesBody) return new Request(url, init)\n\n\tconst body = new ReadableStream<Uint8Array>({\n\t\tasync start(controller) {\n\t\t\ttry {\n\t\t\t\tfor await (const chunk of message) controller.enqueue(chunk)\n\t\t\t\tcontroller.close()\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error)\n\t\t\t}\n\t\t},\n\t})\n\tconst streamed: RequestInit & { readonly duplex: 'half' } = { ...init, body, duplex: 'half' }\n\treturn new Request(url, streamed)\n}\n\n/**\n * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —\n * the reverse half of the fetch/node conversion seam.\n *\n * @remarks\n * Writes `status`/`statusText`, then every response header (`set-cookie`\n * written through {@link Headers.getSetCookie} so multiple cookies stay distinct\n * instead of collapsing into one comma-joined header), then streams the web\n * body to `target` chunk by chunk (`for await` over `response.body`), ending\n * `target` when the stream completes. When a write reports backpressure, the\n * body pump waits for `drain` before pulling again, unless the target closes,\n * errors, or is destroyed first. A `null` body ends `target` immediately with\n * no further writes. Total error posture: if `target` is destroyed mid-stream\n * (the client disconnected), the write loop stops and `target` is left as-is\n * rather than throwing an unhandled rejection — a destroyed target is not\n * this function's error to surface.\n *\n * @param response - The fetch `Response` to write\n * @param target - The `node:http` response to write it to\n * @returns A promise that resolves after `target` has been ended (or the\n * stream stopped because `target` was destroyed)\n *\n * @example\n * ```ts\n * import { sendResponse } from '@src/server'\n * import http from 'node:http'\n *\n * const server = http.createServer(async (_incoming, target) => {\n * \tawait sendResponse(new Response('ok'), target)\n * })\n * ```\n */\nexport async function sendResponse(response: Response, target: ServerResponse): Promise<void> {\n\ttarget.statusCode = response.status\n\ttarget.statusMessage = response.statusText\n\tfor (const [name, value] of response.headers) {\n\t\tif (name === 'set-cookie') continue\n\t\ttarget.setHeader(name, value)\n\t}\n\tconst cookies = response.headers.getSetCookie()\n\tif (cookies.length > 0) target.setHeader('set-cookie', cookies)\n\n\tif (response.body === null) {\n\t\tif (!target.destroyed) target.end()\n\t\treturn\n\t}\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\tif (target.destroyed) return\n\t\t\tif (!target.write(chunk)) {\n\t\t\t\tif (target.destroyed) return\n\t\t\t\tconst abort = new AbortController()\n\t\t\t\ttry {\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tonce(target, 'drain', { signal: abort.signal }),\n\t\t\t\t\t\tonce(target, 'close', { signal: abort.signal }),\n\t\t\t\t\t])\n\t\t\t\t} finally {\n\t\t\t\t\tabort.abort()\n\t\t\t\t}\n\t\t\t\tif (target.destroyed) return\n\t\t\t}\n\t\t}\n\t\tif (!target.destroyed) target.end()\n\t} catch {\n\t\tif (!target.destroyed) target.end()\n\t}\n}\n","// ============================================================================\n// Server request handlers — the centralized-file rule's home for the functions that\n// run one `node:http` exchange through a core `Dispatcher`, plus the listener\n// the whole server face hands to `http.createServer`. Every function\n// is exported per the centralized-file rule.\n// ============================================================================\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { DispatcherInterface } from '@src/core'\nimport type { ListenerFunction, StateFunction } from './types.js'\nimport { buildRequest, sendResponse } from './helpers.js'\n\n/**\n * Handles one `node:http` request through a core dispatcher and writes its\n * fetch-standard response.\n *\n * @remarks\n * This is the named asynchronous orchestration behind {@link createListener}.\n * A rejected dispatch is treated only as a transport-level last resort: write\n * a bare `500` before headers, or destroy a response whose headers have\n * already started.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run\n * @param state - Derives the consumer state from the incoming message\n * @param request - The raw `node:http` request\n * @param response - The raw `node:http` response\n * @returns A promise that settles after the response is written or closed\n *\n * @example\n * ```ts\n * const server = http.createServer((request, response) => {\n * \tvoid handleListenerRequest(dispatcher, () => undefined, request, response)\n * })\n * ```\n */\nexport async function handleListenerRequest<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n\trequest: IncomingMessage,\n\tresponse: ServerResponse,\n): Promise<void> {\n\ttry {\n\t\tconst converted = buildRequest(request, { response })\n\t\tconst result = await dispatcher.handle(converted, state(request))\n\t\tawait sendResponse(result, response)\n\t} catch (error) {\n\t\tif (!response.headersSent && !response.destroyed) {\n\t\t\tresponse.writeHead(500)\n\t\t\tresponse.end()\n\t\t} else if (!response.destroyed) {\n\t\t\tresponse.destroy(error instanceof Error ? error : new Error(String(error)))\n\t\t}\n\t}\n}\n\n/**\n * Creates a `node:http` request listener over a core {@link DispatcherInterface} —\n * the whole server face's entry point: converts the incoming message to\n * a fetch `Request`, hands it to the dispatcher with the consumer's per-request\n * `state`, and writes the resulting `Response` back.\n *\n * @remarks\n * A rejected `dispatcher.handle` (a route handler throw — the dispatcher\n * never invents an error boundary) is this listener's transport-level\n * LAST RESORT, distinct from an application error boundary: when nothing has\n * been sent yet, it destroys the connection with a bare `500` head (never\n * leaking a hanging socket); after headers are already sent, it destroys the\n * connection outright. The router still owns no error POLICY — a consumer\n * that wants mapped error responses installs its own boundary around\n * `dispatcher.handle` (the future `@orkestrel/server` seam).\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param dispatcher - The core dispatcher to run each converted request through\n * @param state - Derives the consumer's per-request `state` from the raw message\n * @returns A `(request, response) => void` listener, passable directly to\n * `http.createServer`\n *\n * @example\n * ```ts\n * import { createListener } from '@src/server'\n * import { createDispatcher } from '@src/core'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher()\n * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })\n * http.createServer(createListener(dispatcher, () => undefined)).listen(0)\n * ```\n */\nexport function createListener<TState>(\n\tdispatcher: DispatcherInterface<TState>,\n\tstate: StateFunction<TState>,\n): ListenerFunction {\n\treturn (request, response) => {\n\t\tvoid handleListenerRequest(dispatcher, state, request, response)\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkestrel/router",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A typed request router for the @orkestrel line — server and browser environments. Part of the @orkestrel line.",
5
5
  "keywords": [
6
6
  "browser",
@@ -91,24 +91,24 @@
91
91
  "test:setup": "vitest run --config vite.config.ts --no-cache --reporter=dot --project setup"
92
92
  },
93
93
  "dependencies": {
94
- "@orkestrel/abort": "^0.0.8",
95
- "@orkestrel/contract": "^0.0.13",
96
- "@orkestrel/emitter": "^0.0.8"
94
+ "@orkestrel/abort": "^0.0.9",
95
+ "@orkestrel/contract": "^0.0.16",
96
+ "@orkestrel/emitter": "^0.0.9"
97
97
  },
98
98
  "devDependencies": {
99
99
  "@microsoft/api-extractor": "^7.59.0",
100
- "@orkestrel/guide": "^0.0.14",
101
- "@orkestrel/probe": "^0.0.9",
102
- "@orkestrel/scaffold": "^0.0.57",
103
- "@orkestrel/test": "^0.0.11",
104
- "@types/node": "^26.4.0",
100
+ "@orkestrel/guide": "^0.0.16",
101
+ "@orkestrel/probe": "^0.0.11",
102
+ "@orkestrel/scaffold": "^0.0.61",
103
+ "@orkestrel/test": "^0.0.13",
104
+ "@types/node": "^26.4.1",
105
105
  "@vitest/browser-playwright": "^4.1.11",
106
- "oxfmt": "^0.65.0",
107
- "oxlint": "^1.80.0",
106
+ "oxfmt": "^0.66.0",
107
+ "oxlint": "^1.81.0",
108
108
  "playwright": "^1.62.1",
109
109
  "typescript": "^6.0.3",
110
110
  "vite": "^8.2.2",
111
- "vite-plugin-dts": "^5.0.3",
111
+ "vite-plugin-dts": "^5.1.0",
112
112
  "vitest": "^4.1.11"
113
113
  },
114
114
  "engines": {