@rivetkit/cloudflare-workers 0.9.0

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.
@@ -0,0 +1,66 @@
1
+ import type { ActorDriver, AnyActorInstance } from "@rivetkit/core/driver-helpers";
2
+ import invariant from "invariant";
3
+ import { KEYS } from "./actor-handler-do";
4
+
5
+ interface DurableObjectGlobalState {
6
+ ctx: DurableObjectState;
7
+ env: unknown;
8
+ }
9
+
10
+ /**
11
+ * Cloudflare DO can have multiple DO running within the same global scope.
12
+ *
13
+ * This allows for storing the actor context globally and looking it up by ID in `CloudflareActorsActorDriver`.
14
+ */
15
+ export class CloudflareDurableObjectGlobalState {
16
+ // Single map for all actor state
17
+ #dos: Map<string, DurableObjectGlobalState> = new Map();
18
+
19
+ getDOState(actorId: string): DurableObjectGlobalState {
20
+ const state = this.#dos.get(actorId);
21
+ invariant(state !== undefined, "durable object state not in global state");
22
+ return state;
23
+ }
24
+
25
+ setDOState(actorId: string, state: DurableObjectGlobalState) {
26
+ this.#dos.set(actorId, state);
27
+ }
28
+ }
29
+
30
+ export interface ActorDriverContext {
31
+ ctx: DurableObjectState;
32
+ env: unknown;
33
+ }
34
+
35
+ export class CloudflareActorsActorDriver implements ActorDriver {
36
+ #globalState: CloudflareDurableObjectGlobalState;
37
+
38
+ constructor(globalState: CloudflareDurableObjectGlobalState) {
39
+ this.#globalState = globalState;
40
+ }
41
+
42
+ #getDOCtx(actorId: string) {
43
+ return this.#globalState.getDOState(actorId).ctx;
44
+ }
45
+
46
+ getContext(actorId: string): ActorDriverContext {
47
+ const state = this.#globalState.getDOState(actorId);
48
+ return { ctx: state.ctx, env: state.env };
49
+ }
50
+
51
+ async readPersistedData(actorId: string): Promise<Uint8Array | undefined> {
52
+ return await this.#getDOCtx(actorId).storage.get(KEYS.PERSIST_DATA);
53
+ }
54
+
55
+ async writePersistedData(actorId: string, data: Uint8Array): Promise<void> {
56
+ await this.#getDOCtx(actorId).storage.put(KEYS.PERSIST_DATA, data);
57
+ }
58
+
59
+ async setAlarm(actor: AnyActorInstance, timestamp: number): Promise<void> {
60
+ await this.#getDOCtx(actor.id).storage.setAlarm(timestamp);
61
+ }
62
+
63
+ async getDatabase(actorId: string): Promise<unknown | undefined> {
64
+ return this.#getDOCtx(actorId).storage.sql;
65
+ }
66
+ }
@@ -0,0 +1,184 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+ import type { Registry, RunConfig, ActorKey } from "@rivetkit/core";
3
+ import { serializeEmptyPersistData } from "@rivetkit/core/driver-helpers";
4
+ import { logger } from "./log";
5
+ import { PartitionTopologyActor } from "@rivetkit/core/topologies/partition";
6
+ import {
7
+ CloudflareDurableObjectGlobalState,
8
+ CloudflareActorsActorDriver,
9
+ } from "./actor-driver";
10
+ import { Bindings, CF_AMBIENT_ENV } from "./handler";
11
+ import { ExecutionContext } from "hono";
12
+
13
+ export const KEYS = {
14
+ NAME: "rivetkit:name",
15
+ KEY: "rivetkit:key",
16
+ PERSIST_DATA: "rivetkit:data",
17
+ };
18
+
19
+ export interface ActorHandlerInterface extends DurableObject {
20
+ initialize(req: ActorInitRequest): Promise<void>;
21
+ }
22
+
23
+ export interface ActorInitRequest {
24
+ name: string;
25
+ key: ActorKey;
26
+ input?: unknown;
27
+ }
28
+
29
+ interface InitializedData {
30
+ name: string;
31
+ key: ActorKey;
32
+ }
33
+
34
+ export type DurableObjectConstructor = new (
35
+ ...args: ConstructorParameters<typeof DurableObject<Bindings>>
36
+ ) => DurableObject<Bindings>;
37
+
38
+ interface LoadedActor {
39
+ actorTopology: PartitionTopologyActor;
40
+ }
41
+
42
+ export function createActorDurableObject(
43
+ registry: Registry<any>,
44
+ runConfig: RunConfig,
45
+ ): DurableObjectConstructor {
46
+ const globalState = new CloudflareDurableObjectGlobalState();
47
+
48
+ /**
49
+ * Startup steps:
50
+ * 1. If not already created call `initialize`, otherwise check KV to ensure it's initialized
51
+ * 2. Load actor
52
+ * 3. Start service requests
53
+ */
54
+ return class ActorHandler
55
+ extends DurableObject<Bindings>
56
+ implements ActorHandlerInterface
57
+ {
58
+ #initialized?: InitializedData;
59
+ #initializedPromise?: PromiseWithResolvers<void>;
60
+
61
+ #actor?: LoadedActor;
62
+
63
+ async #loadActor(): Promise<LoadedActor> {
64
+ // This is always called from another context using CF_AMBIENT_ENV
65
+
66
+ // Wait for init
67
+ if (!this.#initialized) {
68
+ // Wait for init
69
+ if (this.#initializedPromise) {
70
+ await this.#initializedPromise.promise;
71
+ } else {
72
+ this.#initializedPromise = Promise.withResolvers();
73
+ const res = await this.ctx.storage.get([
74
+ KEYS.NAME,
75
+ KEYS.KEY,
76
+ KEYS.PERSIST_DATA,
77
+ ]);
78
+ if (res.get(KEYS.PERSIST_DATA)) {
79
+ const name = res.get(KEYS.NAME) as string;
80
+ if (!name) throw new Error("missing actor name");
81
+ const key = res.get(KEYS.KEY) as ActorKey;
82
+ if (!key) throw new Error("missing actor key");
83
+
84
+ logger().debug("already initialized", { name, key });
85
+
86
+ this.#initialized = { name, key };
87
+ this.#initializedPromise.resolve();
88
+ } else {
89
+ logger().debug("waiting to initialize");
90
+ }
91
+ }
92
+ }
93
+
94
+ // Check if already loaded
95
+ if (this.#actor) {
96
+ return this.#actor;
97
+ }
98
+
99
+ if (!this.#initialized) throw new Error("Not initialized");
100
+
101
+ // Configure actor driver
102
+ runConfig.driver.actor = new CloudflareActorsActorDriver(globalState);
103
+
104
+ const actorTopology = new PartitionTopologyActor(
105
+ registry.config,
106
+ runConfig,
107
+ );
108
+
109
+ // Register DO with global state
110
+ // HACK: This leaks the DO context, but DO does not provide a native way
111
+ // of knowing when the DO shuts down. We're making a broad assumption
112
+ // that DO will boot a new isolate frequenlty enough that this is not an issue.
113
+ const actorId = this.ctx.id.toString();
114
+ globalState.setDOState(actorId, { ctx: this.ctx, env: this.env });
115
+
116
+ // Save actor
117
+ this.#actor = {
118
+ actorTopology,
119
+ };
120
+
121
+ // Start actor
122
+ await actorTopology.start(
123
+ actorId,
124
+ this.#initialized.name,
125
+ this.#initialized.key,
126
+ // TODO:
127
+ "unknown",
128
+ );
129
+
130
+ return this.#actor;
131
+ }
132
+
133
+ /** RPC called by the service that creates the DO to initialize it. */
134
+ async initialize(req: ActorInitRequest) {
135
+ // TODO: Need to add this to a core promise that needs to be resolved before start
136
+
137
+ return await CF_AMBIENT_ENV.run(this.env, async () => {
138
+ await this.ctx.storage.put({
139
+ [KEYS.NAME]: req.name,
140
+ [KEYS.KEY]: req.key,
141
+ [KEYS.PERSIST_DATA]: serializeEmptyPersistData(req.input),
142
+ });
143
+ this.#initialized = {
144
+ name: req.name,
145
+ key: req.key,
146
+ };
147
+
148
+ logger().debug("initialized actor", { key: req.key });
149
+
150
+ // Preemptively actor so the lifecycle hooks are called
151
+ await this.#loadActor();
152
+ });
153
+ }
154
+
155
+ async fetch(request: Request): Promise<Response> {
156
+ return await CF_AMBIENT_ENV.run(this.env, async () => {
157
+ const { actorTopology } = await this.#loadActor();
158
+
159
+ const ctx = this.ctx;
160
+ return await actorTopology.router.fetch(
161
+ request,
162
+ this.env,
163
+ // Implement execution context so we can wait on requests
164
+ {
165
+ waitUntil(promise: Promise<unknown>) {
166
+ ctx.waitUntil(promise);
167
+ },
168
+ passThroughOnException() {
169
+ // Do nothing
170
+ },
171
+ props: {},
172
+ } satisfies ExecutionContext,
173
+ );
174
+ });
175
+ }
176
+
177
+ async alarm(): Promise<void> {
178
+ return await CF_AMBIENT_ENV.run(this.env, async () => {
179
+ const { actorTopology } = await this.#loadActor();
180
+ await actorTopology.actor.onAlarm();
181
+ });
182
+ }
183
+ };
184
+ }
package/src/config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { RunConfigSchema } from "@rivetkit/core/driver-helpers";
2
+ import { Hono } from "hono";
3
+ import { z } from "zod";
4
+
5
+ export const ConfigSchema = RunConfigSchema.removeDefault()
6
+ .omit({ driver: true, getUpgradeWebSocket: true })
7
+ .extend({
8
+ app: z.custom<Hono>().optional(),
9
+ })
10
+ .default({});
11
+ export type InputConfig = z.input<typeof ConfigSchema>;
12
+ export type Config = z.infer<typeof ConfigSchema>;
package/src/handler.ts ADDED
@@ -0,0 +1,98 @@
1
+ import {
2
+ type DurableObjectConstructor,
3
+ type ActorHandlerInterface,
4
+ createActorDurableObject,
5
+ } from "./actor-handler-do";
6
+ import { ConfigSchema, type Config, type InputConfig } from "./config";
7
+ import { Hono } from "hono";
8
+ import { PartitionTopologyManager } from "@rivetkit/core/topologies/partition";
9
+ import type { Client } from "@rivetkit/core/client";
10
+ import { CloudflareActorsManagerDriver } from "./manager-driver";
11
+ import { DriverConfig, Registry, RunConfig } from "@rivetkit/core";
12
+ import { upgradeWebSocket } from "./websocket";
13
+ import invariant from "invariant";
14
+ import { AsyncLocalStorage } from "node:async_hooks";
15
+
16
+ /** Cloudflare Workers env */
17
+ export interface Bindings {
18
+ ACTOR_KV: KVNamespace;
19
+ ACTOR_DO: DurableObjectNamespace<ActorHandlerInterface>;
20
+ }
21
+
22
+ /**
23
+ * Stores the env for the current request. Required since some contexts like the inline client driver does not have access to the Hono context.
24
+ *
25
+ * Use getCloudflareAmbientEnv unless using CF_AMBIENT_ENV.run.
26
+ */
27
+ export const CF_AMBIENT_ENV = new AsyncLocalStorage<Bindings>();
28
+
29
+ export function getCloudflareAmbientEnv(): Bindings {
30
+ const env = CF_AMBIENT_ENV.getStore();
31
+ invariant(env, "missing CF_AMBIENT_ENV");
32
+ return env;
33
+ }
34
+
35
+ interface Handler {
36
+ handler: ExportedHandler<Bindings>;
37
+ ActorHandler: DurableObjectConstructor;
38
+ }
39
+
40
+ interface SetupOutput<A extends Registry<any>> {
41
+ client: Client<A>;
42
+ createHandler: (hono?: Hono) => Handler;
43
+ }
44
+
45
+ export function createServerHandler<R extends Registry<any>>(
46
+ registry: R,
47
+ inputConfig?: InputConfig,
48
+ ): Handler {
49
+ const { createHandler } = createServer(registry, inputConfig);
50
+ return createHandler();
51
+ }
52
+
53
+ export function createServer<R extends Registry<any>>(
54
+ registry: R,
55
+ inputConfig?: InputConfig,
56
+ ): SetupOutput<R> {
57
+ const config = ConfigSchema.parse(inputConfig);
58
+
59
+ // Create config
60
+ const runConfig = {
61
+ driver: {
62
+ topology: "partition",
63
+ manager: new CloudflareActorsManagerDriver(),
64
+ // HACK: We can't build the actor driver until we're inside the Druable Object
65
+ actor: undefined as any,
66
+ },
67
+ getUpgradeWebSocket: () => upgradeWebSocket,
68
+ ...config,
69
+ } satisfies RunConfig;
70
+
71
+ // Create Durable Object
72
+ const ActorHandler = createActorDurableObject(registry, runConfig);
73
+
74
+ const managerTopology = new PartitionTopologyManager(
75
+ registry.config,
76
+ runConfig,
77
+ );
78
+
79
+ return {
80
+ client: managerTopology.inlineClient as Client<R>,
81
+ createHandler: (hono) => {
82
+ // Build base router
83
+ const app = hono ?? new Hono();
84
+
85
+ // Mount registry
86
+ app.route("/registry", managerTopology.router);
87
+
88
+ // Create Cloudflare handler
89
+ const handler = {
90
+ fetch: (request, env, ctx) => {
91
+ return CF_AMBIENT_ENV.run(env, () => app.fetch(request, env, ctx));
92
+ },
93
+ } satisfies ExportedHandler<Bindings>;
94
+
95
+ return { handler, ActorHandler };
96
+ },
97
+ };
98
+ }
package/src/log.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { getLogger } from "@rivetkit/core/log";
2
+
3
+ export const LOGGER_NAME = "driver-cloudflare-workers";
4
+
5
+ export function logger() {
6
+ return getLogger(LOGGER_NAME);
7
+ }
@@ -0,0 +1,335 @@
1
+ import {
2
+ type ManagerDriver,
3
+ type GetForIdInput,
4
+ type GetWithKeyInput,
5
+ type ActorOutput,
6
+ type CreateInput,
7
+ type GetOrCreateWithKeyInput,
8
+ type ConnRoutingHandler,
9
+ HEADER_EXPOSE_INTERNAL_ERROR,
10
+ HEADER_ENCODING,
11
+ HEADER_CONN_PARAMS,
12
+ HEADER_AUTH_DATA,
13
+ } from "@rivetkit/core/driver-helpers";
14
+ import { ActorAlreadyExists, InternalError } from "@rivetkit/core/errors";
15
+ import { Bindings } from "./mod";
16
+ import { logger } from "./log";
17
+ import { serializeNameAndKey, serializeKey } from "./util";
18
+ import { getCloudflareAmbientEnv } from "./handler";
19
+ import { Encoding } from "@rivetkit/core";
20
+
21
+ // Actor metadata structure
22
+ interface ActorData {
23
+ name: string;
24
+ key: string[];
25
+ }
26
+
27
+ // Key constants similar to Redis implementation
28
+ const KEYS = {
29
+ ACTOR: {
30
+ // Combined key for actor metadata (name and key)
31
+ metadata: (actorId: string) => `actor:${actorId}:metadata`,
32
+
33
+ // Key index function for actor lookup
34
+ keyIndex: (name: string, key: string[] = []) => {
35
+ // Use serializeKey for consistent handling of all keys
36
+ return `actor_key:${serializeKey(key)}`;
37
+ },
38
+ },
39
+ };
40
+
41
+ const STANDARD_WEBSOCKET_HEADERS = [
42
+ "connection",
43
+ "upgrade",
44
+ "sec-websocket-key",
45
+ "sec-websocket-version",
46
+ "sec-websocket-protocol",
47
+ "sec-websocket-extensions",
48
+ ];
49
+
50
+ export class CloudflareActorsManagerDriver implements ManagerDriver {
51
+ connRoutingHandler: ConnRoutingHandler;
52
+
53
+ constructor() {
54
+ this.connRoutingHandler = {
55
+ custom: {
56
+ sendRequest: async (actorId, actorRequest): Promise<Response> => {
57
+ const env = getCloudflareAmbientEnv();
58
+
59
+ logger().debug("sending request to durable object", {
60
+ actorId,
61
+ method: actorRequest.method,
62
+ url: actorRequest.url,
63
+ });
64
+
65
+ const id = env.ACTOR_DO.idFromString(actorId);
66
+ const stub = env.ACTOR_DO.get(id);
67
+
68
+ return await stub.fetch(actorRequest);
69
+ },
70
+
71
+ openWebSocket: async (
72
+ actorId,
73
+ encodingKind: Encoding,
74
+ params: unknown,
75
+ ): Promise<WebSocket> => {
76
+ const env = getCloudflareAmbientEnv();
77
+
78
+ logger().debug("opening websocket to durable object", { actorId });
79
+
80
+ // Make a fetch request to the Durable Object with WebSocket upgrade
81
+ const id = env.ACTOR_DO.idFromString(actorId);
82
+ const stub = env.ACTOR_DO.get(id);
83
+
84
+ const headers: Record<string, string> = {
85
+ Upgrade: "websocket",
86
+ Connection: "Upgrade",
87
+ [HEADER_EXPOSE_INTERNAL_ERROR]: "true",
88
+ [HEADER_ENCODING]: encodingKind,
89
+ };
90
+ if (params) {
91
+ headers[HEADER_CONN_PARAMS] = JSON.stringify(params);
92
+ }
93
+ // HACK: See packages/platforms/cloudflare-workers/src/websocket.ts
94
+ headers["sec-websocket-protocol"] = "rivetkit";
95
+
96
+ const response = await stub.fetch("http://actor/connect/websocket", {
97
+ headers,
98
+ });
99
+ const webSocket = response.webSocket;
100
+
101
+ if (!webSocket) {
102
+ throw new InternalError(
103
+ "missing websocket connection in response from DO",
104
+ );
105
+ }
106
+
107
+ logger().debug("durable object websocket connection open", {
108
+ actorId,
109
+ });
110
+
111
+ webSocket.accept();
112
+
113
+ // TODO: Is this still needed?
114
+ // HACK: Cloudflare does not call onopen automatically, so we need
115
+ // to call this on the next tick
116
+ setTimeout(() => {
117
+ (webSocket as any).onopen?.(new Event("open"));
118
+ }, 0);
119
+
120
+ return webSocket as unknown as WebSocket;
121
+ },
122
+
123
+ proxyRequest: async (c, actorRequest, actorId): Promise<Response> => {
124
+ logger().debug("forwarding request to durable object", {
125
+ actorId,
126
+ method: actorRequest.method,
127
+ url: actorRequest.url,
128
+ });
129
+
130
+ const id = c.env.ACTOR_DO.idFromString(actorId);
131
+ const stub = c.env.ACTOR_DO.get(id);
132
+
133
+ return await stub.fetch(actorRequest);
134
+ },
135
+ proxyWebSocket: async (
136
+ c,
137
+ path,
138
+ actorId,
139
+ encoding,
140
+ params,
141
+ authData,
142
+ ) => {
143
+ logger().debug("forwarding websocket to durable object", {
144
+ actorId,
145
+ path,
146
+ });
147
+
148
+ // Validate upgrade
149
+ const upgradeHeader = c.req.header("Upgrade");
150
+ if (!upgradeHeader || upgradeHeader !== "websocket") {
151
+ return new Response("Expected Upgrade: websocket", {
152
+ status: 426,
153
+ });
154
+ }
155
+
156
+ // TODO: strip headers
157
+ const newUrl = new URL(`http://actor${path}`);
158
+ const actorRequest = new Request(newUrl, c.req.raw);
159
+
160
+ // Always build fresh request to prevent forwarding unwanted headers
161
+ // HACK: Since we can't build a new request, we need to remove
162
+ // non-standard headers manually
163
+ const headerKeys: string[] = [];
164
+ actorRequest.headers.forEach((v, k) => headerKeys.push(k));
165
+ for (const k of headerKeys) {
166
+ if (!STANDARD_WEBSOCKET_HEADERS.includes(k)) {
167
+ actorRequest.headers.delete(k);
168
+ }
169
+ }
170
+
171
+ // Add RivetKit headers
172
+ actorRequest.headers.set(HEADER_EXPOSE_INTERNAL_ERROR, "true");
173
+ actorRequest.headers.set(HEADER_ENCODING, encoding);
174
+ if (params) {
175
+ actorRequest.headers.set(
176
+ HEADER_CONN_PARAMS,
177
+ JSON.stringify(params),
178
+ );
179
+ }
180
+ if (authData) {
181
+ actorRequest.headers.set(
182
+ HEADER_AUTH_DATA,
183
+ JSON.stringify(authData),
184
+ );
185
+ }
186
+
187
+ const id = c.env.ACTOR_DO.idFromString(actorId);
188
+ const stub = c.env.ACTOR_DO.get(id);
189
+
190
+ return await stub.fetch(actorRequest);
191
+ },
192
+ },
193
+ };
194
+ }
195
+
196
+ async getForId({
197
+ c,
198
+ actorId,
199
+ }: GetForIdInput<{ Bindings: Bindings }>): Promise<ActorOutput | undefined> {
200
+ const env = getCloudflareAmbientEnv();
201
+
202
+ // Get actor metadata from KV (combined name and key)
203
+ const actorData = (await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
204
+ type: "json",
205
+ })) as ActorData | null;
206
+
207
+ // If the actor doesn't exist, return undefined
208
+ if (!actorData) {
209
+ return undefined;
210
+ }
211
+
212
+ return {
213
+ actorId,
214
+ name: actorData.name,
215
+ key: actorData.key,
216
+ };
217
+ }
218
+
219
+ async getWithKey({
220
+ c,
221
+ name,
222
+ key,
223
+ }: GetWithKeyInput<{ Bindings: Bindings }>): Promise<
224
+ ActorOutput | undefined
225
+ > {
226
+ const env = getCloudflareAmbientEnv();
227
+
228
+ logger().debug("getWithKey: searching for actor", { name, key });
229
+
230
+ // Generate deterministic ID from the name and key
231
+ // This is aligned with how createActor generates IDs
232
+ const nameKeyString = serializeNameAndKey(name, key);
233
+ const actorId = env.ACTOR_DO.idFromName(nameKeyString).toString();
234
+
235
+ // Check if the actor metadata exists
236
+ const actorData = await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
237
+ type: "json",
238
+ });
239
+
240
+ if (!actorData) {
241
+ logger().debug("getWithKey: no actor found with matching name and key", {
242
+ name,
243
+ key,
244
+ actorId,
245
+ });
246
+ return undefined;
247
+ }
248
+
249
+ logger().debug("getWithKey: found actor with matching name and key", {
250
+ actorId,
251
+ name,
252
+ key,
253
+ });
254
+ return this.#buildActorOutput(c, actorId);
255
+ }
256
+
257
+ async getOrCreateWithKey(
258
+ input: GetOrCreateWithKeyInput,
259
+ ): Promise<ActorOutput> {
260
+ // TODO: Prevent race condition here
261
+ const getOutput = await this.getWithKey(input);
262
+ if (getOutput) {
263
+ return getOutput;
264
+ } else {
265
+ return await this.createActor(input);
266
+ }
267
+ }
268
+
269
+ async createActor({
270
+ c,
271
+ name,
272
+ key,
273
+ input,
274
+ }: CreateInput<{ Bindings: Bindings }>): Promise<ActorOutput> {
275
+ const env = getCloudflareAmbientEnv();
276
+
277
+ // Check if actor with the same name and key already exists
278
+ const existingActor = await this.getWithKey({ c, name, key });
279
+ if (existingActor) {
280
+ throw new ActorAlreadyExists(name, key);
281
+ }
282
+
283
+ // Create a deterministic ID from the actor name and key
284
+ // This ensures that actors with the same name and key will have the same ID
285
+ const nameKeyString = serializeNameAndKey(name, key);
286
+ const doId = env.ACTOR_DO.idFromName(nameKeyString);
287
+ const actorId = doId.toString();
288
+
289
+ // Init actor
290
+ const actor = env.ACTOR_DO.get(doId);
291
+ await actor.initialize({
292
+ name,
293
+ key,
294
+ input,
295
+ });
296
+
297
+ // Store combined actor metadata (name and key)
298
+ const actorData: ActorData = { name, key };
299
+ await env.ACTOR_KV.put(
300
+ KEYS.ACTOR.metadata(actorId),
301
+ JSON.stringify(actorData),
302
+ );
303
+
304
+ // Add to key index for lookups by name and key
305
+ await env.ACTOR_KV.put(KEYS.ACTOR.keyIndex(name, key), actorId);
306
+
307
+ return {
308
+ actorId,
309
+ name,
310
+ key,
311
+ };
312
+ }
313
+
314
+ // Helper method to build actor output from an ID
315
+ async #buildActorOutput(
316
+ c: any,
317
+ actorId: string,
318
+ ): Promise<ActorOutput | undefined> {
319
+ const env = getCloudflareAmbientEnv();
320
+
321
+ const actorData = (await env.ACTOR_KV.get(KEYS.ACTOR.metadata(actorId), {
322
+ type: "json",
323
+ })) as ActorData | null;
324
+
325
+ if (!actorData) {
326
+ return undefined;
327
+ }
328
+
329
+ return {
330
+ actorId,
331
+ name: actorData.name,
332
+ key: actorData.key,
333
+ };
334
+ }
335
+ }
package/src/mod.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { type Bindings, createServer, createServerHandler } from "./handler";
2
+ export type { InputConfig as Config } from "./config";