Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit a4af2b1

Browse files
committed
feat(core): actor sleeping (#1178)
Fixes KIT-198
1 parent cf09cfe commit a4af2b1

32 files changed

+1563
-160
lines changed

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,3 @@ Always include a README.md. The `README.md` should always follow this structure:
194194
## Test Notes
195195

196196
- Using setTimeout in tests & test actors will not work unless you call `await waitFor(driverTestConfig, <ts>)`
197-
- Do not use setTimeout in tests or in actors used in tests unless you explictily use `await vi.advanceTimersByTimeAsync(time)`

biome.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{
22
"$schema": "https://biomejs.dev/schemas/2.1.1/schema.json",
33
"files": {
4-
"includes": ["**/*.json", "**/*.ts", "**/*.js", "!examples/snippets/**"],
4+
"includes": [
5+
"**/*.json",
6+
"**/*.ts",
7+
"**/*.js",
8+
"!examples/snippets/**",
9+
"!clients/openapi/openapi.json"
10+
],
511
"ignoreUnknown": true
612
},
713
"vcs": {

clients/openapi/openapi.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"example": "actor-123"
1515
}
1616
},
17-
"required": ["i"]
17+
"required": [
18+
"i"
19+
]
1820
},
1921
"ResolveQuery": {
2022
"type": "object",
@@ -676,4 +678,4 @@
676678
}
677679
}
678680
}
679-
}
681+
}

