rivetkit 2.3.0-rc.11 → 2.3.0-rc.13
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 +407 -20
- package/dist/browser/client.js +101 -86
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.js +12 -2
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/tsup/actor/errors.d.cts +1 -1
- package/dist/tsup/actor/errors.d.ts +1 -1
- package/dist/tsup/agent-os/index.cjs +66 -3
- package/dist/tsup/agent-os/index.cjs.map +1 -1
- package/dist/tsup/agent-os/index.d.cts +404 -17
- package/dist/tsup/agent-os/index.d.ts +404 -17
- package/dist/tsup/agent-os/index.js +66 -3
- package/dist/tsup/agent-os/index.js.map +1 -1
- package/dist/tsup/{chunk-WXYWDLJY.js → chunk-33YE6XCI.js} +4 -4
- package/dist/tsup/{chunk-2NXFKPRB.cjs → chunk-7OR3CHD5.cjs} +10 -10
- package/dist/tsup/{chunk-2NXFKPRB.cjs.map → chunk-7OR3CHD5.cjs.map} +1 -1
- package/dist/tsup/{chunk-LW5HNCWD.cjs → chunk-7XQCARVY.cjs} +3 -3
- package/dist/tsup/{chunk-LW5HNCWD.cjs.map → chunk-7XQCARVY.cjs.map} +1 -1
- package/dist/tsup/{chunk-GX6W4MW3.cjs → chunk-BSPS6NSN.cjs} +5 -5
- package/dist/tsup/{chunk-GX6W4MW3.cjs.map → chunk-BSPS6NSN.cjs.map} +1 -1
- package/dist/tsup/{chunk-T3VCJ4PV.js → chunk-DPIMKYNB.js} +61 -2
- package/dist/tsup/chunk-DPIMKYNB.js.map +1 -0
- package/dist/tsup/{chunk-XG25CGSW.cjs → chunk-E5CLYAUZ.cjs} +146 -143
- package/dist/tsup/chunk-E5CLYAUZ.cjs.map +1 -0
- package/dist/tsup/{chunk-RDBGKI66.cjs → chunk-EBWOJRCC.cjs} +22 -5
- package/dist/tsup/chunk-EBWOJRCC.cjs.map +1 -0
- package/dist/tsup/{chunk-YRQ4F5CD.js → chunk-HHNYEQD3.js} +6 -6
- package/dist/tsup/chunk-HHNYEQD3.js.map +1 -0
- package/dist/tsup/{chunk-4FP4FFB5.js → chunk-IOUSQVXI.js} +21 -4
- package/dist/tsup/chunk-IOUSQVXI.js.map +1 -0
- package/dist/tsup/{chunk-LNP7Q6I6.cjs → chunk-ISDKSSYR.cjs} +4 -4
- package/dist/tsup/{chunk-LNP7Q6I6.cjs.map → chunk-ISDKSSYR.cjs.map} +1 -1
- package/dist/tsup/{chunk-TTLUIDVH.js → chunk-J72WHUBC.js} +12 -9
- package/dist/tsup/chunk-J72WHUBC.js.map +1 -0
- package/dist/tsup/{chunk-Y3JBOFBG.cjs → chunk-KWABEUUA.cjs} +10 -10
- package/dist/tsup/chunk-KWABEUUA.cjs.map +1 -0
- package/dist/tsup/{chunk-XCDCURZ4.cjs → chunk-NIY3RSPX.cjs} +62 -3
- package/dist/tsup/chunk-NIY3RSPX.cjs.map +1 -0
- package/dist/tsup/{chunk-3P2JUHWJ.js → chunk-T44AVAGW.js} +2 -2
- package/dist/tsup/{chunk-GRFBV2U7.js → chunk-TCXEM6PA.js} +2 -2
- package/dist/tsup/{chunk-KRC4L3YB.js → chunk-ZI5CNA2Z.js} +2 -2
- package/dist/tsup/client/mod.cjs +7 -7
- package/dist/tsup/client/mod.cjs.map +1 -1
- package/dist/tsup/client/mod.d.cts +3 -3
- package/dist/tsup/client/mod.d.ts +3 -3
- package/dist/tsup/client/mod.js +6 -6
- 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-De5UVu0V.d.ts → config-BxWAw3iH.d.ts} +476 -20
- package/dist/tsup/{config-CTwe3WwC.d.cts → config-CZQQ-mso.d.cts} +476 -20
- package/dist/tsup/{context-Dmj477Uh.d.cts → context-Bw7xq8w3.d.cts} +1 -1
- package/dist/tsup/{context-DPHISlUi.d.ts → context-D8QA76sV.d.ts} +1 -1
- package/dist/tsup/dynamic/mod.cjs +2 -2
- package/dist/tsup/dynamic/mod.d.cts +2 -2
- package/dist/tsup/dynamic/mod.d.ts +2 -2
- package/dist/tsup/dynamic/mod.js +1 -1
- package/dist/tsup/inspector/mod.cjs +5 -5
- package/dist/tsup/inspector/mod.js +4 -4
- package/dist/tsup/inspector-tab/mod.cjs +173 -0
- package/dist/tsup/inspector-tab/mod.cjs.map +1 -0
- package/dist/tsup/inspector-tab/mod.d.cts +250 -0
- package/dist/tsup/inspector-tab/mod.d.ts +250 -0
- package/dist/tsup/inspector-tab/mod.js +173 -0
- package/dist/tsup/inspector-tab/mod.js.map +1 -0
- package/dist/tsup/mod.cjs +341 -138
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +4 -4
- package/dist/tsup/mod.d.ts +4 -4
- package/dist/tsup/mod.js +277 -74
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +10 -10
- package/dist/tsup/test/mod.d.cts +2 -2
- package/dist/tsup/test/mod.d.ts +2 -2
- package/dist/tsup/test/mod.js +6 -6
- package/dist/tsup/{utils-DVekpm4I.d.ts → utils-DQosb24I.d.cts} +1 -1
- package/dist/tsup/{utils-DVekpm4I.d.cts → utils-DQosb24I.d.ts} +1 -1
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.d.cts +1 -1
- package/dist/tsup/utils.d.ts +1 -1
- package/dist/tsup/utils.js +1 -1
- package/dist/tsup/workflow/mod.cjs +11 -11
- package/dist/tsup/workflow/mod.cjs.map +1 -1
- package/dist/tsup/workflow/mod.d.cts +4 -4
- package/dist/tsup/workflow/mod.d.ts +4 -4
- package/dist/tsup/workflow/mod.js +5 -5
- package/package.json +19 -9
- package/src/actor/config.ts +111 -10
- package/src/actor/definition.ts +6 -5
- package/src/actor/instance/mod.ts +4 -4
- package/src/actor/mod.ts +2 -0
- package/src/client/actor-common.ts +24 -27
- package/src/client/actor-handle.ts +2 -1
- package/src/common/engine.ts +28 -1
- package/src/common/utils.ts +1 -1
- package/src/devtools-loader/index.ts +4 -7
- package/src/devtools-loader/serve-devtools.ts +26 -0
- package/src/drivers/engine/actor-driver.ts +16 -5
- package/src/engine-client/actor-http-client.ts +2 -2
- package/src/engine-client/api-endpoints.ts +5 -1
- package/src/engine-client/ws-proxy.ts +5 -0
- package/src/inspector-tab/mod.ts +315 -0
- package/src/registry/config/index.ts +40 -16
- package/src/registry/index.ts +143 -62
- package/src/registry/napi-runtime.ts +6 -0
- package/src/registry/native.ts +170 -27
- package/src/registry/process-metrics.ts +16 -4
- package/src/registry/runtime.ts +26 -0
- package/src/registry/wasm-runtime.ts +16 -1
- package/src/utils/env-vars.ts +6 -0
- package/dist/tsup/chunk-4FP4FFB5.js.map +0 -1
- package/dist/tsup/chunk-RDBGKI66.cjs.map +0 -1
- package/dist/tsup/chunk-T3VCJ4PV.js.map +0 -1
- package/dist/tsup/chunk-TTLUIDVH.js.map +0 -1
- package/dist/tsup/chunk-XCDCURZ4.cjs.map +0 -1
- package/dist/tsup/chunk-XG25CGSW.cjs.map +0 -1
- package/dist/tsup/chunk-Y3JBOFBG.cjs.map +0 -1
- package/dist/tsup/chunk-YRQ4F5CD.js.map +0 -1
- /package/dist/tsup/{chunk-WXYWDLJY.js.map → chunk-33YE6XCI.js.map} +0 -0
- /package/dist/tsup/{chunk-3P2JUHWJ.js.map → chunk-T44AVAGW.js.map} +0 -0
- /package/dist/tsup/{chunk-GRFBV2U7.js.map → chunk-TCXEM6PA.js.map} +0 -0
- /package/dist/tsup/{chunk-KRC4L3YB.js.map → chunk-ZI5CNA2Z.js.map} +0 -0
package/src/registry/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import { isLocalEngineEndpoint } from "@/common/engine";
|
|
3
3
|
import { configureServerlessPool } from "@/serverless/configure";
|
|
4
4
|
import { detectRuntime, VERSION } from "@/utils";
|
|
5
5
|
import { crossPlatformServe, loadRuntimeServeStatic } from "@/utils/serve";
|
|
@@ -15,6 +15,22 @@ import type { RuntimeServerlessResponseHead } from "./runtime";
|
|
|
15
15
|
|
|
16
16
|
type ShutdownSignal = "SIGINT" | "SIGTERM";
|
|
17
17
|
|
|
18
|
+
function signalExitCode(signal: ShutdownSignal): number {
|
|
19
|
+
switch (signal) {
|
|
20
|
+
case "SIGINT":
|
|
21
|
+
return 130;
|
|
22
|
+
case "SIGTERM":
|
|
23
|
+
return 143;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function finishShutdownSignal(signal: ShutdownSignal): void {
|
|
28
|
+
if (process.pid === 1) {
|
|
29
|
+
process.exit(signalExitCode(signal));
|
|
30
|
+
}
|
|
31
|
+
process.kill(process.pid, signal);
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export type FetchHandler = (
|
|
19
35
|
request: Request,
|
|
20
36
|
...args: any
|
|
@@ -30,8 +46,18 @@ export interface RegistryRoutes {
|
|
|
30
46
|
prometheusMetrics(request?: Request): Promise<Response>;
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Injectable dependencies for {@link Registry}. Production code uses the
|
|
51
|
+
* defaults. Tests override `buildConfiguredRegistry` to drive lifecycle
|
|
52
|
+
* orchestration against a fake `CoreRuntime` without an engine.
|
|
53
|
+
*/
|
|
54
|
+
export interface RegistryDeps {
|
|
55
|
+
buildConfiguredRegistry: typeof buildConfiguredRegistry;
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
export class Registry<A extends RegistryActors> {
|
|
34
59
|
#config: RegistryConfigInput<A>;
|
|
60
|
+
#buildConfiguredRegistry: typeof buildConfiguredRegistry;
|
|
35
61
|
public readonly routes: RegistryRoutes;
|
|
36
62
|
|
|
37
63
|
get config(): RegistryConfigInput<A> {
|
|
@@ -51,8 +77,10 @@ export class Registry<A extends RegistryActors> {
|
|
|
51
77
|
#shutdownInFlight: Promise<void> | null = null;
|
|
52
78
|
#signalHandlers: Partial<Record<ShutdownSignal, () => void>> = {};
|
|
53
79
|
|
|
54
|
-
constructor(config: RegistryConfigInput<A>) {
|
|
80
|
+
constructor(config: RegistryConfigInput<A>, deps?: Partial<RegistryDeps>) {
|
|
55
81
|
this.#config = config;
|
|
82
|
+
this.#buildConfiguredRegistry =
|
|
83
|
+
deps?.buildConfiguredRegistry ?? buildConfiguredRegistry;
|
|
56
84
|
this.routes = {
|
|
57
85
|
health: () => this.#healthRoute(),
|
|
58
86
|
metadata: () => this.#metadataRoute(),
|
|
@@ -94,7 +122,8 @@ export class Registry<A extends RegistryActors> {
|
|
|
94
122
|
this.#printWelcome(config, "serverless");
|
|
95
123
|
|
|
96
124
|
if (!this.#runtimeServerlessPromise) {
|
|
97
|
-
this.#runtimeServerlessPromise =
|
|
125
|
+
this.#runtimeServerlessPromise =
|
|
126
|
+
this.#buildConfiguredRegistry(config);
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
const { runtime, registry, serveConfig } =
|
|
@@ -427,7 +456,8 @@ export class Registry<A extends RegistryActors> {
|
|
|
427
456
|
*/
|
|
428
457
|
#startEnvoy(config: RegistryConfig, printWelcome: boolean) {
|
|
429
458
|
if (!this.#runtimeServePromise) {
|
|
430
|
-
const configuredRegistryPromise =
|
|
459
|
+
const configuredRegistryPromise =
|
|
460
|
+
this.#buildConfiguredRegistry(config);
|
|
431
461
|
this.#runtimeServeConfiguredPromise = configuredRegistryPromise;
|
|
432
462
|
this.#runtimeServePromise = configuredRegistryPromise
|
|
433
463
|
.then(async ({ runtime, registry, serveConfig }) => {
|
|
@@ -445,17 +475,14 @@ export class Registry<A extends RegistryActors> {
|
|
|
445
475
|
// does not install handlers because it runs on Workers/Vercel/Deno
|
|
446
476
|
// Deploy where `process.on` is absent or forbidden; those platforms
|
|
447
477
|
// own their own signal policy.
|
|
448
|
-
this.#installSignalHandlers(config
|
|
478
|
+
this.#installSignalHandlers(config);
|
|
449
479
|
}
|
|
450
480
|
if (printWelcome) {
|
|
451
481
|
this.#printWelcome(config, "serverful");
|
|
452
482
|
}
|
|
453
483
|
}
|
|
454
484
|
|
|
455
|
-
#installSignalHandlers(
|
|
456
|
-
config: RegistryConfig,
|
|
457
|
-
configuredRegistryPromise: ReturnType<typeof buildConfiguredRegistry>,
|
|
458
|
-
): void {
|
|
485
|
+
#installSignalHandlers(config: RegistryConfig): void {
|
|
459
486
|
if (this.#shutdownInstalled) return;
|
|
460
487
|
if (config.shutdown?.disableSignalHandlers) return;
|
|
461
488
|
// Guard against non-Node runtimes (Workers/Edge) where `process` may
|
|
@@ -470,12 +497,7 @@ export class Registry<A extends RegistryActors> {
|
|
|
470
497
|
this.#shutdownInstalled = true;
|
|
471
498
|
|
|
472
499
|
const install = (signal: ShutdownSignal) => {
|
|
473
|
-
const handler = () =>
|
|
474
|
-
this.#onShutdownSignal(
|
|
475
|
-
signal,
|
|
476
|
-
config,
|
|
477
|
-
configuredRegistryPromise,
|
|
478
|
-
);
|
|
500
|
+
const handler = () => this.#onShutdownSignal(signal, config);
|
|
479
501
|
this.#signalHandlers[signal] = handler;
|
|
480
502
|
process.on(signal, handler);
|
|
481
503
|
};
|
|
@@ -483,64 +505,97 @@ export class Registry<A extends RegistryActors> {
|
|
|
483
505
|
install("SIGTERM");
|
|
484
506
|
}
|
|
485
507
|
|
|
486
|
-
#onShutdownSignal(
|
|
487
|
-
signal: ShutdownSignal,
|
|
488
|
-
config: RegistryConfig,
|
|
489
|
-
configuredRegistryPromise: ReturnType<typeof buildConfiguredRegistry>,
|
|
490
|
-
): void {
|
|
508
|
+
#onShutdownSignal(signal: ShutdownSignal, config: RegistryConfig): void {
|
|
491
509
|
if (this.#shutdownInFlight !== null) {
|
|
492
|
-
// Second delivery of the same (or another) shutdown signal
|
|
493
|
-
//
|
|
494
|
-
//
|
|
510
|
+
// Second delivery of the same (or another) shutdown signal, or a
|
|
511
|
+
// drain already started by an explicit `shutdown()` call. Remove
|
|
512
|
+
// our handler only, preserving any user-installed listeners. PID 1
|
|
513
|
+
// must exit directly because re-raised default signals can be
|
|
514
|
+
// swallowed by the container signal path.
|
|
495
515
|
this.#removeSignalHandlers();
|
|
496
|
-
|
|
516
|
+
finishShutdownSignal(signal);
|
|
497
517
|
return;
|
|
498
518
|
}
|
|
499
|
-
this.#shutdownInFlight = this.#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
519
|
+
this.#shutdownInFlight = this.#drain(config)
|
|
520
|
+
.catch((err) => {
|
|
521
|
+
logger().warn({ err }, "shutdown error");
|
|
522
|
+
})
|
|
523
|
+
.then(() => {
|
|
524
|
+
this.#removeSignalHandlers();
|
|
525
|
+
finishShutdownSignal(signal);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Gracefully drains all live registries.
|
|
531
|
+
*
|
|
532
|
+
* Programmatic counterpart to the SIGINT/SIGTERM handlers: tears down
|
|
533
|
+
* every live `CoreRegistry` (both `start()` and `handler()` modes) and
|
|
534
|
+
* waits for the serve promise to resolve, all bounded by the shutdown
|
|
535
|
+
* grace period. Unlike a signal-driven shutdown, this does not re-raise a
|
|
536
|
+
* signal or exit the process. The caller owns process lifetime.
|
|
537
|
+
*
|
|
538
|
+
* Idempotent: concurrent or repeated calls share a single drain. Safe to
|
|
539
|
+
* call even if nothing has been started.
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* const registry = setup({ use: { counter } });
|
|
544
|
+
* registry.start();
|
|
545
|
+
* // ...later, on your own shutdown trigger:
|
|
546
|
+
* await registry.shutdown();
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
public async shutdown(): Promise<void> {
|
|
550
|
+
if (this.#shutdownInFlight !== null) return this.#shutdownInFlight;
|
|
551
|
+
const config = this.parseConfig();
|
|
552
|
+
// Uninstall our signal handlers so a later SIGINT/SIGTERM does not
|
|
553
|
+
// re-trigger a drain on already-torn-down registries. Subsequent
|
|
554
|
+
// signals fall back to Node's default termination behavior.
|
|
555
|
+
this.#removeSignalHandlers();
|
|
556
|
+
this.#shutdownInFlight = this.#drain(config).catch((err) => {
|
|
557
|
+
logger().warn({ err }, "shutdown error");
|
|
505
558
|
});
|
|
559
|
+
return this.#shutdownInFlight;
|
|
506
560
|
}
|
|
507
561
|
|
|
508
|
-
async #
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
562
|
+
async #drain(config: RegistryConfig): Promise<void> {
|
|
563
|
+
const modeAPromise = this.#runtimeServeConfiguredPromise;
|
|
564
|
+
const modeBPromise = this.#runtimeServerlessPromise;
|
|
565
|
+
|
|
566
|
+
const gracePeriodMs =
|
|
567
|
+
config.shutdown?.gracePeriodMs ??
|
|
568
|
+
(await this.#actorStopThresholdMs(modeAPromise ?? modeBPromise)) ??
|
|
569
|
+
30 * 60 * 1000;
|
|
514
570
|
// Race the entire drain sequence (both modes + serve promise) against
|
|
515
|
-
// a single grace ceiling.
|
|
516
|
-
//
|
|
517
|
-
// we re-raise the signal.
|
|
571
|
+
// a single grace ceiling. By default, this uses the engine-provided
|
|
572
|
+
// actor stop threshold, matching Pegboard's hard cutoff for actors.
|
|
518
573
|
const drain = async () => {
|
|
519
574
|
// Shut down every live `CoreRegistry` we know about. Mode A
|
|
520
575
|
// (`start()`) and Mode B (`handler()`) each build a separate
|
|
521
|
-
// runtime registry, so one
|
|
522
|
-
//
|
|
523
|
-
const registries: Promise<void>[] = [
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
576
|
+
// runtime registry, so one drain fans out to both to honor the
|
|
577
|
+
// spec invariant "single shutdown tears down both modes".
|
|
578
|
+
const registries: Promise<void>[] = [];
|
|
579
|
+
if (modeAPromise !== undefined) {
|
|
580
|
+
registries.push(
|
|
581
|
+
(async () => {
|
|
582
|
+
try {
|
|
583
|
+
const { runtime, registry } = await modeAPromise;
|
|
584
|
+
await runtime.shutdownRegistry(registry);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
logger().warn(
|
|
587
|
+
{ err },
|
|
588
|
+
"runtime registry shutdown errored (mode A)",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
})(),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (modeBPromise !== undefined) {
|
|
539
595
|
registries.push(
|
|
540
596
|
(async () => {
|
|
541
597
|
try {
|
|
542
|
-
const { runtime, registry } =
|
|
543
|
-
await runtimeServerlessPromise;
|
|
598
|
+
const { runtime, registry } = await modeBPromise;
|
|
544
599
|
await runtime.shutdownRegistry(registry);
|
|
545
600
|
} catch (err) {
|
|
546
601
|
logger().warn(
|
|
@@ -567,8 +622,32 @@ export class Registry<A extends RegistryActors> {
|
|
|
567
622
|
setTimeout(resolve, gracePeriodMs).unref?.(),
|
|
568
623
|
),
|
|
569
624
|
]);
|
|
570
|
-
|
|
571
|
-
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async #actorStopThresholdMs(
|
|
628
|
+
configuredRegistryPromise:
|
|
629
|
+
| ReturnType<typeof buildConfiguredRegistry>
|
|
630
|
+
| undefined,
|
|
631
|
+
): Promise<number | undefined> {
|
|
632
|
+
if (configuredRegistryPromise === undefined) return undefined;
|
|
633
|
+
try {
|
|
634
|
+
const { runtime, registry } = await configuredRegistryPromise;
|
|
635
|
+
const thresholdMs =
|
|
636
|
+
await runtime.registryActorStopThresholdMs?.(registry);
|
|
637
|
+
if (
|
|
638
|
+
thresholdMs !== undefined &&
|
|
639
|
+
Number.isFinite(thresholdMs) &&
|
|
640
|
+
thresholdMs > 0
|
|
641
|
+
) {
|
|
642
|
+
return thresholdMs;
|
|
643
|
+
}
|
|
644
|
+
} catch (err) {
|
|
645
|
+
logger().warn(
|
|
646
|
+
{ err },
|
|
647
|
+
"failed to read actor stop threshold for shutdown grace",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return undefined;
|
|
572
651
|
}
|
|
573
652
|
|
|
574
653
|
#removeSignalHandlers(): void {
|
|
@@ -621,7 +700,9 @@ export class Registry<A extends RegistryActors> {
|
|
|
621
700
|
|
|
622
701
|
if (config.endpoint) {
|
|
623
702
|
const endpointType =
|
|
624
|
-
config.
|
|
703
|
+
config.startEngine || isLocalEngineEndpoint(config.endpoint)
|
|
704
|
+
? "local native"
|
|
705
|
+
: "remote";
|
|
625
706
|
logLine("Endpoint", `${config.endpoint} (${endpointType})`);
|
|
626
707
|
}
|
|
627
708
|
|
|
@@ -452,6 +452,12 @@ export class NapiCoreRuntime implements CoreRuntime {
|
|
|
452
452
|
return await asNativeActorContext(ctx).waitForTrackedShutdownWork();
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
async actorWaitForTrackedShutdownWorkUnbounded(
|
|
456
|
+
ctx: ActorContextHandle,
|
|
457
|
+
): Promise<void> {
|
|
458
|
+
await asNativeActorContext(ctx).waitForTrackedShutdownWorkUnbounded();
|
|
459
|
+
}
|
|
460
|
+
|
|
455
461
|
actorKeepAwake(ctx: ActorContextHandle, promise: Promise<unknown>): void {
|
|
456
462
|
asNativeActorContext(ctx).keepAwake(promise);
|
|
457
463
|
}
|
package/src/registry/native.ts
CHANGED
|
@@ -49,10 +49,7 @@ import type {
|
|
|
49
49
|
RuntimeKind,
|
|
50
50
|
SqliteBackend,
|
|
51
51
|
} from "@/registry/config";
|
|
52
|
-
import {
|
|
53
|
-
decodeCborCompat,
|
|
54
|
-
encodeCborCompat,
|
|
55
|
-
} from "@/serde";
|
|
52
|
+
import { decodeCborCompat, encodeCborCompat } from "@/serde";
|
|
56
53
|
import { getEnvUniversal, VERSION } from "@/utils";
|
|
57
54
|
import { logger } from "./log";
|
|
58
55
|
import { loadNapiRuntime } from "./napi-runtime";
|
|
@@ -74,12 +71,15 @@ import type {
|
|
|
74
71
|
RuntimeActorConfig,
|
|
75
72
|
RuntimeBytes,
|
|
76
73
|
RuntimeHttpResponse,
|
|
74
|
+
RuntimeInspectorTabEntry,
|
|
77
75
|
RuntimeQueueMessage,
|
|
78
76
|
RuntimeServeConfig,
|
|
79
77
|
RuntimeStateDeltaPayload,
|
|
80
78
|
WebSocketHandle,
|
|
81
79
|
} from "./runtime";
|
|
82
80
|
import { loadWasmRuntime } from "./wasm-runtime";
|
|
81
|
+
import nodeFs from "node:fs";
|
|
82
|
+
import nodePath from "node:path";
|
|
83
83
|
import { createWriteThroughProxy } from "./write-through-proxy";
|
|
84
84
|
|
|
85
85
|
const textEncoder = new TextEncoder();
|
|
@@ -424,8 +424,33 @@ function clearNativeRuntimeState(
|
|
|
424
424
|
async function cleanupNativeSleepRuntimeState(
|
|
425
425
|
runtime: CoreRuntime,
|
|
426
426
|
ctx: ActorContextHandle,
|
|
427
|
+
afterTrackedWorkDrained?: () => Promise<void>,
|
|
427
428
|
): Promise<void> {
|
|
428
|
-
|
|
429
|
+
// The bounded wait gives shutdown work one grace-period chance to finish.
|
|
430
|
+
// Drained means all tracked shutdown work completed before the deadline, so
|
|
431
|
+
// we can save final state and clear runtime state immediately. If it did not
|
|
432
|
+
// drain, close database handles now, then defer the final save and clear until
|
|
433
|
+
// the tracked work finishes without a deadline.
|
|
434
|
+
const drained = await runtime.actorWaitForTrackedShutdownWork(ctx);
|
|
435
|
+
if (!drained) {
|
|
436
|
+
await closeNativeDatabaseClient(runtime, ctx);
|
|
437
|
+
await closeNativeSqlDatabase(runtime, ctx);
|
|
438
|
+
void runtime
|
|
439
|
+
.actorWaitForTrackedShutdownWorkUnbounded(ctx)
|
|
440
|
+
.then(async () => {
|
|
441
|
+
await afterTrackedWorkDrained?.();
|
|
442
|
+
clearNativeRuntimeState(runtime, ctx);
|
|
443
|
+
})
|
|
444
|
+
.catch((error) => {
|
|
445
|
+
logger().warn({
|
|
446
|
+
msg: "deferred native sleep cleanup failed",
|
|
447
|
+
error: stringifyError(error),
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await afterTrackedWorkDrained?.();
|
|
429
454
|
await closeNativeDatabaseClient(runtime, ctx);
|
|
430
455
|
await closeNativeSqlDatabase(runtime, ctx);
|
|
431
456
|
clearNativeRuntimeState(runtime, ctx);
|
|
@@ -605,6 +630,14 @@ function encodeValue(value: unknown): RuntimeBytes {
|
|
|
605
630
|
return encodeCborCompat(value as JsonCompatValue);
|
|
606
631
|
}
|
|
607
632
|
|
|
633
|
+
function normalizeArgs(value: unknown): unknown[] {
|
|
634
|
+
return Array.isArray(value)
|
|
635
|
+
? value
|
|
636
|
+
: value === undefined || value === null
|
|
637
|
+
? []
|
|
638
|
+
: [value];
|
|
639
|
+
}
|
|
640
|
+
|
|
608
641
|
function unwrapTsfnPayload<T>(error: unknown, payload: T): T {
|
|
609
642
|
if (error !== null && error !== undefined) {
|
|
610
643
|
throw error;
|
|
@@ -1071,11 +1104,7 @@ function wrapNativeCallback<Args extends Array<unknown>, Result>(
|
|
|
1071
1104
|
|
|
1072
1105
|
function decodeArgs(value?: RuntimeBytes | null): unknown[] {
|
|
1073
1106
|
const decoded = decodeValue<unknown>(value);
|
|
1074
|
-
return
|
|
1075
|
-
? decoded
|
|
1076
|
-
: decoded === undefined
|
|
1077
|
-
? []
|
|
1078
|
-
: [decoded];
|
|
1107
|
+
return normalizeArgs(decoded);
|
|
1079
1108
|
}
|
|
1080
1109
|
|
|
1081
1110
|
function buildRequest(init: {
|
|
@@ -2876,6 +2905,7 @@ export class ActorContextHandleAdapter {
|
|
|
2876
2905
|
}
|
|
2877
2906
|
|
|
2878
2907
|
sleep(): void {
|
|
2908
|
+
this.#flushStateChange();
|
|
2879
2909
|
callNativeSync(() => this.#runtime.actorSleep(this.#ctx));
|
|
2880
2910
|
}
|
|
2881
2911
|
|
|
@@ -3300,9 +3330,87 @@ function buildActorConfig(
|
|
|
3300
3330
|
actions: Object.keys((config.actions ?? {}) as Record<string, unknown>)
|
|
3301
3331
|
.sort()
|
|
3302
3332
|
.map((name) => ({ name })),
|
|
3333
|
+
inspectorTabs: buildInspectorTabs(config.inspector),
|
|
3303
3334
|
};
|
|
3304
3335
|
}
|
|
3305
3336
|
|
|
3337
|
+
function buildInspectorTabs(
|
|
3338
|
+
inspector: unknown,
|
|
3339
|
+
): Array<RuntimeInspectorTabEntry> | undefined {
|
|
3340
|
+
if (!inspector || typeof inspector !== "object") return undefined;
|
|
3341
|
+
const tabs = (inspector as { tabs?: unknown }).tabs;
|
|
3342
|
+
if (!Array.isArray(tabs) || tabs.length === 0) return undefined;
|
|
3343
|
+
return tabs.map((raw) => {
|
|
3344
|
+
const entry = raw as {
|
|
3345
|
+
id: string;
|
|
3346
|
+
label?: string;
|
|
3347
|
+
source?: string;
|
|
3348
|
+
icon?: string;
|
|
3349
|
+
hidden?: boolean;
|
|
3350
|
+
};
|
|
3351
|
+
if (entry.hidden === true) {
|
|
3352
|
+
return { id: entry.id, hidden: true };
|
|
3353
|
+
}
|
|
3354
|
+
// Resolve the author's source path against the current working
|
|
3355
|
+
// directory so the Rust runtime gets an absolute path. The author
|
|
3356
|
+
// runs the actor process from their project root by convention.
|
|
3357
|
+
const resolved =
|
|
3358
|
+
entry.source !== undefined
|
|
3359
|
+
? nodePath.resolve(entry.source)
|
|
3360
|
+
: undefined;
|
|
3361
|
+
if (resolved !== undefined) {
|
|
3362
|
+
validateInspectorTabSource(entry.id, resolved);
|
|
3363
|
+
}
|
|
3364
|
+
return {
|
|
3365
|
+
id: entry.id,
|
|
3366
|
+
label: entry.label,
|
|
3367
|
+
icon: entry.icon,
|
|
3368
|
+
source: resolved,
|
|
3369
|
+
};
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
function validateInspectorTabSource(tabId: string, resolved: string): void {
|
|
3374
|
+
// Catch obviously dangerous misconfigurations at registry construction
|
|
3375
|
+
// rather than silently exposing the wrong subtree over the unauthenticated
|
|
3376
|
+
// `/inspector/custom-tabs/<id>/*` route. Fail loudly so misconfigured
|
|
3377
|
+
// actors never start.
|
|
3378
|
+
if (resolved === nodePath.parse(resolved).root) {
|
|
3379
|
+
throw new Error(
|
|
3380
|
+
`inspector.tabs[id="${tabId}"].source resolves to the filesystem root (${resolved}). ` +
|
|
3381
|
+
"Point it at the tab's own static-asset directory instead.",
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
let stat: import("node:fs").Stats;
|
|
3385
|
+
try {
|
|
3386
|
+
stat = nodeFs.statSync(resolved);
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
3389
|
+
if (code === "ENOENT") {
|
|
3390
|
+
throw new Error(
|
|
3391
|
+
`inspector.tabs[id="${tabId}"].source (${resolved}) does not exist.`,
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3394
|
+
if (code === "EACCES") {
|
|
3395
|
+
throw new Error(
|
|
3396
|
+
`inspector.tabs[id="${tabId}"].source (${resolved}) is not readable (EACCES).`,
|
|
3397
|
+
);
|
|
3398
|
+
}
|
|
3399
|
+
throw new Error(
|
|
3400
|
+
`inspector.tabs[id="${tabId}"].source (${resolved}) could not be stat'd: ${
|
|
3401
|
+
(err as Error)?.message ?? err
|
|
3402
|
+
}`,
|
|
3403
|
+
);
|
|
3404
|
+
}
|
|
3405
|
+
if (!stat.isDirectory()) {
|
|
3406
|
+
throw new Error(
|
|
3407
|
+
`inspector.tabs[id="${tabId}"].source (${resolved}) must be a directory, got ${
|
|
3408
|
+
stat.isFile() ? "file" : "non-directory"
|
|
3409
|
+
}.`,
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3306
3414
|
export function buildNativeFactory(
|
|
3307
3415
|
runtime: CoreRuntime,
|
|
3308
3416
|
registryConfig: RegistryConfig,
|
|
@@ -3733,14 +3841,38 @@ export function buildNativeFactory(
|
|
|
3733
3841
|
404,
|
|
3734
3842
|
);
|
|
3735
3843
|
}
|
|
3736
|
-
const body = (await jsRequest.json()) as {
|
|
3844
|
+
const body = (await jsRequest.json()) as {
|
|
3845
|
+
args?: unknown;
|
|
3846
|
+
properties?: unknown;
|
|
3847
|
+
};
|
|
3848
|
+
if (body.args !== undefined && body.properties !== undefined) {
|
|
3849
|
+
return jsonResponse(
|
|
3850
|
+
{ error: "use either args or properties, not both" },
|
|
3851
|
+
{ status: 400 },
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
if (
|
|
3855
|
+
body.properties !== undefined &&
|
|
3856
|
+
(body.properties === null ||
|
|
3857
|
+
typeof body.properties !== "object" ||
|
|
3858
|
+
Array.isArray(body.properties))
|
|
3859
|
+
) {
|
|
3860
|
+
return jsonResponse(
|
|
3861
|
+
{ error: "properties must be an object" },
|
|
3862
|
+
{ status: 400 },
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
const args =
|
|
3866
|
+
body.properties !== undefined
|
|
3867
|
+
? [body.properties]
|
|
3868
|
+
: normalizeArgs(body.args);
|
|
3737
3869
|
try {
|
|
3738
3870
|
const output = await action(
|
|
3739
3871
|
actorCtx,
|
|
3740
3872
|
...validateActionArgs(
|
|
3741
3873
|
schemaConfig.actionInputSchemas,
|
|
3742
3874
|
actionName,
|
|
3743
|
-
|
|
3875
|
+
args,
|
|
3744
3876
|
),
|
|
3745
3877
|
);
|
|
3746
3878
|
return jsonResponse({ output });
|
|
@@ -3939,26 +4071,35 @@ export function buildNativeFactory(
|
|
|
3939
4071
|
async (error: unknown, payload: { ctx: ActorContextHandle }) => {
|
|
3940
4072
|
const { ctx } = unwrapTsfnPayload(error, payload);
|
|
3941
4073
|
const actorCtx = makeActorCtx(ctx);
|
|
4074
|
+
// TODO: Move this save hook into cleanupNativeSleepRuntimeState
|
|
4075
|
+
// so immediate and deferred sleep cleanup share one save-state
|
|
4076
|
+
// path instead of passing a callback through cleanup.
|
|
4077
|
+
const saveActorState = async () => {
|
|
4078
|
+
if (runtime.kind === "wasm") {
|
|
4079
|
+
// Wasm cannot use the native context save helper here because
|
|
4080
|
+
// the runtime owns the serialized state handoff.
|
|
4081
|
+
await runtime.actorSaveState(
|
|
4082
|
+
ctx,
|
|
4083
|
+
actorCtx.serializeForTick("save"),
|
|
4084
|
+
);
|
|
4085
|
+
} else {
|
|
4086
|
+
await actorCtx.saveState({
|
|
4087
|
+
immediate: true,
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
3942
4091
|
try {
|
|
3943
4092
|
if (onSleep) {
|
|
3944
|
-
|
|
3945
|
-
await onSleep(actorCtx);
|
|
3946
|
-
} finally {
|
|
3947
|
-
if (runtime.kind === "wasm") {
|
|
3948
|
-
// Wasm cannot use the native context save helper here because
|
|
3949
|
-
// the runtime owns the serialized state handoff.
|
|
3950
|
-
await runtime.actorSaveState(
|
|
3951
|
-
ctx,
|
|
3952
|
-
actorCtx.serializeForTick("save"),
|
|
3953
|
-
);
|
|
3954
|
-
} else {
|
|
3955
|
-
await actorCtx.saveState({ immediate: true });
|
|
3956
|
-
}
|
|
3957
|
-
}
|
|
4093
|
+
await onSleep(actorCtx);
|
|
3958
4094
|
}
|
|
4095
|
+
await saveActorState();
|
|
3959
4096
|
} finally {
|
|
3960
4097
|
try {
|
|
3961
|
-
await cleanupNativeSleepRuntimeState(
|
|
4098
|
+
await cleanupNativeSleepRuntimeState(
|
|
4099
|
+
runtime,
|
|
4100
|
+
ctx,
|
|
4101
|
+
saveActorState,
|
|
4102
|
+
);
|
|
3962
4103
|
} finally {
|
|
3963
4104
|
await actorCtx.dispose();
|
|
3964
4105
|
}
|
|
@@ -4618,6 +4759,8 @@ export async function buildServeConfig(
|
|
|
4618
4759
|
if (config.startEngine) {
|
|
4619
4760
|
const { getEnginePath } = await loadEngineCli();
|
|
4620
4761
|
serveConfig.engineBinaryPath = getEnginePath();
|
|
4762
|
+
serveConfig.engineHost = config.engineHost;
|
|
4763
|
+
serveConfig.enginePort = config.enginePort;
|
|
4621
4764
|
}
|
|
4622
4765
|
if (config.test?.enabled) {
|
|
4623
4766
|
serveConfig.inspectorTestToken =
|
|
@@ -22,7 +22,10 @@ import * as napi from "@rivetkit/rivetkit-napi";
|
|
|
22
22
|
type OptionalProcessMetricsNapi = typeof napi & {
|
|
23
23
|
jsObserveGcDuration?: (kind: string, durationSeconds: number) => void;
|
|
24
24
|
jsSetEventloopHeartbeatTsMs?: (timestampMs: number) => void;
|
|
25
|
-
jsSetEventloopLagQuantile?: (
|
|
25
|
+
jsSetEventloopLagQuantile?: (
|
|
26
|
+
quantile: string,
|
|
27
|
+
valueSeconds: number,
|
|
28
|
+
) => void;
|
|
26
29
|
jsSetEventloopUtilization?: (utilization: number) => void;
|
|
27
30
|
jsAddProcessCpuSeconds?: (mode: string, valueSeconds: number) => void;
|
|
28
31
|
jsSetProcessResidentMemoryBytes?: (bytes: number) => void;
|
|
@@ -191,7 +194,10 @@ function collectAndPush(): void {
|
|
|
191
194
|
state.lastEventLoopUtilization,
|
|
192
195
|
);
|
|
193
196
|
state.lastEventLoopUtilization = nextElu;
|
|
194
|
-
callIfFn(
|
|
197
|
+
callIfFn(
|
|
198
|
+
processMetricsNapi.jsSetEventloopUtilization,
|
|
199
|
+
eluDelta.utilization,
|
|
200
|
+
);
|
|
195
201
|
|
|
196
202
|
// CPU usage delta. `process.cpuUsage()` returns microseconds.
|
|
197
203
|
const nextCpu = process.cpuUsage();
|
|
@@ -230,9 +236,15 @@ function collectAndPush(): void {
|
|
|
230
236
|
_getActiveRequests?: () => unknown[];
|
|
231
237
|
};
|
|
232
238
|
if (typeof proc._getActiveHandles === "function") {
|
|
233
|
-
callIfFn(
|
|
239
|
+
callIfFn(
|
|
240
|
+
processMetricsNapi.jsSetActiveHandles,
|
|
241
|
+
proc._getActiveHandles().length,
|
|
242
|
+
);
|
|
234
243
|
}
|
|
235
244
|
if (typeof proc._getActiveRequests === "function") {
|
|
236
|
-
callIfFn(
|
|
245
|
+
callIfFn(
|
|
246
|
+
processMetricsNapi.jsSetActiveRequests,
|
|
247
|
+
proc._getActiveRequests().length,
|
|
248
|
+
);
|
|
237
249
|
}
|
|
238
250
|
}
|