@beignet/core 0.0.48 → 0.0.50

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.
Files changed (173) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +224 -20
  3. package/dist/client/client.d.ts +0 -2
  4. package/dist/client/client.d.ts.map +1 -1
  5. package/dist/client/client.js +28 -25
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/contracts/contract-builder.d.ts +7 -2
  8. package/dist/contracts/contract-builder.d.ts.map +1 -1
  9. package/dist/contracts/contract-builder.js +20 -2
  10. package/dist/contracts/contract-builder.js.map +1 -1
  11. package/dist/contracts/contract-group.d.ts.map +1 -1
  12. package/dist/contracts/contract-group.js +1 -0
  13. package/dist/contracts/contract-group.js.map +1 -1
  14. package/dist/contracts/contract-like.d.ts +2 -0
  15. package/dist/contracts/contract-like.d.ts.map +1 -1
  16. package/dist/contracts/contract-like.js +27 -1
  17. package/dist/contracts/contract-like.js.map +1 -1
  18. package/dist/contracts/index.d.ts +4 -0
  19. package/dist/contracts/index.d.ts.map +1 -1
  20. package/dist/contracts/index.js +4 -0
  21. package/dist/contracts/index.js.map +1 -1
  22. package/dist/contracts/query-transport.d.ts +126 -0
  23. package/dist/contracts/query-transport.d.ts.map +1 -0
  24. package/dist/contracts/query-transport.js +406 -0
  25. package/dist/contracts/query-transport.js.map +1 -0
  26. package/dist/contracts/schema-shape.d.ts +11 -0
  27. package/dist/contracts/schema-shape.d.ts.map +1 -1
  28. package/dist/contracts/schema-shape.js +13 -0
  29. package/dist/contracts/schema-shape.js.map +1 -1
  30. package/dist/contracts/types.d.ts +5 -0
  31. package/dist/contracts/types.d.ts.map +1 -1
  32. package/dist/contracts/types.js.map +1 -1
  33. package/dist/events/index.d.ts +54 -5
  34. package/dist/events/index.d.ts.map +1 -1
  35. package/dist/events/index.js +183 -32
  36. package/dist/events/index.js.map +1 -1
  37. package/dist/idempotency/index.d.ts +7 -3
  38. package/dist/idempotency/index.d.ts.map +1 -1
  39. package/dist/idempotency/index.js +45 -12
  40. package/dist/idempotency/index.js.map +1 -1
  41. package/dist/mail/index.d.ts.map +1 -1
  42. package/dist/mail/index.js +6 -3
  43. package/dist/mail/index.js.map +1 -1
  44. package/dist/openapi/index.d.ts +8 -0
  45. package/dist/openapi/index.d.ts.map +1 -1
  46. package/dist/openapi/index.js +79 -5
  47. package/dist/openapi/index.js.map +1 -1
  48. package/dist/outbox/index.d.ts +8 -5
  49. package/dist/outbox/index.d.ts.map +1 -1
  50. package/dist/outbox/index.js +17 -3
  51. package/dist/outbox/index.js.map +1 -1
  52. package/dist/ports/best-effort-work.d.ts +21 -0
  53. package/dist/ports/best-effort-work.d.ts.map +1 -0
  54. package/dist/ports/best-effort-work.js +2 -0
  55. package/dist/ports/best-effort-work.js.map +1 -0
  56. package/dist/ports/cache.d.ts +9 -1
  57. package/dist/ports/cache.d.ts.map +1 -1
  58. package/dist/ports/cache.js +20 -5
  59. package/dist/ports/cache.js.map +1 -1
  60. package/dist/ports/events.d.ts +7 -5
  61. package/dist/ports/events.d.ts.map +1 -1
  62. package/dist/ports/index.d.ts +7 -2
  63. package/dist/ports/index.d.ts.map +1 -1
  64. package/dist/ports/index.js +2 -1
  65. package/dist/ports/index.js.map +1 -1
  66. package/dist/ports/testing.d.ts +15 -0
  67. package/dist/ports/testing.d.ts.map +1 -1
  68. package/dist/ports/testing.js +38 -0
  69. package/dist/ports/testing.js.map +1 -1
  70. package/dist/providers/provider.d.ts +8 -5
  71. package/dist/providers/provider.d.ts.map +1 -1
  72. package/dist/providers/provider.js.map +1 -1
  73. package/dist/server/hooks/cors.d.ts +2 -2
  74. package/dist/server/hooks/cors.d.ts.map +1 -1
  75. package/dist/server/hooks/cors.js +2 -1
  76. package/dist/server/hooks/cors.js.map +1 -1
  77. package/dist/server/hooks/logging.d.ts +2 -2
  78. package/dist/server/hooks/logging.d.ts.map +1 -1
  79. package/dist/server/hooks/logging.js.map +1 -1
  80. package/dist/server/hooks/rate-limit.d.ts +16 -8
  81. package/dist/server/hooks/rate-limit.d.ts.map +1 -1
  82. package/dist/server/hooks/rate-limit.js +31 -17
  83. package/dist/server/hooks/rate-limit.js.map +1 -1
  84. package/dist/server/hooks/security.d.ts +2 -2
  85. package/dist/server/hooks/security.d.ts.map +1 -1
  86. package/dist/server/hooks/security.js.map +1 -1
  87. package/dist/server/http.d.ts +21 -2
  88. package/dist/server/http.d.ts.map +1 -1
  89. package/dist/server/index.d.ts +4 -0
  90. package/dist/server/index.d.ts.map +1 -1
  91. package/dist/server/index.js +4 -0
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/instrumentation.d.ts.map +1 -1
  94. package/dist/server/instrumentation.js +5 -3
  95. package/dist/server/instrumentation.js.map +1 -1
  96. package/dist/server/request-executor.d.ts.map +1 -1
  97. package/dist/server/request-executor.js +18 -9
  98. package/dist/server/request-executor.js.map +1 -1
  99. package/dist/server/request-preparation.d.ts.map +1 -1
  100. package/dist/server/request-preparation.js +31 -11
  101. package/dist/server/request-preparation.js.map +1 -1
  102. package/dist/server/response-finalization.d.ts +2 -2
  103. package/dist/server/response-finalization.d.ts.map +1 -1
  104. package/dist/server/response-finalization.js +25 -8
  105. package/dist/server/response-finalization.js.map +1 -1
  106. package/dist/server/route-matching.d.ts.map +1 -1
  107. package/dist/server/route-matching.js +12 -1
  108. package/dist/server/route-matching.js.map +1 -1
  109. package/dist/server/server-sent-events.d.ts +94 -0
  110. package/dist/server/server-sent-events.d.ts.map +1 -0
  111. package/dist/server/server-sent-events.js +275 -0
  112. package/dist/server/server-sent-events.js.map +1 -0
  113. package/dist/server/server.d.ts.map +1 -1
  114. package/dist/server/server.js +43 -22
  115. package/dist/server/server.js.map +1 -1
  116. package/dist/server/trusted-proxy-internal.d.ts +4 -0
  117. package/dist/server/trusted-proxy-internal.d.ts.map +1 -1
  118. package/dist/server/trusted-proxy-internal.js +20 -0
  119. package/dist/server/trusted-proxy-internal.js.map +1 -1
  120. package/dist/server/trusted-proxy.d.ts.map +1 -1
  121. package/dist/server/trusted-proxy.js +3 -8
  122. package/dist/server/trusted-proxy.js.map +1 -1
  123. package/dist/server/use-case-route.d.ts +8 -5
  124. package/dist/server/use-case-route.d.ts.map +1 -1
  125. package/dist/server/use-case-route.js +44 -17
  126. package/dist/server/use-case-route.js.map +1 -1
  127. package/dist/testing/index.d.ts +17 -0
  128. package/dist/testing/index.d.ts.map +1 -1
  129. package/dist/testing/index.js +6 -1
  130. package/dist/testing/index.js.map +1 -1
  131. package/package.json +3 -3
  132. package/skills/app-architecture/SKILL.md +50 -4
  133. package/src/client/client.ts +29 -28
  134. package/src/contracts/contract-builder.ts +32 -2
  135. package/src/contracts/contract-group.ts +1 -0
  136. package/src/contracts/contract-like.ts +40 -1
  137. package/src/contracts/index.ts +23 -0
  138. package/src/contracts/query-transport.ts +697 -0
  139. package/src/contracts/schema-shape.ts +24 -0
  140. package/src/contracts/types.ts +5 -0
  141. package/src/events/index.ts +263 -38
  142. package/src/idempotency/index.ts +65 -17
  143. package/src/mail/index.ts +7 -3
  144. package/src/openapi/index.ts +126 -2
  145. package/src/outbox/index.ts +26 -5
  146. package/src/ports/best-effort-work.ts +21 -0
  147. package/src/ports/cache.ts +29 -7
  148. package/src/ports/events.ts +9 -4
  149. package/src/ports/index.ts +10 -1
  150. package/src/ports/testing.ts +45 -0
  151. package/src/providers/provider.ts +8 -5
  152. package/src/server/hooks/cors.ts +11 -5
  153. package/src/server/hooks/logging.ts +6 -2
  154. package/src/server/hooks/rate-limit.ts +50 -24
  155. package/src/server/hooks/security.ts +8 -4
  156. package/src/server/http.ts +23 -2
  157. package/src/server/index.ts +4 -0
  158. package/src/server/instrumentation.ts +12 -4
  159. package/src/server/request-executor.ts +31 -9
  160. package/src/server/request-preparation.ts +45 -12
  161. package/src/server/response-finalization.ts +51 -15
  162. package/src/server/route-matching.ts +24 -1
  163. package/src/server/server-sent-events.ts +415 -0
  164. package/src/server/server.ts +48 -22
  165. package/src/server/trusted-proxy-internal.ts +20 -0
  166. package/src/server/trusted-proxy.ts +6 -7
  167. package/src/server/use-case-route.ts +62 -23
  168. package/src/testing/index.ts +30 -0
  169. package/dist/query-codec.d.ts +0 -3
  170. package/dist/query-codec.d.ts.map +0 -1
  171. package/dist/query-codec.js +0 -110
  172. package/dist/query-codec.js.map +0 -1
  173. package/src/query-codec.ts +0 -130