packages/core/fixtures/driver-test-suite/action-timeout.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ export const shortTimeoutActor = actor({
55
onAuth: () => {},
66
state: { value: 0 },
77
options: {
8-
action: {
9-
timeout: 50, // 50ms timeout
10-
},
8+
actionTimeout: 50, // 50ms timeout
119
},
1210
actions: {
1311
quickAction: async (c) => {
@@ -26,9 +24,7 @@ export const longTimeoutActor = actor({
2624
onAuth: () => {},
2725
state: { value: 0 },
2826
options: {
29-
action: {
30-
timeout: 200, // 200ms timeout
31-
},
27+
actionTimeout: 200, // 200ms timeout
3228
},
3329
actions: {
3430
delayedAction: async (c) => {
@@ -56,9 +52,7 @@ export const syncTimeoutActor = actor({
5652
onAuth: () => {},
5753
state: { value: 0 },
5854
options: {
59-
action: {
60-
timeout: 50, // 50ms timeout
61-
},
55+
actionTimeout: 50, // 50ms timeout
6256
},
6357
actions: {
6458
syncAction: (c) => {

packages/core/fixtures/driver-test-suite/conn-liveness.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ export const connLivenessActor = actor({
77
acceptingConnections: true,
88
},
99
options: {
10-
lifecycle: {
11-
connectionLivenessInterval: 5_000,
12-
connectionLivenessTimeout: 2_500,
13-
},
10+
connectionLivenessInterval: 5_000,
11+
connectionLivenessTimeout: 2_500,
1412
},
1513
onConnect: (c, conn) => {
1614
if (!c.state.acceptingConnections) {

packages/core/fixtures/driver-test-suite/error-handling.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ export const errorHandlingActor = actor({
6969
},
7070
},
7171
options: {
72-
// Set a short timeout for this actor's actions
73-
action: {
74-
timeout: 500, // 500ms timeout for actions
75-
},
72+
actionTimeout: 500, // 500ms timeout for actions
7673
},
7774
});
7875

@@ -90,8 +87,6 @@ export const customTimeoutActor = actor({
9087
},
9188
},
9289
options: {
93-
action: {
94-
timeout: 200, // 200ms timeout
95-
},
90+
actionTimeout: 200, // 200ms timeout
9691
},
9792
});

packages/core/fixtures/driver-test-suite/registry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ import {
5151
import { requestAccessActor } from "./request-access";
5252
import { requestAccessAuthActor } from "./request-access-auth";
5353
import { scheduled } from "./scheduled";
54+
import {
55+
sleep,
56+
sleepWithLongRpc,
57+
sleepWithNoSleepOption,
58+
sleepWithRawHttp,
59+
sleepWithRawWebSocket,
60+
} from "./sleep";
5461
import {
5562
driverCtxActor,
5663
dynamicVarActor,
@@ -68,6 +75,12 @@ export const registry = setup({
6875
counterWithLifecycle,
6976
// From scheduled.ts
7077
scheduled,
78+
// From sleep.ts
79+
sleep,
80+
sleepWithLongRpc,
81+
sleepWithRawHttp,
82+
sleepWithRawWebSocket,
83+
sleepWithNoSleepOption,
7184
// From error-handling.ts
7285
errorHandlingActor,
7386
customTimeoutActor,
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { actor, type UniversalWebSocket } from "@rivetkit/core";
2+
3+
export const SLEEP_TIMEOUT = 500;
4+
5+
export const sleep = actor({
6+
onAuth: () => {},
7+
state: { startCount: 0, sleepCount: 0 },
8+
onStart: (c) => {
9+
c.state.startCount += 1;
10+
},
11+
onStop: (c) => {
12+
c.state.sleepCount += 1;
13+
},
14+
actions: {
15+
triggerSleep: (c) => {
16+
c.sleep();
17+
},
18+
getCounts: (c) => {
19+
return { startCount: c.state.startCount, sleepCount: c.state.sleepCount };
20+
},
21+
setAlarm: async (c, duration: number) => {
22+
await c.schedule.after(duration, "onAlarm");
23+
},
24+
onAlarm: (c) => {
25+
c.log.info("alarm called");
26+
},
27+
},
28+
options: {
29+
sleepTimeout: SLEEP_TIMEOUT,
30+
},
31+
});
32+
33+
export const sleepWithLongRpc = actor({
34+
onAuth: () => {},
35+
state: { startCount: 0, sleepCount: 0 },
36+
createVars: () => ({}) as { longRunningResolve: PromiseWithResolvers<void> },
37+
onStart: (c) => {
38+
c.state.startCount += 1;
39+
},
40+
onStop: (c) => {
41+
c.state.sleepCount += 1;
42+
},
43+
actions: {
44+
getCounts: (c) => {
45+
return { startCount: c.state.startCount, sleepCount: c.state.sleepCount };
46+
},
47+
longRunningRpc: async (c) => {
48+
c.log.info("starting long running rpc");
49+
c.vars.longRunningResolve = Promise.withResolvers();
50+
c.broadcast("waiting");
51+
await c.vars.longRunningResolve.promise;
52+
c.log.info("finished long running rpc");
53+
},
54+
finishLongRunningRpc: (c) => c.vars.longRunningResolve?.resolve(),
55+
},
56+
options: {
57+
sleepTimeout: SLEEP_TIMEOUT,
58+
},
59+
});
60+
61+
export const sleepWithRawHttp = actor({
62+
onAuth: () => {},
63+
state: { startCount: 0, sleepCount: 0, requestCount: 0 },
64+
onStart: (c) => {
65+
c.state.startCount += 1;
66+
},
67+
onStop: (c) => {
68+
c.state.sleepCount += 1;
69+
},
70+
onFetch: async (c, request) => {
71+
c.state.requestCount += 1;
72+
const url = new URL(request.url);
73+
74+
if (url.pathname === "/long-request") {
75+
const duration = parseInt(url.searchParams.get("duration") || "1000");
76+
c.log.info("starting long fetch request", { duration });
77+
await new Promise((resolve) => setTimeout(resolve, duration));
78+
c.log.info("finished long fetch request");
79+
return new Response(JSON.stringify({ completed: true }), {
80+
headers: { "Content-Type": "application/json" },
81+
});
82+
}
83+
84+
return new Response("Not Found", { status: 404 });
85+
},
86+
actions: {
87+
getCounts: (c) => {
88+
return {
89+
startCount: c.state.startCount,
90+
sleepCount: c.state.sleepCount,
91+
requestCount: c.state.requestCount,
92+
};
93+
},
94+
},
95+
options: {
96+
sleepTimeout: SLEEP_TIMEOUT,
97+
},
98+
});
99+
100+
export const sleepWithRawWebSocket = actor({
101+
onAuth: () => {},
102+
state: { startCount: 0, sleepCount: 0, connectionCount: 0 },
103+
onStart: (c) => {
104+
c.state.startCount += 1;
105+
},
106+
onStop: (c) => {
107+
c.state.sleepCount += 1;
108+
},
109+
onWebSocket: (c, websocket: UniversalWebSocket, opts) => {
110+
c.state.connectionCount += 1;
111+
c.log.info("websocket connected", {
112+
connectionCount: c.state.connectionCount,
113+
});
114+
115+
websocket.send(
116+
JSON.stringify({
117+
type: "connected",
118+
connectionCount: c.state.connectionCount,
119+
}),
120+
);
121+
122+
websocket.addEventListener("message", (event: any) => {
123+
const data = event.data;
124+
if (typeof data === "string") {
125+
try {
126+
const parsed = JSON.parse(data);
127+
if (parsed.type === "getCounts") {
128+
websocket.send(
129+
JSON.stringify({
130+
type: "counts",
131+
startCount: c.state.startCount,
132+
sleepCount: c.state.sleepCount,
133+
connectionCount: c.state.connectionCount,
134+
}),
135+
);
136+
} else if (parsed.type === "keepAlive") {
137+
// Just acknowledge to keep connection alive
138+
websocket.send(JSON.stringify({ type: "ack" }));
139+
}
140+
} catch {
141+
// Echo non-JSON messages
142+
websocket.send(data);
143+
}
144+
}
145+
});
146+
147+
websocket.addEventListener("close", () => {
148+
c.state.connectionCount -= 1;
149+
c.log.info("websocket disconnected", {
150+
connectionCount: c.state.connectionCount,
151+
});
152+
});
153+
},
154+
actions: {
155+
getCounts: (c) => {
156+
return {
157+
startCount: c.state.startCount,
158+
sleepCount: c.state.sleepCount,
159+
connectionCount: c.state.connectionCount,
160+
};
161+
},
162+
},
163+
options: {
164+
sleepTimeout: SLEEP_TIMEOUT,
165+
},
166+
});
167+
168+
export const sleepWithNoSleepOption = actor({
169+
onAuth: () => {},
170+
state: { startCount: 0, sleepCount: 0 },
171+
onStart: (c) => {
172+
c.state.startCount += 1;
173+
},
174+
onStop: (c) => {
175+
c.state.sleepCount += 1;
176+
},
177+
actions: {
178+
getCounts: (c) => {
179+
return { startCount: c.state.startCount, sleepCount: c.state.sleepCount };
180+
},
181+
},
182+
options: {
183+
sleepTimeout: SLEEP_TIMEOUT,
184+
noSleep: true,
185+
},
186+
});

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
"on-change": "^5.0.1",
168168
"p-retry": "^6.2.1",
169169
"zod": "^3.25.76",
170-
"@rivetkit/engine-runner": "https://pkg.pr.new/rivet-gg/rivet/@rivetkit/engine-runner@f1c054d"
170+
"@rivetkit/engine-runner": "https://pkg.pr.new/rivet-gg/engine/@rivetkit/engine-runner@664a377"
171171
},
172172
"devDependencies": {
173173
"@hono/node-server": "^1.18.2",

packages/core/src/actor/action.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,15 @@ export class ActionContext<
161161
runInBackground(promise: Promise<void>): void {
162162
this.#actorContext.runInBackground(promise);
163163
}
164+
165+
/**
166+
* Forces the actor to sleep.
167+
*
168+
* Not supported on all drivers.
169+
*
170+
* @experimental
171+
*/
172+
sleep() {
173+
this.#actorContext.sleep();
174+
}
164175
}

0 commit comments

Comments
 (0)