rivetkit 2.3.7 → 2.3.9

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 (90) hide show
  1. package/dist/browser/client.d.ts +34 -6
  2. package/dist/browser/client.js +33 -14
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +1 -1
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/agent-os/index.cjs +1 -1
  7. package/dist/tsup/agent-os/index.cjs.map +1 -1
  8. package/dist/tsup/agent-os/index.d.cts +34 -6
  9. package/dist/tsup/agent-os/index.d.ts +34 -6
  10. package/dist/tsup/agent-os/index.js +1 -1
  11. package/dist/tsup/agent-os/index.js.map +1 -1
  12. package/dist/tsup/{chunk-46AE7JOI.cjs → chunk-3IDKUXQT.cjs} +173 -152
  13. package/dist/tsup/chunk-3IDKUXQT.cjs.map +1 -0
  14. package/dist/tsup/{chunk-DOPH7DT2.cjs → chunk-AVUMOGUE.cjs} +7 -7
  15. package/dist/tsup/{chunk-DOPH7DT2.cjs.map → chunk-AVUMOGUE.cjs.map} +1 -1
  16. package/dist/tsup/{chunk-7YQJXWBW.js → chunk-BQU4XTKM.js} +3 -3
  17. package/dist/tsup/{chunk-6D2WRHJV.js → chunk-CQ23UFQ4.js} +2 -2
  18. package/dist/tsup/{chunk-UIZZCJ5W.cjs → chunk-GZ5DYSIR.cjs} +4 -4
  19. package/dist/tsup/{chunk-UIZZCJ5W.cjs.map → chunk-GZ5DYSIR.cjs.map} +1 -1
  20. package/dist/tsup/{chunk-TQHMPWL6.cjs → chunk-KPP2YRS5.cjs} +9 -9
  21. package/dist/tsup/{chunk-TQHMPWL6.cjs.map → chunk-KPP2YRS5.cjs.map} +1 -1
  22. package/dist/tsup/{chunk-CH25U5NO.js → chunk-KVKWTOW7.js} +3 -3
  23. package/dist/tsup/{chunk-2SGPZEAJ.cjs → chunk-M4R2O5NJ.cjs} +6 -3
  24. package/dist/tsup/chunk-M4R2O5NJ.cjs.map +1 -0
  25. package/dist/tsup/{chunk-YU3C5OCU.js → chunk-MQDQ3A72.js} +39 -18
  26. package/dist/tsup/chunk-MQDQ3A72.js.map +1 -0
  27. package/dist/tsup/{chunk-MNZFNIVI.cjs → chunk-S57DMBTM.cjs} +3 -3
  28. package/dist/tsup/{chunk-MNZFNIVI.cjs.map → chunk-S57DMBTM.cjs.map} +1 -1
  29. package/dist/tsup/{chunk-OG4OR2CM.js → chunk-UBYPRYJR.js} +6 -3
  30. package/dist/tsup/chunk-UBYPRYJR.js.map +1 -0
  31. package/dist/tsup/{chunk-YHPFCYL3.js → chunk-X25YYVFL.js} +2 -2
  32. package/dist/tsup/client/mod.cjs +5 -5
  33. package/dist/tsup/client/mod.d.cts +2 -2
  34. package/dist/tsup/client/mod.d.ts +2 -2
  35. package/dist/tsup/client/mod.js +4 -4
  36. package/dist/tsup/common/log.cjs +2 -2
  37. package/dist/tsup/common/log.js +1 -1
  38. package/dist/tsup/common/websocket.cjs +3 -3
  39. package/dist/tsup/common/websocket.js +2 -2
  40. package/dist/tsup/{config-BRpVbJCD.d.ts → config-CUymbyM6.d.ts} +35 -7
  41. package/dist/tsup/{config-BobvQpex.d.cts → config-CpB-e4I9.d.cts} +35 -7
  42. package/dist/tsup/{context-Cj-DFyiy.d.ts → context-YBUBTwTS.d.ts} +1 -1
  43. package/dist/tsup/{context-YEBNJrws.d.cts → context-c-6G4mjk.d.cts} +1 -1
  44. package/dist/tsup/db/drizzle.cjs +2 -2
  45. package/dist/tsup/db/drizzle.js +1 -1
  46. package/dist/tsup/db/mod.cjs +2 -2
  47. package/dist/tsup/db/mod.js +1 -1
  48. package/dist/tsup/dynamic/mod.d.cts +1 -1
  49. package/dist/tsup/dynamic/mod.d.ts +1 -1
  50. package/dist/tsup/inspector/mod.cjs +4 -4
  51. package/dist/tsup/inspector/mod.js +3 -3
  52. package/dist/tsup/inspector-tab/mod.d.cts +2 -2
  53. package/dist/tsup/inspector-tab/mod.d.ts +2 -2
  54. package/dist/tsup/mod.cjs +449 -124
  55. package/dist/tsup/mod.cjs.map +1 -1
  56. package/dist/tsup/mod.d.cts +3 -3
  57. package/dist/tsup/mod.d.ts +3 -3
  58. package/dist/tsup/mod.js +388 -63
  59. package/dist/tsup/mod.js.map +1 -1
  60. package/dist/tsup/test/mod.cjs +6 -6
  61. package/dist/tsup/test/mod.d.cts +1 -1
  62. package/dist/tsup/test/mod.d.ts +1 -1
  63. package/dist/tsup/test/mod.js +4 -4
  64. package/dist/tsup/utils.cjs +2 -2
  65. package/dist/tsup/utils.js +1 -1
  66. package/dist/tsup/workflow/mod.cjs +6 -6
  67. package/dist/tsup/workflow/mod.d.cts +3 -3
  68. package/dist/tsup/workflow/mod.d.ts +3 -3
  69. package/dist/tsup/workflow/mod.js +3 -3
  70. package/package.json +8 -8
  71. package/src/client/actor-handle.ts +3 -1
  72. package/src/client/raw-utils.ts +11 -2
  73. package/src/common/actor-router-consts.ts +1 -0
  74. package/src/common/encoding.ts +4 -1
  75. package/src/common/fetch-like.ts +49 -0
  76. package/src/engine-client/actor-http-client.ts +6 -22
  77. package/src/registry/index.ts +159 -14
  78. package/src/registry/napi-runtime.ts +74 -0
  79. package/src/registry/native-http.ts +208 -0
  80. package/src/registry/native.ts +111 -63
  81. package/src/registry/runtime.ts +35 -0
  82. package/src/registry/wasm-runtime.ts +11 -0
  83. package/dist/tsup/chunk-2SGPZEAJ.cjs.map +0 -1
  84. package/dist/tsup/chunk-46AE7JOI.cjs.map +0 -1
  85. package/dist/tsup/chunk-OG4OR2CM.js.map +0 -1
  86. package/dist/tsup/chunk-YU3C5OCU.js.map +0 -1
  87. /package/dist/tsup/{chunk-7YQJXWBW.js.map → chunk-BQU4XTKM.js.map} +0 -0
  88. /package/dist/tsup/{chunk-6D2WRHJV.js.map → chunk-CQ23UFQ4.js.map} +0 -0
  89. /package/dist/tsup/{chunk-CH25U5NO.js.map → chunk-KVKWTOW7.js.map} +0 -0
  90. /package/dist/tsup/{chunk-YHPFCYL3.js.map → chunk-X25YYVFL.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { isLocalEngineEndpoint } from "@/common/engine";
2
+ import { isResponseLike } from "@/common/fetch-like";
2
3
  import { configureBaseLogger } from "@/common/log";
3
4
  import { configureServerlessPool } from "@/serverless/configure";
4
5
  import { VERSION } from "@/utils";
@@ -14,8 +15,16 @@ import {
14
15
  RegistryConfigSchema,
15
16
  } from "./config";
16
17
  import { logger } from "./log";
17
- import { buildConfiguredRegistry } from "./native";
18
- import type { RuntimeServerlessResponseHead } from "./runtime";
18
+ import {
19
+ buildConfiguredRegistry,
20
+ } from "./native";
21
+ import { convertNativeHttpResponse } from "./native-http";
22
+ import type {
23
+ CoreRuntime,
24
+ RuntimeApplicationFetch,
25
+ RuntimeHttpResponseBodyStream,
26
+ RuntimeServerlessResponseHead,
27
+ } from "./runtime";
19
28
 
20
29
  type ShutdownSignal = "SIGINT" | "SIGTERM";
21
30
 
@@ -37,6 +46,66 @@ function finishShutdownSignal(signal: ShutdownSignal): void {
37
46
  process.kill(process.pid, signal);
38
47
  }
39
48
 
49
+ function createApplicationFetch(
50
+ application: RegistryApplication,
51
+ runtime: CoreRuntime,
52
+ ): RuntimeApplicationFetch {
53
+ return async (
54
+ request,
55
+ responseBodyStream?: RuntimeHttpResponseBodyStream,
56
+ ) => {
57
+ const method = request.method.toUpperCase();
58
+ const body =
59
+ method === "GET" || method === "HEAD" || request.body.length === 0
60
+ ? undefined
61
+ : Uint8Array.from(request.body).buffer;
62
+ const abortController = new AbortController();
63
+ if (request.cancelToken) {
64
+ if (runtime.cancellationTokenAborted(request.cancelToken)) {
65
+ abortController.abort();
66
+ } else {
67
+ runtime.onCancellationTokenCancelled(
68
+ request.cancelToken,
69
+ () => abortController.abort(),
70
+ );
71
+ }
72
+ }
73
+ const response = await application.fetch(
74
+ new Request(request.url, {
75
+ method,
76
+ headers: request.headers,
77
+ body,
78
+ signal: abortController.signal,
79
+ }),
80
+ );
81
+ if (!isResponseLike(response)) {
82
+ throw new TypeError(
83
+ "registry.listen() application fetch must return a Response",
84
+ );
85
+ }
86
+ const conversion = await convertNativeHttpResponse(
87
+ response,
88
+ responseBodyStream,
89
+ );
90
+ if (conversion.bodyCompletion) {
91
+ void conversion.bodyCompletion.catch((error) => {
92
+ logger().error({
93
+ msg: "application response stream failed",
94
+ error,
95
+ });
96
+ });
97
+ }
98
+ return {
99
+ status: conversion.response.status ?? response.status,
100
+ headers:
101
+ conversion.response.headers ??
102
+ Object.fromEntries(response.headers.entries()),
103
+ body: conversion.response.body,
104
+ stream: conversion.response.stream,
105
+ };
106
+ };
107
+ }
108
+
40
109
  export type FetchHandler = (
41
110
  request: Request,
42
111
  ...args: any
@@ -46,6 +115,17 @@ export interface ServerlessHandler {
46
115
  fetch: FetchHandler;
47
116
  }
48
117
 
118
+ export interface RegistryApplication {
119
+ fetch: FetchHandler;
120
+ }
121
+
122
+ export interface RegistryListenOptions {
123
+ port?: number;
124
+ host?: string;
125
+ publicDir?: string;
126
+ application?: RegistryApplication;
127
+ }
128
+
49
129
  export interface RegistryRoutes {
50
130
  health(): Promise<Response>;
51
131
  metadata(): Promise<Response>;
@@ -79,6 +159,7 @@ export class Registry<A extends RegistryActors> {
79
159
  #runtimeReadyPromise?: Promise<void>;
80
160
  #runtimeServeConfiguredPromise?: ReturnType<typeof buildConfiguredRegistry>;
81
161
  #runtimeServerlessPromise?: ReturnType<typeof buildConfiguredRegistry>;
162
+ #applicationListenerPromise?: Promise<void>;
82
163
  #configureServerlessPoolPromise?: Promise<void>;
83
164
  #welcomePrinted = false;
84
165
  #shutdownInstalled = false;
@@ -348,22 +429,72 @@ export class Registry<A extends RegistryActors> {
348
429
  * @param opts.host Address to bind. Defaults to `0.0.0.0`.
349
430
  * @param opts.publicDir If set, serves static files from this directory
350
431
  * as a fallback below the framework routes.
432
+ * @param opts.application If set, handles requests that do not match a
433
+ * framework route.
351
434
  *
352
435
  * @example
353
436
  * ```ts
354
437
  * await registry.listen();
355
- * await registry.listen({ port: 8080, publicDir: "./public" });
438
+ * await registry.listen({ application: app });
356
439
  * ```
357
440
  */
358
- public async listen(
359
- opts: { port?: number; host?: string; publicDir?: string } = {},
360
- ): Promise<void> {
441
+ public async listen(opts: RegistryListenOptions = {}): Promise<void> {
361
442
  const port = opts.port ?? parsePortEnv(process.env.RIVET_PORT) ?? 3000;
362
443
  const publicDir = opts.publicDir ?? getRivetkitPublicDir();
363
444
  const config = this.parseConfig();
364
445
 
446
+ if (opts.application && getRivetkitRuntimeMode() !== "serverless") {
447
+ if (config.runtime === "wasm") {
448
+ throw new Error(
449
+ "registry.listen() requires the native runtime; use an application-owned HTTP server with WebAssembly",
450
+ );
451
+ }
452
+ this.#installSignalHandlers(config);
453
+ this.#printWelcome(config, "serverful", {
454
+ port,
455
+ host: opts.host,
456
+ publicDir,
457
+ });
458
+ this.#startEnvoy(config, true);
459
+ const readyPromise = this.startAndWait();
460
+ const configuredRegistryPromise =
461
+ this.#runtimeServeConfiguredPromise;
462
+ if (!configuredRegistryPromise) {
463
+ throw new Error("registry envoy startup did not initialize");
464
+ }
465
+ const { runtime, registry, serveConfig } =
466
+ await configuredRegistryPromise;
467
+ const application = createApplicationFetch(
468
+ opts.application,
469
+ runtime,
470
+ );
471
+ await readyPromise;
472
+ const listenerPromise = runtime.serveApplicationListener(
473
+ registry,
474
+ {
475
+ port,
476
+ host: opts.host,
477
+ publicDir,
478
+ application,
479
+ },
480
+ serveConfig,
481
+ );
482
+ this.#applicationListenerPromise = listenerPromise;
483
+ const serveLifecyclePromise = this.#runtimeServeLifecyclePromise;
484
+ if (!serveLifecyclePromise) {
485
+ throw new Error(
486
+ "registry envoy serve lifecycle did not initialize",
487
+ );
488
+ }
489
+ await Promise.all([
490
+ listenerPromise,
491
+ serveLifecyclePromise,
492
+ ]);
493
+ return;
494
+ }
495
+
365
496
  // Cache on both promise fields so the shutdown drain sees Mode A and B.
366
- const configuredRegistryPromise = buildConfiguredRegistry(config);
497
+ const configuredRegistryPromise = this.#buildConfiguredRegistry(config);
367
498
  this.#runtimeServeConfiguredPromise = configuredRegistryPromise;
368
499
  this.#runtimeServerlessPromise = configuredRegistryPromise;
369
500
  this.#installSignalHandlers(config);
@@ -379,9 +510,17 @@ export class Registry<A extends RegistryActors> {
379
510
 
380
511
  const { runtime, registry, serveConfig } =
381
512
  await configuredRegistryPromise;
513
+ const application = opts.application
514
+ ? createApplicationFetch(opts.application, runtime)
515
+ : undefined;
382
516
  await runtime.serveListener(
383
517
  registry,
384
- { port, host: opts.host, publicDir },
518
+ {
519
+ port,
520
+ host: opts.host,
521
+ publicDir,
522
+ application,
523
+ },
385
524
  serveConfig,
386
525
  );
387
526
  }
@@ -499,15 +638,14 @@ export class Registry<A extends RegistryActors> {
499
638
  await runtime.serveRegistry(registry, serveConfig);
500
639
  },
501
640
  );
502
- this.#runtimeServePromise = this.#runtimeServeLifecyclePromise.catch(
503
- (error) => {
641
+ this.#runtimeServePromise =
642
+ this.#runtimeServeLifecyclePromise.catch((error) => {
504
643
  // Always-attached catch so the stored promise never leaves a
505
644
  // rejection unhandled. Downstream awaits (e.g. #runShutdown's
506
645
  // Promise.race) attach their own catches and still observe
507
646
  // resolution via the race.
508
647
  logger().warn({ error }, "runtime registry serve errored");
509
- },
510
- );
648
+ });
511
649
  // Install signal handlers once an envoy lifecycle has begun. Only
512
650
  // Mode A ever reaches here. Mode B (handler(request)) intentionally
513
651
  // does not install handlers because it runs on Workers/Vercel/Deno
@@ -653,6 +791,9 @@ export class Registry<A extends RegistryActors> {
653
791
  // already logged any serve-side error.
654
792
  await runtimeServePromise.catch(() => undefined);
655
793
  }
794
+ if (this.#applicationListenerPromise !== undefined) {
795
+ await this.#applicationListenerPromise.catch(() => undefined);
796
+ }
656
797
  };
