@rivetkit/cloudflare-workers 2.0.3 → 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,41 +1,31 @@
1
- import type { Context as HonoContext } from "hono";
2
- import type { Encoding } from "rivetkit";
1
+ import type { Hono, Context as HonoContext } from "hono";
2
+ import type { Encoding, RegistryConfig, UniversalWebSocket } from "rivetkit";
3
3
  import {
4
4
  type ActorOutput,
5
5
  type CreateInput,
6
6
  type GetForIdInput,
7
7
  type GetOrCreateWithKeyInput,
8
8
  type GetWithKeyInput,
9
- HEADER_AUTH_DATA,
10
- HEADER_CONN_PARAMS,
11
- HEADER_ENCODING,
12
- HEADER_EXPOSE_INTERNAL_ERROR,
9
+ type ListActorsInput,
10
+ type ManagerDisplayInformation,
13
11
  type ManagerDriver,
12
+ WS_PROTOCOL_ACTOR,
13
+ WS_PROTOCOL_CONN_PARAMS,
14
+ WS_PROTOCOL_ENCODING,
15
+ WS_PROTOCOL_STANDARD,
16
+ WS_PROTOCOL_TARGET,
14
17
  } from "rivetkit/driver-helpers";
15
- import { ActorAlreadyExists, InternalError } from "rivetkit/errors";
18
+ import {
19
+ ActorDuplicateKey,
20
+ ActorNotFound,
21
+ InternalError,
22
+ } from "rivetkit/errors";
23
+ import { assertUnreachable } from "rivetkit/utils";
24
+ import { parseActorId } from "./actor-id";
16
25
  import { getCloudflareAmbientEnv } from "./handler";
17
26
  import { logger } from "./log";
18
27
  import type { Bindings } from "./mod";
19
- import { serializeKey, serializeNameAndKey } from "./util";
20
-
21
- // Actor metadata structure
22
- interface ActorData {
23
- name: string;
24
- key: string[];
25
- }
26
-
27
- const KEYS = {
28
- ACTOR: {
29
- // Combined key for actor metadata (name and key)
30
- metadata: (actorId: string) => `actor:${actorId}:metadata`,
31
-
32
- // Key index function for actor lookup
33
- keyIndex: (name: string, key: string[] = []) => {
34
- // Use serializeKey for consistent handling of all keys
35
- return `actor_key:${serializeKey(key)}`;
36
- },
37
- },
38
- };
28
+ import { serializeNameAndKey } from "./util";
39
29
 
