@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.
@@ -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
- type ManagerDriver,
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
- initialize(req: ActorInitRequest): Promise<void>;
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
- interface InitializedData {
36
- name: string;
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
- #initialized?: InitializedData;
69
- #initializedPromise?: ReturnType<typeof promiseWithResolvers<void>>;
70
-
71
- #actor?: LoadedActor;
72
-
73
- async #loadActor(): Promise<LoadedActor> {
74
- // Wait for init
75
- if (!this.#initialized) {
76
- // Wait for init
77
- if (this.#initializedPromise) {
78
- await this.#initializedPromise.promise;
79
- } else {
80
- this.#initializedPromise = promiseWithResolvers();
81
- const res = await this.ctx.storage.get([
82
- KEYS.NAME,
83
- KEYS.KEY,
84
- KEYS.PERSIST_DATA,
85
- ]);
86
- if (res.get(KEYS.PERSIST_DATA)) {
87
- const name = res.get(KEYS.NAME) as string;
88
- if (!name) throw new Error("missing actor name");
89
- const key = res.get(KEYS.KEY) as ActorKey;
90
- if (!key) throw new Error("missing actor key");
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("waiting to initialize");
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
- return this.#actor;
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(actorId);
220
+ await actorDriver.loadActor(actorIdWithGen);
161
221
 
162
- return this.#actor;
222
+ return this.#state.actor;
163
223
  }
164
224
 
165
- /** RPC called by the service that creates the DO to initialize it. */
166
- async initialize(req: ActorInitRequest) {
167
- // TODO: Need to add this to a core promise that needs to be resolved before start
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
- await this.ctx.storage.put({
170
- [KEYS.NAME]: req.name,
171
- [KEYS.KEY]: req.key,
172
- [KEYS.PERSIST_DATA]: serializeEmptyPersistData(req.input),
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
- this.#initialized = {
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
- logger().debug({ msg: "initialized actor", key: req.key });
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
- const actorId = this.ctx.id.toString();
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._onAlarm();
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
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
+ };
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 Handler {
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
- export function createHandler<R extends Registry<any>>(
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
- ): Handler {
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 serverOutput = registry.start(runConfig);
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: serverOutput.client }, cfEnv);
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 serverOutput.fetch(modifiedRequest, env, ctx);
116
+ return fetch(modifiedRequest, env, ctx);
78
117
  }
79
118
 
80
119
  if (config.fetch) {