657
798
  await Promise.race([
658
799
  drain(),
@@ -712,7 +853,9 @@ export class Registry<A extends RegistryActors> {
712
853
  public startAndWait(): Promise<void> {
713
854
  if (this.#shutdownInFlight !== null) {
714
855
  return Promise.reject(
715
- new Error("registry.startAndWait() cannot run after shutdown has begun"),
856
+ new Error(
857
+ "registry.startAndWait() cannot run after shutdown has begun",
858
+ ),
716
859
  );
717
860
  }
718
861
  if (this.#runtimeReadyPromise) return this.#runtimeReadyPromise;
@@ -755,7 +898,9 @@ export class Registry<A extends RegistryActors> {
755
898
  const readinessPromise = (async () => {
756
899
  const { runtime, registry } = await configuredRegistryPromise;
757
900
  const stoppedBeforeReady = serveLifecyclePromise.then(() => {
758
- throw new Error("RivetKit registry stopped before becoming ready");
901
+ throw new Error(
902
+ "RivetKit registry stopped before becoming ready",
903
+ );
759
904
  });
760
905
  await Promise.race([
761
906
  runtime.waitRegistryReady(registry),
@@ -4,6 +4,7 @@ import type {
4
4
  CancellationToken as NativeCancellationToken,
5
5
  ConnHandle as NativeConnHandle,
6
6
  CoreRegistry as NativeCoreRegistry,
7
+ HttpResponseBodyStream as NativeHttpResponseBodyStream,
7
8
  WebSocket as NativeWebSocket,
8
9
  } from "@rivetkit/rivetkit-napi";
9
10
  import type {
@@ -14,10 +15,13 @@ import type {
14
15
  CoreRuntime,
15
16
  RegistryHandle,
16
17
  RuntimeActorConfig,
18
+ RuntimeApplicationFetch,
19
+ RuntimeApplicationListenerConfig,
17
20
  RuntimeBytes,
18
21
  RuntimeCronFire,
19
22
  RuntimeCronJobInfo,
20
23
  RuntimeHttpRequest,
24
+ RuntimeHttpResponseBodyStream,
21
25
  RuntimeKvEntry,
22
26
  RuntimeKvListOptions,
23
27
  RuntimeListenerConfig,
@@ -135,6 +139,56 @@ function toNapiBuffer(value: RuntimeBytes): Buffer {
135
139
  return Buffer.from(value);
136
140
  }
137
141
 
142
+ function toNapiApplication(application: RuntimeApplicationFetch) {
143
+ return async (
144
+ error: unknown,
145
+ request?: {
146
+ method: string;
147
+ url: string;
148
+ headers: Record<string, string>;
149
+ body: Buffer;
150
+ cancelToken?: NativeCancellationToken;
151
+ responseBodyStream?: RuntimeHttpResponseBodyStream;
152
+ },
153
+ ) => {
154
+ if (error) throw error;
155
+ if (!request) {
156
+ throw new Error("application fetch callback received no request");
157
+ }
158
+ const response = await application(
159
+ {
160
+ ...request,
161
+ body: new Uint8Array(request.body),
162
+ cancelToken:
163
+ request.cancelToken as unknown as CancellationTokenHandle,
164
+ },
165
+ request.responseBodyStream
166
+ ? fromNapiHttpResponseBodyStream(
167
+ request.responseBodyStream as unknown as NativeHttpResponseBodyStream,
168
+ )
169
+ : undefined,
170
+ );
171
+ return {
172
+ ...response,
173
+ body:
174
+ response.body === undefined
175
+ ? undefined
176
+ : toNapiBuffer(response.body),
177
+ };
178
+ };
179
+ }
180
+
181
+ function fromNapiHttpResponseBodyStream(
182
+ stream: NativeHttpResponseBodyStream,
183
+ ): RuntimeHttpResponseBodyStream {
184
+ return {
185
+ cancelled: () => stream.cancelled(),
186
+ write: (chunk) => stream.write(Buffer.from(chunk)),
187
+ end: () => stream.end(),
188
+ error: (message) => stream.error(message),
189
+ };
190
+ }
191
+
138
192
  function toNapiHttpRequest(
139
193
  request?: RuntimeHttpRequest | undefined | null,
140
194
  ): Parameters<NativeActorContext["connectConn"]>[1] {
@@ -296,6 +350,9 @@ export class NapiCoreRuntime implements CoreRuntime {
296
350
  listener: RuntimeListenerConfig,
297
351
  config: RuntimeServeConfig,
298
352
  ): Promise<void> {
353
+ const application = listener.application
354
+ ? toNapiApplication(listener.application)
355
+ : undefined;
299
356
  await asNativeRegistry(registry).serveListener(
300
357
  {
301
358
  port: listener.port,
@@ -303,6 +360,23 @@ export class NapiCoreRuntime implements CoreRuntime {
303
360
  publicDir: listener.publicDir,
304
361
  },
305
362
  config,
363
+ application,
364
+ );
365
+ }
366
+
367
+ async serveApplicationListener(
368
+ registry: RegistryHandle,
369
+ listener: RuntimeApplicationListenerConfig,
370
+ config: RuntimeServeConfig,
371
+ ): Promise<void> {
372
+ await asNativeRegistry(registry).serveApplicationListener(
373
+ {
374
+ port: listener.port,
375
+ host: listener.host,
376
+ publicDir: listener.publicDir,
377
+ },
378
+ toNapiApplication(listener.application),
379
+ config,
306
380
  );
307
381
  }
308
382
 
@@ -0,0 +1,208 @@
1
+ import type { ResponseLike } from "@/common/fetch-like";
2
+ import { HEADER_ORIGINAL_REQUEST_URL } from "@/common/actor-router-consts";
3
+ import { stringifyError } from "@/common/utils";
4
+ import { logger } from "./log";
5
+ import type { RuntimeBytes, RuntimeHttpResponse } from "./runtime";
6
+
7
+ const HTTP_BODY_CHUNK_SIZE = 64 * 1024;
8
+
9
+ // Structural types keep the HTTP adapter independent of the N-API package.
10
+ export interface NativeHttpResponseBodyStream {
11
+ cancelled(): Promise<void>;
12
+ write(chunk: Uint8Array): Promise<void>;
13
+ end(): Promise<void>;
14
+ error(message: string): Promise<void>;
15
+ }
16
+
17
+ export interface NativeHttpRequestBodyStream {
18
+ read(): Promise<Uint8Array | null | undefined>;
19
+ cancel(): Promise<void>;
20
+ }
21
+
22
+ interface NativeHttpRequestInit {
23
+ method: string;
24
+ uri: string;
25
+ headers?: Record<string, string>;
26
+ body?: RuntimeBytes;
27
+ bodyStream?: NativeHttpRequestBodyStream;
28
+ signal?: AbortSignal;
29
+ abortController?: AbortController;
30
+ }
31
+
32
+ interface NativeHttpResponseConversion {
33
+ response: RuntimeHttpResponse;
34
+ // Keeps request-scoped actor resources alive until the response body closes.
35
+ bodyCompletion?: Promise<void>;
36
+ }
37
+
38
+ function runtimeBytesToArrayBuffer(value: RuntimeBytes): ArrayBuffer {
39
+ return value.buffer.slice(
40
+ value.byteOffset,
41
+ value.byteOffset + value.byteLength,
42
+ ) as ArrayBuffer;
43
+ }
44
+
45
+ export function buildNativeHttpRequest(init: NativeHttpRequestInit): Request {
46
+ const headers = new Headers(init.headers);
47
+ const originalUrl = headers.get(HEADER_ORIGINAL_REQUEST_URL);
48
+ headers.delete(HEADER_ORIGINAL_REQUEST_URL);
49
+ const url =
50
+ originalUrl ??
51
+ (init.uri.startsWith("http")
52
+ ? init.uri
53
+ : new URL(init.uri, "http://127.0.0.1").toString());
54
+ const method = init.method.toUpperCase();
55
+ const bodyForbidden = method === "GET" || method === "HEAD";
56
+ const body = bodyForbidden
57
+ ? undefined
58
+ : init.bodyStream
59
+ ? new ReadableStream<Uint8Array>({
60
+ async pull(controller) {
61
+ try {
62
+ if (init.body && init.body.length > 0) {
63
+ controller.enqueue(new Uint8Array(init.body));
64
+ init.body = undefined;
65
+ return;
66
+ }
67
+ const chunk = await init.bodyStream?.read();
68
+ if (!chunk) {
69
+ controller.close();
70
+ } else {
71
+ controller.enqueue(new Uint8Array(chunk));
72
+ }
73
+ } catch (error) {
74
+ init.abortController?.abort(error);
75
+ controller.error(error);
76
+ }
77
+ },
78
+ async cancel() {
79
+ await init.bodyStream?.cancel();
80
+ },
81
+ })
82
+ : init.body && init.body.length > 0
83
+ ? runtimeBytesToArrayBuffer(init.body)
84
+ : undefined;
85
+ const streamInit =
86
+ init.bodyStream && !bodyForbidden ? { duplex: "half" } : {};
87
+ return new Request(url, {
88
+ method,
89
+ headers,
90
+ body,
91
+ signal: init.abortController?.signal ?? init.signal,
92
+ ...streamInit,
93
+ } as RequestInit);
94
+ }
95
+
96
+ export async function cancelNativeHttpRequestBody(
97
+ bodyStream?: NativeHttpRequestBodyStream,
98
+ ) {
99
+ await bodyStream?.cancel();
100
+ }
101
+
102
+ async function writeResponseChunk(
103
+ stream: NativeHttpResponseBodyStream,
104
+ chunk: Uint8Array,
105
+ ) {
106
+ for (
107
+ let offset = 0;
108
+ offset < chunk.byteLength;
109
+ offset += HTTP_BODY_CHUNK_SIZE
110
+ ) {
111
+ await stream.write(
112
+ chunk.subarray(offset, offset + HTTP_BODY_CHUNK_SIZE),
113
+ );
114
+ }
115
+ }
116
+
117
+ async function pumpResponseBody(
118
+ reader: ReadableStreamDefaultReader<Uint8Array>,
119
+ stream: NativeHttpResponseBodyStream,
120
+ ) {
121
+ // Awaiting each write applies native-side backpressure. Racing the next read
122
+ // against cancellation also stops an otherwise unbounded SSE response when
123
+ // its downstream receiver disappears.
124
+ const cancelled = stream
125
+ .cancelled()
126
+ .then(() => ({ cancelled: true }) as const);
127
+ try {
128
+ for (;;) {
129
+ const read = reader
130
+ .read()
131
+ .then((next) => ({ cancelled: false, next }) as const);
132
+ const result = await Promise.race([read, cancelled]);
133
+ if (result.cancelled) {
134
+ await reader.cancel(
135
+ new Error("native http response stream receiver dropped"),
136
+ );
137
+ return;
138
+ }
139
+ const { next } = result;
140
+ if (next.done) {
141
+ await stream.end();
142
+ return;
143
+ }
144
+ if (next.value?.byteLength) {
145
+ await writeResponseChunk(stream, next.value);
146
+ }
147
+ }
148
+ } catch (error) {
149
+ try {
150
+ await stream.error(stringifyError(error));
151
+ } catch (streamError) {
152
+ logger().debug({
153
+ msg: "failed to report native http response stream error",
154
+ error: streamError,
155
+ });
156
+ }
157
+ try {
158
+ await reader.cancel(error);
159
+ } catch {
160
+ // Reader may already be closed after a native-side disconnect.
161
+ }
162
+ } finally {
163
+ reader.releaseLock();
164
+ }
165
+ }
166
+
167
+ export async function convertNativeHttpResponse(
168
+ response: ResponseLike,
169
+ responseBodyStream?: NativeHttpResponseBodyStream,
170
+ ): Promise<NativeHttpResponseConversion> {
171
+ const headers = Object.fromEntries(response.headers.entries());
172
+ if (!response.body) {
173
+ return {
174
+ response: {
175
+ status: response.status,
176
+ headers,
177
+ body: new Uint8Array(),
178
+ },
179
+ };
180
+ }
181
+
182
+ if (responseBodyStream) {
183
+ const reader = response.body.getReader();
184
+ const bodyCompletion = pumpResponseBody(reader, responseBodyStream);
185
+ return {
186
+ response: {
187
+ status: response.status,
188
+ headers,
189
+ stream: true,
190
+ },
191
+ bodyCompletion,
192
+ };
193
+ }
194
+
195
+ return {
196
+ response: {
197
+ status: response.status,
198
+ headers,
199
+ body: new Uint8Array(await response.arrayBuffer()),
200
+ },
201
+ };
202
+ }
203
+
204
+ export const nativeHttpTestInternals = {
205
+ buildRequest: buildNativeHttpRequest,
206
+ cancelRequestBody: cancelNativeHttpRequestBody,
207
+ convertRuntimeHttpResponse: convertNativeHttpResponse,
208
+ };