40
30
  const STANDARD_WEBSOCKET_HEADERS = [
41
31
  "connection",
@@ -47,16 +37,24 @@ const STANDARD_WEBSOCKET_HEADERS = [
47
37
  ];
48
38
 
49
39
  export class CloudflareActorsManagerDriver implements ManagerDriver {
50
- async sendRequest(actorId: string, actorRequest: Request): Promise<Response> {
40
+ async sendRequest(
41
+ actorId: string,
42
+ actorRequest: Request,
43
+ ): Promise<Response> {
51
44
  const env = getCloudflareAmbientEnv();
52
45
 
53
- logger().debug("sending request to durable object", {
46
+ // Parse actor ID to get DO ID
47
+ const [doId] = parseActorId(actorId);
48
+
49
+ logger().debug({
50
+ msg: "sending request to durable object",
54
51
  actorId,
52
+ doId,
55
53
  method: actorRequest.method,
56
54
  url: actorRequest.url,
57
55
  });
58
56
 
59
- const id = env.ACTOR_DO.idFromString(actorId);
57
+ const id = env.ACTOR_DO.idFromString(doId);
60
58
  const stub = env.ACTOR_DO.get(id);
61
59
 
62
60
  return await stub.fetch(actorRequest);
@@ -67,34 +65,45 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
67
65
  actorId: string,
68
66
  encoding: Encoding,
69
67
  params: unknown,
70
- ): Promise<WebSocket> {
68
+ ): Promise<UniversalWebSocket> {
71
69
  const env = getCloudflareAmbientEnv();
72
70
 
73
- logger().debug("opening websocket to durable object", { actorId, path });
71
+ // Parse actor ID to get DO ID
72
+ const [doId] = parseActorId(actorId);
73
+
74
+ logger().debug({
75
+ msg: "opening websocket to durable object",
76
+ actorId,
77
+ doId,
78
+ path,
79
+ });
74
80
 
75
81
  // Make a fetch request to the Durable Object with WebSocket upgrade
76
- const id = env.ACTOR_DO.idFromString(actorId);
82
+ const id = env.ACTOR_DO.idFromString(doId);
77
83
  const stub = env.ACTOR_DO.get(id);
78
84
 
85
+ const protocols: string[] = [];
86
+ protocols.push(WS_PROTOCOL_STANDARD);
87
+ protocols.push(`${WS_PROTOCOL_TARGET}actor`);
88
+ protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
89
+ protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
90
+ if (params) {
91
+ protocols.push(
92
+ `${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
93
+ );
94
+ }
95
+
79
96
  const headers: Record<string, string> = {
80
97
  Upgrade: "websocket",
81
98
  Connection: "Upgrade",
82
- [HEADER_EXPOSE_INTERNAL_ERROR]: "true",
83
- [HEADER_ENCODING]: encoding,
99
+ "sec-websocket-protocol": protocols.join(", "),
84
100
  };
85
- if (params) {
86
- headers[HEADER_CONN_PARAMS] = JSON.stringify(params);
87
- }
88
- // HACK: See packages/drivers/cloudflare-workers/src/websocket.ts
89
- headers["sec-websocket-protocol"] = "rivetkit";
90
101
 
91
102
  // Use the path parameter to determine the URL
92
- const url = `http://actor${path}`;
103
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
104
+ const url = `http://actor${normalizedPath}`;
93
105
 
94
- logger().debug("rewriting websocket url", {
95
- from: path,
96
- to: url,
97
- });
106
+ logger().debug({ msg: "rewriting websocket url", from: path, to: url });
98
107
 
99
108
  const response = await stub.fetch(url, {
100
109
  headers,
@@ -103,11 +112,12 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
103
112
 
104
113
  if (!webSocket) {
105
114
  throw new InternalError(
106
- "missing websocket connection in response from DO",
115
+ `missing websocket connection in response from DO\n\nStatus: ${response.status}\nResponse: ${await response.text()}`,
107
116
  );
108
117
  }
109
118
 
110
- logger().debug("durable object websocket connection open", {
119
+ logger().debug({
120
+ msg: "durable object websocket connection open",
111
121
  actorId,
112
122
  });
113
123
 
@@ -122,7 +132,11 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
122
132
  (webSocket as any).dispatchEvent(event);
123
133
  }, 0);
124
134
 
125
- return webSocket as unknown as WebSocket;
135
+ return webSocket as unknown as UniversalWebSocket;
136
+ }
137
+
138
+ async buildGatewayUrl(actorId: string): Promise<string> {
139
+ return `http://actor/gateway/${encodeURIComponent(actorId)}`;
126
140
  }
127
141
 
128
142
  async proxyRequest(
@@ -130,14 +144,21 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
130
144
  actorRequest: Request,
131
145
  actorId: string,
132
146
  ): Promise<Response> {
133
- logger().debug("forwarding request to durable object", {
147
+ const env = getCloudflareAmbientEnv();
148
+
149
+ // Parse actor ID to get DO ID
150
+ const [doId] = parseActorId(actorId);
151
+
152
+ logger().debug({
153
+ msg: "forwarding request to durable object",
134
154
  actorId,
155
+ doId,
135
156
  method: actorRequest.method,
136
157
  url: actorRequest.url,
137
158
  });
138
159
 
139
- const id = c.env.ACTOR_DO.idFromString(actorId);
140
- const stub = c.env.ACTOR_DO.get(id);
160
+ const id = env.ACTOR_DO.idFromString(doId);
161
+ const stub = env.ACTOR_DO.get(id);
141
162
 
142
163
  return await stub.fetch(actorRequest);
143
164
  }
@@ -148,9 +169,9 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
148
169
  actorId: string,
149
170
  encoding: Encoding,
150
171
  params: unknown,
151
- authData: unknown,
152
172
  ): Promise<Response> {
153
- logger().debug("forwarding websocket to durable object", {
173
+ logger().debug({
174
+ msg: "forwarding websocket to durable object",
154
175
  actorId,
155
176
  path,
156
177
  });
@@ -166,7 +187,8 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
166
187
  const newUrl = new URL(`http://actor${path}`);
167
188
  const actorRequest = new Request(newUrl, c.req.raw);
168
189
 
169
- logger().debug("rewriting websocket url", {
190
+ logger().debug({
191
+ msg: "rewriting websocket url",
170
192
  from: c.req.url,
171
193
  to: actorRequest.url,
172
194
  });
@@ -175,49 +197,85 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
175
197
  // HACK: Since we can't build a new request, we need to remove
176
198
  // non-standard headers manually
177
199
  const headerKeys: string[] = [];
178
- actorRequest.headers.forEach((v, k) => headerKeys.push(k));
200
+ actorRequest.headers.forEach((v, k) => {
201
+ headerKeys.push(k);
202
+ });
179
203
  for (const k of headerKeys) {
180
204
  if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
181
205
  actorRequest.headers.delete(k);
182
206
  }
183
207
  }
184
208
 
185
- // Add RivetKit headers
186
- actorRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true");
187
- actorRequest.headers.set(HEADER_ENCODING, encoding);
209
+ // Build protocols for WebSocket connection
210
+ const protocols: string[] = [];
211
+ protocols.push(WS_PROTOCOL_STANDARD);
212
+ protocols.push(`${WS_PROTOCOL_TARGET}actor`);
213
+ protocols.push(`${WS_PROTOCOL_ACTOR}${encodeURIComponent(actorId)}`);
214
+ protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`);
188
215
  if (params) {
189
- actorRequest.headers.set(HEADER_CONN_PARAMS, JSON.stringify(params));
190
- }
191
- if (authData) {
192
- actorRequest.headers.set(HEADER_AUTH_DATA, JSON.stringify(authData));
216
+ protocols.push(
217
+ `${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
218
+ );
193
219
  }
220
+ actorRequest.headers.set(
221
+ "sec-websocket-protocol",
222
+ protocols.join(", "),
223
+ );
194
224
 
195
- const id = c.env.ACTOR_DO.idFromString(actorId);
196
- const stub = c.env.ACTOR_DO.get(id);
225
+ // Parse actor ID to get DO ID
226
+ const env = getCloudflareAmbientEnv();
227
+ const [doId] = parseActorId(actorId);
228
+ const id = env.ACTOR_DO.idFromString(doId);
229
+ const stub = env.ACTOR_DO.get(id);
197
230
 
198
231
  return await stub.fetch(actorRequest);
199
232
  }
200
233
 
201
234
  async getForId({
202
235
  c,
236
+ name,
203
237
  actorId,
204
- }: GetForIdInput<{ Bindings: Bindings }>): Promise<ActorOutput | undefined> {
238
+ }: GetForIdInput<{ Bindings: Bindings }>): Promise<
239
+ ActorOutput | undefined
240
+ > {
205
241
  const env = getCloudflareAmbientEnv();
206
242
 
207
- // Get actor metadata from KV (combined name and key)
208
- const actorData = (await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
209
- type: "json",
210
- })) as ActorData | null;
243
+ // Parse actor ID to get DO ID and expected generation
244
+ const [doId, expectedGeneration] = parseActorId(actorId);
245
+
246
+ // Get the Durable Object stub
247
+ const id = env.ACTOR_DO.idFromString(doId);
248
+ const stub = env.ACTOR_DO.get(id);
249
+
250
+ // Call the DO's getMetadata method
251
+ const result = await stub.getMetadata();
252
+
253
+ if (!result) {
254
+ logger().debug({
255
+ msg: "getForId: actor not found",
256
+ actorId,
257
+ });
258
+ return undefined;
259
+ }
211
260
 
212
- // If the actor doesn't exist, return undefined
213
- if (!actorData) {
261
+ // Check if the actor IDs match in order to check if the generation matches
262
+ if (result.actorId !== actorId) {
263
+ logger().debug({
264
+ msg: "getForId: generation mismatch",
265
+ requestedActorId: actorId,
266
+ actualActorId: result.actorId,
267
+ });
214
268
  return undefined;
215
269
  }
216
270
 
271
+ if (result.destroying) {
272
+ throw new ActorNotFound(actorId);
273
+ }
274
+
217
275
  return {
218
- actorId,
219
- name: actorData.name,
220
- key: actorData.key,
276
+ actorId: result.actorId,
277
+ name: result.name,
278
+ key: result.key,
221
279
  };
222
280
  }
223
281
 
@@ -230,44 +288,82 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
230
288
  > {
231
289
  const env = getCloudflareAmbientEnv();
232
290
 
233
- logger().debug("getWithKey: searching for actor", { name, key });
291
+ logger().debug({ msg: "getWithKey: searching for actor", name, key });
234
292
 
235
293
  // Generate deterministic ID from the name and key
236
- // This is aligned with how createActor generates IDs
237
294
  const nameKeyString = serializeNameAndKey(name, key);
238
- const actorId = env.ACTOR_DO.idFromName(nameKeyString).toString();
295
+ const doId = env.ACTOR_DO.idFromName(nameKeyString).toString();
239
296
 
240
- // Check if the actor metadata exists
241
- const actorData = await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
242
- type: "json",
243
- });
297
+ // Try to get the Durable Object to see if it exists
298
+ const id = env.ACTOR_DO.idFromString(doId);
299
+ const stub = env.ACTOR_DO.get(id);
244
300
 
245
- if (!actorData) {
246
- logger().debug("getWithKey: no actor found with matching name and key", {
301
+ // Check if actor exists without creating it
302
+ const result = await stub.getMetadata();
303
+
304
+ if (result) {
305
+ logger().debug({
306
+ msg: "getWithKey: found actor with matching name and key",
307
+ actorId: result.actorId,
308
+ name: result.name,
309
+ key: result.key,
310
+ });
311
+ return {
312
+ actorId: result.actorId,
313
+ name: result.name,
314
+ key: result.key,
315
+ };
316
+ } else {
317
+ logger().debug({
318
+ msg: "getWithKey: no actor found with matching name and key",
247
319
  name,
248
320
  key,
249
- actorId,
321
+ doId,
250
322
  });
251
323
  return undefined;
252
324
  }
325
+ }
253
326
 
254
- logger().debug("getWithKey: found actor with matching name and key", {
255
- actorId,
327
+ async getOrCreateWithKey({
328
+ c,
329
+ name,
330
+ key,
331
+ input,
332
+ }: GetOrCreateWithKeyInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
333
+ const env = getCloudflareAmbientEnv();
334
+
335
+ // Create a deterministic ID from the actor name and key
336
+ // This ensures that actors with the same name and key will have the same ID
337
+ const nameKeyString = serializeNameAndKey(name, key);
338
+ const doId = env.ACTOR_DO.idFromName(nameKeyString);
339
+
340
+ // Get or create actor using the Durable Object's method
341
+ const actor = env.ACTOR_DO.get(doId);
342
+ const result = await actor.create({
256
343
  name,
257
344
  key,
345
+ input,
346
+ allowExisting: true,
258
347
  });
259
- return this.#buildActorOutput(c, actorId);
260
- }
348
+ if ("success" in result) {
349
+ const { actorId, created } = result.success;
350
+ logger().debug({
351
+ msg: "getOrCreateWithKey result",
352
+ actorId,
353
+ name,
354
+ key,
355
+ created,
356
+ });
261
357
 
262
- async getOrCreateWithKey(
263
- input: GetOrCreateWithKeyInput,
264
- ): Promise<ActorOutput> {
265
- // TODO: Prevent race condition here
266
- const getOutput = await this.getWithKey(input);
267
- if (getOutput) {
268
- return getOutput;
358
+ return {
359
+ actorId,
360
+ name,
361
+ key,
362
+ };
363
+ } else if ("error" in result) {
364
+ throw new Error(`Error: ${JSON.stringify(result.error)}`);
269
365
  } else {
270
- return await this.createActor(input);
366
+ assertUnreachable(result);
271
367
  }
272
368
  }
273
369
 
@@ -279,62 +375,70 @@ export class CloudflareActorsManagerDriver implements ManagerDriver {
279
375
  }: CreateInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
280
376
  const env = getCloudflareAmbientEnv();
281
377
 
282
- // Check if actor with the same name and key already exists
283
- const existingActor = await this.getWithKey({ c, name, key });
284
- if (existingActor) {
285
- throw new ActorAlreadyExists(name, key);
286
- }
287
-
288
378
  // Create a deterministic ID from the actor name and key
289
379
  // This ensures that actors with the same name and key will have the same ID
290
380
  const nameKeyString = serializeNameAndKey(name, key);
291
381
  const doId = env.ACTOR_DO.idFromName(nameKeyString);
292
- const actorId = doId.toString();
293
382
 
294
- // Init actor
383
+ // Create actor - this will fail if it already exists
295
384
  const actor = env.ACTOR_DO.get(doId);
296
- await actor.initialize({
385
+ const result = await actor.create({
297
386
  name,
298
387
  key,
299
388
  input,
389
+ allowExisting: false,
300
390
  });
301
391
 
302
- // Store combined actor metadata (name and key)
303
- const actorData: ActorData = { name, key };
304
- await env.ACTOR_KV.put(
305
- KEYS.ACTOR.metadata(actorId),
306
- JSON.stringify(actorData),
307
- );
392
+ if ("success" in result) {
393
+ const { actorId } = result.success;
394
+ return {
395
+ actorId,
396
+ name,
397
+ key,
398
+ };
399
+ } else if ("error" in result) {
400
+ if (result.error.actorAlreadyExists) {
401
+ throw new ActorDuplicateKey(name, key);
402
+ }
308
403
 
309
- // Add to key index for lookups by name and key
310
- await env.ACTOR_KV.put(KEYS.ACTOR.keyIndex(name, key), actorId);
404
+ throw new InternalError(
405
+ `Unknown error creating actor: ${JSON.stringify(result.error)}`,
406
+ );
407
+ } else {
408
+ assertUnreachable(result);
409
+ }
410
+ }
311
411
 
312
- return {
313
- actorId,
412
+ async listActors({ c, name }: ListActorsInput): Promise<ActorOutput[]> {
413
+ logger().warn({
414
+ msg: "listActors not fully implemented for Cloudflare Workers",
314
415
  name,
315
- key,
416
+ });
417
+ return [];
418
+ }
419
+
420
+ displayInformation(): ManagerDisplayInformation {
421
+ return {
422
+ properties: {
423
+ Driver: "Cloudflare Workers",
424
+ },
316
425
  };
317
426
  }
318
427
 
319
- // Helper method to build actor output from an ID
320
- async #buildActorOutput(
321
- c: any,
322
- actorId: string,
323
- ): Promise<ActorOutput | undefined> {
428
+ setGetUpgradeWebSocket(): void {
429
+ // No-op for Cloudflare Workers - WebSocket upgrades are handled by the DO
430
+ }
431
+
432
+ async kvGet(actorId: string, key: Uint8Array): Promise<string | null> {
324
433
  const env = getCloudflareAmbientEnv();
325
434
 
326
- const actorData = (await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
327
- type: "json",
328
- })) as ActorData | null;
435
+ // Parse actor ID to get DO ID
436
+ const [doId] = parseActorId(actorId);
329
437
 
330
- if (!actorData) {
331
- return undefined;
332
- }
438
+ const id = env.ACTOR_DO.idFromString(doId);
439
+ const stub = env.ACTOR_DO.get(id);
333
440
 
334
- return {
335
- actorId,
336
- name: actorData.name,
337
- key: actorData.key,
338
- };
441
+ const value = await stub.managerKvGet(key);
442
+ return value !== null ? new TextDecoder().decode(value) : null;
339
443
  }
340
444
  }
package/src/mod.ts CHANGED
@@ -1,3 +1,11 @@
1
+ export type { Client } from "rivetkit";
1
2
  export type { DriverContext } from "./actor-driver";
3
+ export { createActorDurableObject } from "./actor-handler-do";
2
4
  export type { InputConfig as Config } from "./config";
3
- export { type Bindings, createServer, createServerHandler } from "./handler";
5
+ export {
6
+ type Bindings,
7
+ createHandler,
8
+ createInlineClient,
9
+ HandlerOutput,
10
+ InlineOutput,
11
+ } from "./handler";
package/src/websocket.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { UpgradeWebSocket, WSEvents, WSReadyState } from "hono/ws";
6
6
  import { defineWebSocketHelper, WSContext } from "hono/ws";
7
+ import { WS_PROTOCOL_STANDARD } from "rivetkit/driver-helpers";
7
8
 
8
9
  // Based on https://github.com/honojs/hono/issues/1153#issuecomment-1767321332
9
10
  export const upgradeWebSocket: UpgradeWebSocket<
@@ -57,14 +58,24 @@ export const upgradeWebSocket: UpgradeWebSocket<
57
58
  // we have to do this after `server.accept() is called`
58
59
  events.onOpen?.(new Event("open"), wsContext);
59
60
 
61
+ // Build response headers
62
+ const headers: Record<string, string> = {};
63
+
64
+ // Set Sec-WebSocket-Protocol if does not exist
65
+ const protocols = c.req.header("Sec-WebSocket-Protocol");
66
+ if (
67
+ typeof protocols === "string" &&
68
+ protocols
69
+ .split(",")
70
+ .map((x) => x.trim())
71
+ .includes(WS_PROTOCOL_STANDARD)
72
+ ) {
73
+ headers["Sec-WebSocket-Protocol"] = WS_PROTOCOL_STANDARD;
74
+ }
75
+
60
76
  return new Response(null, {
61
77
  status: 101,
62
- headers: {
63
- // HACK: Required in order for Cloudflare to not error with "Network connection lost"
64
- //
65
- // This bug undocumented. Cannot easily reproduce outside of RivetKit.
66
- "Sec-WebSocket-Protocol": "rivetkit",
67
- },
78
+ headers,
68
79
  webSocket: client,
69
80
  });
70
81
  });