@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.
@@ -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
- RunConfig,
5
- } from "@rivetkit/core";
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 "@rivetkit/core/driver-helpers";
17
- import invariant from "invariant";
18
- import { KEYS } from "./actor-handler-do";
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
- // Single map for all actor state
33
+ // Map of actor ID -> DO state
32
34
  #dos: Map<string, DurableObjectGlobalState> = new Map();
33
35
 
34
- getDOState(actorId: string): DurableObjectGlobalState {
35
- const state = this.#dos.get(actorId);
36
- invariant(state !== undefined, "durable object state not in global state");
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(actorId: string, state: DurableObjectGlobalState) {
41
- this.#dos.set(actorId, state);
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
- // Actor handler to track running instances
50
- class ActorHandler {
51
- actor?: AnyActorInstance;
52
- actorPromise?: PromiseWithResolvers<void> = Promise.withResolvers();
53
- genericConnGlobalState = new GenericConnGlobalState();
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
- return this.#globalState.getDOState(actorId).ctx;
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 handler = this.#actors.get(actorId);
85
- if (handler) {
86
- if (handler.actorPromise) await handler.actorPromise.promise;
87
- if (!handler.actor) throw new Error("Actor should be loaded");
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 handler
92
- handler = new ActorHandler();
93
- this.#actors.set(actorId, handler);
94
-
95
- // Get the actor metadata from Durable Object storage
96
- const doState = this.#globalState.getDOState(actorId);
97
- const storage = doState.ctx.storage;
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 [name, key] = await Promise.all([
101
- storage.get<string>(KEYS.NAME),
102
- storage.get<string[]>(KEYS.KEY),
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 (!name) {
106
- throw new Error(`Actor ${actorId} is not initialized - missing name`);
177
+ // Check if actor is destroyed
178
+ if (destroyed) {
179
+ throw new Error(`Actor ${actorId} is destroyed`);
107
180
  }
108
- if (!key) {
109
- throw new Error(`Actor ${actorId} is not initialized - missing key`);
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
- handler.actor = definition.instantiate();
191
+ actorState.actorInstance = definition.instantiate();
115
192
 
116
193
  // Start actor
117
- const connDrivers = createGenericConnDrivers(
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
- handler.actorPromise?.resolve();
132
- handler.actorPromise = undefined;
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
- return handler.actor;
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
- getGenericConnGlobalState(actorId: string): GenericConnGlobalState {
138
- const handler = this.#actors.get(actorId);
139
- if (!handler) {
140
- throw new Error(`Actor ${actorId} not loaded`);
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
- getContext(actorId: string): DriverContext {
146
- const state = this.#globalState.getDOState(actorId);
147
- return { state: state.ctx };
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 readPersistedData(actorId: string): Promise<Uint8Array | undefined> {
151
- return await this.#getDOCtx(actorId).storage.get(KEYS.PERSIST_DATA);
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 writePersistedData(actorId: string, data: Uint8Array): Promise<void> {
155
- await this.#getDOCtx(actorId).storage.put(KEYS.PERSIST_DATA, data);
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
- async setAlarm(actor: AnyActorInstance, timestamp: number): Promise<void> {
159
- await this.#getDOCtx(actor.id).storage.setAlarm(timestamp);
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 getDatabase(actorId: string): Promise<unknown | undefined> {
163
- return this.#getDOCtx(actorId).storage.sql;
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
- registryConfig: RegistryConfig,
172
- runConfig: RunConfig,
324
+ config: RegistryConfig,
173
325
  managerDriver: ManagerDriver,
174
326
  inlineClient: Client<any>,
175
327
  ) => {
176
328
  return new CloudflareActorsActorDriver(
177
- registryConfig,
178
- runConfig,
329
+ config,
179
330
  managerDriver,
180
331
  inlineClient,
181
332
  globalState,