@rivetkit/cloudflare-workers 2.0.2 → 2.0.4-rc.1
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/README.md +3 -3
- package/dist/mod.cjs +793 -366
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +70 -161
- package/dist/mod.d.ts +70 -161
- package/dist/mod.js +804 -377
- package/dist/mod.js.map +1 -1
- package/package.json +9 -5
- package/src/actor-driver.ts +227 -76
- package/src/actor-handler-do.ts +338 -113
- package/src/actor-id.ts +38 -0
- package/src/actor-kv.ts +71 -0
- package/src/config.ts +22 -10
- package/src/global-kv.ts +6 -0
- package/src/handler.ts +103 -52
- package/src/log.ts +2 -4
- package/src/manager-driver.ts +237 -134
- package/src/mod.ts +9 -1
- package/src/websocket.ts +17 -6
package/src/actor-driver.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
+
import invariant from "invariant";
|
|
1
2
|
import type {
|
|
3
|
+
ActorKey,
|
|
4
|
+
ActorRouter,
|
|
2
5
|
AnyActorInstance as CoreAnyActorInstance,
|
|
3
6
|
RegistryConfig,
|
|
4
|
-
|
|
5
|
-
} from "
|
|
6
|
-
import {
|
|
7
|
-
createGenericConnDrivers,
|
|
8
|
-
GenericConnGlobalState,
|
|
9
|
-
lookupInRegistry,
|
|
10
|
-
} from "@rivetkit/core";
|
|
11
|
-
import type { Client } from "@rivetkit/core/client";
|
|
7
|
+
} from "rivetkit";
|
|
8
|
+
import { lookupInRegistry } from "rivetkit";
|
|
9
|
+
import type { Client } from "rivetkit/client";
|
|
12
10
|
import type {
|
|
13
11
|
ActorDriver,
|
|
14
12
|
AnyActorInstance,
|
|
15
13
|
ManagerDriver,
|
|
16
|
-
} from "
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
14
|
+
} from "rivetkit/driver-helpers";
|
|
15
|
+
import { promiseWithResolvers } from "rivetkit/utils";
|
|
16
|
+
import { logger } from "./log";
|
|
17
|
+
import { kvDelete, kvGet, kvListPrefix, kvPut } from "./actor-kv";
|
|
18
|
+
import { GLOBAL_KV_KEYS } from "./global-kv";
|
|
19
|
+
import { getCloudflareAmbientEnv } from "./handler";
|
|
20
|
+
import { parseActorId } from "./actor-id";
|
|
19
21
|
|
|
20
22
|
interface DurableObjectGlobalState {
|
|
21
23
|
ctx: DurableObjectState;
|
|
@@ -28,17 +30,31 @@ interface DurableObjectGlobalState {
|
|
|
28
30
|
* This allows for storing the actor context globally and looking it up by ID in `CloudflareActorsActorDriver`.
|
|
29
31
|
*/
|
|
30
32
|
export class CloudflareDurableObjectGlobalState {
|
|
31
|
-
//
|
|
33
|
+
// Map of actor ID -> DO state
|
|
32
34
|
#dos: Map<string, DurableObjectGlobalState> = new Map();
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
// WeakMap of DO state -> ActorGlobalState for proper GC
|
|
37
|
+
#actors: WeakMap<DurableObjectState, ActorGlobalState> = new WeakMap();
|
|
38
|
+
|
|
39
|
+
getDOState(doId: string): DurableObjectGlobalState {
|
|
40
|
+
const state = this.#dos.get(doId);
|
|
41
|
+
invariant(
|
|
42
|
+
state !== undefined,
|
|
43
|
+
"durable object state not in global state",
|
|
44
|
+
);
|
|
37
45
|
return state;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
setDOState(
|
|
41
|
-
this.#dos.set(
|
|
48
|
+
setDOState(doId: string, state: DurableObjectGlobalState) {
|
|
49
|
+
this.#dos.set(doId, state);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getActorState(ctx: DurableObjectState): ActorGlobalState | undefined {
|
|
53
|
+
return this.#actors.get(ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setActorState(ctx: DurableObjectState, actorState: ActorGlobalState): void {
|
|
57
|
+
this.#actors.set(ctx, actorState);
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
|
|
@@ -46,79 +62,136 @@ export interface DriverContext {
|
|
|
46
62
|
state: DurableObjectState;
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
interface InitializedData {
|
|
66
|
+
name: string;
|
|
67
|
+
key: ActorKey;
|
|
68
|
+
generation: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface LoadedActor {
|
|
72
|
+
actorRouter: ActorRouter;
|
|
73
|
+
actorDriver: ActorDriver;
|
|
74
|
+
generation: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Actor global state to track running instances
|
|
78
|
+
export class ActorGlobalState {
|
|
79
|
+
// Initialization state
|
|
80
|
+
initialized?: InitializedData;
|
|
81
|
+
|
|
82
|
+
// Loaded actor state
|
|
83
|
+
actor?: LoadedActor;
|
|
84
|
+
actorInstance?: AnyActorInstance;
|
|
85
|
+
actorPromise?: ReturnType<typeof promiseWithResolvers<void>>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Indicates if `startDestroy` has been called.
|
|
89
|
+
*
|
|
90
|
+
* This is stored in memory instead of SQLite since the destroy may be cancelled.
|
|
91
|
+
*
|
|
92
|
+
* See the corresponding `destroyed` property in SQLite metadata.
|
|
93
|
+
*/
|
|
94
|
+
destroying: boolean = false;
|
|
95
|
+
|
|
96
|
+
reset() {
|
|
97
|
+
this.initialized = undefined;
|
|
98
|
+
this.actor = undefined;
|
|
99
|
+
this.actorInstance = undefined;
|
|
100
|
+
this.actorPromise = undefined;
|
|
101
|
+
this.destroying = false;
|
|
102
|
+
}
|
|
54
103
|
}
|
|
55
104
|
|
|
56
105
|
export class CloudflareActorsActorDriver implements ActorDriver {
|
|
57
106
|
#registryConfig: RegistryConfig;
|
|
58
|
-
#runConfig: RunConfig;
|
|
59
107
|
#managerDriver: ManagerDriver;
|
|
60
108
|
#inlineClient: Client<any>;
|
|
61
109
|
#globalState: CloudflareDurableObjectGlobalState;
|
|
62
|
-
#actors: Map<string, ActorHandler> = new Map();
|
|
63
110
|
|
|
64
111
|
constructor(
|
|
65
112
|
registryConfig: RegistryConfig,
|
|
66
|
-
runConfig: RunConfig,
|
|
67
113
|
managerDriver: ManagerDriver,
|
|
68
114
|
inlineClient: Client<any>,
|
|
69
115
|
globalState: CloudflareDurableObjectGlobalState,
|
|
70
116
|
) {
|
|
71
117
|
this.#registryConfig = registryConfig;
|
|
72
|
-
this.#runConfig = runConfig;
|
|
73
118
|
this.#managerDriver = managerDriver;
|
|
74
119
|
this.#inlineClient = inlineClient;
|
|
75
120
|
this.#globalState = globalState;
|
|
76
121
|
}
|
|
77
122
|
|
|
78
123
|
#getDOCtx(actorId: string) {
|
|
79
|
-
|
|
124
|
+
// Parse actor ID to get DO ID
|
|
125
|
+
const [doId] = parseActorId(actorId);
|
|
126
|
+
return this.#globalState.getDOState(doId).ctx;
|
|
80
127
|
}
|
|
81
128
|
|
|
82
129
|
async loadActor(actorId: string): Promise<AnyActorInstance> {
|
|
130
|
+
// Parse actor ID to get DO ID and generation
|
|
131
|
+
const [doId, expectedGeneration] = parseActorId(actorId);
|
|
132
|
+
|
|
133
|
+
// Get the DO state
|
|
134
|
+
const doState = this.#globalState.getDOState(doId);
|
|
135
|
+
|
|
83
136
|
// Check if actor is already loaded
|
|
84
|
-
let
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return handler.actor;
|
|
137
|
+
let actorState = this.#globalState.getActorState(doState.ctx);
|
|
138
|
+
if (actorState?.actorInstance) {
|
|
139
|
+
// Actor is already loaded, return it
|
|
140
|
+
return actorState.actorInstance;
|
|
89
141
|
}
|
|
90
142
|
|
|
91
|
-
// Create new actor
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
// Create new actor state if it doesn't exist
|
|
144
|
+
if (!actorState) {
|
|
145
|
+
actorState = new ActorGlobalState();
|
|
146
|
+
actorState.actorPromise = promiseWithResolvers((reason) => logger().warn({ msg: "unhandled actor promise rejection", reason }));
|
|
147
|
+
this.#globalState.setActorState(doState.ctx, actorState);
|
|
148
|
+
} else if (actorState.actorPromise) {
|
|
149
|
+
// Another request is already loading this actor, wait for it
|
|
150
|
+
await actorState.actorPromise.promise;
|
|
151
|
+
if (!actorState.actorInstance) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Actor ${actorId} failed to load in concurrent request`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return actorState.actorInstance;
|
|
157
|
+
}
|
|
98
158
|
|
|
99
159
|
// Load actor metadata
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
160
|
+
const sql = doState.ctx.storage.sql;
|
|
161
|
+
const cursor = sql.exec(
|
|
162
|
+
"SELECT name, key, destroyed, generation FROM _rivetkit_metadata LIMIT 1",
|
|
163
|
+
);
|
|
164
|
+
const result = cursor.raw().next();
|
|
165
|
+
|
|
166
|
+
if (result.done || !result.value) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Actor ${actorId} is not initialized - missing metadata`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const name = result.value[0] as string;
|
|
173
|
+
const key = JSON.parse(result.value[1] as string) as string[];
|
|
174
|
+
const destroyed = result.value[2] as number;
|
|
175
|
+
const generation = result.value[3] as number;
|
|
104
176
|
|
|
105
|
-
if
|
|
106
|
-
|
|
177
|
+
// Check if actor is destroyed
|
|
178
|
+
if (destroyed) {
|
|
179
|
+
throw new Error(`Actor ${actorId} is destroyed`);
|
|
107
180
|
}
|
|
108
|
-
|
|
109
|
-
|
|
181
|
+
|
|
182
|
+
// Check if generation matches
|
|
183
|
+
if (generation !== expectedGeneration) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Actor ${actorId} generation mismatch - expected ${expectedGeneration}, got ${generation}`,
|
|
186
|
+
);
|
|
110
187
|
}
|
|
111
188
|
|
|
112
189
|
// Create actor instance
|
|
113
190
|
const definition = lookupInRegistry(this.#registryConfig, name);
|
|
114
|
-
|
|
191
|
+
actorState.actorInstance = definition.instantiate();
|
|
115
192
|
|
|
116
193
|
// Start actor
|
|
117
|
-
|
|
118
|
-
handler.genericConnGlobalState,
|
|
119
|
-
);
|
|
120
|
-
await handler.actor.start(
|
|
121
|
-
connDrivers,
|
|
194
|
+
await actorState.actorInstance.start(
|
|
122
195
|
this,
|
|
123
196
|
this.#inlineClient,
|
|
124
197
|
actorId,
|
|
@@ -128,39 +201,119 @@ export class CloudflareActorsActorDriver implements ActorDriver {
|
|
|
128
201
|
);
|
|
129
202
|
|
|
130
203
|
// Finish
|
|
131
|
-
|
|
132
|
-
|
|
204
|
+
actorState.actorPromise?.resolve();
|
|
205
|
+
actorState.actorPromise = undefined;
|
|
206
|
+
|
|
207
|
+
return actorState.actorInstance;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getContext(actorId: string): DriverContext {
|
|
211
|
+
// Parse actor ID to get DO ID
|
|
212
|
+
const [doId] = parseActorId(actorId);
|
|
213
|
+
const state = this.#globalState.getDOState(doId);
|
|
214
|
+
return { state: state.ctx };
|
|
215
|
+
}
|
|
133
216
|
|
|
134
|
-
|
|
217
|
+
async setAlarm(actor: AnyActorInstance, timestamp: number): Promise<void> {
|
|
218
|
+
await this.#getDOCtx(actor.id).storage.setAlarm(timestamp);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getDatabase(actorId: string): Promise<unknown | undefined> {
|
|
222
|
+
return this.#getDOCtx(actorId).storage.sql;
|
|
135
223
|
}
|
|
136
224
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
// Batch KV operations
|
|
226
|
+
async kvBatchPut(
|
|
227
|
+
actorId: string,
|
|
228
|
+
entries: [Uint8Array, Uint8Array][],
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const sql = this.#getDOCtx(actorId).storage.sql;
|
|
231
|
+
|
|
232
|
+
for (const [key, value] of entries) {
|
|
233
|
+
kvPut(sql, key, value);
|
|
141
234
|
}
|
|
142
|
-
return handler.genericConnGlobalState;
|
|
143
235
|
}
|
|
144
236
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
237
|
+
async kvBatchGet(
|
|
238
|
+
actorId: string,
|
|
239
|
+
keys: Uint8Array[],
|
|
240
|
+
): Promise<(Uint8Array | null)[]> {
|
|
241
|
+
const sql = this.#getDOCtx(actorId).storage.sql;
|
|
242
|
+
|
|
243
|
+
const results: (Uint8Array | null)[] = [];
|
|
244
|
+
for (const key of keys) {
|
|
245
|
+
results.push(kvGet(sql, key));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
148
249
|
}
|
|
149
250
|
|
|
150
|
-
async
|
|
151
|
-
|
|
251
|
+
async kvBatchDelete(actorId: string, keys: Uint8Array[]): Promise<void> {
|
|
252
|
+
const sql = this.#getDOCtx(actorId).storage.sql;
|
|
253
|
+
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
kvDelete(sql, key);
|
|
256
|
+
}
|
|
152
257
|
}
|
|
153
258
|
|
|
154
|
-
async
|
|
155
|
-
|
|
259
|
+
async kvListPrefix(
|
|
260
|
+
actorId: string,
|
|
261
|
+
prefix: Uint8Array,
|
|
262
|
+
): Promise<[Uint8Array, Uint8Array][]> {
|
|
263
|
+
const sql = this.#getDOCtx(actorId).storage.sql;
|
|
264
|
+
|
|
265
|
+
return kvListPrefix(sql, prefix);
|
|
156
266
|
}
|
|
157
267
|
|
|
158
|
-
|
|
159
|
-
|
|
268
|
+
startDestroy(actorId: string): void {
|
|
269
|
+
// Parse actor ID to get DO ID and generation
|
|
270
|
+
const [doId, generation] = parseActorId(actorId);
|
|
271
|
+
|
|
272
|
+
// Get the DO state
|
|
273
|
+
const doState = this.#globalState.getDOState(doId);
|
|
274
|
+
const actorState = this.#globalState.getActorState(doState.ctx);
|
|
275
|
+
|
|
276
|
+
// Actor not loaded, nothing to destroy
|
|
277
|
+
if (!actorState?.actorInstance) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check if already destroying
|
|
282
|
+
if (actorState.destroying) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
actorState.destroying = true;
|
|
286
|
+
|
|
287
|
+
// Spawn onStop in background
|
|
288
|
+
this.#callOnStopAsync(actorId, doId, actorState.actorInstance);
|
|
160
289
|
}
|
|
161
290
|
|
|
162
|
-
async
|
|
163
|
-
|
|
291
|
+
async #callOnStopAsync(
|
|
292
|
+
actorId: string,
|
|
293
|
+
doId: string,
|
|
294
|
+
actor: CoreAnyActorInstance,
|
|
295
|
+
) {
|
|
296
|
+
// Stop
|
|
297
|
+
await actor.onStop("destroy");
|
|
298
|
+
|
|
299
|
+
// Remove state
|
|
300
|
+
const doState = this.#globalState.getDOState(doId);
|
|
301
|
+
const sql = doState.ctx.storage.sql;
|
|
302
|
+
sql.exec("UPDATE _rivetkit_metadata SET destroyed = 1 WHERE 1=1");
|
|
303
|
+
sql.exec("DELETE FROM _rivetkit_kv_storage");
|
|
304
|
+
|
|
305
|
+
// Clear any scheduled alarms
|
|
306
|
+
await doState.ctx.storage.deleteAlarm();
|
|
307
|
+
|
|
308
|
+
// Delete from ACTOR_KV in the background - use full actorId including generation
|
|
309
|
+
const env = getCloudflareAmbientEnv();
|
|
310
|
+
doState.ctx.waitUntil(
|
|
311
|
+
env.ACTOR_KV.delete(GLOBAL_KV_KEYS.actorMetadata(actorId)),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Reset global state using the DO context
|
|
315
|
+
const actorHandle = this.#globalState.getActorState(doState.ctx);
|
|
316
|
+
actorHandle?.reset();
|
|
164
317
|
}
|
|
165
318
|
}
|
|
166
319
|
|
|
@@ -168,14 +321,12 @@ export function createCloudflareActorsActorDriverBuilder(
|
|
|
168
321
|
globalState: CloudflareDurableObjectGlobalState,
|
|
169
322
|
) {
|
|
170
323
|
return (
|
|
171
|
-
|
|
172
|
-
runConfig: RunConfig,
|
|
324
|
+
config: RegistryConfig,
|
|
173
325
|
managerDriver: ManagerDriver,
|
|
174
326
|
inlineClient: Client<any>,
|
|
175
327
|
) => {
|
|
176
328
|
return new CloudflareActorsActorDriver(
|
|
177
|
-
|
|
178
|
-
runConfig,
|
|
329
|
+
config,
|
|
179
330
|
managerDriver,
|
|
180
331
|
inlineClient,
|
|
181
332
|
globalState,
|