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.
Files changed (123) hide show
  1. package/dist/browser/client.d.ts +407 -20
  2. package/dist/browser/client.js +101 -86
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +12 -2
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/actor/errors.d.cts +1 -1
  7. package/dist/tsup/actor/errors.d.ts +1 -1
  8. package/dist/tsup/agent-os/index.cjs +66 -3
  9. package/dist/tsup/agent-os/index.cjs.map +1 -1
  10. package/dist/tsup/agent-os/index.d.cts +404 -17
  11. package/dist/tsup/agent-os/index.d.ts +404 -17
  12. package/dist/tsup/agent-os/index.js +66 -3
  13. package/dist/tsup/agent-os/index.js.map +1 -1
  14. package/dist/tsup/{chunk-WXYWDLJY.js → chunk-33YE6XCI.js} +4 -4
  15. package/dist/tsup/{chunk-2NXFKPRB.cjs → chunk-7OR3CHD5.cjs} +10 -10
  16. package/dist/tsup/{chunk-2NXFKPRB.cjs.map → chunk-7OR3CHD5.cjs.map} +1 -1
  17. package/dist/tsup/{chunk-LW5HNCWD.cjs → chunk-7XQCARVY.cjs} +3 -3
  18. package/dist/tsup/{chunk-LW5HNCWD.cjs.map → chunk-7XQCARVY.cjs.map} +1 -1
  19. package/dist/tsup/{chunk-GX6W4MW3.cjs → chunk-BSPS6NSN.cjs} +5 -5
  20. package/dist/tsup/{chunk-GX6W4MW3.cjs.map → chunk-BSPS6NSN.cjs.map} +1 -1
  21. package/dist/tsup/{chunk-T3VCJ4PV.js → chunk-DPIMKYNB.js} +61 -2
  22. package/dist/tsup/chunk-DPIMKYNB.js.map +1 -0
  23. package/dist/tsup/{chunk-XG25CGSW.cjs → chunk-E5CLYAUZ.cjs} +146 -143
  24. package/dist/tsup/chunk-E5CLYAUZ.cjs.map +1 -0
  25. package/dist/tsup/{chunk-RDBGKI66.cjs → chunk-EBWOJRCC.cjs} +22 -5
  26. package/dist/tsup/chunk-EBWOJRCC.cjs.map +1 -0
  27. package/dist/tsup/{chunk-YRQ4F5CD.js → chunk-HHNYEQD3.js} +6 -6
  28. package/dist/tsup/chunk-HHNYEQD3.js.map +1 -0
  29. package/dist/tsup/{chunk-4FP4FFB5.js → chunk-IOUSQVXI.js} +21 -4
  30. package/dist/tsup/chunk-IOUSQVXI.js.map +1 -0
  31. package/dist/tsup/{chunk-LNP7Q6I6.cjs → chunk-ISDKSSYR.cjs} +4 -4
  32. package/dist/tsup/{chunk-LNP7Q6I6.cjs.map → chunk-ISDKSSYR.cjs.map} +1 -1
  33. package/dist/tsup/{chunk-TTLUIDVH.js → chunk-J72WHUBC.js} +12 -9
  34. package/dist/tsup/chunk-J72WHUBC.js.map +1 -0
  35. package/dist/tsup/{chunk-Y3JBOFBG.cjs → chunk-KWABEUUA.cjs} +10 -10
  36. package/dist/tsup/chunk-KWABEUUA.cjs.map +1 -0
  37. package/dist/tsup/{chunk-XCDCURZ4.cjs → chunk-NIY3RSPX.cjs} +62 -3
  38. package/dist/tsup/chunk-NIY3RSPX.cjs.map +1 -0
  39. package/dist/tsup/{chunk-3P2JUHWJ.js → chunk-T44AVAGW.js} +2 -2
  40. package/dist/tsup/{chunk-GRFBV2U7.js → chunk-TCXEM6PA.js} +2 -2
  41. package/dist/tsup/{chunk-KRC4L3YB.js → chunk-ZI5CNA2Z.js} +2 -2
  42. package/dist/tsup/client/mod.cjs +7 -7
  43. package/dist/tsup/client/mod.cjs.map +1 -1
  44. package/dist/tsup/client/mod.d.cts +3 -3
  45. package/dist/tsup/client/mod.d.ts +3 -3
  46. package/dist/tsup/client/mod.js +6 -6
  47. package/dist/tsup/common/log.cjs +2 -2
  48. package/dist/tsup/common/log.js +1 -1
  49. package/dist/tsup/common/websocket.cjs +3 -3
  50. package/dist/tsup/common/websocket.js +2 -2
  51. package/dist/tsup/{config-De5UVu0V.d.ts → config-BxWAw3iH.d.ts} +476 -20
  52. package/dist/tsup/{config-CTwe3WwC.d.cts → config-CZQQ-mso.d.cts} +476 -20
  53. package/dist/tsup/{context-Dmj477Uh.d.cts → context-Bw7xq8w3.d.cts} +1 -1
  54. package/dist/tsup/{context-DPHISlUi.d.ts → context-D8QA76sV.d.ts} +1 -1
  55. package/dist/tsup/dynamic/mod.cjs +2 -2
  56. package/dist/tsup/dynamic/mod.d.cts +2 -2
  57. package/dist/tsup/dynamic/mod.d.ts +2 -2
  58. package/dist/tsup/dynamic/mod.js +1 -1
  59. package/dist/tsup/inspector/mod.cjs +5 -5
  60. package/dist/tsup/inspector/mod.js +4 -4
  61. package/dist/tsup/inspector-tab/mod.cjs +173 -0
  62. package/dist/tsup/inspector-tab/mod.cjs.map +1 -0
  63. package/dist/tsup/inspector-tab/mod.d.cts +250 -0
  64. package/dist/tsup/inspector-tab/mod.d.ts +250 -0
  65. package/dist/tsup/inspector-tab/mod.js +173 -0
  66. package/dist/tsup/inspector-tab/mod.js.map +1 -0
  67. package/dist/tsup/mod.cjs +341 -138
  68. package/dist/tsup/mod.cjs.map +1 -1
  69. package/dist/tsup/mod.d.cts +4 -4
  70. package/dist/tsup/mod.d.ts +4 -4
  71. package/dist/tsup/mod.js +277 -74
  72. package/dist/tsup/mod.js.map +1 -1
  73. package/dist/tsup/test/mod.cjs +10 -10
  74. package/dist/tsup/test/mod.d.cts +2 -2
  75. package/dist/tsup/test/mod.d.ts +2 -2
  76. package/dist/tsup/test/mod.js +6 -6
  77. package/dist/tsup/{utils-DVekpm4I.d.ts → utils-DQosb24I.d.cts} +1 -1
  78. package/dist/tsup/{utils-DVekpm4I.d.cts → utils-DQosb24I.d.ts} +1 -1
  79. package/dist/tsup/utils.cjs +2 -2
  80. package/dist/tsup/utils.d.cts +1 -1
  81. package/dist/tsup/utils.d.ts +1 -1
  82. package/dist/tsup/utils.js +1 -1
  83. package/dist/tsup/workflow/mod.cjs +11 -11
  84. package/dist/tsup/workflow/mod.cjs.map +1 -1
  85. package/dist/tsup/workflow/mod.d.cts +4 -4
  86. package/dist/tsup/workflow/mod.d.ts +4 -4
  87. package/dist/tsup/workflow/mod.js +5 -5
  88. package/package.json +19 -9
  89. package/src/actor/config.ts +111 -10
  90. package/src/actor/definition.ts +6 -5
  91. package/src/actor/instance/mod.ts +4 -4
  92. package/src/actor/mod.ts +2 -0
  93. package/src/client/actor-common.ts +24 -27
  94. package/src/client/actor-handle.ts +2 -1
  95. package/src/common/engine.ts +28 -1
  96. package/src/common/utils.ts +1 -1
  97. package/src/devtools-loader/index.ts +4 -7
  98. package/src/devtools-loader/serve-devtools.ts +26 -0
  99. package/src/drivers/engine/actor-driver.ts +16 -5
  100. package/src/engine-client/actor-http-client.ts +2 -2
  101. package/src/engine-client/api-endpoints.ts +5 -1
  102. package/src/engine-client/ws-proxy.ts +5 -0
  103. package/src/inspector-tab/mod.ts +315 -0
  104. package/src/registry/config/index.ts +40 -16
  105. package/src/registry/index.ts +143 -62
  106. package/src/registry/napi-runtime.ts +6 -0
  107. package/src/registry/native.ts +170 -27
  108. package/src/registry/process-metrics.ts +16 -4
  109. package/src/registry/runtime.ts +26 -0
  110. package/src/registry/wasm-runtime.ts +16 -1
  111. package/src/utils/env-vars.ts +6 -0
  112. package/dist/tsup/chunk-4FP4FFB5.js.map +0 -1
  113. package/dist/tsup/chunk-RDBGKI66.cjs.map +0 -1
  114. package/dist/tsup/chunk-T3VCJ4PV.js.map +0 -1
  115. package/dist/tsup/chunk-TTLUIDVH.js.map +0 -1
  116. package/dist/tsup/chunk-XCDCURZ4.cjs.map +0 -1
  117. package/dist/tsup/chunk-XG25CGSW.cjs.map +0 -1
  118. package/dist/tsup/chunk-Y3JBOFBG.cjs.map +0 -1
  119. package/dist/tsup/chunk-YRQ4F5CD.js.map +0 -1
  120. /package/dist/tsup/{chunk-WXYWDLJY.js.map → chunk-33YE6XCI.js.map} +0 -0
  121. /package/dist/tsup/{chunk-3P2JUHWJ.js.map → chunk-T44AVAGW.js.map} +0 -0
  122. /package/dist/tsup/{chunk-GRFBV2U7.js.map → chunk-TCXEM6PA.js.map} +0 -0
  123. /package/dist/tsup/{chunk-KRC4L3YB.js.map → chunk-ZI5CNA2Z.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import { ENGINE_ENDPOINT } from "@/common/engine";
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 = buildConfiguredRegistry(config);
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 = buildConfiguredRegistry(config);
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, configuredRegistryPromise);
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
- // Remove our handler only (preserving any user-installed listeners)
494
- // and re-raise so Node proceeds with its default exit path.
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
- process.kill(process.pid, signal);
516
+ finishShutdownSignal(signal);
497
517
  return;
