@orkestrel/router 0.0.12 → 0.0.14

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.
@@ -1,10 +1,10 @@
1
- import { DispatcherInterface } from '@orkestrel/router';
2
- import { IncomingMessage } from 'node:http';
3
- import { ServerResponse } from 'node:http';
1
+ import type { DispatcherInterface } from '@orkestrel/router';
2
+ import type { IncomingMessage } from 'node:http';
3
+ import type { 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
@@ -70,19 +70,28 @@ export declare function buildRequest(message: IncomingMessage, options?: Request
70
70
  *
71
71
  * @example
72
72
  * ```ts
73
- * import { createListener } from '@src/server'
74
- * import { createDispatcher } from '@src/core'
73
+ * import { createListener } from '@orkestrel/router/server'
74
+ * import { createDispatcher } from '@orkestrel/router'
75
75
  * import http from 'node:http'
76
76
  *
77
- * const dispatcher = createDispatcher()
78
- * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
79
- * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
77
+ * const dispatcher = createDispatcher<{ readonly requestId: string }>()
78
+ * dispatcher.add({
79
+ * method: 'GET',
80
+ * path: '/users/:id',
81
+ * handler: (_request, context) =>
82
+ * Response.json({ id: context.params.id, requestId: context.state.requestId }),
83
+ * })
84
+ *
85
+ * const server = http.createServer(
86
+ * createListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),
87
+ * )
88
+ * server.listen(0)
80
89
  * ```
81
90
  */
82
91
  export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
83
92
 
84
93
  /**
85
- * Handle one `node:http` request through a core dispatcher and write its
94
+ * Handles one `node:http` request through a core dispatcher and writes its
86
95
  * fetch-standard response.
87
96
  *
88
97
  * @remarks
@@ -108,13 +117,13 @@ export declare function createListener<TState>(dispatcher: DispatcherInterface<T
108
117
  export declare function handleListenerRequest<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>, request: IncomingMessage, response: ServerResponse): Promise<void>;
109
118
 
110
119
  /**
111
- * Determine whether a `node:http` connection socket is TLS-encrypted — the
112
- * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
120
+ * Determines whether a `node:http` connection socket is TLS-encrypted — the
121
+ * total, never-throwing narrow `buildRequest` uses to pick the
113
122
  * derived scheme (`https` vs `http`).
114
123
  *
115
124
  * @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`)
125
+ * @returns True if `socket` carries a truthy `encrypted` property (a
126
+ * `tls.TLSSocket`); false otherwise, including for `undefined`
118
127
  *
119
128
  * @example
120
129
  * ```ts
@@ -129,7 +138,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
129
138
  };
130
139
 
131
140
  /**
132
- * A `node:http` request handler — the function `createListener` returns,
141
+ * Represents a `node:http` request handler — the function `createListener` returns,
133
142
  * matching `http.createServer`'s handler signature.
134
143
  *
135
144
  * @remarks
@@ -140,7 +149,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
140
149
  export declare type ListenerFunction = (request: IncomingMessage, response: ServerResponse) => void;
141
150
 
142
151
  /**
143
- * Options for `buildRequest` — URL origin and response-side disconnect tracking.
152
+ * Represents the options for `buildRequest` — URL origin and response-side disconnect tracking.
144
153
  *
145
154
  * @remarks
146
155
  * - `origin` — an explicit scheme + host to build the request URL against
@@ -156,12 +165,12 @@ export declare interface RequestOptions {
156
165
  }
157
166
 
158
167
  /**
159
- * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
160
- * the reverse half of the §5.3 conversion seam.
168
+ * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —
169
+ * the reverse half of the fetch/node conversion seam.
161
170
  *
162
171
  * @remarks
163
172
  * Writes `status`/`statusText`, then every response header (`set-cookie`
164
- * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
173
+ * written through {@link Headers.getSetCookie} so multiple cookies stay distinct
165
174
  * instead of collapsing into one comma-joined header), then streams the web
166
175
  * body to `target` chunk by chunk (`for await` over `response.body`), ending
167
176
  * `target` when the stream completes. When a write reports backpressure, the
@@ -174,7 +183,7 @@ export declare interface RequestOptions {
174
183
  *
175
184
  * @param response - The fetch `Response` to write
176
185
  * @param target - The `node:http` response to write it to
177
- * @returns A promise that resolves once `target` has been ended (or the
186
+ * @returns A promise that resolves after `target` has been ended (or the
178
187
  * stream stopped because `target` was destroyed)
179
188
  *
180
189
  * @example
@@ -1,10 +1,10 @@
1
- import { DispatcherInterface } from '@orkestrel/router';
2
- import { IncomingMessage } from 'node:http';
3
- import { ServerResponse } from 'node:http';
1
+ import type { DispatcherInterface } from '@orkestrel/router';
2
+ import type { IncomingMessage } from 'node:http';
3
+ import type { 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
@@ -70,19 +70,28 @@ export declare function buildRequest(message: IncomingMessage, options?: Request
70
70
  *
71
71
  * @example
72
72
  * ```ts
73
- * import { createListener } from '@src/server'
74
- * import { createDispatcher } from '@src/core'
73
+ * import { createListener } from '@orkestrel/router/server'
74
+ * import { createDispatcher } from '@orkestrel/router'
75
75
  * import http from 'node:http'
76
76
  *
77
- * const dispatcher = createDispatcher()
78
- * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
79
- * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
77
+ * const dispatcher = createDispatcher<{ readonly requestId: string }>()
78
+ * dispatcher.add({
79
+ * method: 'GET',
80
+ * path: '/users/:id',
81
+ * handler: (_request, context) =>
82
+ * Response.json({ id: context.params.id, requestId: context.state.requestId }),
83
+ * })
84
+ *
85
+ * const server = http.createServer(
86
+ * createListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),
87
+ * )
88
+ * server.listen(0)
80
89
  * ```
81
90
  */
82
91
  export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
83
92
 
84
93
  /**
85
- * Handle one `node:http` request through a core dispatcher and write its
94
+ * Handles one `node:http` request through a core dispatcher and writes its
86
95
  * fetch-standard response.
87
96
  *
88
97
  * @remarks
@@ -108,13 +117,13 @@ export declare function createListener<TState>(dispatcher: DispatcherInterface<T
108
117
  export declare function handleListenerRequest<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>, request: IncomingMessage, response: ServerResponse): Promise<void>;
109
118
 
110
119
  /**
111
- * Determine whether a `node:http` connection socket is TLS-encrypted — the
112
- * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
120
+ * Determines whether a `node:http` connection socket is TLS-encrypted — the
121
+ * total, never-throwing narrow `buildRequest` uses to pick the
113
122
  * derived scheme (`https` vs `http`).
114
123
  *
115
124
  * @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`)
125
+ * @returns True if `socket` carries a truthy `encrypted` property (a
126
+ * `tls.TLSSocket`); false otherwise, including for `undefined`
118
127
  *
119
128
  * @example
120
129
  * ```ts
@@ -129,7 +138,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
129
138
  };
130
139
 
131
140
  /**
132
- * A `node:http` request handler — the function `createListener` returns,
141
+ * Represents a `node:http` request handler — the function `createListener` returns,
133
142
  * matching `http.createServer`'s handler signature.
134
143
  *
135
144
  * @remarks
@@ -140,7 +149,7 @@ export declare function isEncryptedSocket(socket: unknown): socket is {
140
149
  export declare type ListenerFunction = (request: IncomingMessage, response: ServerResponse) => void;
141
150
 
142
151
  /**
143
- * Options for `buildRequest` — URL origin and response-side disconnect tracking.
152
+ * Represents the options for `buildRequest` — URL origin and response-side disconnect tracking.
144
153
  *
145
154
  * @remarks
146
155
  * - `origin` — an explicit scheme + host to build the request URL against
@@ -156,12 +165,12 @@ export declare interface RequestOptions {
156
165
  }
157
166
 
158
167
  /**
159
- * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
160
- * the reverse half of the §5.3 conversion seam.
168
+ * Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` —
169
+ * the reverse half of the fetch/node conversion seam.
161
170
  *
162
171
  * @remarks
163
172
  * Writes `status`/`statusText`, then every response header (`set-cookie`
164
- * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
173
+ * written through {@link Headers.getSetCookie} so multiple cookies stay distinct
165
174
  * instead of collapsing into one comma-joined header), then streams the web
166
175
  * body to `target` chunk by chunk (`for await` over `response.body`), ending
167
176
  * `target` when the stream completes. When a write reports backpressure, the
@@ -174,7 +183,7 @@ export declare interface RequestOptions {
174
183
  *
175
184
  * @param response - The fetch `Response` to write
176
185
  * @param target - The `node:http` response to write it to
177
- * @returns A promise that resolves once `target` has been ended (or the
186
+ * @returns A promise that resolves after `target` has been ended (or the
178
187
  * stream stopped because `target` was destroyed)
179
188
  *
180
189
  * @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
@@ -232,13 +236,22 @@ async function handleListenerRequest(dispatcher, state, request, response) {
232
236
  *
233
237
  * @example
234
238
  * ```ts
235
- * import { createListener } from '@src/server'
236
- * import { createDispatcher } from '@src/core'
239
+ * import { createListener } from '@orkestrel/router/server'
240
+ * import { createDispatcher } from '@orkestrel/router'
237
241
  * import http from 'node:http'
238
242
  *
239
- * const dispatcher = createDispatcher()
240
- * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
241
- * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
243
+ * const dispatcher = createDispatcher<{ readonly requestId: string }>()
244
+ * dispatcher.add({
245
+ * method: 'GET',
246
+ * path: '/users/:id',
247
+ * handler: (_request, context) =>
248
+ * Response.json({ id: context.params.id, requestId: context.state.requestId }),
249
+ * })
250
+ *
251
+ * const server = http.createServer(
252
+ * createListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),
253
+ * )
254
+ * server.listen(0)
242
255
  * ```
243
256
  */
244
257
  function createListener(dispatcher, state) {
@@ -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":";;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,kBAAkB,QAAyD;CAC1F,OAAO,SAAS,MAAM,KAAK,OAAO,cAAc;AACjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,aAAa,SAA0B,SAAmC;CACzF,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OAAO,QAAQ,QAAQ,QAAQ;CACrC,MAAM,SAAS,kBAAkB,QAAQ,MAAM,IAAI,UAAU;CAC7D,MAAM,SAAS,SAAS,UAAU,GAAG,OAAO,KAAK;CACjD,MAAM,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,MAAM;CAE9C,MAAM,UAAU,IAAI,QAAQ;CAC5B,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,GAAG;EAC5D,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,SAAS,cAAc;GAC1B,KAAK,MAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,QAAQ,OAAO,MAAM,MAAM;GACxF;EACD;EACA,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK;CAClE;CAEA,MAAM,QAAQ,YAAY;CAC1B,QAAQ,KAAK,eAAe;EAC3B,IAAI,CAAC,QAAQ,UACZ,MAAM,sBAAM,IAAI,MAAM,cAAc,IAAI,SAAS,gCAAgC,CAAC;CACpF,CAAC;CACD,MAAM,WAAW,SAAS;CAC1B,IAAI,aAAa,KAAA,GAChB,SAAS,KAAK,eAAe;EAC5B,IAAI,CAAC,SAAS,eACb,MAAM,sBAAM,IAAI,MAAM,cAAc,IAAI,SAAS,wCAAwC,CAAC;CAC5F,CAAC;CAEF,MAAM,cAAc,WAAW,SAAS,WAAW;CACnD,MAAM,OAAoB;EAAE;EAAQ;EAAS,QAAQ,MAAM;CAAO;CAClE,IAAI,CAAC,aAAa,OAAO,IAAI,QAAQ,KAAK,IAAI;CAE9C,MAAM,OAAO,IAAI,eAA2B,EAC3C,MAAM,MAAM,YAAY;EACvB,IAAI;GACH,WAAW,MAAM,SAAS,SAAS,WAAW,QAAQ,KAAK;GAC3D,WAAW,MAAM;EAClB,SAAS,OAAO;GACf,WAAW,MAAM,KAAK;EACvB;CACD,EACD,CAAC;CACD,MAAM,WAAsD;EAAE,GAAG;EAAM;EAAM,QAAQ;CAAO;CAC5F,OAAO,IAAI,QAAQ,KAAK,QAAQ;AACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,aAAa,UAAoB,QAAuC;CAC7F,OAAO,aAAa,SAAS;CAC7B,OAAO,gBAAgB,SAAS;CAChC,KAAK,MAAM,CAAC,MAAM,UAAU,SAAS,SAAS;EAC7C,IAAI,SAAS,cAAc;EAC3B,OAAO,UAAU,MAAM,KAAK;CAC7B;CACA,MAAM,UAAU,SAAS,QAAQ,aAAa;CAC9C,IAAI,QAAQ,SAAS,GAAG,OAAO,UAAU,cAAc,OAAO;CAE9D,IAAI,SAAS,SAAS,MAAM;EAC3B,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;EAClC;CACD;CACA,IAAI;EACH,WAAW,MAAM,SAAS,SAAS,MAAM;GACxC,IAAI,OAAO,WAAW;GACtB,IAAI,CAAC,OAAO,MAAM,KAAK,GAAG;IACzB,IAAI,OAAO,WAAW;IACtB,MAAM,QAAQ,IAAI,gBAAgB;IAClC,IAAI;KACH,MAAM,QAAQ,KAAK,CAClB,KAAK,QAAQ,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC,GAC9C,KAAK,QAAQ,SAAS,EAAE,QAAQ,MAAM,OAAO,CAAC,CAC/C,CAAC;IACF,UAAU;KACT,MAAM,MAAM;IACb;IACA,IAAI,OAAO,WAAW;GACvB;EACD;EACA,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;CACnC,QAAQ;EACP,IAAI,CAAC,OAAO,WAAW,OAAO,IAAI;CACnC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,sBACrB,YACA,OACA,SACA,UACgB;CAChB,IAAI;EACH,MAAM,YAAY,aAAa,SAAS,EAAE,SAAS,CAAC;EAEpD,MAAM,aAAa,MADE,WAAW,OAAO,WAAW,MAAM,OAAO,CAAC,GACrC,QAAQ;CACpC,SAAS,OAAO;EACf,IAAI,CAAC,SAAS,eAAe,CAAC,SAAS,WAAW;GACjD,SAAS,UAAU,GAAG;GACtB,SAAS,IAAI;EACd,OAAO,IAAI,CAAC,SAAS,WACpB,SAAS,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;CAE5E;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,eACf,YACA,OACmB;CACnB,QAAQ,SAAS,aAAa;EAC7B,sBAA2B,YAAY,OAAO,SAAS,QAAQ;CAChE;AACD"}
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 '@orkestrel/router/server'\n * import { createDispatcher } from '@orkestrel/router'\n * import http from 'node:http'\n *\n * const dispatcher = createDispatcher<{ readonly requestId: string }>()\n * dispatcher.add({\n * \tmethod: 'GET',\n * \tpath: '/users/:id',\n * \thandler: (_request, context) =>\n * \t\tResponse.json({ id: context.params.id, requestId: context.state.requestId }),\n * })\n *\n * const server = http.createServer(\n * \tcreateListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),\n * )\n * server.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.14",
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",
@@ -61,7 +61,7 @@
61
61
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
62
62
  "copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
63
63
  "scaffold": "scaffold",
64
- "lint": "oxlint --config .oxlintrc.json --fix --deny-warnings .",
64
+ "lint": "oxlint --config .oxlintrc.json --fix .",
65
65
  "check": "tsc --noEmit --project tsconfig.json && npm run check:src",
66
66
  "check:src": "npm run check:src:core && npm run check:src:browser && npm run check:src:server",
67
67
  "check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
@@ -77,7 +77,7 @@
77
77
  "test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
78
78
  "test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
79
79
  "test:config": "vitest run --config vite.config.ts --no-cache --reporter=dot --project config",
80
- "test:guides": "vitest run --config vite.config.ts --no-cache --reporter=dot --project guides",
80
+ "test:guides": "node --experimental-strip-types tests/guides.test.ts",
81
81
  "build": "npm run clean && npm run build:src",
82
82
  "build:src": "npm run build:src:core && npm run build:src:browser && npm run build:src:server",
83
83
  "build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
@@ -91,24 +91,23 @@
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.10",
95
+ "@orkestrel/contract": "^0.0.17",
96
+ "@orkestrel/emitter": "^0.0.10"
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.17",
101
+ "@orkestrel/probe": "^0.0.12",
102
+ "@orkestrel/scaffold": "^0.0.63",
103
+ "@orkestrel/test": "^0.0.14",
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",
112
111
  "vitest": "^4.1.11"
113
112
  },
114
113
  "engines": {