@orkestrel/router 0.0.1 → 0.0.2

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,127 +1,166 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type { DispatcherInterface } from '@src/core';
3
- import type { ListenerFunction, RequestOptions, StateFunction } from './types.js';
4
- /**
5
- * Determine whether a `node:http` connection socket is TLS-encrypted — the
6
- * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
7
- * derived scheme (`https` vs `http`).
8
- *
9
- * @param socket - The connection value to test (typically `message.socket`)
10
- * @returns `true` when `socket` carries a truthy `encrypted` property (a
11
- * `tls.TLSSocket`), `false` for anything else (including `undefined`)
12
- *
13
- * @example
14
- * ```ts
15
- * import { isEncryptedSocket } from '@src/server'
16
- *
17
- * isEncryptedSocket({ encrypted: true }) // true
18
- * isEncryptedSocket({}) // false
19
- * ```
20
- */
21
- export declare function isEncryptedSocket(socket: unknown): socket is {
22
- readonly encrypted: true;
23
- };
24
- /**
25
- * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` the
26
- * server-adapter half of the §5.3 conversion seam.
27
- *
28
- * @remarks
29
- * - `method` is carried over verbatim (defaulting to `GET` when absent).
30
- * - The URL is built against `options.origin` when given, otherwise a scheme
31
- * derived from the connection (`https` when {@link isEncryptedSocket}, else
32
- * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).
33
- * - Every request header is copied; multi-value headers are joined per fetch
34
- * semantics (`', '`-joined), except `set-cookie`, whose values are each
35
- * appended individually (fetch `Headers` preserves multiple `set-cookie`
36
- * entries distinctly).
37
- * - For a method that carries a body (anything but `GET`/`HEAD`), the message
38
- * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`
39
- * (reconciling the DOM + node type worlds under the root config), with
40
- * `duplex: 'half'` set as Node's fetch implementation requires for a
41
- * streamed request body.
42
- * - A fresh `@orkestrel/abort` handle backs `request.signal`: if the
43
- * connection closes before the message finished (`!message.complete`), the
44
- * handle aborts — so a handler awaiting `request.signal` observes a client
45
- * disconnect the fetch-standard way, with zero router-specific API.
46
- *
47
- * @param message - The raw `node:http` request
48
- * @param options - Optional `origin` override (§5.3 {@link RequestOptions})
49
- * @returns A fetch `Request` whose `signal` fires on client disconnect
50
- *
51
- * @example
52
- * ```ts
53
- * import { buildRequest } from '@src/server'
54
- * import http from 'node:http'
55
- *
56
- * const server = http.createServer((incoming) => {
57
- * const request = buildRequest(incoming)
58
- * console.log(request.method, request.url)
59
- * })
60
- * ```
61
- */
62
- export declare function buildRequest(message: IncomingMessage, options?: RequestOptions): Request;
63
- /**
64
- * Write a fetch-standard `Response` back to a `node:http` `ServerResponse`
65
- * the reverse half of the §5.3 conversion seam.
66
- *
67
- * @remarks
68
- * Writes `status`/`statusText`, then every response header (`set-cookie`
69
- * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
70
- * instead of collapsing into one comma-joined header), then streams the web
71
- * body to `target` chunk by chunk (`for await` over `response.body`), ending
72
- * `target` when the stream completes. A `null` body ends `target` immediately
73
- * with no further writes. Total error posture: if `target` is destroyed
74
- * mid-stream (the client disconnected), the write loop stops and `target` is
75
- * left as-is rather than throwing an unhandled rejection — a destroyed
76
- * target is not this function's error to surface.
77
- *
78
- * @param response - The fetch `Response` to write
79
- * @param target - The `node:http` response to write it to
80
- * @returns A promise that resolves once `target` has been ended (or the
81
- * stream stopped because `target` was destroyed)
82
- *
83
- * @example
84
- * ```ts
85
- * import { sendResponse } from '@src/server'
86
- * import http from 'node:http'
87
- *
88
- * const server = http.createServer(async (_incoming, target) => {
89
- * await sendResponse(new Response('ok'), target)
90
- * })
91
- * ```
92
- */
93
- export declare function sendResponse(response: Response, target: ServerResponse): Promise<void>;
94
- /**
95
- * Create a `node:http` request listener over a core {@link DispatcherInterface} —
96
- * the whole server face's entry point (§5.3): convert the incoming message to
97
- * a fetch `Request`, hand it to the dispatcher with the consumer's per-request
98
- * `state`, and write the resulting `Response` back.
99
- *
100
- * @remarks
101
- * A rejected `dispatcher.handle` (a route handler throw — the dispatcher
102
- * never invents an error boundary, §5.1) is this listener's transport-level
103
- * LAST RESORT, distinct from an application error boundary: when nothing has
104
- * been sent yet, it destroys the connection with a bare `500` head (never
105
- * leaking a hanging socket); once headers are already sent, it destroys the
106
- * connection outright. The router still owns no error POLICY — a consumer
107
- * that wants mapped error responses installs its own boundary around
108
- * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).
109
- *
110
- * @typeParam TState - The consumer's opaque per-request state type
111
- * @param dispatcher - The core dispatcher to run each converted request through
112
- * @param state - Derives the consumer's per-request `state` from the raw message
113
- * @returns A `(request, response) => void` listener, passable directly to
114
- * `http.createServer`
115
- *
116
- * @example
117
- * ```ts
118
- * import { createListener } from '@src/server'
119
- * import { createDispatcher } from '@src/core'
120
- * import http from 'node:http'
121
- *
122
- * const dispatcher = createDispatcher()
123
- * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
124
- * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
125
- * ```
126
- */
127
- export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
1
+ import { DispatcherInterface } from '../core/index.ts';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { ServerResponse } from 'node:http';
4
+
5
+ /**
6
+ * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` the
7
+ * server-adapter half of the §5.3 conversion seam.
8
+ *
9
+ * @remarks
10
+ * - `method` is carried over verbatim (defaulting to `GET` when absent).
11
+ * - The URL is built against `options.origin` when given, otherwise a scheme
12
+ * derived from the connection (`https` when {@link isEncryptedSocket}, else
13
+ * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).
14
+ * - Every request header is copied; multi-value headers are joined per fetch
15
+ * semantics (`', '`-joined), except `set-cookie`, whose values are each
16
+ * appended individually (fetch `Headers` preserves multiple `set-cookie`
17
+ * entries distinctly).
18
+ * - For a method that carries a body (anything but `GET`/`HEAD`), the message
19
+ * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`
20
+ * (reconciling the DOM + node type worlds under the root config), with
21
+ * `duplex: 'half'` set as Node's fetch implementation requires for a
22
+ * streamed request body.
23
+ * - A fresh `@orkestrel/abort` handle backs `request.signal`: if the
24
+ * connection closes before the message finished (`!message.complete`), the
25
+ * handle aborts so a handler awaiting `request.signal` observes a client
26
+ * disconnect the fetch-standard way, with zero router-specific API.
27
+ *
28
+ * @param message - The raw `node:http` request
29
+ * @param options - Optional `origin` override (§5.3 {@link RequestOptions})
30
+ * @returns A fetch `Request` whose `signal` fires on client disconnect
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { buildRequest } from '@src/server'
35
+ * import http from 'node:http'
36
+ *
37
+ * const server = http.createServer((incoming) => {
38
+ * const request = buildRequest(incoming)
39
+ * console.log(request.method, request.url)
40
+ * })
41
+ * ```
42
+ */
43
+ export declare function buildRequest(message: IncomingMessage, options?: RequestOptions): Request;
44
+
45
+ /**
46
+ * Create a `node:http` request listener over a core {@link DispatcherInterface} —
47
+ * the whole server face's entry point (§5.3): convert the incoming message to
48
+ * a fetch `Request`, hand it to the dispatcher with the consumer's per-request
49
+ * `state`, and write the resulting `Response` back.
50
+ *
51
+ * @remarks
52
+ * A rejected `dispatcher.handle` (a route handler throw — the dispatcher
53
+ * never invents an error boundary, §5.1) is this listener's transport-level
54
+ * LAST RESORT, distinct from an application error boundary: when nothing has
55
+ * been sent yet, it destroys the connection with a bare `500` head (never
56
+ * leaking a hanging socket); once headers are already sent, it destroys the
57
+ * connection outright. The router still owns no error POLICY — a consumer
58
+ * that wants mapped error responses installs its own boundary around
59
+ * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).
60
+ *
61
+ * @typeParam TState - The consumer's opaque per-request state type
62
+ * @param dispatcher - The core dispatcher to run each converted request through
63
+ * @param state - Derives the consumer's per-request `state` from the raw message
64
+ * @returns A `(request, response) => void` listener, passable directly to
65
+ * `http.createServer`
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { createListener } from '@src/server'
70
+ * import { createDispatcher } from '@src/core'
71
+ * import http from 'node:http'
72
+ *
73
+ * const dispatcher = createDispatcher()
74
+ * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
75
+ * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
76
+ * ```
77
+ */
78
+ export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
79
+
80
+ /**
81
+ * Determine whether a `node:http` connection socket is TLS-encrypted — the
82
+ * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
83
+ * derived scheme (`https` vs `http`).
84
+ *
85
+ * @param socket - The connection value to test (typically `message.socket`)
86
+ * @returns `true` when `socket` carries a truthy `encrypted` property (a
87
+ * `tls.TLSSocket`), `false` for anything else (including `undefined`)
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { isEncryptedSocket } from '@src/server'
92
+ *
93
+ * isEncryptedSocket({ encrypted: true }) // true
94
+ * isEncryptedSocket({}) // false
95
+ * ```
96
+ */
97
+ export declare function isEncryptedSocket(socket: unknown): socket is {
98
+ readonly encrypted: true;
99
+ };
100
+
101
+ /**
102
+ * A `node:http` request handler the function `createListener` returns,
103
+ * matching `http.createServer`'s handler signature.
104
+ *
105
+ * @remarks
106
+ * Invoked once per incoming message with the raw `IncomingMessage`/
107
+ * `ServerResponse` pair; never returns a value (writes the response as a
108
+ * side effect).
109
+ */
110
+ export declare type ListenerFunction = (request: IncomingMessage, response: ServerResponse) => void;
111
+
112
+ /**
113
+ * Options for `buildRequest` how to derive the built `Request`'s origin.
114
+ *
115
+ * @remarks
116
+ * - `origin` — an explicit scheme + host to build the request URL against
117
+ * (`https://api.example.com`). Omitted ⇒ derived from the connection: the
118
+ * socket's `encrypted` presence picks `https`/`http`, and the `Host`
119
+ * header supplies the host (absent `Host` ⇒ `localhost`).
120
+ */
121
+ export declare interface RequestOptions {
122
+ readonly origin?: string;
123
+ }
124
+
125
+ /**
126
+ * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
127
+ * the reverse half of the §5.3 conversion seam.
128
+ *
129
+ * @remarks
130
+ * Writes `status`/`statusText`, then every response header (`set-cookie`
131
+ * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
132
+ * instead of collapsing into one comma-joined header), then streams the web
133
+ * body to `target` chunk by chunk (`for await` over `response.body`), ending
134
+ * `target` when the stream completes. A `null` body ends `target` immediately
135
+ * with no further writes. Total error posture: if `target` is destroyed
136
+ * mid-stream (the client disconnected), the write loop stops and `target` is
137
+ * left as-is rather than throwing an unhandled rejection — a destroyed
138
+ * target is not this function's error to surface.
139
+ *
140
+ * @param response - The fetch `Response` to write
141
+ * @param target - The `node:http` response to write it to
142
+ * @returns A promise that resolves once `target` has been ended (or the
143
+ * stream stopped because `target` was destroyed)
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { sendResponse } from '@src/server'
148
+ * import http from 'node:http'
149
+ *
150
+ * const server = http.createServer(async (_incoming, target) => {
151
+ * await sendResponse(new Response('ok'), target)
152
+ * })
153
+ * ```
154
+ */
155
+ export declare function sendResponse(response: Response, target: ServerResponse): Promise<void>;
156
+
157
+ /**
158
+ * Derives a consumer's opaque per-request `TState` from the raw
159
+ * `IncomingMessage` — the `state` argument `createListener` threads into
160
+ * `dispatcher.handle`.
161
+ *
162
+ * @typeParam TState - The consumer's opaque per-request state type
163
+ */
164
+ export declare type StateFunction<TState> = (message: IncomingMessage) => TState;
165
+
166
+ export { }
@@ -1,2 +1,166 @@
1
- export type * from './types.js';
2
- export * from './helpers.js';
1
+ import { DispatcherInterface } from '../core/index.ts';
2
+ import { IncomingMessage } from 'node:http';
3
+ import { ServerResponse } from 'node:http';
4
+
5
+ /**
6
+ * Build a fetch-standard `Request` from a `node:http` `IncomingMessage` — the
7
+ * server-adapter half of the §5.3 conversion seam.
8
+ *
9
+ * @remarks
10
+ * - `method` is carried over verbatim (defaulting to `GET` when absent).
11
+ * - The URL is built against `options.origin` when given, otherwise a scheme
12
+ * derived from the connection (`https` when {@link isEncryptedSocket}, else
13
+ * `http`) plus the `Host` header (absent `Host` ⇒ `localhost`).
14
+ * - Every request header is copied; multi-value headers are joined per fetch
15
+ * semantics (`', '`-joined), except `set-cookie`, whose values are each
16
+ * appended individually (fetch `Headers` preserves multiple `set-cookie`
17
+ * entries distinctly).
18
+ * - For a method that carries a body (anything but `GET`/`HEAD`), the message
19
+ * is pumped chunk by chunk into a DOM-compatible `ReadableStream<Uint8Array>`
20
+ * (reconciling the DOM + node type worlds under the root config), with
21
+ * `duplex: 'half'` set as Node's fetch implementation requires for a
22
+ * streamed request body.
23
+ * - A fresh `@orkestrel/abort` handle backs `request.signal`: if the
24
+ * connection closes before the message finished (`!message.complete`), the
25
+ * handle aborts — so a handler awaiting `request.signal` observes a client
26
+ * disconnect the fetch-standard way, with zero router-specific API.
27
+ *
28
+ * @param message - The raw `node:http` request
29
+ * @param options - Optional `origin` override (§5.3 {@link RequestOptions})
30
+ * @returns A fetch `Request` whose `signal` fires on client disconnect
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { buildRequest } from '@src/server'
35
+ * import http from 'node:http'
36
+ *
37
+ * const server = http.createServer((incoming) => {
38
+ * const request = buildRequest(incoming)
39
+ * console.log(request.method, request.url)
40
+ * })
41
+ * ```
42
+ */
43
+ export declare function buildRequest(message: IncomingMessage, options?: RequestOptions): Request;
44
+
45
+ /**
46
+ * Create a `node:http` request listener over a core {@link DispatcherInterface} —
47
+ * the whole server face's entry point (§5.3): convert the incoming message to
48
+ * a fetch `Request`, hand it to the dispatcher with the consumer's per-request
49
+ * `state`, and write the resulting `Response` back.
50
+ *
51
+ * @remarks
52
+ * A rejected `dispatcher.handle` (a route handler throw — the dispatcher
53
+ * never invents an error boundary, §5.1) is this listener's transport-level
54
+ * LAST RESORT, distinct from an application error boundary: when nothing has
55
+ * been sent yet, it destroys the connection with a bare `500` head (never
56
+ * leaking a hanging socket); once headers are already sent, it destroys the
57
+ * connection outright. The router still owns no error POLICY — a consumer
58
+ * that wants mapped error responses installs its own boundary around
59
+ * `dispatcher.handle` (the future `@orkestrel/server` seam, §7).
60
+ *
61
+ * @typeParam TState - The consumer's opaque per-request state type
62
+ * @param dispatcher - The core dispatcher to run each converted request through
63
+ * @param state - Derives the consumer's per-request `state` from the raw message
64
+ * @returns A `(request, response) => void` listener, passable directly to
65
+ * `http.createServer`
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { createListener } from '@src/server'
70
+ * import { createDispatcher } from '@src/core'
71
+ * import http from 'node:http'
72
+ *
73
+ * const dispatcher = createDispatcher()
74
+ * dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
75
+ * http.createServer(createListener(dispatcher, () => undefined)).listen(0)
76
+ * ```
77
+ */
78
+ export declare function createListener<TState>(dispatcher: DispatcherInterface<TState>, state: StateFunction<TState>): ListenerFunction;
79
+
80
+ /**
81
+ * Determine whether a `node:http` connection socket is TLS-encrypted — the
82
+ * total, never-throwing narrow (AGENTS §14) `buildRequest` uses to pick the
83
+ * derived scheme (`https` vs `http`).
84
+ *
85
+ * @param socket - The connection value to test (typically `message.socket`)
86
+ * @returns `true` when `socket` carries a truthy `encrypted` property (a
87
+ * `tls.TLSSocket`), `false` for anything else (including `undefined`)
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { isEncryptedSocket } from '@src/server'
92
+ *
93
+ * isEncryptedSocket({ encrypted: true }) // true
94
+ * isEncryptedSocket({}) // false
95
+ * ```
96
+ */
97
+ export declare function isEncryptedSocket(socket: unknown): socket is {
98
+ readonly encrypted: true;
99
+ };
100
+
101
+ /**
102
+ * A `node:http` request handler — the function `createListener` returns,
103
+ * matching `http.createServer`'s handler signature.
104
+ *
105
+ * @remarks
106
+ * Invoked once per incoming message with the raw `IncomingMessage`/
107
+ * `ServerResponse` pair; never returns a value (writes the response as a
108
+ * side effect).
109
+ */
110
+ export declare type ListenerFunction = (request: IncomingMessage, response: ServerResponse) => void;
111
+
112
+ /**
113
+ * Options for `buildRequest` — how to derive the built `Request`'s origin.
114
+ *
115
+ * @remarks
116
+ * - `origin` — an explicit scheme + host to build the request URL against
117
+ * (`https://api.example.com`). Omitted ⇒ derived from the connection: the
118
+ * socket's `encrypted` presence picks `https`/`http`, and the `Host`
119
+ * header supplies the host (absent `Host` ⇒ `localhost`).
120
+ */
121
+ export declare interface RequestOptions {
122
+ readonly origin?: string;
123
+ }
124
+
125
+ /**
126
+ * Write a fetch-standard `Response` back to a `node:http` `ServerResponse` —
127
+ * the reverse half of the §5.3 conversion seam.
128
+ *
129
+ * @remarks
130
+ * Writes `status`/`statusText`, then every response header (`set-cookie`
131
+ * written via {@link Headers.getSetCookie} so multiple cookies stay distinct
132
+ * instead of collapsing into one comma-joined header), then streams the web
133
+ * body to `target` chunk by chunk (`for await` over `response.body`), ending
134
+ * `target` when the stream completes. A `null` body ends `target` immediately
135
+ * with no further writes. Total error posture: if `target` is destroyed
136
+ * mid-stream (the client disconnected), the write loop stops and `target` is
137
+ * left as-is rather than throwing an unhandled rejection — a destroyed
138
+ * target is not this function's error to surface.
139
+ *
140
+ * @param response - The fetch `Response` to write
141
+ * @param target - The `node:http` response to write it to
142
+ * @returns A promise that resolves once `target` has been ended (or the
143
+ * stream stopped because `target` was destroyed)
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { sendResponse } from '@src/server'
148
+ * import http from 'node:http'
149
+ *
150
+ * const server = http.createServer(async (_incoming, target) => {
151
+ * await sendResponse(new Response('ok'), target)
152
+ * })
153
+ * ```
154
+ */
155
+ export declare function sendResponse(response: Response, target: ServerResponse): Promise<void>;
156
+
157
+ /**
158
+ * Derives a consumer's opaque per-request `TState` from the raw
159
+ * `IncomingMessage` — the `state` argument `createListener` threads into
160
+ * `dispatcher.handle`.
161
+ *
162
+ * @typeParam TState - The consumer's opaque per-request state type
163
+ */
164
+ export declare type StateFunction<TState> = (message: IncomingMessage) => TState;
165
+
166
+ export { }