rivetkit 2.3.6 → 2.3.8
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.
- package/dist/browser/client.d.ts +34 -6
- package/dist/browser/client.js +33 -14
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.js +1 -1
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/tsup/agent-os/index.cjs +1 -1
- package/dist/tsup/agent-os/index.cjs.map +1 -1
- package/dist/tsup/agent-os/index.d.cts +34 -6
- package/dist/tsup/agent-os/index.d.ts +34 -6
- package/dist/tsup/agent-os/index.js +1 -1
- package/dist/tsup/agent-os/index.js.map +1 -1
- package/dist/tsup/{chunk-AXEGEUYB.cjs → chunk-5MJPN3QU.cjs} +172 -151
- package/dist/tsup/chunk-5MJPN3QU.cjs.map +1 -0
- package/dist/tsup/{chunk-WS3PP253.cjs → chunk-7TNEDX3Y.cjs} +3 -3
- package/dist/tsup/{chunk-WS3PP253.cjs.map → chunk-7TNEDX3Y.cjs.map} +1 -1
- package/dist/tsup/{chunk-F6ZW7JZQ.js → chunk-BL6ERZQO.js} +36 -15
- package/dist/tsup/chunk-BL6ERZQO.js.map +1 -0
- package/dist/tsup/{chunk-HVOKIYUB.cjs → chunk-F6GCLL5Z.cjs} +7 -7
- package/dist/tsup/{chunk-HVOKIYUB.cjs.map → chunk-F6GCLL5Z.cjs.map} +1 -1
- package/dist/tsup/{chunk-PLLHHJWR.js → chunk-HWBISPRP.js} +2 -2
- package/dist/tsup/{chunk-FNTTW25Q.cjs → chunk-IJMVVULS.cjs} +4 -4
- package/dist/tsup/{chunk-FNTTW25Q.cjs.map → chunk-IJMVVULS.cjs.map} +1 -1
- package/dist/tsup/{chunk-BX4G2O6M.js → chunk-MY6BRV3S.js} +3 -3
- package/dist/tsup/{chunk-234EFKN2.cjs → chunk-NUMGL52R.cjs} +6 -3
- package/dist/tsup/chunk-NUMGL52R.cjs.map +1 -0
- package/dist/tsup/{chunk-PD7SZKHY.cjs → chunk-TMTJPSHQ.cjs} +9 -9
- package/dist/tsup/{chunk-PD7SZKHY.cjs.map → chunk-TMTJPSHQ.cjs.map} +1 -1
- package/dist/tsup/{chunk-OH6CBWUD.js → chunk-V7PJYBA5.js} +2 -2
- package/dist/tsup/{chunk-3ZE5UDOA.js → chunk-YFXQ7DBH.js} +3 -3
- package/dist/tsup/{chunk-RDTFPGU2.js → chunk-ZKDZW5DQ.js} +6 -3
- package/dist/tsup/chunk-ZKDZW5DQ.js.map +1 -0
- package/dist/tsup/client/mod.cjs +5 -5
- package/dist/tsup/client/mod.d.cts +2 -2
- package/dist/tsup/client/mod.d.ts +2 -2
- package/dist/tsup/client/mod.js +4 -4
- package/dist/tsup/common/log.cjs +2 -2
- package/dist/tsup/common/log.js +1 -1
- package/dist/tsup/common/websocket.cjs +3 -3
- package/dist/tsup/common/websocket.js +2 -2
- package/dist/tsup/{config-BRpVbJCD.d.ts → config-CUymbyM6.d.ts} +35 -7
- package/dist/tsup/{config-BobvQpex.d.cts → config-CpB-e4I9.d.cts} +35 -7
- package/dist/tsup/{context-Cj-DFyiy.d.ts → context-YBUBTwTS.d.ts} +1 -1
- package/dist/tsup/{context-YEBNJrws.d.cts → context-c-6G4mjk.d.cts} +1 -1
- package/dist/tsup/db/drizzle.cjs +2 -2
- package/dist/tsup/db/drizzle.js +1 -1
- package/dist/tsup/db/mod.cjs +2 -2
- package/dist/tsup/db/mod.js +1 -1
- package/dist/tsup/dynamic/mod.d.cts +1 -1
- package/dist/tsup/dynamic/mod.d.ts +1 -1
- package/dist/tsup/inspector/mod.cjs +4 -4
- package/dist/tsup/inspector/mod.js +3 -3
- package/dist/tsup/inspector-tab/mod.d.cts +2 -2
- package/dist/tsup/inspector-tab/mod.d.ts +2 -2
- package/dist/tsup/mod.cjs +449 -124
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +3 -3
- package/dist/tsup/mod.d.ts +3 -3
- package/dist/tsup/mod.js +388 -63
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +6 -6
- package/dist/tsup/test/mod.d.cts +1 -1
- package/dist/tsup/test/mod.d.ts +1 -1
- package/dist/tsup/test/mod.js +4 -4
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.js +1 -1
- package/dist/tsup/workflow/mod.cjs +6 -6
- package/dist/tsup/workflow/mod.d.cts +3 -3
- package/dist/tsup/workflow/mod.d.ts +3 -3
- package/dist/tsup/workflow/mod.js +3 -3
- package/package.json +8 -8
- package/src/client/actor-handle.ts +3 -1
- package/src/client/raw-utils.ts +11 -2
- package/src/common/actor-router-consts.ts +1 -0
- package/src/common/encoding.ts +4 -1
- package/src/common/fetch-like.ts +49 -0
- package/src/engine-client/actor-http-client.ts +6 -22
- package/src/registry/index.ts +159 -14
- package/src/registry/napi-runtime.ts +74 -0
- package/src/registry/native-http.ts +208 -0
- package/src/registry/native.ts +111 -63
- package/src/registry/runtime.ts +35 -0
- package/src/registry/wasm-runtime.ts +11 -0
- package/dist/tsup/chunk-234EFKN2.cjs.map +0 -1
- package/dist/tsup/chunk-AXEGEUYB.cjs.map +0 -1
- package/dist/tsup/chunk-F6ZW7JZQ.js.map +0 -1
- package/dist/tsup/chunk-RDTFPGU2.js.map +0 -1
- /package/dist/tsup/{chunk-PLLHHJWR.js.map → chunk-HWBISPRP.js.map} +0 -0
- /package/dist/tsup/{chunk-BX4G2O6M.js.map → chunk-MY6BRV3S.js.map} +0 -0
- /package/dist/tsup/{chunk-OH6CBWUD.js.map → chunk-V7PJYBA5.js.map} +0 -0
- /package/dist/tsup/{chunk-3ZE5UDOA.js.map → chunk-YFXQ7DBH.js.map} +0 -0
package/src/registry/index.ts
CHANGED
|
@@ -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 {
|
|
18
|
-
|
|
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({
|
|
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
|
-
{
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
+
};
|