@rivetkit/cloudflare-workers 2.0.24-rc.1 → 2.0.25-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.
@@ -7,40 +7,26 @@ import {
7
7
  type GetOrCreateWithKeyInput,
8
8
  type GetWithKeyInput,
9
9
  generateRandomString,
10
+ type ListActorsInput,
10
11
  type ManagerDisplayInformation,
11
12
  type ManagerDriver,
12
13
  WS_PROTOCOL_ACTOR,
13
- WS_PROTOCOL_CONN_ID,
14
14
  WS_PROTOCOL_CONN_PARAMS,
15
- WS_PROTOCOL_CONN_TOKEN,
16
15
  WS_PROTOCOL_ENCODING,
17
16
  WS_PROTOCOL_STANDARD,
18
17
  WS_PROTOCOL_TARGET,
19
18
  } from "rivetkit/driver-helpers";
20
- import { ActorAlreadyExists, InternalError } from "rivetkit/errors";
19
+ import {
20
+ ActorDuplicateKey,
21
+ ActorNotFound,
22
+ InternalError,
23
+ } from "rivetkit/errors";
24
+ import { assertUnreachable } from "rivetkit/utils";
25
+ import { parseActorId } from "./actor-id";
21
26
  import { getCloudflareAmbientEnv } from "./handler";
22
27
  import { logger } from "./log";
23
28
  import type { Bindings } from "./mod";
24
- import { serializeKey, serializeNameAndKey } from "./util";
25
-
26
- // Actor metadata structure
27
- interface ActorData {
28
- name: string;
29
- key: string[];
30
- }
31
-
32
- const KEYS = {
33
- ACTOR: {
34
- // Combined key for actor metadata (name and key)
35
- metadata: (actorId: string) => `actor:${actorId}:metadata`,
36
-
37
- // Key index function for actor lookup
38
- keyIndex: (name: string, key: string[] = []) => {
39
- // Use serializeKey for consistent handling of all keys
40
- return `actor_key:${serializeKey(key)}`;
41
- },
42
- },
43
- };
29
+ import { serializeNameAndKey } from "./util";
44
30
 
