@rivetkit/cloudflare-workers 2.2.2-rc.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.
package/src/handler.ts DELETED
@@ -1,150 +0,0 @@
1
- import { env } from "cloudflare:workers";
2
- import type { Client, Registry } from "rivetkit";
3
- import { createClientWithDriver } from "rivetkit";
4
- import { buildManagerRouter } from "rivetkit/driver-helpers";
5
- import {
6
- type ActorHandlerInterface,
7
- createActorDurableObject,
8
- type DurableObjectConstructor,
9
- } from "./actor-handler-do";
10
- import { type Config, ConfigSchema, type InputConfig } from "./config";
11
- import { CloudflareActorsManagerDriver } from "./manager-driver";
12
- import { upgradeWebSocket } from "./websocket";
13
-
14
- /** Cloudflare Workers env */
15
- export interface Bindings {
16
- ACTOR_KV: KVNamespace;
17
- ACTOR_DO: DurableObjectNamespace<ActorHandlerInterface>;
18
- }
19
-
20
- /**
21
- * Stores the env for the current request. Required since some contexts like the inline client driver does not have access to the Hono context.
22
- *
23
- * Use getCloudflareAmbientEnv unless using CF_AMBIENT_ENV.run.
24
- */
25
- export function getCloudflareAmbientEnv(): Bindings {
26
- return env as unknown as Bindings;
27
- }
28
-
29
- export interface InlineOutput<A extends Registry<any>> {
30
- /** Client to communicate with the actors. */
31
- client: Client<A>;
32
-
33
- /** Fetch handler to manually route requests to the Rivet manager API. */
34
- fetch: (request: Request, ...args: any) => Response | Promise<Response>;
35
-
36
- config: Config;
37
-
38
- ActorHandler: DurableObjectConstructor;
39
- }
40
-
41
- export interface HandlerOutput {
42
- handler: ExportedHandler<Bindings>;
43
- ActorHandler: DurableObjectConstructor;
44
- }
45
-
46
- /**
47
- * Creates an inline client for accessing Rivet Actors privately without a public manager API.
48
- *
49
- * If you want to expose a public manager API, either:
50
- *
51
- * - Use `createHandler` to expose the Rivet API on `/api/rivet`
52
- * - Forward Rivet API requests to `InlineOutput::fetch`
53
- */
54
- export function createInlineClient<R extends Registry<any>>(
55
- registry: R,
56
- inputConfig?: InputConfig,
57
- ): InlineOutput<R> {
58
- // HACK: Cloudflare does not support using `crypto.randomUUID()` before start, so we pass a default value
59
- //
60
- // Runner key is not used on Cloudflare
61
- inputConfig = { ...inputConfig, runnerKey: "" };
62
-
63
- // Parse config
64
- const config = ConfigSchema.parse(inputConfig);
65
-
66
- // Create Durable Object
67
- const ActorHandler = createActorDurableObject(
68
- registry,
69
- () => upgradeWebSocket,
70
- );
71
-
72
- // Configure registry for cloudflare-workers
73
- registry.config.noWelcome = true;
74
- // Disable inspector since it's not supported on Cloudflare Workers
75
- registry.config.inspector = {
76
- enabled: false,
77
- token: () => "",
78
- };
79
- // Set manager base path to "/" since the cloudflare handler strips the /api/rivet prefix
80
- registry.config.managerBasePath = "/";
81
- const parsedConfig = registry.parseConfig();
82
-
83
- // Create manager driver
84
- const managerDriver = new CloudflareActorsManagerDriver();
85
-
86
- // Build the manager router (has actor management endpoints like /actors)
87
- const { router } = buildManagerRouter(
88
- parsedConfig,
89
- managerDriver,
90
- () => upgradeWebSocket,
91
- );
92
-
93
- // Create client using the manager driver
94
- // Avoid excessive generic expansion in DTS generation.
95
- const client = (createClientWithDriver as any)(managerDriver) as Client<R>;
96
-
97
- return { client, fetch: router.fetch.bind(router), config, ActorHandler };
98
- }
99
-
100
- /**
101
- * Creates a handler to be exported from a Cloudflare Worker.
102
- *
103
- * This will automatically expose the Rivet manager API on `/api/rivet`.
104
- *
105
- * This includes a `fetch` handler and `ActorHandler` Durable Object.
106
- */
107
- export function createHandler(
108
- registry: Registry<any>,
109
- inputConfig?: InputConfig,
110
- ): HandlerOutput {
111
- const inline = (createInlineClient as any)(registry, inputConfig);
112
- const client = inline.client as any;
113
- const fetch = inline.fetch as (
114
- request: Request,
115
- ...args: any
116
- ) => Response | Promise<Response>;
117
- const config = inline.config as Config;
118
- const ActorHandler = inline.ActorHandler as DurableObjectConstructor;
119
-
120
- // Create Cloudflare handler
121
- const handler = {
122
- fetch: async (request, cfEnv, ctx) => {
123
- const url = new URL(request.url);
124
-
125
- // Inject Rivet env
126
- const env = Object.assign({ RIVET: client }, cfEnv);
127
-
128
- // Mount Rivet manager API
129
- if (url.pathname.startsWith(config.managerPath)) {
130
- const strippedPath = url.pathname.substring(
131
- config.managerPath.length,
132
- );
133
- url.pathname = strippedPath;
134
- const modifiedRequest = new Request(url.toString(), request);
135
- return fetch(modifiedRequest, env, ctx);
136
- }
137
-
138
- if (config.fetch) {
139
- return config.fetch(request, env, ctx);
140
- } else {
141
- return new Response(
142
- "This is a RivetKit server.\n\nLearn more at https://rivet.dev\n",
143
- { status: 200 },
144
- );
145
- }
146
- },
147
- } satisfies ExportedHandler<Bindings>;
148
-
149
- return { handler, ActorHandler };
150
- }
package/src/log.ts DELETED
@@ -1,5 +0,0 @@
1
- import { getLogger } from "rivetkit/log";
2
-
3
- export function logger() {
4
- return getLogger("driver-cloudflare-workers");
5
- }
@@ -1,444 +0,0 @@
1
- import type { Hono, Context as HonoContext } from "hono";
2
- import type { Encoding, RegistryConfig, UniversalWebSocket } from "rivetkit";
3
- import {
4
- type ActorOutput,
5
- type CreateInput,
6
- type GetForIdInput,
7
- type GetOrCreateWithKeyInput,
8
- type GetWithKeyInput,
9
- type ListActorsInput,
10
- type ManagerDisplayInformation,
11
- type ManagerDriver,
12
- WS_PROTOCOL_ACTOR,
13
- WS_PROTOCOL_CONN_PARAMS,
14
- WS_PROTOCOL_ENCODING,
15
- WS_PROTOCOL_STANDARD,
16
- WS_PROTOCOL_TARGET,
17
- } from "rivetkit/driver-helpers";
18
- import {
19
- ActorDuplicateKey,
20
- ActorNotFound,
21
- InternalError,
22
- } from "rivetkit/errors";
23
- import { assertUnreachable } from "rivetkit/utils";
24
- import { parseActorId } from "./actor-id";
25
- import { getCloudflareAmbientEnv } from "./handler";
26
- import { logger } from "./log";
27
- import type { Bindings } from "./mod";
28
- import { serializeNameAndKey } from "./util";
29
-
30
- const STANDARD_WEBSOCKET_HEADERS = [
31
- "connection",
32
- "upgrade",
33
- "sec-websocket-key",
34
- "sec-websocket-version",
35
- "sec-websocket-protocol",
36
- "sec-websocket-extensions",
37
- ];
38
-
39
- export class CloudflareActorsManagerDriver implements ManagerDriver {
40
- async sendRequest(
41
- actorId: string,
42
- actorRequest: Request,
43
- ): Promise<Response> {
44
- const env = getCloudflareAmbientEnv();
45
-
46
- // Parse actor ID to get DO ID
47
- const [doId] = parseActorId(actorId);
48
-
49
- logger().debug({
50
- msg: "sending request to durable object",
51
- actorId,
52
- doId,
53
- method: actorRequest.method,
54
- url: actorRequest.url,
55
- });
56
-
57
- const id = env.ACTOR_DO.idFromString(doId);
58
- const stub = env.ACTOR_DO.get(id);
59
-
60
- return await stub.fetch(actorRequest);
61
- }
62
-
63
- async openWebSocket(
64
- path: string,
65
- actorId: string,
66
- encoding: Encoding,
67
- params: unknown,
68
- ): Promise<UniversalWebSocket> {
69
- const env = getCloudflareAmbientEnv();
70
-
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
- });
80
-
81
- // Make a fetch request to the Durable Object with WebSocket upgrade
82
- const id = env.ACTOR_DO.idFromString(doId);
83
- const stub = env.ACTOR_DO.get(id);
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
-
96
- const headers: Record<string, string> = {
97
- Upgrade: "websocket",
98
- Connection: "Upgrade",
99
- "sec-websocket-protocol": protocols.join(", "),
100
- };
101
-
102
- // Use the path parameter to determine the URL
103
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
104
- const url = `http://actor${normalizedPath}`;
105
-
106
- logger().debug({ msg: "rewriting websocket url", from: path, to: url });
107
-
108
- const response = await stub.fetch(url, {
109
- headers,
110
- });
111
- const webSocket = response.webSocket;
112
-
113
- if (!webSocket) {
114
- throw new InternalError(
115
- `missing websocket connection in response from DO\n\nStatus: ${response.status}\nResponse: ${await response.text()}`,
116
- );
117
- }
118
-
119
- logger().debug({
120
- msg: "durable object websocket connection open",
121
- actorId,
122
- });
123
-
124
- webSocket.accept();
125
-
126
- // TODO: Is this still needed?
127
- // HACK: Cloudflare does not call onopen automatically, so we need
128
- // to call this on the next tick
129
- setTimeout(() => {
130
- const event = new Event("open");
131
- (webSocket as any).onopen?.(event);
132
- (webSocket as any).dispatchEvent(event);
133
- }, 0);
134
-
135
- return webSocket as unknown as UniversalWebSocket;
136
- }
137
-
138
- async buildGatewayUrl(actorId: string): Promise<string> {
139
- return `http://actor/gateway/${encodeURIComponent(actorId)}`;
140
- }
141
-
142
- async proxyRequest(
143
- c: HonoContext<{ Bindings: Bindings }>,
144
- actorRequest: Request,
145
- actorId: string,
146
- ): Promise<Response> {
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",
154
- actorId,
155
- doId,
156
- method: actorRequest.method,
157
- url: actorRequest.url,
158
- });
159
-
160
- const id = env.ACTOR_DO.idFromString(doId);
161
- const stub = env.ACTOR_DO.get(id);
162
-
163
- return await stub.fetch(actorRequest);
164
- }
165
-
166
- async proxyWebSocket(
167
- c: HonoContext<{ Bindings: Bindings }>,
168
- path: string,
169
- actorId: string,
170
- encoding: Encoding,
171
- params: unknown,
172
- ): Promise<Response> {
173
- logger().debug({
174
- msg: "forwarding websocket to durable object",
175
- actorId,
176
- path,
177
- });
178
-
179
- // Validate upgrade
180
- const upgradeHeader = c.req.header("Upgrade");
181
- if (!upgradeHeader || upgradeHeader !== "websocket") {
182
- return new Response("Expected Upgrade: websocket", {
183
- status: 426,
184
- });
185
- }
186
-
187
- const newUrl = new URL(`http://actor${path}`);
188
- const actorRequest = new Request(newUrl, c.req.raw);
189
-
190
- logger().debug({
191
- msg: "rewriting websocket url",
192
- from: c.req.url,
193
- to: actorRequest.url,
194
- });
195
-
196
- // Always build fresh request to prevent forwarding unwanted headers
197
- // HACK: Since we can't build a new request, we need to remove
198
- // non-standard headers manually
199
- const headerKeys: string[] = [];
200
- actorRequest.headers.forEach((v, k) => {
201
- headerKeys.push(k);
202
- });
203
- for (const k of headerKeys) {
204
- if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
205
- actorRequest.headers.delete(k);
206
- }
207
- }
208
-
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}`);
215
- if (params) {
216
- protocols.push(
217
- `${WS_PROTOCOL_CONN_PARAMS}${encodeURIComponent(JSON.stringify(params))}`,
218
- );
219
- }
220
- actorRequest.headers.set(
221
- "sec-websocket-protocol",
222
- protocols.join(", "),
223
- );
224
-
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);
230
-
231
- return await stub.fetch(actorRequest);
232
- }
233
-
234
- async getForId({
235
- c,
236
- name,
237
- actorId,
238
- }: GetForIdInput<{ Bindings: Bindings }>): Promise<
239
- ActorOutput | undefined
240
- > {
241
- const env = getCloudflareAmbientEnv();
242
-
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
- }
260
-
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
- });
268
- return undefined;
269
- }
270
-
271
- if (result.destroying) {
272
- throw new ActorNotFound(actorId);
273
- }
274
-
275
- return {
276
- actorId: result.actorId,
277
- name: result.name,
278
- key: result.key,
279
- };
280
- }
281
-
282
- async getWithKey({
283
- c,
284
- name,
285
- key,
286
- }: GetWithKeyInput<{ Bindings: Bindings }>): Promise<
287
- ActorOutput | undefined
288
- > {
289
- const env = getCloudflareAmbientEnv();
290
-
291
- logger().debug({ msg: "getWithKey: searching for actor", name, key });
292
-
293
- // Generate deterministic ID from the name and key
294
- const nameKeyString = serializeNameAndKey(name, key);
295
- const doId = env.ACTOR_DO.idFromName(nameKeyString).toString();
296
-
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);
300
-
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",
319
- name,
320
- key,
321
- doId,
322
- });
323
- return undefined;
324
- }
325
- }
326
-
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({
343
- name,
344
- key,
345
- input,
346
- allowExisting: true,
347
- });
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
- });
357
-
358
- return {
359
- actorId,
360
- name,
361
- key,
362
- };
363
- } else if ("error" in result) {
364
- throw new Error(`Error: ${JSON.stringify(result.error)}`);
365
- } else {
366
- assertUnreachable(result);
367
- }
368
- }
369
-
370
- async createActor({
371
- c,
372
- name,
373
- key,
374
- input,
375
- }: CreateInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
376
- const env = getCloudflareAmbientEnv();
377
-
378
- // Create a deterministic ID from the actor name and key
379
- // This ensures that actors with the same name and key will have the same ID
380
- const nameKeyString = serializeNameAndKey(name, key);
381
- const doId = env.ACTOR_DO.idFromName(nameKeyString);
382
-
383
- // Create actor - this will fail if it already exists
384
- const actor = env.ACTOR_DO.get(doId);
385
- const result = await actor.create({
386
- name,
387
- key,
388
- input,
389
- allowExisting: false,
390
- });
391
-
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
- }
403
-
404
- throw new InternalError(
405
- `Unknown error creating actor: ${JSON.stringify(result.error)}`,
406
- );
407
- } else {
408
- assertUnreachable(result);
409
- }
410
- }
411
-
412
- async listActors({ c, name }: ListActorsInput): Promise<ActorOutput[]> {
413
- logger().warn({
414
- msg: "listActors not fully implemented for Cloudflare Workers",
415
- name,
416
- });
417
- return [];
418
- }
419
-
420
- displayInformation(): ManagerDisplayInformation {
421
- return {
422
- properties: {
423
- Driver: "Cloudflare Workers",
424
- },
425
- };
426
- }
427
-
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> {
433
- const env = getCloudflareAmbientEnv();
434
-
435
- // Parse actor ID to get DO ID
436
- const [doId] = parseActorId(actorId);
437
-
438
- const id = env.ACTOR_DO.idFromString(doId);
439
- const stub = env.ACTOR_DO.get(id);
440
-
441
- const value = await stub.managerKvGet(key);
442
- return value !== null ? new TextDecoder().decode(value) : null;
443
- }
444
- }
package/src/mod.ts DELETED
@@ -1,11 +0,0 @@
1
- export type { Client } from "rivetkit";
2
- export type { DriverContext } from "./actor-driver";
3
- export { createActorDurableObject } from "./actor-handler-do";
4
- export type { InputConfig as Config } from "./config";
5
- export {
6
- type Bindings,
7
- createHandler,
8
- createInlineClient,
9
- HandlerOutput,
10
- InlineOutput,
11
- } from "./handler";
package/src/util.ts DELETED
@@ -1,104 +0,0 @@
1
- // Constants for key handling
2
- export const EMPTY_KEY = "(none)";
3
- export const KEY_SEPARATOR = ",";
4
-
5
- /**
6
- * Serializes an array of key strings into a single string for use with idFromName
7
- *
8
- * @param name The actor name
9
- * @param key Array of key strings to serialize
10
- * @returns A single string containing the serialized name and key
11
- */
12
- export function serializeNameAndKey(name: string, key: string[]): string {
13
- // Escape colons in the name
14
- const escapedName = name.replace(/:/g, "\\:");
15
-
16
- // For empty keys, just return the name and a marker
17
- if (key.length === 0) {
18
- return `${escapedName}:${EMPTY_KEY}`;
19
- }
20
-
21
- // Serialize the key array
22
- const serializedKey = serializeKey(key);
23
-
24
- // Combine name and serialized key
25
- return `${escapedName}:${serializedKey}`;
26
- }
27
-
28
- /**
29
- * Serializes an array of key strings into a single string
30
- *
31
- * @param key Array of key strings to serialize
32
- * @returns A single string containing the serialized key
33
- */
34
- export function serializeKey(key: string[]): string {
35
- // Use a special marker for empty key arrays
36
- if (key.length === 0) {
37
- return EMPTY_KEY;
38
- }
39
-
40
- // Escape each key part to handle the separator and the empty key marker
41
- const escapedParts = key.map((part) => {
42
- // First check if it matches our empty key marker
43
- if (part === EMPTY_KEY) {
44
- return `\\${EMPTY_KEY}`;
45
- }
46
-
47
- // Escape backslashes first, then commas
48
- let escaped = part.replace(/\\/g, "\\\\");
49
- escaped = escaped.replace(/,/g, "\\,");
50
- return escaped;
51
- });
52
-
53
- return escapedParts.join(KEY_SEPARATOR);
54
- }
55
-
56
- /**
57
- * Deserializes a key string back into an array of key strings
58
- *
59
- * @param keyString The serialized key string
60
- * @returns Array of key strings
61
- */
62
- export function deserializeKey(keyString: string): string[] {
63
- // Handle empty values
64
- if (!keyString) {
65
- return [];
66
- }
67
-
68
- // Check for special empty key marker
69
- if (keyString === EMPTY_KEY) {
70
- return [];
71
- }
72
-
73
- // Split by unescaped commas and unescape the escaped characters
74
- const parts: string[] = [];
75
- let currentPart = "";
76
- let escaping = false;
77
-
78
- for (let i = 0; i < keyString.length; i++) {
79
- const char = keyString[i];
80
-
81
- if (escaping) {
82
- // This is an escaped character, add it directly
83
- currentPart += char;
84
- escaping = false;
85
- } else if (char === "\\") {
86
- // Start of an escape sequence
87
- escaping = true;
88
- } else if (char === KEY_SEPARATOR) {
89
- // This is a separator
90
- parts.push(currentPart);
91
- currentPart = "";
92
- } else {
93
- // Regular character
94
- currentPart += char;
95
- }
96
- }
97
-
98
- // Add the last part if it exists
99
- if (currentPart || parts.length > 0) {
100
- parts.push(currentPart);
101
- }
102
-
103
- return parts;
104
- }