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