45
31
  const STANDARD_WEBSOCKET_HEADERS = [
46
32
  "connection",
@@ -58,14 +44,18 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
58
44
  ): Promise<Response> {
59
45
  const env = getCloudflareAmbientEnv();
60
46
 
47
+ // Parse actor ID to get DO ID
48
+ const [doId] = parseActorId(actorId);
49
+
61
50
  logger().debug({
62
51
  msg: "sending request to durable object",
63
52
  actorId,
53
+ doId,
64
54
  method: actorRequest.method,
65
55
  url: actorRequest.url,
66
56
  });
67
57
 
68
- const id = env.ACTOR_DO.idFromString(actorId);
58
+ const id = env.ACTOR_DO.idFromString(doId);
69
59
  const stub = env.ACTOR_DO.get(id);
70
60
 
71
61
  return await stub.fetch(actorRequest);
@@ -76,37 +66,33 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
76
66
  actorId: string,
77
67
  encoding: Encoding,
78
68
  params: unknown,
79
- connId?: string,
80
- connToken?: string,
81
69
  ): Promise<UniversalWebSocket> {
82
70
  const env = getCloudflareAmbientEnv();
83
71
 
72
+ // Parse actor ID to get DO ID
73
+ const [doId] = parseActorId(actorId);
74
+
84
75
  logger().debug({
85
76
  msg: "opening websocket to durable object",
86
77
  actorId,
78
+ doId,
87
79
  path,
88
80
  });
89
81
 
90
82
  // Make a fetch request to the Durable Object with WebSocket upgrade
91
- const id = env.ACTOR_DO.idFromString(actorId);
83
+ const id = env.ACTOR_DO.idFromString(doId);
92
84
  const stub = env.ACTOR_DO.get(id);
93
85
 
94
86
  const protocols: string[] = [];
95
87
  protocols.push(WS_PROTOCOL_STANDARD);
96
88
  protocols.push(`${WS_PROTOCOL_TARGET}actor`);
97
- protocols.push(`${WS_PROTOCOL_ACTOR}${actorId}`);
89
+ protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
98
90
  protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
99
91
  if (params) {
100
92
  protocols.push(
101
93
  `${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
102
94
  );
103
95
  }
104
- if (connId) {
105
- protocols.push(`${WS_PROTOCOL_CONN_ID}${connId}`);
106
- }
107
- if (connToken) {
108
- protocols.push(`${WS_PROTOCOL_CONN_TOKEN}${connToken}`);
109
- }
110
96
 
111
97
  const headers: Record<string, string> = {
112
98
  Upgrade: "websocket",
@@ -155,14 +141,18 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
155
141
  actorRequest: Request,
156
142
  actorId: string,
157
143
  ): Promise<Response> {
144
+ // Parse actor ID to get DO ID
145
+ const [doId] = parseActorId(actorId);
146
+
158
147
  logger().debug({
159
148
  msg: "forwarding request to durable object",
160
149
  actorId,
150
+ doId,
161
151
  method: actorRequest.method,
162
152
  url: actorRequest.url,
163
153
  });
164
154
 
165
- const id = c.env.ACTOR_DO.idFromString(actorId);
155
+ const id = c.env.ACTOR_DO.idFromString(doId);
166
156
  const stub = c.env.ACTOR_DO.get(id);
167
157
 
168
158
  return await stub.fetch(actorRequest);
@@ -215,7 +205,7 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
215
205
  const protocols: string[] = [];
216
206
  protocols.push(WS_PROTOCOL_STANDARD);
217
207
  protocols.push(`${WS_PROTOCOL_TARGET}actor`);
218
- protocols.push(`${WS_PROTOCOL_ACTOR}${actorId}`);
208
+ protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
219
209
  protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
220
210
  if (params) {
221
211
  protocols.push(
@@ -227,7 +217,9 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
227
217
  protocols.join(", "),
228
218
  );
229
219
 
230
- const id = c.env.ACTOR_DO.idFromString(actorId);
220
+ // Parse actor ID to get DO ID
221
+ const [doId] = parseActorId(actorId);
222
+ const id = c.env.ACTOR_DO.idFromString(doId);
231
223
  const stub = c.env.ACTOR_DO.get(id);
232
224
 
233
225
  return await stub.fetch(actorRequest);
@@ -241,23 +233,42 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
241
233
  > {
242
234
  const env = getCloudflareAmbientEnv();
243
235
 
244
- // Get actor metadata from KV (combined name and key)
245
- const actorData = (await env.ACTOR_KV.get(
246
- KEYS.ACTOR.metadata(actorId),
247
- {
248
- type: "json",
249
- },
250
- )) as ActorData | null;
236
+ // Parse actor ID to get DO ID and expected generation
237
+ const [doId, expectedGeneration] = parseActorId(actorId);
238
+
239
+ // Get the Durable Object stub
240
+ const id = env.ACTOR_DO.idFromString(doId);
241
+ const stub = env.ACTOR_DO.get(id);
242
+
243
+ // Call the DO's getMetadata method
244
+ const result = await stub.getMetadata();
251
245
 
252
- // If the actor doesn't exist, return undefined
253
- if (!actorData) {
246
+ if (!result) {
247
+ logger().debug({
248
+ msg: "getForId: actor not found",
249
+ actorId,
250
+ });
254
251
  return undefined;
255
252
  }
256
253
 
254
+ // Check if the actor IDs match in order to check if the generation matches
255
+ if (result.actorId !== actorId) {
256
+ logger().debug({
257
+ msg: "getForId: generation mismatch",
258
+ requestedActorId: actorId,
259
+ actualActorId: result.actorId,
260
+ });
261
+ return undefined;
262
+ }
263
+
264
+ if (result.destroying) {
265
+ throw new ActorNotFound(actorId);
266
+ }
267
+
257
268
  return {
258
- actorId,
259
- name: actorData.name,
260
- key: actorData.key,
269
+ actorId: result.actorId,
270
+ name: result.name,
271
+ key: result.key,
261
272
  };
262
273
  }
263
274
 
@@ -273,43 +284,79 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
273
284
  logger().debug({ msg: "getWithKey: searching for actor", name, key });
274
285
 
275
286
  // Generate deterministic ID from the name and key
276
- // This is aligned with how createActor generates IDs
277
287
  const nameKeyString = serializeNameAndKey(name, key);
278
- const actorId = env.ACTOR_DO.idFromName(nameKeyString).toString();
288
+ const doId = env.ACTOR_DO.idFromName(nameKeyString).toString();
279
289
 
280
- // Check if the actor metadata exists
281
- const actorData = await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
282
- type: "json",
283
- });
290
+ // Try to get the Durable Object to see if it exists
291
+ const id = env.ACTOR_DO.idFromString(doId);
292
+ const stub = env.ACTOR_DO.get(id);
293
+
294
+ // Check if actor exists without creating it
295
+ const result = await stub.getMetadata();
284
296
 
285
- if (!actorData) {
297
+ if (result) {
298
+ logger().debug({
299
+ msg: "getWithKey: found actor with matching name and key",
300
+ actorId: result.actorId,
301
+ name: result.name,
302
+ key: result.key,
303
+ });
304
+ return {
305
+ actorId: result.actorId,
306
+ name: result.name,
307
+ key: result.key,
308
+ };
309
+ } else {
286
310
  logger().debug({
287
311
  msg: "getWithKey: no actor found with matching name and key",
288
312
  name,
289
313
  key,
290
- actorId,
314
+ doId,
291
315
  });
292
316
  return undefined;
293
317
  }
318
+ }
294
319
 
295
- logger().debug({
296
- msg: "getWithKey: found actor with matching name and key",
297
- actorId,
320
+ async getOrCreateWithKey({
321
+ c,
322
+ name,
323
+ key,
324
+ input,
325
+ }: GetOrCreateWithKeyInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
326
+ const env = getCloudflareAmbientEnv();
327
+
328
+ // Create a deterministic ID from the actor name and key
329
+ // This ensures that actors with the same name and key will have the same ID
330
+ const nameKeyString = serializeNameAndKey(name, key);
331
+ const doId = env.ACTOR_DO.idFromName(nameKeyString);
332
+
333
+ // Get or create actor using the Durable Object's method
334
+ const actor = env.ACTOR_DO.get(doId);
335
+ const result = await actor.create({
298
336
  name,
299
337
  key,
338
+ input,
339
+ allowExisting: true,
300
340
  });
301
- return this.#buildActorOutput(c, actorId);
302
- }
341
+ if ("success" in result) {
342
+ const { actorId, created } = result.success;
343
+ logger().debug({
344
+ msg: "getOrCreateWithKey result",
345
+ actorId,
346
+ name,
347
+ key,
348
+ created,
349
+ });
303
350
 
304
- async getOrCreateWithKey(
305
- input: GetOrCreateWithKeyInput,
306
- ): Promise<ActorOutput> {
307
- // TODO: Prevent race condition here
308
- const getOutput = await this.getWithKey(input);
309
- if (getOutput) {
310
- return getOutput;
351
+ return {
352
+ actorId,
353
+ name,
354
+ key,
355
+ };
356
+ } else if ("error" in result) {
357
+ throw new Error(`Error: ${JSON.stringify(result.error)}`);
311
358
  } else {
312
- return await this.createActor(input);
359
+ assertUnreachable(result);
313
360
  }
314
361
  }
315
362
 
@@ -321,66 +368,46 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
321
368
  }: CreateInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
322
369
  const env = getCloudflareAmbientEnv();
323
370
 
324
- // Check if actor with the same name and key already exists
325
- const existingActor = await this.getWithKey({ c, name, key });
326
- if (existingActor) {
327
- throw new ActorAlreadyExists(name, key);
328
- }
329
-
330
371
  // Create a deterministic ID from the actor name and key
331
372
  // This ensures that actors with the same name and key will have the same ID
332
373
  const nameKeyString = serializeNameAndKey(name, key);
333
374
  const doId = env.ACTOR_DO.idFromName(nameKeyString);
334
- const actorId = doId.toString();
335
375
 
336
- // Init actor
376
+ // Create actor - this will fail if it already exists
337
377
  const actor = env.ACTOR_DO.get(doId);
338
- await actor.initialize({
378
+ const result = await actor.create({
339
379
  name,
340
380
  key,
341
381
  input,
382
+ allowExisting: false,
342
383
  });
343
384
 
344
- // Store combined actor metadata (name and key)
345
- const actorData: ActorData = { name, key };
346
- await env.ACTOR_KV.put(
347
- KEYS.ACTOR.metadata(actorId),
348
- JSON.stringify(actorData),
349
- );
350
-
351
- // Add to key index for lookups by name and key
352
- await env.ACTOR_KV.put(KEYS.ACTOR.keyIndex(name, key), actorId);
353
-
354
- return {
355
- actorId,
356
- name,
357
- key,
358
- };
359
- }
360
-
361
- // Helper method to build actor output from an ID
362
- async #buildActorOutput(
363
- c: any,
364
- actorId: string,
365
- ): Promise<ActorOutput | undefined> {
366
- const env = getCloudflareAmbientEnv();
367
-
368
- const actorData = (await env.ACTOR_KV.get(
369
- KEYS.ACTOR.metadata(actorId),
370
- {
371
- type: "json",
372
- },
373
- )) as ActorData | null;
385
+ if ("success" in result) {
386
+ const { actorId } = result.success;
387
+ return {
388
+ actorId,
389
+ name,
390
+ key,
391
+ };
392
+ } else if ("error" in result) {
393
+ if (result.error.actorAlreadyExists) {
394
+ throw new ActorDuplicateKey(name, key);
395
+ }
374
396
 
375
- if (!actorData) {
376
- return undefined;
397
+ throw new InternalError(
398
+ `Unknown error creating actor: ${JSON.stringify(result.error)}`,
399
+ );
400
+ } else {
401
+ assertUnreachable(result);
377
402
  }
403
+ }
378
404
 
379
- return {
380
- actorId,
381
- name: actorData.name,
382
- key: actorData.key,
383
- };
405
+ async listActors({ c, name }: ListActorsInput): Promise<ActorOutput[]> {
406
+ logger().warn({
407
+ msg: "listActors not fully implemented for Cloudflare Workers",
408
+ name,
409
+ });
410
+ return [];
384
411
  }
385
412
 
386
413
  displayInformation(): ManagerDisplayInformation {
package/src/mod.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export type { Client } from "rivetkit";
2
2
  export type { DriverContext } from "./actor-driver";
3
+ export { createActorDurableObject } from "./actor-handler-do";
3
4
  export type { InputConfig as Config } from "./config";
4
- export { type Bindings, createHandler } from "./handler";
5
+ export {
6
+ type Bindings,
7
+ createHandler,
8
+ createInlineClient,
9
+ HandlerOutput,
10
+ InlineOutput,
11
+ } from "./handler";