@rivetkit/cloudflare-workers 2.2.1 → 2.3.0-rc.13

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,440 +0,0 @@
1
- import { DurableObject, env } from "cloudflare:workers";
2
- import type { ExecutionContext } from "hono";
3
- import invariant from "invariant";
4
- import type { ActorKey, ActorRouter, Registry, RegistryConfig } from "rivetkit";
5
- import { createActorRouter, createClientWithDriver } from "rivetkit";
6
- import type { ActorDriver, ManagerDriver } from "rivetkit/driver-helpers";
7
- import { getInitialActorKvState } from "rivetkit/driver-helpers";
8
- import type { GetUpgradeWebSocket } from "rivetkit/utils";
9
- import { stringifyError } from "rivetkit/utils";
10
- import {
11
- ActorGlobalState,
12
- CloudflareDurableObjectGlobalState,
13
- createCloudflareActorsActorDriverBuilder,
14
- } from "./actor-driver";
15
- import { buildActorId, parseActorId } from "./actor-id";
16
- import { kvGet, kvPut } from "./actor-kv";
17
- import { GLOBAL_KV_KEYS } from "./global-kv";
18
- import type { Bindings } from "./handler";
19
- import { getCloudflareAmbientEnv } from "./handler";
20
- import { logger } from "./log";
21
- import { CloudflareActorsManagerDriver } from "./manager-driver";
22
-
23
- export interface ActorHandlerInterface extends DurableObject {
24
- create(req: ActorInitRequest): Promise<ActorInitResponse>;
25
- getMetadata(): Promise<
26
- | {
27
- actorId: string;
28
- name: string;
29
- key: ActorKey;
30
- destroying: boolean;
31
- }
32
- | undefined
33
- >;
34
- managerKvGet(key: Uint8Array): Promise<Uint8Array | null>;
35
- }
36
-
37
- export interface ActorInitRequest {
38
- name: string;
39
- key: ActorKey;
40
- input?: unknown;
41
- allowExisting: boolean;
42
- }
43
- export type ActorInitResponse =
44
- | { success: { actorId: string; created: boolean } }
45
- | { error: { actorAlreadyExists: true } };
46
-
47
- export type DurableObjectConstructor = new (
48
- ...args: ConstructorParameters<typeof DurableObject<Bindings>>
49
- ) => DurableObject<Bindings>;
50
-
51
- export function createActorDurableObject(
52
- registry: Registry<any>,
53
- getUpgradeWebSocket: GetUpgradeWebSocket,
54
- ): DurableObjectConstructor {
55
- const globalState = new CloudflareDurableObjectGlobalState();
56
- const parsedConfig = registry.parseConfig();
57
-
58
- /**
59
- * Startup steps:
60
- * 1. If not already created call `initialize`, otherwise check KV to ensure it's initialized
61
- * 2. Load actor
62
- * 3. Start service requests
63
- */
64
- return class ActorHandler
65
- extends DurableObject<Bindings>
66
- implements ActorHandlerInterface
67
- {
68
- /**
69
- * This holds a strong reference to ActorGlobalState.
70
- * CloudflareDurableObjectGlobalState holds a weak reference so we can
71
- * access it elsewhere.
72
- **/
73
- #state: ActorGlobalState;
74
-
75
- constructor(
76
- ...args: ConstructorParameters<typeof DurableObject<Bindings>>
77
- ) {
78
- super(...args);
79
-
80
- // Initialize SQL table for key-value storage
81
- //
82
- // We do this instead of using the native KV storage so we can store blob keys. The native CF KV API only supports string keys.
83
- this.ctx.storage.sql.exec(`
84
- CREATE TABLE IF NOT EXISTS _rivetkit_kv_storage(
85
- key BLOB PRIMARY KEY,
86
- value BLOB
87
- );
88
- `);
89
-
90
- // Initialize SQL table for actor metadata
91
- //
92
- // id always equals 1 in order to ensure that there's always exactly 1 row in this table
93
- this.ctx.storage.sql.exec(`
94
- CREATE TABLE IF NOT EXISTS _rivetkit_metadata(
95
- id INTEGER PRIMARY KEY CHECK (id = 1),
96
- name TEXT NOT NULL,
97
- key TEXT NOT NULL,
98
- destroyed INTEGER DEFAULT 0,
99
- generation INTEGER DEFAULT 0
100
- );
101
- `);
102
-
103
- // Get or create the actor state from the global WeakMap
104
- const state = globalState.getActorState(this.ctx);
105
- if (state) {
106
- this.#state = state;
107
- } else {
108
- this.#state = new ActorGlobalState();
109
- globalState.setActorState(this.ctx, this.#state);
110
- }
111
- }
112
-
113
- async #loadActor() {
114
- invariant(this.#state, "State should be initialized");
115
-
116
- // Check if initialized
117
- if (!this.#state.initialized) {
118
- // Query SQL for initialization data
119
- const cursor = this.ctx.storage.sql.exec(
120
- "SELECT name, key, destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
121
- );
122
- const result = cursor.raw().next();
123
-
124
- if (!result.done && result.value) {
125
- const name = result.value[0] as string;
126
- const key = JSON.parse(
127
- result.value[1] as string,
128
- ) as ActorKey;
129
- const destroyed = result.value[2] as number;
130
- const generation = result.value[3] as number;
131
-
132
- // Only initialize if not destroyed
133
- if (!destroyed) {
134
- logger().debug({
135
- msg: "already initialized",
136
- name,
137
- key,
138
- generation,
139
- });
140
-
141
- this.#state.initialized = { name, key, generation };
142
- } else {
143
- logger().debug("actor is destroyed, cannot load");
144
- throw new Error("Actor is destroyed");
145
- }
146
- } else {
147
- logger().debug("not initialized");
148
- throw new Error("Actor is not initialized");
149
- }
150
- }
151
-
152
- // Check if already loaded
153
- if (this.#state.actor) {
154
- // Assert that the cached actor has the correct generation
155
- // This will catch any cases where #state.actor has a stale generation
156
- invariant(
157
- !this.#state.initialized ||
158
- this.#state.actor.generation ===
159
- this.#state.initialized.generation,
160
- `Stale actor cached: actor generation ${this.#state.actor.generation} != initialized generation ${this.#state.initialized?.generation}. This should not happen.`,
161
- );
162
- return this.#state.actor;
163
- }
164
-
165
- if (!this.#state.initialized) throw new Error("Not initialized");
166
-
167
- // Register DO with global state first
168
- // HACK: This leaks the DO context, but DO does not provide a native way
169
- // of knowing when the DO shuts down. We're making a broad assumption
170
- // that DO will boot a new isolate frequenlty enough that this is not an issue.
171
- const actorId = this.ctx.id.toString();
172
- globalState.setDOState(actorId, { ctx: this.ctx, env: env });
173
-
174
- // Create manager driver
175
- const managerDriver = new CloudflareActorsManagerDriver();
176
-
177
- // Create inline client
178
- // Avoid expensive type expansion in downstream DTS generation.
179
- const inlineClient: any = (createClientWithDriver as any)(
180
- managerDriver,
181
- );
182
-
183
- // Create actor driver builder
184
- const actorDriverBuilder =
185
- createCloudflareActorsActorDriverBuilder(globalState);
186
-
187
- // Create actor driver
188
- const actorDriver = actorDriverBuilder(
189
- parsedConfig,
190
- managerDriver,
191
- inlineClient,
192
- );
193
-
194
- // Create actor router
195
- const actorRouter = createActorRouter(
196
- parsedConfig,
197
- actorDriver,
198
- getUpgradeWebSocket,
199
- registry.config.test?.enabled ?? false,
200
- );
201
-
202
- // Save actor with generation
203
- this.#state.actor = {
204
- actorRouter,
205
- actorDriver,
206
- generation: this.#state.initialized.generation,
207
- };
208
-
209
- // Build actor ID with generation for loading
210
- const actorIdWithGen = buildActorId(
211
- actorId,
212
- this.#state.initialized.generation,
213
- );
214
-
215
- // Initialize the actor instance with proper metadata
216
- // This ensures the actor driver knows about this actor
217
- await actorDriver.loadActor(actorIdWithGen);
218
-
219
- return this.#state.actor;
220
- }
221
-
222
- /** RPC called to get actor metadata without creating it */
223
- async getMetadata(): Promise<
224
- | {
225
- actorId: string;
226
- name: string;
227
- key: ActorKey;
228
- destroying: boolean;
229
- }
230
- | undefined
231
- > {
232
- // Query the metadata
233
- const cursor = this.ctx.storage.sql.exec(
234
- "SELECT name, key, destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
235
- );
236
- const result = cursor.raw().next();
237
-
238
- if (!result.done && result.value) {
239
- const name = result.value[0] as string;
240
- const key = JSON.parse(result.value[1] as string) as ActorKey;
241
- const destroyed = result.value[2] as number;
242
- const generation = result.value[3] as number;
243
-
244
- // Check if destroyed
245
- if (destroyed) {
246
- logger().debug({
247
- msg: "getMetadata: actor is destroyed",
248
- name,
249
- key,
250
- generation,
251
- });
252
- return undefined;
253
- }
254
-
255
- // Build actor ID with generation
256
- const doId = this.ctx.id.toString();
257
- const actorId = buildActorId(doId, generation);
258
- const destroying =
259
- globalState.getActorState(this.ctx)?.destroying ?? false;
260
-
261
- logger().debug({
262
- msg: "getMetadata: found actor metadata",
263
- actorId,
264
- name,
265
- key,
266
- generation,
267
- destroying,
268
- });
269
-
270
- return { actorId, name, key, destroying };
271
- }
272
-
273
- logger().debug({
274
- msg: "getMetadata: no metadata found",
275
- });
276
- return undefined;
277
- }
278
-
279
- /** RPC called by ManagerDriver.kvGet to read from KV. */
280
- async managerKvGet(key: Uint8Array): Promise<Uint8Array | null> {
281
- return kvGet(this.ctx.storage.sql, key);
282
- }
283
-
284
- /** RPC called by the manager to create a DO. Can optionally allow existing actors. */
285
- async create(req: ActorInitRequest): Promise<ActorInitResponse> {
286
- // Check if actor exists
287
- const checkCursor = this.ctx.storage.sql.exec(
288
- "SELECT destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
289
- );
290
- const checkResult = checkCursor.raw().next();
291
-
292
- let created = false;
293
- let generation = 0;
294
-
295
- if (!checkResult.done && checkResult.value) {
296
- const destroyed = checkResult.value[0] as number;
297
- generation = checkResult.value[1] as number;
298
-
299
- if (!destroyed) {
300
- // Actor exists and is not destroyed
301
- if (!req.allowExisting) {
302
- // Fail if not allowing existing actors
303
- logger().debug({
304
- msg: "create failed: actor already exists",
305
- name: req.name,
306
- key: req.key,
307
- generation,
308
- });
309
- return { error: { actorAlreadyExists: true } };
310
- }
311
-
312
- // Return existing actor
313
- logger().debug({
314
- msg: "actor already exists",
315
- key: req.key,
316
- generation,
317
- });
318
- const doId = this.ctx.id.toString();
319
- const actorId = buildActorId(doId, generation);
320
- return { success: { actorId, created: false } };
321
- }
322
-
323
- // Actor exists but is destroyed - resurrect with incremented generation
324
- generation = generation + 1;
325
- created = true;
326
-
327
- // Clear stale actor from previous generation
328
- // This is necessary because the DO instance may still be in memory
329
- // with the old #state.actor field from before the destroy
330
- if (this.#state) {
331
- this.#state.actor = undefined;
332
- }
333
-
334
- logger().debug({
335
- msg: "resurrecting destroyed actor",
336
- key: req.key,
337
- oldGeneration: generation - 1,
338
- newGeneration: generation,
339
- });
340
- } else {
341
- // No actor exists - will create with generation 0
342
- generation = 0;
343
- created = true;
344
- logger().debug({
345
- msg: "creating new actor",
346
- key: req.key,
347
- generation,
348
- });
349
- }
350
-
351
- // Perform upsert - either inserts new or updates destroyed actor
352
- this.ctx.storage.sql.exec(
353
- `INSERT INTO _rivetkit_metadata (id, name, key, destroyed, generation)
354
- VALUES (1, ?, ?, 0, ?)
355
- ON CONFLICT(id) DO UPDATE SET
356
- name = excluded.name,
357
- key = excluded.key,
358
- destroyed = 0,
359
- generation = excluded.generation`,
360
- req.name,
361
- JSON.stringify(req.key),
362
- generation,
363
- );
364
-
365
- this.#state.initialized = {
366
- name: req.name,
367
- key: req.key,
368
- generation,
369
- };
370
-
371
- // Build actor ID with generation
372
- const doId = this.ctx.id.toString();
373
- const actorId = buildActorId(doId, generation);
374
-
375
- // Initialize storage and update KV when created or resurrected
376
- if (created) {
377
- // Initialize persist data in KV storage
378
- initializeActorKvStorage(this.ctx.storage.sql, req.input);
379
-
380
- // Update metadata in the background
381
- const env = getCloudflareAmbientEnv();
382
- const actorData = { name: req.name, key: req.key, generation };
383
- this.ctx.waitUntil(
384
- env.ACTOR_KV.put(
385
- GLOBAL_KV_KEYS.actorMetadata(actorId),
386
- JSON.stringify(actorData),
387
- ),
388
- );
389
- }
390
-
391
- // Preemptively load actor so the lifecycle hooks are called
392
- await this.#loadActor();
393
-
394
- logger().debug({
395
- msg: created
396
- ? "actor created/resurrected"
397
- : "returning existing actor",
398
- actorId,
399
- created,
400
- generation,
401
- });
402
-
403
- return { success: { actorId, created } };
404
- }
405
-
406
- async fetch(request: Request): Promise<Response> {
407
- const { actorRouter, generation } = await this.#loadActor();
408
-
409
- // Build actor ID with generation
410
- const doId = this.ctx.id.toString();
411
- const actorId = buildActorId(doId, generation);
412
-
413
- return await actorRouter.fetch(request, {
414
- actorId,
415
- });
416
- }
417
-
418
- async alarm(): Promise<void> {
419
- const { actorDriver, generation } = await this.#loadActor();
420
-
421
- // Build actor ID with generation
422
- const doId = this.ctx.id.toString();
423
- const actorId = buildActorId(doId, generation);
424
-
425
- // Load the actor instance and trigger alarm
426
- const actor = await actorDriver.loadActor(actorId);
427
- await actor.onAlarm();
428
- }
429
- };
430
- }
431
-
432
- function initializeActorKvStorage(
433
- sql: SqlStorage,
434
- input: unknown | undefined,
435
- ): void {
436
- const initialKvState = getInitialActorKvState(input);
437
- for (const [key, value] of initialKvState) {
438
- kvPut(sql, key, value);
439
- }
440
- }
package/src/actor-id.ts DELETED
@@ -1,38 +0,0 @@
1
- /**
2
- * Actor ID utilities for managing actor IDs with generation tracking.
3
- *
4
- * Actor IDs are formatted as: `{doId}:{generation}`
5
- * This allows tracking actor resurrection and preventing stale references.
6
- */
7
-
8
- /**
9
- * Build an actor ID from a Durable Object ID and generation number.
10
- * @param doId The Durable Object ID
11
- * @param generation The generation number (increments on resurrection)
12
- * @returns The formatted actor ID
13
- */
14
- export function buildActorId(doId: string, generation: number): string {
15
- return `${doId}:${generation}`;
16
- }
17
-
18
- /**
19
- * Parse an actor ID into its components.
20
- * @param actorId The actor ID to parse
21
- * @returns A tuple of [doId, generation]
22
- * @throws Error if the actor ID format is invalid
23
- */
24
- export function parseActorId(actorId: string): [string, number] {
25
- const parts = actorId.split(":");
26
- if (parts.length !== 2) {
27
- throw new Error(`Invalid actor ID format: ${actorId}`);
28
- }
29
-
30
- const [doId, generationStr] = parts;
31
- const generation = parseInt(generationStr, 10);
32
-
33
- if (Number.isNaN(generation)) {
34
- throw new Error(`Invalid generation number in actor ID: ${actorId}`);
35
- }
36
-
37
- return [doId, generation];
38
- }
package/src/actor-kv.ts DELETED
@@ -1,120 +0,0 @@
1
- const DEFAULT_LIST_LIMIT = 16_384;
2
-
3
- export function kvGet(sql: SqlStorage, key: Uint8Array): Uint8Array | null {
4
- const cursor = sql.exec(
5
- "SELECT value FROM _rivetkit_kv_storage WHERE key = ?",
6
- key,
7
- );
8
- const result = cursor.raw().next();
9
-
10
- if (!result.done && result.value) {
11
- return toUint8Array(result.value[0]);
12
- }
13
- return null;
14
- }
15
-
16
- export function kvPut(
17
- sql: SqlStorage,
18
- key: Uint8Array,
19
- value: Uint8Array,
20
- ): void {
21
- sql.exec(
22
- "INSERT OR REPLACE INTO _rivetkit_kv_storage (key, value) VALUES (?, ?)",
23
- key,
24
- value,
25
- );
26
- }
27
-
28
- export function kvDelete(sql: SqlStorage, key: Uint8Array): void {
29
- sql.exec("DELETE FROM _rivetkit_kv_storage WHERE key = ?", key);
30
- }
31
-
32
- export function kvDeleteRange(
33
- sql: SqlStorage,
34
- start: Uint8Array,
35
- end: Uint8Array,
36
- ): void {
37
- sql.exec(
38
- "DELETE FROM _rivetkit_kv_storage WHERE key >= ? AND key < ?",
39
- start,
40
- end,
41
- );
42
- }
43
-
44
- export function kvListPrefix(
45
- sql: SqlStorage,
46
- prefix: Uint8Array,
47
- options?: {
48
- reverse?: boolean;
49
- limit?: number;
50
- },
51
- ): [Uint8Array, Uint8Array][] {
52
- const upperBound = computePrefixUpperBound(prefix);
53
- if (upperBound) {
54
- return kvListRange(sql, prefix, upperBound, options);
55
- }
56
-
57
- const direction = options?.reverse ? "DESC" : "ASC";
58
- const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
59
- const cursor = sql.exec(
60
- `SELECT key, value FROM _rivetkit_kv_storage WHERE key >= ? ORDER BY key ${direction} LIMIT ?`,
61
- prefix,
62
- limit,
63
- );
64
- return readEntries(cursor);
65
- }
66
-
67
- export function kvListRange(
68
- sql: SqlStorage,
69
- start: Uint8Array,
70
- end: Uint8Array,
71
- options?: {
72
- reverse?: boolean;
73
- limit?: number;
74
- },
75
- ): [Uint8Array, Uint8Array][] {
76
- const direction = options?.reverse ? "DESC" : "ASC";
77
- const limit = options?.limit ?? DEFAULT_LIST_LIMIT;
78
- const cursor = sql.exec(
79
- `SELECT key, value FROM _rivetkit_kv_storage WHERE key >= ? AND key < ? ORDER BY key ${direction} LIMIT ?`,
80
- start,
81
- end,
82
- limit,
83
- );
84
- return readEntries(cursor);
85
- }
86
-
87
- function toUint8Array(
88
- value: string | number | ArrayBuffer | Uint8Array | null,
89
- ): Uint8Array {
90
- if (value instanceof Uint8Array) {
91
- return value;
92
- }
93
- if (value instanceof ArrayBuffer) {
94
- return new Uint8Array(value);
95
- }
96
- throw new Error(
97
- `Unexpected SQL value type: ${typeof value} (${value?.constructor?.name})`,
98
- );
99
- }
100
-
101
- function readEntries(
102
- cursor: ReturnType<SqlStorage["exec"]>,
103
- ): [Uint8Array, Uint8Array][] {
104
- const entries: [Uint8Array, Uint8Array][] = [];
105
- for (const row of cursor.raw()) {
106
- entries.push([toUint8Array(row[0]), toUint8Array(row[1])]);
107
- }
108
- return entries;
109
- }
110
-
111
- function computePrefixUpperBound(prefix: Uint8Array): Uint8Array | null {
112
- const upperBound = prefix.slice();
113
- for (let i = upperBound.length - 1; i >= 0; i--) {
114
- if (upperBound[i] !== 0xff) {
115
- upperBound[i]++;
116
- return upperBound.slice(0, i + 1);
117
- }
118
- }
119
- return null;
120
- }
package/src/config.ts DELETED
@@ -1,22 +0,0 @@
1
- import type { Client } from "rivetkit";
2
- import { z } from "zod/v4";
3
-
4
- const ConfigSchemaBase = z.object({
5
- /** Path that the Rivet manager API will be mounted. */
6
- managerPath: z.string().optional().default("/api/rivet"),
7
-
8
- /** Deprecated. Runner key for authentication. */
9
- runnerKey: z.string().optional(),
10
-
11
- /** Disable the welcome message. */
12
- noWelcome: z.boolean().optional().default(false),
13
-
14
- fetch: z
15
- .custom<ExportedHandlerFetchHandler<{ RIVET: Client<any> }, unknown>>()
16
- .optional(),
17
- });
18
- export const ConfigSchema = ConfigSchemaBase.default(() =>
19
- ConfigSchemaBase.parse({}),
20
- );
21
- export type InputConfig = z.input<typeof ConfigSchema>;
22
- export type Config = z.infer<typeof ConfigSchema>;
package/src/global-kv.ts DELETED
@@ -1,6 +0,0 @@
1
- /** KV keys for using Workers KV to store actor metadata globally. */
2
- export const GLOBAL_KV_KEYS = {
3
- actorMetadata: (actorId: string): string => {
4
- return `actor:${actorId}:metadata`;
5
- },
6
- };