498
518
  }
499
- this.#shutdownInFlight = this.#runShutdown(
500
- signal,
501
- config,
502
- configuredRegistryPromise,
503
- ).catch((error) => {
504
- logger().warn({ error }, "shutdown error");
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 #runShutdown(
509
- signal: ShutdownSignal,
510
- config: RegistryConfig,
511
- configuredRegistryPromise: ReturnType<typeof buildConfiguredRegistry>,
512
- ): Promise<void> {
513
- const gracePeriodMs = config.shutdown?.gracePeriodMs ?? 30_000;
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. Without this, each mode's Rust-side drain
516
- // (20s) could stack sequentially and blow past gracePeriodMs before
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 signal handler fans out to both to
522
- // honor the spec invariant "single shutdown tears down both modes".
523
- const registries: Promise<void>[] = [
524
- (async () => {
525
- try {
526
- const { runtime, registry } =
527
- await configuredRegistryPromise;
528
- await runtime.shutdownRegistry(registry);
529
- } catch (error) {
530
- logger().warn(
531
- { error },
532
- "runtime registry shutdown errored (mode A)",
533
- );
534
- }
535
- })(),
536
- ];
537
- const runtimeServerlessPromise = this.#runtimeServerlessPromise;
538
- if (runtimeServerlessPromise !== undefined) {
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
- this.#removeSignalHandlers();
571
- process.kill(process.pid, signal);
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.endpoint === ENGINE_ENDPOINT ? "local native" : "remote";
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
  }
@@ -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
- await runtime.actorWaitForTrackedShutdownWork(ctx);
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 Array.isArray(decoded)
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 { args?: unknown[] };
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
- body.args ?? [],
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
- try {
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(runtime, ctx);
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?: (quantile: string, valueSeconds: number) => void;
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(processMetricsNapi.jsSetEventloopUtilization, eluDelta.utilization);
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(processMetricsNapi.jsSetActiveHandles, proc._getActiveHandles().length);
239
+ callIfFn(
240
+ processMetricsNapi.jsSetActiveHandles,
241
+ proc._getActiveHandles().length,
242
+ );
234
243
  }
235
244
  if (typeof proc._getActiveRequests === "function") {
236
- callIfFn(processMetricsNapi.jsSetActiveRequests, proc._getActiveRequests().length);
245
+ callIfFn(
246
+ processMetricsNapi.jsSetActiveRequests,
247
+ proc._getActiveRequests().length,
248
+ );
237
249
  }
238
250
  }