rivetkit 2.1.2 → 2.1.4

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 (117) hide show
  1. package/dist/browser/client.d.ts +11 -0
  2. package/dist/browser/client.js +1 -1
  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/inspector.tar.gz +0 -0
  7. package/dist/tsup/{chunk-MNS5LY6M.cjs → chunk-3B6PCYJB.cjs} +280 -115
  8. package/dist/tsup/chunk-3B6PCYJB.cjs.map +1 -0
  9. package/dist/tsup/{chunk-YQ5P6KMN.js → chunk-3GTO6H3E.js} +12 -5
  10. package/dist/tsup/chunk-3GTO6H3E.js.map +1 -0
  11. package/dist/tsup/{chunk-RMJJE43B.cjs → chunk-4KSHPFXF.cjs} +2 -2
  12. package/dist/tsup/{chunk-RMJJE43B.cjs.map → chunk-4KSHPFXF.cjs.map} +1 -1
  13. package/dist/tsup/{chunk-PW3YONDJ.js → chunk-5UEFNG7P.js} +2 -2
  14. package/dist/tsup/{chunk-PSUVV4HM.js → chunk-ANKZ2FS6.js} +2 -4
  15. package/dist/tsup/chunk-ANKZ2FS6.js.map +1 -0
  16. package/dist/tsup/{chunk-GVQAVU7R.cjs → chunk-AQD4CBZ2.cjs} +4 -4
  17. package/dist/tsup/{chunk-GVQAVU7R.cjs.map → chunk-AQD4CBZ2.cjs.map} +1 -1
  18. package/dist/tsup/{chunk-WUXR722E.js → chunk-DZXDUGLL.js} +2 -2
  19. package/dist/tsup/{chunk-WUXR722E.js.map → chunk-DZXDUGLL.js.map} +1 -1
  20. package/dist/tsup/{chunk-NXEHFUDB.cjs → chunk-GXRVSSVD.cjs} +28 -21
  21. package/dist/tsup/chunk-GXRVSSVD.cjs.map +1 -0
  22. package/dist/tsup/{chunk-UZV7NXC6.cjs → chunk-H5TSEPN4.cjs} +30 -30
  23. package/dist/tsup/{chunk-UZV7NXC6.cjs.map → chunk-H5TSEPN4.cjs.map} +1 -1
  24. package/dist/tsup/{chunk-TDFDR7AO.js → chunk-HBYEYBIC.js} +2 -2
  25. package/dist/tsup/{chunk-772NPMTY.cjs → chunk-HKOSZKKZ.cjs} +263 -299
  26. package/dist/tsup/chunk-HKOSZKKZ.cjs.map +1 -0
  27. package/dist/tsup/{chunk-HB4RGGMC.js → chunk-I6PL6QIY.js} +5 -5
  28. package/dist/tsup/{chunk-RHUII57M.js → chunk-KTWY3K6Z.js} +23 -12
  29. package/dist/tsup/chunk-KTWY3K6Z.js.map +1 -0
  30. package/dist/tsup/{chunk-HFWRHT5T.cjs → chunk-LK36OGGO.cjs} +3 -5
  31. package/dist/tsup/chunk-LK36OGGO.cjs.map +1 -0
  32. package/dist/tsup/{chunk-BSIJG3LG.js → chunk-M6H4XIF4.js} +179 -215
  33. package/dist/tsup/chunk-M6H4XIF4.js.map +1 -0
  34. package/dist/tsup/{chunk-ZHQDRRMY.cjs → chunk-QPADHLDU.cjs} +3 -3
  35. package/dist/tsup/{chunk-ZHQDRRMY.cjs.map → chunk-QPADHLDU.cjs.map} +1 -1
  36. package/dist/tsup/{chunk-BFI4LYS2.js → chunk-TEFYRRAK.js} +4 -4
  37. package/dist/tsup/{chunk-PZAV6PP2.cjs → chunk-TEUL4UYN.cjs} +152 -152
  38. package/dist/tsup/{chunk-PZAV6PP2.cjs.map → chunk-TEUL4UYN.cjs.map} +1 -1
  39. package/dist/tsup/{chunk-VMX4I4MP.js → chunk-UDMRZR6A.js} +212 -47
  40. package/dist/tsup/chunk-UDMRZR6A.js.map +1 -0
  41. package/dist/tsup/{chunk-QABDKI3W.cjs → chunk-UWAGLDT6.cjs} +263 -252
  42. package/dist/tsup/chunk-UWAGLDT6.cjs.map +1 -0
  43. package/dist/tsup/client/mod.cjs +6 -6
  44. package/dist/tsup/client/mod.d.cts +2 -2
  45. package/dist/tsup/client/mod.d.ts +2 -2
  46. package/dist/tsup/client/mod.js +5 -5
  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-P3XujgRr.d.ts → config-Qj-zLJPc.d.ts} +11 -0
  52. package/dist/tsup/{config-_gfywqqI.d.cts → config-iPj5l1bL.d.cts} +11 -0
  53. package/dist/tsup/{context-uNA4TRn3.d.ts → context-CQCMuHND.d.ts} +1 -1
  54. package/dist/tsup/{context-Bxd8Cx4H.d.cts → context-DzvH1PBK.d.cts} +1 -1
  55. package/dist/tsup/{driver-CPGHKXyh.d.ts → driver-Jo8v-kbU.d.ts} +1 -1
  56. package/dist/tsup/driver-helpers/mod.cjs +4 -4
  57. package/dist/tsup/driver-helpers/mod.d.cts +4 -4
  58. package/dist/tsup/driver-helpers/mod.d.ts +4 -4
  59. package/dist/tsup/driver-helpers/mod.js +3 -3
  60. package/dist/tsup/{driver-BcLvZcKl.d.cts → driver-iV8J-WMv.d.cts} +1 -1
  61. package/dist/tsup/driver-test-suite/mod.cjs +556 -333
  62. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
  63. package/dist/tsup/driver-test-suite/mod.d.cts +2 -2
  64. package/dist/tsup/driver-test-suite/mod.d.ts +2 -2
  65. package/dist/tsup/driver-test-suite/mod.js +1332 -1109
  66. package/dist/tsup/driver-test-suite/mod.js.map +1 -1
  67. package/dist/tsup/inspector/mod.cjs +3 -3
  68. package/dist/tsup/inspector/mod.js +2 -2
  69. package/dist/tsup/mod.cjs +8 -8
  70. package/dist/tsup/mod.d.cts +5 -5
  71. package/dist/tsup/mod.d.ts +5 -5
  72. package/dist/tsup/mod.js +7 -7
  73. package/dist/tsup/serve-test-suite/mod.cjs +194 -100
  74. package/dist/tsup/serve-test-suite/mod.cjs.map +1 -1
  75. package/dist/tsup/serve-test-suite/mod.js +105 -11
  76. package/dist/tsup/serve-test-suite/mod.js.map +1 -1
  77. package/dist/tsup/test/mod.cjs +10 -10
  78. package/dist/tsup/test/mod.d.cts +1 -1
  79. package/dist/tsup/test/mod.d.ts +1 -1
  80. package/dist/tsup/test/mod.js +6 -6
  81. package/dist/tsup/utils.cjs +2 -2
  82. package/dist/tsup/utils.js +1 -1
  83. package/dist/tsup/workflow/mod.cjs +5 -5
  84. package/dist/tsup/workflow/mod.d.cts +3 -3
  85. package/dist/tsup/workflow/mod.d.ts +3 -3
  86. package/dist/tsup/workflow/mod.js +4 -4
  87. package/package.json +5 -5
  88. package/src/actor/config.ts +0 -2
  89. package/src/actor/instance/mod.ts +30 -6
  90. package/src/actor/router.ts +9 -6
  91. package/src/driver-test-suite/mod.ts +3 -0
  92. package/src/driver-test-suite/tests/actor-db.ts +299 -216
  93. package/src/driver-test-suite/tests/actor-driver.ts +4 -0
  94. package/src/driver-test-suite/tests/actor-lifecycle.ts +157 -0
  95. package/src/driver-test-suite/tests/actor-queue.ts +10 -9
  96. package/src/driver-test-suite/tests/actor-workflow.ts +12 -2
  97. package/src/driver-test-suite/tests/conn-error-serialization.ts +64 -0
  98. package/src/driver-test-suite/utils.ts +8 -8
  99. package/src/drivers/engine/actor-driver.ts +113 -11
  100. package/src/manager/router.ts +20 -6
  101. package/src/{registry → utils}/serve.ts +38 -4
  102. package/src/workflow/context.ts +4 -0
  103. package/src/workflow/driver.ts +4 -1
  104. package/dist/tsup/chunk-772NPMTY.cjs.map +0 -1
  105. package/dist/tsup/chunk-BSIJG3LG.js.map +0 -1
  106. package/dist/tsup/chunk-HFWRHT5T.cjs.map +0 -1
  107. package/dist/tsup/chunk-MNS5LY6M.cjs.map +0 -1
  108. package/dist/tsup/chunk-NXEHFUDB.cjs.map +0 -1
  109. package/dist/tsup/chunk-PSUVV4HM.js.map +0 -1
  110. package/dist/tsup/chunk-QABDKI3W.cjs.map +0 -1
  111. package/dist/tsup/chunk-RHUII57M.js.map +0 -1
  112. package/dist/tsup/chunk-VMX4I4MP.js.map +0 -1
  113. package/dist/tsup/chunk-YQ5P6KMN.js.map +0 -1
  114. /package/dist/tsup/{chunk-PW3YONDJ.js.map → chunk-5UEFNG7P.js.map} +0 -0
  115. /package/dist/tsup/{chunk-TDFDR7AO.js.map → chunk-HBYEYBIC.js.map} +0 -0
  116. /package/dist/tsup/{chunk-HB4RGGMC.js.map → chunk-I6PL6QIY.js.map} +0 -0
  117. /package/dist/tsup/{chunk-BFI4LYS2.js.map → chunk-TEFYRRAK.js.map} +0 -0
