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.
- package/dist/browser/client.d.ts +11 -0
- package/dist/browser/client.js +1 -1
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.js +1 -1
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/inspector.tar.gz +0 -0
- package/dist/tsup/{chunk-MNS5LY6M.cjs → chunk-3B6PCYJB.cjs} +280 -115
- package/dist/tsup/chunk-3B6PCYJB.cjs.map +1 -0
- package/dist/tsup/{chunk-YQ5P6KMN.js → chunk-3GTO6H3E.js} +12 -5
- package/dist/tsup/chunk-3GTO6H3E.js.map +1 -0
- package/dist/tsup/{chunk-RMJJE43B.cjs → chunk-4KSHPFXF.cjs} +2 -2
- package/dist/tsup/{chunk-RMJJE43B.cjs.map → chunk-4KSHPFXF.cjs.map} +1 -1
- package/dist/tsup/{chunk-PW3YONDJ.js → chunk-5UEFNG7P.js} +2 -2
- package/dist/tsup/{chunk-PSUVV4HM.js → chunk-ANKZ2FS6.js} +2 -4
- package/dist/tsup/chunk-ANKZ2FS6.js.map +1 -0
- package/dist/tsup/{chunk-GVQAVU7R.cjs → chunk-AQD4CBZ2.cjs} +4 -4
- package/dist/tsup/{chunk-GVQAVU7R.cjs.map → chunk-AQD4CBZ2.cjs.map} +1 -1
- package/dist/tsup/{chunk-WUXR722E.js → chunk-DZXDUGLL.js} +2 -2
- package/dist/tsup/{chunk-WUXR722E.js.map → chunk-DZXDUGLL.js.map} +1 -1
- package/dist/tsup/{chunk-NXEHFUDB.cjs → chunk-GXRVSSVD.cjs} +28 -21
- package/dist/tsup/chunk-GXRVSSVD.cjs.map +1 -0
- package/dist/tsup/{chunk-UZV7NXC6.cjs → chunk-H5TSEPN4.cjs} +30 -30
- package/dist/tsup/{chunk-UZV7NXC6.cjs.map → chunk-H5TSEPN4.cjs.map} +1 -1
- package/dist/tsup/{chunk-TDFDR7AO.js → chunk-HBYEYBIC.js} +2 -2
- package/dist/tsup/{chunk-772NPMTY.cjs → chunk-HKOSZKKZ.cjs} +263 -299
- package/dist/tsup/chunk-HKOSZKKZ.cjs.map +1 -0
- package/dist/tsup/{chunk-HB4RGGMC.js → chunk-I6PL6QIY.js} +5 -5
- package/dist/tsup/{chunk-RHUII57M.js → chunk-KTWY3K6Z.js} +23 -12
- package/dist/tsup/chunk-KTWY3K6Z.js.map +1 -0
- package/dist/tsup/{chunk-HFWRHT5T.cjs → chunk-LK36OGGO.cjs} +3 -5
- package/dist/tsup/chunk-LK36OGGO.cjs.map +1 -0
- package/dist/tsup/{chunk-BSIJG3LG.js → chunk-M6H4XIF4.js} +179 -215
- package/dist/tsup/chunk-M6H4XIF4.js.map +1 -0
- package/dist/tsup/{chunk-ZHQDRRMY.cjs → chunk-QPADHLDU.cjs} +3 -3
- package/dist/tsup/{chunk-ZHQDRRMY.cjs.map → chunk-QPADHLDU.cjs.map} +1 -1
- package/dist/tsup/{chunk-BFI4LYS2.js → chunk-TEFYRRAK.js} +4 -4
- package/dist/tsup/{chunk-PZAV6PP2.cjs → chunk-TEUL4UYN.cjs} +152 -152
- package/dist/tsup/{chunk-PZAV6PP2.cjs.map → chunk-TEUL4UYN.cjs.map} +1 -1
- package/dist/tsup/{chunk-VMX4I4MP.js → chunk-UDMRZR6A.js} +212 -47
- package/dist/tsup/chunk-UDMRZR6A.js.map +1 -0
- package/dist/tsup/{chunk-QABDKI3W.cjs → chunk-UWAGLDT6.cjs} +263 -252
- package/dist/tsup/chunk-UWAGLDT6.cjs.map +1 -0
- package/dist/tsup/client/mod.cjs +6 -6
- package/dist/tsup/client/mod.d.cts +2 -2
- package/dist/tsup/client/mod.d.ts +2 -2
- package/dist/tsup/client/mod.js +5 -5
- 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-P3XujgRr.d.ts → config-Qj-zLJPc.d.ts} +11 -0
- package/dist/tsup/{config-_gfywqqI.d.cts → config-iPj5l1bL.d.cts} +11 -0
- package/dist/tsup/{context-uNA4TRn3.d.ts → context-CQCMuHND.d.ts} +1 -1
- package/dist/tsup/{context-Bxd8Cx4H.d.cts → context-DzvH1PBK.d.cts} +1 -1
- package/dist/tsup/{driver-CPGHKXyh.d.ts → driver-Jo8v-kbU.d.ts} +1 -1
- package/dist/tsup/driver-helpers/mod.cjs +4 -4
- package/dist/tsup/driver-helpers/mod.d.cts +4 -4
- package/dist/tsup/driver-helpers/mod.d.ts +4 -4
- package/dist/tsup/driver-helpers/mod.js +3 -3
- package/dist/tsup/{driver-BcLvZcKl.d.cts → driver-iV8J-WMv.d.cts} +1 -1
- package/dist/tsup/driver-test-suite/mod.cjs +556 -333
- package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
- package/dist/tsup/driver-test-suite/mod.d.cts +2 -2
- package/dist/tsup/driver-test-suite/mod.d.ts +2 -2
- package/dist/tsup/driver-test-suite/mod.js +1332 -1109
- package/dist/tsup/driver-test-suite/mod.js.map +1 -1
- package/dist/tsup/inspector/mod.cjs +3 -3
- package/dist/tsup/inspector/mod.js +2 -2
- package/dist/tsup/mod.cjs +8 -8
- package/dist/tsup/mod.d.cts +5 -5
- package/dist/tsup/mod.d.ts +5 -5
- package/dist/tsup/mod.js +7 -7
- package/dist/tsup/serve-test-suite/mod.cjs +194 -100
- package/dist/tsup/serve-test-suite/mod.cjs.map +1 -1
- package/dist/tsup/serve-test-suite/mod.js +105 -11
- package/dist/tsup/serve-test-suite/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +10 -10
- package/dist/tsup/test/mod.d.cts +1 -1
- package/dist/tsup/test/mod.d.ts +1 -1
- package/dist/tsup/test/mod.js +6 -6
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.js +1 -1
- package/dist/tsup/workflow/mod.cjs +5 -5
- package/dist/tsup/workflow/mod.d.cts +3 -3
- package/dist/tsup/workflow/mod.d.ts +3 -3
- package/dist/tsup/workflow/mod.js +4 -4
- package/package.json +5 -5
- package/src/actor/config.ts +0 -2
- package/src/actor/instance/mod.ts +30 -6
- package/src/actor/router.ts +9 -6
- package/src/driver-test-suite/mod.ts +3 -0
- package/src/driver-test-suite/tests/actor-db.ts +299 -216
- package/src/driver-test-suite/tests/actor-driver.ts +4 -0
- package/src/driver-test-suite/tests/actor-lifecycle.ts +157 -0
- package/src/driver-test-suite/tests/actor-queue.ts +10 -9
- package/src/driver-test-suite/tests/actor-workflow.ts +12 -2
- package/src/driver-test-suite/tests/conn-error-serialization.ts +64 -0
- package/src/driver-test-suite/utils.ts +8 -8
- package/src/drivers/engine/actor-driver.ts +113 -11
- package/src/manager/router.ts +20 -6
- package/src/{registry → utils}/serve.ts +38 -4
- package/src/workflow/context.ts +4 -0
- package/src/workflow/driver.ts +4 -1
- package/dist/tsup/chunk-772NPMTY.cjs.map +0 -1
- package/dist/tsup/chunk-BSIJG3LG.js.map +0 -1
- package/dist/tsup/chunk-HFWRHT5T.cjs.map +0 -1
- package/dist/tsup/chunk-MNS5LY6M.cjs.map +0 -1
- package/dist/tsup/chunk-NXEHFUDB.cjs.map +0 -1
- package/dist/tsup/chunk-PSUVV4HM.js.map +0 -1
- package/dist/tsup/chunk-QABDKI3W.cjs.map +0 -1
- package/dist/tsup/chunk-RHUII57M.js.map +0 -1
- package/dist/tsup/chunk-VMX4I4MP.js.map +0 -1
- package/dist/tsup/chunk-YQ5P6KMN.js.map +0 -1
- /package/dist/tsup/{chunk-PW3YONDJ.js.map → chunk-5UEFNG7P.js.map} +0 -0
- /package/dist/tsup/{chunk-TDFDR7AO.js.map → chunk-HBYEYBIC.js.map} +0 -0
- /package/dist/tsup/{chunk-HB4RGGMC.js.map → chunk-I6PL6QIY.js.map} +0 -0
- /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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 =
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/manager/router.ts
CHANGED
|
@@ -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
|
|
40
|
+
import { loadRuntimeServeStatic } from "@/utils/serve";
|
|
41
|
+
import type { GetUpgradeWebSocket, Runtime } from "@/utils";
|
|
42
42
|
import { timingSafeEqual } from "@/utils/crypto";
|
|
43
|
-
import {
|
|
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 (
|
|
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 (
|
|
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 {
|
|
4
|
-
import {
|
|
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,
|
package/src/workflow/context.ts
CHANGED
|
@@ -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[]),
|