@@ -0,0 +1,415 @@
1
+ const DEFAULT_HEARTBEAT_MS = 25_000;
2
+ const DEFAULT_MAX_BUFFERED_BYTES = 1_048_576;
3
+ const MAX_BUFFERED_BYTES = 2_147_483_647;
4
+ const MAX_TIMER_MS = 2_147_483_647;
5
+
6
+ /**
7
+ * One JSON-encoded Server-Sent Event.
8
+ */
9
+ export interface ServerSentEventMessage<TData = unknown> {
10
+ /** JSON-serializable event payload. */
11
+ data: TData;
12
+ /** Optional event type consumed by `EventSource.addEventListener(...)`. */
13
+ event?: string;
14
+ /** Optional event identifier used by browsers for `Last-Event-ID`. */
15
+ id?: string;
16
+ /** Optional browser reconnection delay in milliseconds. */
17
+ retry?: number;
18
+ }
19
+
20
+ /**
21
+ * Controls exposed while an SSE response is active.
22
+ */
23
+ export interface ServerSentEventStream {
24
+ /**
25
+ * Aborts whenever this stream closes, including response cancellation.
26
+ * Async setup should pass it to cancellable subscription APIs.
27
+ */
28
+ readonly signal: AbortSignal;
29
+ /**
30
+ * Send a JSON-encoded event.
31
+ *
32
+ * Returns `false` after the stream closes, when encoding fails, or when the
33
+ * unread byte limit would be exceeded.
34
+ */
35
+ send<TData>(message: ServerSentEventMessage<TData>): boolean;
36
+ /**
37
+ * Send an SSE comment. Comments are useful as connection heartbeats.
38
+ *
39
+ * Returns `false` when the stream is no longer writable or its unread byte
40
+ * limit would be exceeded.
41
+ */
42
+ comment(value?: string): boolean;
43
+ /** Close the stream and run its cleanup exactly once. */
44
+ close(): void;
45
+ }
46
+
47
+ /** Closeable subscription returned by an SSE stream's `start` callback. */
48
+ export interface ServerSentEventSubscription {
49
+ /** Close the associated subscription. */
50
+ close(): Promise<void> | void;
51
+ }
52
+
53
+ /** Cleanup returned by an SSE stream's `start` callback. */
54
+ export type ServerSentEventCleanup =
55
+ | (() => Promise<void> | void)
56
+ | ServerSentEventSubscription;
57
+
58
+ /** Options for `createServerSentEventResponse(...)`. */
59
+ export interface ServerSentEventResponseOptions {
60
+ /**
61
+ * Begin producing events. Return a cleanup callback or closeable
62
+ * subscription for resources associated with this connection. Pending
63
+ * asynchronous setup does not block response cancellation; honor the stream
64
+ * signal and return cleanup when setup settles.
65
+ */
66
+ start(
67
+ stream: ServerSentEventStream,
68
+ ):
69
+ | Promise<ServerSentEventCleanup | undefined>
70
+ | ServerSentEventCleanup
71
+ | undefined;
72
+ /**
73
+ * Abort the stream with the surrounding request or application lifecycle.
74
+ */
75
+ signal?: AbortSignal;
76
+ /**
77
+ * Interval for SSE heartbeat comments. Defaults to 25 seconds. Set to
78
+ * `false` to disable heartbeats.
79
+ */
80
+ heartbeatMs?: number | false;
81
+ /**
82
+ * Close the connection after this duration so clients can reconnect and
83
+ * reconcile. Disabled by default.
84
+ */
85
+ maxLifetimeMs?: number | false;
86
+ /**
87
+ * Maximum bytes of unread event data held by the response stream. Defaults
88
+ * to 1 MiB. The connection closes when one frame or the accumulated queue
89
+ * would exceed this limit.
90
+ */
91
+ maxBufferedBytes?: number;
92
+ /** Additional response headers such as CORS or `Vary`. */
93
+ headers?: HeadersInit;
94
+ /**
95
+ * Observe producer, serialization, buffer, stream, or cleanup failures.
96
+ * Expected `AbortError` rejections caused by stream closure are ignored.
97
+ */
98
+ onError?(error: unknown): Promise<void> | void;
99
+ }
100
+
101
+ function assertTimerOption(
102
+ name: "heartbeatMs" | "maxLifetimeMs",
103
+ value: number | false | undefined,
104
+ ): void {
105
+ if (
106
+ value === false ||
107
+ value === undefined ||
108
+ (Number.isInteger(value) && value >= 1 && value <= MAX_TIMER_MS)
109
+ ) {
110
+ return;
111
+ }
112
+
113
+ throw new RangeError(
114
+ `${name} must be false or an integer between 1 and ${MAX_TIMER_MS} milliseconds.`,
115
+ );
116
+ }
117
+
118
+ function assertMaxBufferedBytes(value: number): void {
119
+ if (Number.isInteger(value) && value >= 1 && value <= MAX_BUFFERED_BYTES) {
120
+ return;
121
+ }
122
+
123
+ throw new RangeError(
124
+ `maxBufferedBytes must be an integer between 1 and ${MAX_BUFFERED_BYTES} bytes.`,
125
+ );
126
+ }
127
+
128
+ function assertSingleLine(name: "event" | "id", value: string): void {
129
+ if (value.includes("\r") || value.includes("\n")) {
130
+ throw new TypeError(`SSE ${name} must not contain line breaks.`);
131
+ }
132
+ if (name === "id" && value.includes("\0")) {
133
+ throw new TypeError("SSE id must not contain null characters.");
134
+ }
135
+ }
136
+
137
+ function isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {
138
+ return (
139
+ value !== null &&
140
+ (typeof value === "object" || typeof value === "function") &&
141
+ "then" in value &&
142
+ typeof value.then === "function"
143
+ );
144
+ }
145
+
146
+ function isAbortError(error: unknown): boolean {
147
+ try {
148
+ return (
149
+ typeof error === "object" &&
150
+ error !== null &&
151
+ "name" in error &&
152
+ error.name === "AbortError"
153
+ );
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function encodeMessage<TData>(message: ServerSentEventMessage<TData>): string {
160
+ if (message.event !== undefined) {
161
+ assertSingleLine("event", message.event);
162
+ }
163
+ if (message.id !== undefined) {
164
+ assertSingleLine("id", message.id);
165
+ }
166
+ if (
167
+ message.retry !== undefined &&
168
+ (!Number.isInteger(message.retry) ||
169
+ message.retry < 0 ||
170
+ message.retry > MAX_TIMER_MS)
171
+ ) {
172
+ throw new RangeError(
173
+ `SSE retry must be an integer between 0 and ${MAX_TIMER_MS} milliseconds.`,
174
+ );
175
+ }
176
+
177
+ const data = JSON.stringify(message.data);
178
+ if (data === undefined) {
179
+ throw new TypeError("SSE data must be JSON-serializable.");
180
+ }
181
+
182
+ const fields: string[] = [];
183
+ if (message.event !== undefined) fields.push(`event: ${message.event}`);
184
+ if (message.id !== undefined) fields.push(`id: ${message.id}`);
185
+ if (message.retry !== undefined) fields.push(`retry: ${message.retry}`);
186
+ fields.push(`data: ${data}`);
187
+ return `${fields.join("\n")}\n\n`;
188
+ }
189
+
190
+ function encodeComment(value: string): string {
191
+ return `${value
192
+ .split(/\r\n|\r|\n/)
193
+ .map((line) => `: ${line}`)
194
+ .join("\n")}\n\n`;
195
+ }
196
+
197
+ function createResponseHeaders(init: HeadersInit | undefined): Headers {
198
+ const headers = new Headers(init);
199
+ headers.set("Content-Type", "text/event-stream; charset=utf-8");
200
+ headers.set("Cache-Control", "no-store, no-transform");
201
+ headers.set("X-Accel-Buffering", "no");
202
+ headers.delete("Connection");
203
+ headers.delete("Content-Length");
204
+ headers.delete("Transfer-Encoding");
205
+ return headers;
206
+ }
207
+
208
+ /**
209
+ * Create a portable Fetch `Response` that safely manages a Server-Sent Events
210
+ * stream.
211
+ *
212
+ * The helper owns SSE framing, JSON encoding, heartbeats, abort handling,
213
+ * bounded unread buffering, maximum lifetime, and cleanup. Authentication,
214
+ * replay, authorization, connection limits, and application reconciliation
215
+ * remain the caller's responsibility.
216
+ */
217
+ export function createServerSentEventResponse(
218
+ options: ServerSentEventResponseOptions,
219
+ ): Response {
220
+ const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
221
+ const maxBufferedBytes =
222
+ options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
223
+ assertTimerOption("heartbeatMs", heartbeatMs);
224
+ assertTimerOption("maxLifetimeMs", options.maxLifetimeMs);
225
+ assertMaxBufferedBytes(maxBufferedBytes);
226
+ const headers = createResponseHeaders(options.headers);
227
+
228
+ const encoder = new TextEncoder();
229
+ const lifecycleController = new AbortController();
230
+ let cleanup: ServerSentEventCleanup | undefined;
231
+ let cleanupPromise: Promise<void> | undefined;
232
+ let closed = false;
233
+ let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
234
+ let lifetimeTimer: ReturnType<typeof setTimeout> | undefined;
235
+ let abortListener: (() => void) | undefined;
236
+ let closeStream: (() => void) | undefined;
237
+
238
+ const reportError = (error: unknown): void => {
239
+ if (!options.onError) return;
240
+ void Promise.resolve()
241
+ .then(() => options.onError?.(error))
242
+ .catch(() => undefined);
243
+ };
244
+
245
+ const runCleanup = (): Promise<void> => {
246
+ if (cleanupPromise) return cleanupPromise;
247
+ if (!cleanup) return Promise.resolve();
248
+ const activeCleanup = cleanup;
249
+ cleanupPromise = Promise.resolve()
250
+ .then(() =>
251
+ typeof activeCleanup === "function"
252
+ ? activeCleanup()
253
+ : activeCleanup.close(),
254
+ )
255
+ .catch(reportError);
256
+ return cleanupPromise;
257
+ };
258
+
259
+ const stream = new ReadableStream<Uint8Array>(
260
+ {
261
+ start(controller) {
262
+ const clearLifecycle = () => {
263
+ if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
264
+ if (lifetimeTimer !== undefined) clearTimeout(lifetimeTimer);
265
+ if (abortListener) {
266
+ options.signal?.removeEventListener("abort", abortListener);
267
+ }
268
+ };
269
+
270
+ const close = () => {
271
+ if (closed) return;
272
+ closed = true;
273
+ clearLifecycle();
274
+ lifecycleController.abort();
275
+ void runCleanup();
276
+ try {
277
+ controller.close();
278
+ } catch {
279
+ // Cancellation may already have detached the stream controller.
280
+ }
281
+ };
282
+ closeStream = close;
283
+
284
+ const enqueue = (value: string): boolean => {
285
+ if (closed) return false;
286
+ try {
287
+ const chunk = encoder.encode(value);
288
+ const availableBytes = controller.desiredSize;
289
+ if (availableBytes === null || chunk.byteLength > availableBytes) {
290
+ reportError(
291
+ new RangeError(
292
+ `SSE buffer limit of ${maxBufferedBytes} bytes exceeded.`,
293
+ ),
294
+ );
295
+ close();
296
+ return false;
297
+ }
298
+ controller.enqueue(chunk);
299
+ return true;
300
+ } catch (error) {
301
+ reportError(error);
302
+ close();
303
+ return false;
304
+ }
305
+ };
306
+
307
+ const controls: ServerSentEventStream = {
308
+ signal: lifecycleController.signal,
309
+ send(message) {
310
+ if (closed) return false;
311
+ try {
312
+ return enqueue(encodeMessage(message));
313
+ } catch (error) {
314
+ reportError(error);
315
+ close();
316
+ return false;
317
+ }
318
+ },
319
+ comment(value = "") {
320
+ if (closed) return false;
321
+ try {
322
+ return enqueue(encodeComment(value));
323
+ } catch (error) {
324
+ reportError(error);
325
+ close();
326
+ return false;
327
+ }
328
+ },
329
+ close,
330
+ };
331
+
332
+ if (options.signal?.aborted) {
333
+ close();
334
+ return;
335
+ }
336
+
337
+ abortListener = close;
338
+ options.signal?.addEventListener("abort", abortListener, {
339
+ once: true,
340
+ });
341
+
342
+ if (heartbeatMs !== false) {
343
+ heartbeatTimer = setInterval(() => {
344
+ controls.comment("heartbeat");
345
+ }, heartbeatMs);
346
+ }
347
+ if (
348
+ options.maxLifetimeMs !== undefined &&
349
+ options.maxLifetimeMs !== false
350
+ ) {
351
+ lifetimeTimer = setTimeout(close, options.maxLifetimeMs);
352
+ }
353
+
354
+ const registerCleanup = (
355
+ resolvedCleanup: ServerSentEventCleanup | undefined,
356
+ ): void => {
357
+ try {
358
+ if (
359
+ typeof resolvedCleanup === "function" ||
360
+ (typeof resolvedCleanup === "object" &&
361
+ resolvedCleanup !== null &&
362
+ "close" in resolvedCleanup &&
363
+ typeof resolvedCleanup.close === "function")
364
+ ) {
365
+ cleanup = resolvedCleanup;
366
+ if (closed) void runCleanup();
367
+ } else if (resolvedCleanup !== undefined) {
368
+ reportError(
369
+ new TypeError(
370
+ "SSE start must return a cleanup function, a closeable subscription, or nothing.",
371
+ ),
372
+ );
373
+ close();
374
+ }
375
+ } catch (error) {
376
+ reportError(error);
377
+ close();
378
+ }
379
+ };
380
+
381
+ const handleStartError = (error: unknown): void => {
382
+ if (!(closed && isAbortError(error))) reportError(error);
383
+ close();
384
+ };
385
+
386
+ try {
387
+ const result = options.start(controls);
388
+ if (isPromiseLike<ServerSentEventCleanup | undefined>(result)) {
389
+ void Promise.resolve(result).then(
390
+ registerCleanup,
391
+ handleStartError,
392
+ );
393
+ } else {
394
+ registerCleanup(result);
395
+ }
396
+ } catch (error) {
397
+ handleStartError(error);
398
+ }
399
+ },
400
+ cancel() {
401
+ closeStream?.();
402
+ return runCleanup();
403
+ },
404
+ },
405
+ {
406
+ highWaterMark: maxBufferedBytes,
407
+ size: (chunk) => chunk?.byteLength ?? 0,
408
+ },
409
+ );
410
+
411
+ return new Response(stream, {
412
+ status: 200,
413
+ headers,
414
+ });
415
+ }
@@ -78,7 +78,10 @@ import {
78
78
  type TrustedProxyConfig,
79
79
  type TrustedRequestInfo,
80
80
  } from "./trusted-proxy.js";
81
- import { assertValidTrustedProxyConfig } from "./trusted-proxy-internal.js";
81
+ import {
82
+ assertValidTrustedProxyConfig,
83
+ parseHttpRequestUrl,
84
+ } from "./trusted-proxy-internal.js";
82
85
  import {
83
86
  createUseCaseRouteHandler,
84
87
  isUseCaseRouteDef,
@@ -539,7 +542,7 @@ export async function createServer<
539
542
  ): Promise<unknown> => {
540
543
  if (!serviceContextsAvailable) {
541
544
  throw new Error(
542
- "Service contexts are unavailable until providers have started.",
545
+ "Service contexts are unavailable during provider setup and after provider shutdown.",
543
546
  );
544
547
  }
545
548
 
@@ -553,16 +556,20 @@ export async function createServer<
553
556
  if (stopped) return;
554
557
  stopped = true;
555
558
  const errors: unknown[] = [];
556
- for (let i = providerResults.length - 1; i >= 0; i -= 1) {
557
- const result = providerResults[i];
558
- try {
559
- await result?.stop?.({
560
- ports: finalPorts,
561
- createServiceContext: lifecycleCreateServiceContext,
562
- });
563
- } catch (err) {
564
- errors.push(err);
559
+ try {
560
+ for (let i = providerResults.length - 1; i >= 0; i -= 1) {
561
+ const result = providerResults[i];
562
+ try {
563
+ await result?.stop?.({
564
+ ports: finalPorts,
565
+ createServiceContext: lifecycleCreateServiceContext,
566
+ });
567
+ } catch (err) {
568
+ errors.push(err);
569
+ }
565
570
  }
571
+ } finally {
572
+ serviceContextsAvailable = false;
566
573
  }
567
574
  if (errors.length) {
568
575
  throw new AggregateError(errors, "Provider shutdown errors");
@@ -744,16 +751,6 @@ export async function createServer<
744
751
  providerResults.push(result);
745
752
  }
746
753
 
747
- for (const result of providerResults) {
748
- if (!result.start) continue;
749
- await result.start({
750
- ports: finalPorts,
751
- createServiceContext: lifecycleCreateServiceContext,
752
- });
753
- }
754
-
755
- instrumentation.attachPorts(finalPorts);
756
-
757
754
  const onUnboundPorts = options.onUnboundPorts ?? "error";
758
755
  if (onUnboundPorts !== "ignore") {
759
756
  const unboundKeys = Object.keys(finalPorts).filter((key) =>
@@ -772,7 +769,20 @@ export async function createServer<
772
769
  }
773
770
  }
774
771
 
772
+ // Every provider has now contributed its ports and the unbound-port guard
773
+ // has passed. Start hooks may safely build service contexts while they
774
+ // activate consumers such as event listeners.
775
775
  serviceContextsAvailable = true;
776
+
777
+ for (const result of providerResults) {
778
+ if (!result.start) continue;
779
+ await result.start({
780
+ ports: finalPorts,
781
+ createServiceContext: lifecycleCreateServiceContext,
782
+ });
783
+ }
784
+
785
+ instrumentation.attachPorts(finalPorts);
776
786
  } catch (error) {
777
787
  try {
778
788
  await stop();
@@ -807,6 +817,7 @@ export async function createServer<
807
817
  path,
808
818
  pathParams: null,
809
819
  query: null,
820
+ queryTransport: null,
810
821
  body: null,
811
822
  responses: {},
812
823
  metadata: {},
@@ -821,8 +832,22 @@ export async function createServer<
821
832
  registryNeedsSort = false;
822
833
  }
823
834
 
824
- const url = new URL(req.url);
825
835
  const method = req.method.toUpperCase();
836
+ let url: URL;
837
+ try {
838
+ url = parseHttpRequestUrl(req.url);
839
+ } catch (error) {
840
+ return await executeFallback(
841
+ {
842
+ contract: fallbackContract("invalidRequestUrl", method, "/"),
843
+ handler: async () => {
844
+ throw error;
845
+ },
846
+ },
847
+ req,
848
+ {},
849
+ );
850
+ }
826
851
 
827
852
  let pathMatchedMethods: Set<string> | undefined;
828
853
  let headCandidate: RegisteredRoute | undefined;
@@ -929,6 +954,7 @@ function rawRouteContract(init: RawRouteInit): HttpContractConfig {
929
954
  path: init.path,
930
955
  pathParams: null,
931
956
  query: null,
957
+ queryTransport: null,
932
958
  body: null,
933
959
  responses: {},
934
960
  metadata: init.metadata ?? {},
@@ -1,5 +1,25 @@
1
1
  import type { TrustedProxyConfig } from "./trusted-proxy.js";
2
2
 
3
+ export class InvalidRequestUrlError extends Error {
4
+ constructor() {
5
+ super("req.url must be an absolute HTTP or HTTPS URL.");
6
+ this.name = "InvalidRequestUrlError";
7
+ }
8
+ }
9
+
10
+ export function parseHttpRequestUrl(value: string): URL {
11
+ try {
12
+ const url = new URL(value);
13
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
14
+ throw new InvalidRequestUrlError();
15
+ }
16
+ return url;
17
+ } catch (error) {
18
+ if (error instanceof InvalidRequestUrlError) throw error;
19
+ throw new InvalidRequestUrlError();
20
+ }
21
+ }
22
+
3
23
  export function requireTrustedProxyHeaderName(
4
24
  name: string,
5
25
  optionName: string,
@@ -1,5 +1,8 @@
1
1
  import type { HttpRequestLike } from "./http.js";
2
- import { requireTrustedProxyHeaderName } from "./trusted-proxy-internal.js";
2
+ import {
3
+ parseHttpRequestUrl,
4
+ requireTrustedProxyHeaderName,
5
+ } from "./trusted-proxy-internal.js";
3
6
 
4
7
  /**
5
8
  * Header source used to resolve a client IP after an app has explicitly opted
@@ -141,17 +144,13 @@ function normalizeHost(
141
144
  }
142
145
 
143
146
  function baseRequestUrl(req: HttpRequestLike): URL {
144
- try {
145
- return new URL(req.url);
146
- } catch {
147
- throw new Error("req.url must be an absolute URL.");
148
- }
147
+ return parseHttpRequestUrl(req.url);
149
148
  }
150
149
 
151
150
  function normalizedBaseProtocol(url: URL): "http" | "https" {
152
151
  const protocol = normalizeProtocol(url.protocol);
153
152
  if (protocol) return protocol;
154
- throw new Error("req.url must use http or https.");
153
+ throw new Error("Resolved request URL must use HTTP or HTTPS.");
155
154
  }
156
155
 
157
156
  /**