@@ -0,0 +1,157 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { DriverTestConfig } from "../mod";
3
+ import { setupDriverTest } from "../utils";
4
+
5
+ export function runActorLifecycleTests(driverTestConfig: DriverTestConfig) {
6
+ describe("Actor Lifecycle Tests", () => {
7
+ test("actor stop during start waits for start to complete", async (c) => {
8
+ const { client } = await setupDriverTest(c, driverTestConfig);
9
+
10
+ const actorKey = `test-stop-during-start-${Date.now()}`;
11
+
12
+ // Create actor - this starts the actor
13
+ const actor = client.startStopRaceActor.getOrCreate([actorKey]);
14
+
15
+ // Immediately try to call an action and then destroy
16
+ // This creates a race where the actor might not be fully started yet
17
+ const pingPromise = actor.ping();
18
+
19
+ // Get actor ID
20
+ const actorId = await actor.resolve();
21
+
22
+ // Destroy immediately while start might still be in progress
23
+ await actor.destroy();
24
+
25
+ // The ping should still complete successfully because destroy waits for start
26
+ const result = await pingPromise;
27
+ expect(result).toBe("pong");
28
+
29
+ // Verify actor was actually destroyed
30
+ let destroyed = false;
31
+ try {
32
+ await client.startStopRaceActor.getForId(actorId).ping();
33
+ } catch (err: any) {
34
+ destroyed = true;
35
+ expect(err.group).toBe("actor");
36
+ expect(err.code).toBe("not_found");
37
+ }
38
+ expect(destroyed).toBe(true);
39
+ });
40
+
41
+ test("actor stop before actor instantiation completes cleans up handler", async (c) => {
42
+ const { client } = await setupDriverTest(c, driverTestConfig);
43
+
44
+ const actorKey = `test-stop-before-instantiation-${Date.now()}`;
45
+
46
+ // Create multiple actors rapidly to increase chance of race
47
+ const actors = Array.from({ length: 5 }, (_, i) =>
48
+ client.startStopRaceActor.getOrCreate([
49
+ `${actorKey}-${i}`,
50
+ ]),
51
+ );
52
+
53
+ // Resolve all actor IDs (this triggers start)
54
+ const ids = await Promise.all(actors.map((a) => a.resolve()));
55
+
56
+ // Immediately destroy all actors
57
+ await Promise.all(actors.map((a) => a.destroy()));
58
+
59
+ // Verify all actors were cleaned up
60
+ for (const id of ids) {
61
+ let destroyed = false;
62
+ try {
63
+ await client.startStopRaceActor.getForId(id).ping();
64
+ } catch (err: any) {
65
+ destroyed = true;
66
+ expect(err.group).toBe("actor");
67
+ expect(err.code).toBe("not_found");
68
+ }
69
+ expect(destroyed, `actor ${id} should be destroyed`).toBe(
70
+ true,
71
+ );
72
+ }
73
+ });
74
+
75
+ test("onBeforeActorStart completes before stop proceeds", async (c) => {
76
+ const { client } = await setupDriverTest(c, driverTestConfig);
77
+
78
+ const actorKey = `test-before-actor-start-${Date.now()}`;
79
+
80
+ // Create actor
81
+ const actor = client.startStopRaceActor.getOrCreate([actorKey]);
82
+
83
+ // Call action to ensure actor is starting
84
+ const statePromise = actor.getState();
85
+
86
+ // Destroy immediately
87
+ await actor.destroy();
88
+
89
+ // State should be initialized because onBeforeActorStart must complete
90
+ const state = await statePromise;
91
+ expect(state.initialized).toBe(true);
92
+ expect(state.startCompleted).toBe(true);
93
+ });
94
+
95
+ test("multiple rapid create/destroy cycles handle race correctly", async (c) => {
96
+ const { client } = await setupDriverTest(c, driverTestConfig);
97
+
98
+ // Perform multiple rapid create/destroy cycles
99
+ for (let i = 0; i < 10; i++) {
100
+ const actorKey = `test-rapid-cycle-${Date.now()}-${i}`;
101
+ const actor = client.startStopRaceActor.getOrCreate([
102
+ actorKey,
103
+ ]);
104
+
105
+ // Trigger start
106
+ const resolvePromise = actor.resolve();
107
+
108
+ // Immediately destroy
109
+ const destroyPromise = actor.destroy();
110
+
111
+ // Both should complete without errors
112
+ await Promise.all([resolvePromise, destroyPromise]);
113
+ }
114
+
115
+ // If we get here without errors, the race condition is handled correctly
116
+ expect(true).toBe(true);
117
+ });
118
+
119
+ test("actor stop called with no actor instance cleans up handler", async (c) => {
120
+ const { client } = await setupDriverTest(c, driverTestConfig);
121
+
122
+ const actorKey = `test-cleanup-no-instance-${Date.now()}`;
123
+
124
+ // Create and immediately destroy
125
+ const actor = client.startStopRaceActor.getOrCreate([actorKey]);
126
+ const id = await actor.resolve();
127
+ await actor.destroy();
128
+
129
+ // Try to recreate with same key - should work without issues
130
+ const newActor = client.startStopRaceActor.getOrCreate([
131
+ actorKey,
132
+ ]);
133
+ const result = await newActor.ping();
134
+ expect(result).toBe("pong");
135
+
136
+ // Clean up
137
+ await newActor.destroy();
138
+ });
139
+
140
+ test("onDestroy is called even when actor is destroyed during start", async (c) => {
141
+ const { client } = await setupDriverTest(c, driverTestConfig);
142
+
143
+ const actorKey = `test-ondestroy-during-start-${Date.now()}`;
144
+
145
+ // Create actor
146
+ const actor = client.startStopRaceActor.getOrCreate([actorKey]);
147
+
148
+ // Start and immediately destroy
149
+ const statePromise = actor.getState();
150
+ await actor.destroy();
151
+
152
+ // Verify onDestroy was called (requires actor to be started)
153
+ const state = await statePromise;
154
+ expect(state.destroyCalled).toBe(true);
155
+ });
156
+ });
157
+ }
@@ -182,15 +182,16 @@ export function runActorQueueTests(driverTestConfig: DriverTestConfig) {
182
182
  }
183
183
  });
184
184
 
185
- test("wait send returns completion response", async (c) => {
186
- const { client } = await setupDriverTest(c, driverTestConfig);
187
- const handle = client.queueActor.getOrCreate(["wait-complete"]);
188
-
189
- const actionPromise = handle.receiveAndComplete("tasks");
190
- const result = await handle.send("tasks",
191
- { value: 123 },
192
- { wait: true, timeout: 1_000 },
193
- );
185
+ test("wait send returns completion response", async (c) => {
186
+ const { client } = await setupDriverTest(c, driverTestConfig);
187
+ const handle = client.queueActor.getOrCreate(["wait-complete"]);
188
+ const waitTimeout = driverTestConfig.useRealTimers ? 5_000 : 1_000;
189
+
190
+ const actionPromise = handle.receiveAndComplete("tasks");
191
+ const result = await handle.send("tasks",
192
+ { value: 123 },
193
+ { wait: true, timeout: waitTimeout },
194
+ );
194
195
 
195
196
  await actionPromise;
196
197
  expect(result).toEqual({
@@ -13,8 +13,18 @@ export function runActorWorkflowTests(driverTestConfig: DriverTestConfig) {
13
13
  "workflow-basic",
14
14
  ]);
15
15
 
16
- await waitFor(driverTestConfig, 1000);
17
- const state = await actor.getState();
16
+ let state = await actor.getState();
17
+ for (let i = 0; i < 50; i++) {
18
+ if (
19
+ state.runCount > 0 &&
20
+ state.history.length > 0 &&
21
+ state.guardTriggered
22
+ ) {
23
+ break;
24
+ }
25
+ await waitFor(driverTestConfig, 100);
26
+ state = await actor.getState();
27
+ }
18
28
  expect(state.runCount).toBeGreaterThan(0);
19
29
  expect(state.history.length).toBeGreaterThan(0);
20
30
  expect(state.guardTriggered).toBe(true);
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { DriverTestConfig } from "../mod";
3
+ import { setupDriverTest } from "../utils";
4
+
5
+ export function runConnErrorSerializationTests(driverTestConfig: DriverTestConfig) {
6
+ describe("Connection Error Serialization Tests", () => {
7
+ test("error thrown in createConnState preserves group and code through WebSocket serialization", async (c) => {
8
+ const { client } = await setupDriverTest(c, driverTestConfig);
9
+
10
+ const actorKey = `test-error-serialization-${Date.now()}`;
11
+
12
+ // Create actor handle with params that will trigger error in createConnState
13
+ const actor = client.connErrorSerializationActor.getOrCreate(
14
+ [actorKey],
15
+ { params: { shouldThrow: true } },
16
+ );
17
+
18
+ // Try to connect, which will trigger error in createConnState
19
+ const conn = actor.connect();
20
+
21
+ // Wait for connection to fail
22
+ let caughtError: any;
23
+ try {
24
+ // Try to call an action, which should fail because connection couldn't be established
25
+ await conn.getValue();
26
+ } catch (err) {
27
+ caughtError = err;
28
+ }
29
+
30
+ // Verify the error was caught
31
+ expect(caughtError).toBeDefined();
32
+
33
+ // Verify the error has the correct group and code from the original error
34
+ // Original error: new CustomConnectionError("...") with group="connection", code="custom_error"
35
+ expect(caughtError.group).toBe("connection");
36
+ expect(caughtError.code).toBe("custom_error");
37
+
38
+ // Clean up
39
+ await conn.dispose();
40
+ });
41
+
42
+ test("successful createConnState does not throw error", async (c) => {
43
+ const { client } = await setupDriverTest(c, driverTestConfig);
44
+
45
+ const actorKey = `test-no-error-${Date.now()}`;
46
+
47
+ // Create actor handle with params that will NOT trigger error
48
+ const actor = client.connErrorSerializationActor.getOrCreate(
49
+ [actorKey],
50
+ { params: { shouldThrow: false } },
51
+ );
52
+
53
+ // Connect without triggering error
54
+ const conn = actor.connect();
55
+
56
+ // This should succeed
57
+ const value = await conn.getValue();
58
+ expect(value).toBe(0);
59
+
60
+ // Clean up
61
+ await conn.dispose();
62
+ });
63
+ });
64
+ }
@@ -26,10 +26,6 @@ export async function setupDriverTest(
26
26
  // Build drivers
27
27
  const { endpoint, namespace, runnerName, cleanup } =
28
28
  await driverTestConfig.start();
29
- c.onTestFinished(() => {
30
- logger().info("cleaning up test");
31
- cleanup();
32
- });
33
29
 
34
30
  let client: Client<typeof registry>;
35
31
  if (driverTestConfig.clientType === "http") {
@@ -56,10 +52,14 @@ export async function setupDriverTest(
56
52
  assertUnreachable(driverTestConfig.clientType);
57
53
  }
58
54
 
59
- // Cleanup client
60
- if (!driverTestConfig.HACK_skipCleanupNet) {
61
- c.onTestFinished(async () => await client.dispose());
62
- }
55
+ c.onTestFinished(async () => {
56
+ if (!driverTestConfig.HACK_skipCleanupNet) {
57
+ await client.dispose();
58
+ }
59
+
60
+ logger().info("cleaning up test");
61
+ await cleanup();
62
+ });
63
63
 
64
64
  return {
65
65
  client,
@@ -3,6 +3,7 @@ import type {
3
3
  RunnerConfig as EngineRunnerConfig,
4
4
  HibernatingWebSocketMetadata,
5
5
  } from "@rivetkit/engine-runner";
6
+ import type { SqliteVfs } from "@rivetkit/sqlite-vfs";
6
7
  import { idToStr, Runner } from "@rivetkit/engine-runner";
7
8
  import * as cbor from "cbor-x";
8
9
  import type { Context as HonoContext } from "hono";
@@ -51,6 +52,7 @@ import {
51
52
  import { logger } from "./log";
52
53
 
53
54
  const RUNNER_SSE_PING_INTERVAL = 1000;
55
+ const RUNNER_STOP_WAIT_MS = 15_000;
54
56
 
55
57
  // Message ack deadline is 30s on the gateway, but we will ack more frequently
56
58
  // in order to minimize the message buffer size on the gateway and to give
@@ -69,6 +71,7 @@ const CONN_BUFFERED_MESSAGE_SIZE_THRESHOLD = 500_000;
69
71
  interface ActorHandler {
70
72
  actor?: AnyActorInstance;
71
73
  actorStartPromise?: ReturnType<typeof promiseWithResolvers<void>>;
74
+ actorStartError?: Error;
72
75
  alarmTimeout?: LongTimeoutHandle;
73
76
  }
74
77
 
@@ -153,7 +156,7 @@ export class EngineActorDriver implements ActorDriver {
153
156
  onConnected: () => {
154
157
  this.#runnerStarted.resolve(undefined);
155
158
  },
156
- onDisconnected: (_code, _reason) => {},
159
+ onDisconnected: (_code, _reason) => { },
157
160
  onShutdown: () => {
158
161
  this.#runnerStopped.resolve(undefined);
159
162
  this.#isRunnerStopped = true;
@@ -189,6 +192,7 @@ export class EngineActorDriver implements ActorDriver {
189
192
  if (!handler)
190
193
  throw new Error(`Actor handler does not exist ${actorId}`);
191
194
  if (handler.actorStartPromise) await handler.actorStartPromise.promise;
195
+ if (handler.actorStartError) throw handler.actorStartError;
192
196
  if (!handler.actor) throw new Error("Actor should be loaded");
193
197
  return handler;
194
198
  }
@@ -283,6 +287,16 @@ export class EngineActorDriver implements ActorDriver {
283
287
  return result;
284
288
  }
285
289
 
290
+ /** Creates a SQLite VFS instance for creating KV-backed databases */
291
+ async createSqliteVfs(): Promise<SqliteVfs> {
292
+ // Dynamic import keeps @rivetkit/sqlite out of the main entrypoint bundle.
293
+ // Returning a fresh SqliteVfs gives each actor an isolated sqlite module
294
+ // instance, avoiding async re-entrancy across actors.
295
+ const specifier = "@rivetkit/" + "sqlite-vfs";
296
+ const { SqliteVfs } = await import(specifier);
297
+ return new SqliteVfs();
298
+ }
299
+
286
300
  // MARK: - Actor Lifecycle
287
301
  async loadActor(actorId: string): Promise<AnyActorInstance> {
288
302
  const handler = await this.#loadActorHandler(actorId);
@@ -350,13 +364,38 @@ export class EngineActorDriver implements ActorDriver {
350
364
  await Promise.all(stopPromises);
351
365
  logger().debug({ msg: "all actors stopped" });
352
366
 
353
- await this.#runner.shutdown(immediate);
367
+ try {
368
+ await this.#runner.shutdown(immediate);
369
+ } catch (error) {
370
+ const message = error instanceof Error ? error.message : String(error);
371
+ if (message.includes("WebSocket connection closed during shutdown")) {
372
+ logger().debug({
373
+ msg: "ignoring shutdown websocket close race",
374
+ error: message,
375
+ });
376
+ } else {
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ const stopped = await Promise.race([
382
+ this.#runnerStopped.promise.then(() => true),
383
+ new Promise<false>((resolve) =>
384
+ setTimeout(() => resolve(false), RUNNER_STOP_WAIT_MS),
385
+ ),
386
+ ]);
387
+ if (!stopped) {
388
+ logger().warn({
389
+ msg: "timed out waiting for runner shutdown",
390
+ waitMs: RUNNER_STOP_WAIT_MS,
391
+ });
392
+ }
354
393
  }
355
394
 
356
395
  async serverlessHandleStart(c: HonoContext): Promise<Response> {
357
396
  return streamSSE(c, async (stream) => {
358
397
  // NOTE: onAbort does not work reliably
359
- stream.onAbort(() => {});
398
+ stream.onAbort(() => { });
360
399
  c.req.raw.signal.addEventListener("abort", () => {
361
400
  logger().debug("SSE aborted, shutting down runner");
362
401
 
@@ -430,6 +469,7 @@ export class EngineActorDriver implements ActorDriver {
430
469
  };
431
470
  this.#actors.set(actorId, handler);
432
471
  }
472
+ handler.actorStartError = undefined;
433
473
 
434
474
  const name = actorConfig.name as string;
435
475
  invariant(actorConfig.key, "actor should have a key");
@@ -457,8 +497,28 @@ export class EngineActorDriver implements ActorDriver {
457
497
 
458
498
  // Create actor instance
459
499
  const definition = lookupInRegistry(this.#config, actorConfig.name);
500
+
460
501
  handler.actor = await definition.instantiate();
461
502
 
503
+ // Apply protocol limits as per-instance overrides without mutating the shared definition
504
+ const protocolMetadata = this.#runner.getProtocolMetadata();
505
+ if (protocolMetadata) {
506
+ logger().debug({
507
+ msg: "applying config limits from protocol",
508
+ protocolMetadata,
509
+ });
510
+
511
+ const stopThresholdMax = Math.max(Number(protocolMetadata.actorStopThreshold) - 1000, 0);
512
+ handler.actor.overrides.onSleepTimeout = stopThresholdMax;
513
+ handler.actor.overrides.onDestroyTimeout = stopThresholdMax;
514
+
515
+ if (protocolMetadata.serverlessDrainGracePeriod) {
516
+ const drainMax = Math.max(Number(protocolMetadata.serverlessDrainGracePeriod) - 1000, 0);
517
+ handler.actor.overrides.runStopTimeout = drainMax;
518
+ handler.actor.overrides.waitUntilTimeout = drainMax;
519
+ }
520
+ }
521
+
462
522
  // Start actor
463
523
  await handler.actor.start(
464
524
  this,
@@ -471,13 +531,34 @@ export class EngineActorDriver implements ActorDriver {
471
531
 
472
532
  logger().debug({ msg: "runner actor started", actorId, name, key });
473
533
  } catch (innerError) {
474
- const error = new Error(
475
- `Failed to start actor ${actorId}: ${innerError}`,
476
- { cause: innerError },
477
- );
534
+ const error =
535
+ innerError instanceof Error
536
+ ? new Error(
537
+ `Failed to start actor ${actorId}: ${innerError.message}`,
538
+ { cause: innerError },
539
+ )
540
+ : new Error(`Failed to start actor ${actorId}: ${String(innerError)}`);
541
+ handler.actor = undefined;
542
+ handler.actorStartError = error;
478
543
  handler.actorStartPromise?.reject(error);
479
544
  handler.actorStartPromise = undefined;
480
- throw error;
545
+ logger().error({
546
+ msg: "runner actor failed to start",
547
+ actorId,
548
+ name,
549
+ key,
550
+ err: stringifyError(error),
551
+ });
552
+
553
+ try {
554
+ this.#runner.stopActor(actorId);
555
+ } catch (stopError) {
556
+ logger().debug({
557
+ msg: "failed to stop actor after start failure",
558
+ actorId,
559
+ err: stringifyError(stopError),
560
+ });
561
+ }
481
562
  }
482
563
  }
483
564
 
@@ -498,7 +579,26 @@ export class EngineActorDriver implements ActorDriver {
498
579
  this.#actorStopIntent.delete(actorId);
499
580
 
500
581
  const handler = this.#actors.get(actorId);
501
- if (handler?.actor) {
582
+ if (!handler) {
583
+ logger().debug({ msg: "no runner actor handler to stop", actorId, reason });
584
+ return;
585
+ }
586
+
587
+ if (handler.actorStartPromise) {
588
+ try {
589
+ logger().debug({ msg: "runner actor stopping before it started, waiting", actorId, generation });
590
+ await handler.actorStartPromise.promise;
591
+ } catch (err) {
592
+ // Start failed, but we still want to clean up the handler
593
+ logger().debug({
594
+ msg: "actor start failed during stop, cleaning up handler",
595
+ actorId,
596
+ err: stringifyError(err),
597
+ });
598
+ }
599
+ }
600
+
601
+ if (handler.actor) {
502
602
  try {
503
603
  await handler.actor.onStop(reason);
504
604
  } catch (err) {
@@ -507,9 +607,10 @@ export class EngineActorDriver implements ActorDriver {
507
607
  err: stringifyError(err),
508
608
  });
509
609
  }
510
- this.#actors.delete(actorId);
511
610
  }
512
611
 
612
+ this.#actors.delete(actorId);
613
+
513
614
  logger().debug({ msg: "runner actor stopped", actorId, reason });
514
615
  }
515
616
 
@@ -693,7 +794,7 @@ export class EngineActorDriver implements ActorDriver {
693
794
  entry.bufferedMessageSize >=
694
795
  CONN_BUFFERED_MESSAGE_SIZE_THRESHOLD
695
796
  ) {
696
- // Reset buffered message size immeidatley (instead
797
+ // Reset buffered message size immediately (instead
697
798
  // of waiting for onAfterPersistConn) since we may
698
799
  // receive more messages before onAfterPersistConn
699
800
  // is called, which would called saveState
@@ -872,6 +973,7 @@ export class EngineActorDriver implements ActorDriver {
872
973
  // Resolve promise if waiting
873
974
  const handler = this.#actors.get(actor.id);
874
975
  invariant(handler, "missing actor handler in onBeforeActorReady");
976
+ handler.actorStartError = undefined;
875
977
  handler.actorStartPromise?.resolve();
876
978
  handler.actorStartPromise = undefined;
877
979
 
@@ -1,4 +1,3 @@
1
- import { serveStatic } from "@hono/node-server/serve-static";
2
1
  import { createRoute } from "@hono/zod-openapi";
3
2
  import * as cbor from "cbor-x";
4
3
  import type { Hono } from "hono";
@@ -38,9 +37,10 @@ import {
38
37
  type Actor as ApiActor,
39
38
  } from "@/manager-api/actors";
40
39
  import { buildActorNames, type RegistryConfig } from "@/registry/config";
41
- import type { GetUpgradeWebSocket } from "@/utils";
40
+ import { loadRuntimeServeStatic } from "@/utils/serve";
41
+ import type { GetUpgradeWebSocket, Runtime } from "@/utils";
42
42
  import { timingSafeEqual } from "@/utils/crypto";
43
- import { getNodeEnv } from "@/utils/env-vars";
43
+ import { isDev } from "@/utils/env-vars";
44
44
  import {
45
45
  buildOpenApiRequestBody,
46
46
  buildOpenApiResponses,
@@ -54,6 +54,7 @@ export function buildManagerRouter(
54
54
  config: RegistryConfig,
55
55
  managerDriver: ManagerDriver,
56
56
  getUpgradeWebSocket: GetUpgradeWebSocket | undefined,
57
+ runtime: Runtime = "node",
57
58
  ) {
58
59
  return createRouter(config.managerBasePath, (router) => {
59
60
  // Actor gateway
@@ -319,13 +320,13 @@ export function buildManagerRouter(
319
320
  });
320
321
 
321
322
  router.openapi(route, async (c) => {
322
- if (getNodeEnv() === "development" && !config.token) {
323
+ if (isDev() && !config.token) {
323
324
  logger().warn({
324
325
  msg: "RIVET_TOKEN is not set, skipping KV store access checks in development mode. This endpoint will be disabled in production, unless you set the token.",
325
326
  });
326
327
  }
327
328
 
328
- if (getNodeEnv() !== "development") {
329
+ if (!isDev()) {
329
330
  if (!config.token) {
330
331
  throw new RestrictedFeature("KV store access");
331
332
  }
@@ -589,8 +590,21 @@ export function buildManagerRouter(
589
590
  if (config.inspector.enabled) {
590
591
  let inspectorRoot: string | undefined;
591
592
 
592
-
593
593
  router.get("/ui/*", async (c, next) => {
594
+ let serveStatic;
595
+ try {
596
+ serveStatic = await loadRuntimeServeStatic(runtime);
597
+ } catch (error) {
598
+ logger().error({
599
+ msg: "failed to load inspector static file handler",
600
+ error: stringifyError(error),
601
+ });
602
+ return c.text(
603
+ `Failed to load static file handler for runtime '${runtime}'.`,
604
+ 500,
605
+ );
606
+ }
607
+
594
608
  if (!inspectorRoot) {
595
609
  inspectorRoot = await getInspectorDir();
596
610
  }
@@ -1,12 +1,16 @@
1
1
  import type { Hono } from "hono";
2
- import { detectRuntime, stringifyError } from "../utils";
3
- import { logger } from "./log";
4
- import { RegistryConfig } from "./config";
2
+ import { detectRuntime, stringifyError, type Runtime } from "../utils";
3
+ import { RegistryConfig } from "@/registry/config";
4
+ import { logger } from "@/registry/log";
5
5
 
6
6
  // TODO: Go back to dynamic import for this
7
7
  import getPort from "get-port";
8
8
 
9
9
  const DEFAULT_PORT = 6420;
10
+ export type ServeStatic =
11
+ typeof import("@hono/node-server/serve-static").serveStatic;
12
+ const serveStaticLoaderPromises: Partial<Record<Runtime, Promise<ServeStatic>>> =
13
+ {};
10
14
 
11
15
  /**
12
16
  * Finds a free port starting from the given port.
@@ -34,8 +38,8 @@ export async function crossPlatformServe(
34
38
  config: RegistryConfig,
35
39
  managerPort: number,
36
40
  app: Hono<any>,
41
+ runtime: Runtime = detectRuntime(),
37
42
  ): Promise<{ upgradeWebSocket: any }> {
38
- const runtime = detectRuntime();
39
43
  logger().debug({ msg: "detected runtime for serve", runtime });
40
44
 
41
45
  switch (runtime) {
@@ -50,6 +54,36 @@ export async function crossPlatformServe(
50
54
  }
51
55
  }
52
56
 
57
+ export async function loadRuntimeServeStatic(
58
+ runtime: Runtime,
59
+ ): Promise<ServeStatic> {
60
+ if (!serveStaticLoaderPromises[runtime]) {
61
+ if (runtime === "node") {
62
+ const nodeServeStaticModule = "@hono/node-server/serve-static";
63
+ serveStaticLoaderPromises[runtime] = import(
64
+ /* webpackIgnore: true */
65
+ nodeServeStaticModule
66
+ ).then((x) => x.serveStatic);
67
+ } else if (runtime === "bun") {
68
+ const bunModule = "hono/bun";
69
+ serveStaticLoaderPromises[runtime] = import(
70
+ /* webpackIgnore: true */
71
+ bunModule
72
+ ).then((x) => x.serveStatic as ServeStatic);
73
+ } else if (runtime === "deno") {
74
+ const denoModule = "hono/deno";
75
+ serveStaticLoaderPromises[runtime] = import(
76
+ /* webpackIgnore: true */
77
+ denoModule
78
+ ).then((x) => x.serveStatic as ServeStatic);
79
+ } else {
80
+ throw new Error(`unsupported runtime: ${runtime}`);
81
+ }
82
+ }
83
+
84
+ return await serveStaticLoaderPromises[runtime]!;
85
+ }
86
+
53
87
  async function serveNode(
54
88
  config: RegistryConfig,
55
89
  managerPort: number,
@@ -212,6 +212,7 @@ export class ActorWorkflowContext<
212
212
  body: unknown,
213
213
  ): Promise<void>;
214
214
  async function send(name: string, body: unknown): Promise<void> {
215
+ self.#ensureActorAccess("queue.send");
215
216
  await self.#runCtx.queue.send(name as never, body as never);
216
217
  }
217
218
 
@@ -445,10 +446,12 @@ export class ActorWorkflowContext<
445
446
  }
446
447
 
447
448
  keepAwake<T>(promise: Promise<T>): Promise<T> {
449
+ this.#ensureActorAccess("keepAwake");
448
450
  return this.#runCtx.keepAwake(promise);
449
451
  }
450
452
 
451
453
  waitUntil(promise: Promise<void>): void {
454
+ this.#ensureActorAccess("waitUntil");
452
455
  this.#runCtx.waitUntil(promise);
453
456
  }
454
457
 
@@ -465,6 +468,7 @@ export class ActorWorkflowContext<
465
468
  ...args: Array<unknown>
466
469
  ): void;
467
470
  broadcast(name: string, ...args: Array<unknown>): void {
471
+ this.#ensureActorAccess("broadcast");
468
472
  this.#runCtx.broadcast(
469
473
  name as never,
470
474
  ...((args as unknown[]) as never[]),