actor-kit
Version:
Actor Kit is a library for running state machines in Cloudflare Workers, leveraging XState for robust state management. It provides a framework for managing the logic, lifecycle, persistence, synchronization, and access control of actors in a distributed
1 lines • 57.7 kB
Source Map (JSON)
{"version":3,"file":"worker.mjs","names":["#ensureActorRunning","#subscribeSocketToActor","#startPeriodicCacheCleanup","#setupStatePersistence","#enqueueSendStateUpdate","#sendQueues","#sendStateUpdate","#calculateChecksum","#scheduleSnapshotCacheCleanup","#createCallerSnapshot","#persistSnapshot","#setupActorFromRequest","#getValidatedCaller","#storeActorData","#isActorRunning","#getCurrentSnapshot","#matchesEvent","#matchesState","#cleanupSnapshotCache"],"sources":["../src/utils.ts","../src/createActorKitRouter.ts","../src/constants.ts","../src/createMachineServer.ts","../src/fromActorKit.ts"],"sourcesContent":["import { jwtVerify } from \"jose\";\nimport { CallerStringSchema } from \"./schemas\";\nimport { Caller } from \"./types\";\nimport { Actor, SnapshotFrom } from \"xstate\";\nimport { BaseActorKitStateMachine } from \"./types\";\n\n// Define log levels\nexport enum LogLevel {\n ERROR = 0,\n WARN = 1,\n INFO = 2,\n DEBUG = 3,\n}\n\n// Declare a global variable to control debug level\ndeclare global {\n var DEBUG_LEVEL: LogLevel | undefined;\n}\n\n// Set the current log level based on the global DEBUG_LEVEL variable\n// Default to INFO if not set\nconst getCurrentLogLevel = (): LogLevel => {\n return globalThis.DEBUG_LEVEL !== undefined\n ? globalThis.DEBUG_LEVEL\n : LogLevel.INFO;\n};\n\n/**\n * Debug logging function\n * @param message The message to log\n * @param level The log level (default: DEBUG)\n * @param data Additional data to log (optional)\n */\nexport function debug(\n message: string,\n level: LogLevel = LogLevel.DEBUG,\n data?: unknown\n) {\n const currentLogLevel = getCurrentLogLevel();\n if (level <= currentLogLevel) {\n const timestamp = new Date().toISOString();\n const logMessage = `[${timestamp}] ${LogLevel[level]}: ${message}`;\n\n if (data) {\n console.log(logMessage, data);\n } else {\n console.log(logMessage);\n }\n\n // You can extend this to send logs to a service if needed\n // For example:\n // if (typeof fetch !== 'undefined') {\n // fetch('https://your-logging-service.com', {\n // method: 'POST',\n // body: JSON.stringify({ message: logMessage, level, data }),\n // });\n // }\n }\n}\n\n// Convenience methods for different log levels\nexport const logError = (message: string, data?: unknown) =>\n debug(message, LogLevel.ERROR, data);\nexport const logWarn = (message: string, data?: unknown) =>\n debug(message, LogLevel.WARN, data);\nexport const logInfo = (message: string, data?: unknown) =>\n debug(message, LogLevel.INFO, data);\n\nexport const json = <T>(data: T, status = 200) =>\n Response.json(data, { status });\n\nexport const ok = () => json({ ok: true });\n\nexport const error = (err: string | { message: string }, status = 500) => {\n console.error(\"Error response\", err);\n return json(\n {\n ok: false,\n error: typeof err === \"string\" ? err : err.message ?? \"Unknown error\",\n },\n status\n );\n};\n\nexport const notFound = () => error(\"Not found\", 404);\n\n/**\n * Parses query parameters from a URL string.\n * @param url The URL to parse.\n * @returns A URLSearchParams object containing the parsed query parameters.\n */\nexport const parseQueryParams = (url: string) => {\n const index = url.indexOf(\"?\");\n const search = index !== -1 ? url.substring(index + 1) : \"\";\n return new URLSearchParams(search);\n};\n\nexport function assert<T>(\n expression: T,\n errorMessage: string\n): asserts expression {\n if (!expression) {\n const error = new Error(errorMessage);\n const stack = error.stack?.split(\"\\n\");\n\n // Find the line in the stack trace that corresponds to where the assert was called.\n // This is typically the third line in the stack, but this may vary depending on the JS environment.\n const assertLine =\n stack && stack.length >= 3 ? stack[2] : \"unknown location\";\n\n throw new Error(`${errorMessage} (Assert failed at ${assertLine?.trim()})`);\n }\n}\n\nexport async function getCallerFromRequest(\n request: Request,\n actorType: string,\n actorId: string,\n secret: string\n): Promise<Caller> {\n let accessToken: string;\n if (request.headers.get(\"Upgrade\") !== \"websocket\") {\n const authHeader = request.headers.get(\"Authorization\");\n const stringPart = authHeader?.split(\" \")[1];\n assert(stringPart, \"Expected authorization header to be set\");\n accessToken = stringPart;\n } else {\n const searchParams = new URLSearchParams(request.url.split(\"?\")[1]);\n const paramString = searchParams.get(\"accessToken\");\n assert(paramString, \"expected accessToken when connecting to socket\");\n accessToken = paramString;\n }\n\n return parseAccessTokenForCaller({\n accessToken,\n type: actorType,\n id: actorId,\n secret,\n });\n}\n\nexport async function parseAccessTokenForCaller({\n accessToken,\n type,\n id,\n secret,\n}: {\n accessToken: string;\n type: string;\n id: string;\n secret: string;\n}): Promise<Caller> {\n const verified = await jwtVerify(accessToken, new TextEncoder().encode(secret));\n if (!verified.payload.jti) {\n throw new Error(\"Expected JTI on accessToken\");\n }\n if (verified.payload.jti !== id) {\n throw new Error(`Expected JTI on accessToken to match actor id: ${id}`);\n }\n if (!verified.payload.aud) {\n throw new Error(\n `Expected accessToken audience to match actor type: ${type}`\n );\n }\n const audience = Array.isArray(verified.payload.aud)\n ? verified.payload.aud\n : [verified.payload.aud];\n if (!audience.includes(type)) {\n throw new Error(\n `Expected accessToken audience to match actor type: ${type}`\n );\n }\n if (!verified.payload.sub) {\n throw new Error(\"Expected accessToken to have subject\");\n }\n return CallerStringSchema.parse(verified.payload.sub);\n}\n\nexport function getSnapshot<TMachine extends BaseActorKitStateMachine>(\n actor: Actor<TMachine>\n): SnapshotFrom<TMachine> | undefined {\n return actor.getSnapshot();\n}\n","import { z } from \"zod\";\nimport { AnyEventSchema } from \"./schemas\";\nimport {\n AnyEvent,\n Caller,\n ActorKitEnv,\n KebabToScreamingSnake,\n ScreamingSnakeToKebab,\n} from \"./types\";\nimport { getCallerFromRequest } from \"./utils\";\n\nconst SnapshotRequestSearchSchema = z.object({\n waitForEvent: z.string().optional(),\n waitForState: z.string().optional(),\n timeout: z\n .string()\n .transform((value) => Number.parseInt(value, 10))\n .pipe(z.number().int().positive())\n .optional(),\n errorOnWaitTimeout: z\n .enum([\"true\", \"false\"])\n .transform((value) => value === \"true\")\n .optional(),\n});\n\ntype ActorKitDurableObjectStub = {\n spawn(props: {\n actorType: string;\n actorId: string;\n caller: Caller;\n input: Record<string, unknown>;\n }): Promise<void> | void;\n getSnapshot(\n caller: Caller,\n options?: {\n waitForEvent?: unknown;\n waitForState?: unknown;\n timeout?: number;\n errorOnWaitTimeout?: boolean;\n }\n ): Promise<{\n checksum: string;\n snapshot: unknown;\n }>;\n send(event: { caller: Caller; type: string }): void;\n fetch(request: Request): Promise<Response>;\n};\n\ntype ActorKitNamespace = {\n idFromName(name: string): unknown;\n get(id: unknown): ActorKitDurableObjectStub;\n};\n\nexport const createActorKitRouter = <Env extends ActorKitEnv>(\n routes: Array<ScreamingSnakeToKebab<Extract<keyof Env, string>>>\n) => {\n type ActorType = ScreamingSnakeToKebab<Extract<keyof Env, string>>;\n\n // Add a Set to keep track of spawned actors\n const spawnedActors = new Set<string>();\n\n function getDurableObjectNamespace<\n T extends ScreamingSnakeToKebab<Extract<keyof Env, string>>\n >(\n env: Env,\n key: T\n ): ActorKitNamespace | undefined {\n const envKey = key.toUpperCase() as KebabToScreamingSnake<T> & keyof Env;\n const namespace = env[envKey];\n if (\n namespace &&\n typeof namespace === \"object\" &&\n \"get\" in namespace &&\n \"idFromName\" in namespace\n ) {\n return namespace as unknown as ActorKitNamespace;\n }\n return undefined;\n }\n\n return async (\n request: Request,\n env: Env,\n _ctx?: unknown\n ): Promise<Response> => {\n const url = new URL(request.url);\n const pathParts = url.pathname.split(\"/\").filter(Boolean);\n if (pathParts.length !== 3 || pathParts[0] !== \"api\") {\n return new Response(\"Not Found\", { status: 404 });\n }\n const [, actorType, actorId] = pathParts;\n\n if (!routes.includes(actorType as ActorType)) {\n return new Response(`Unknown actor type: ${actorType}`, { status: 400 });\n }\n\n const durableObjectNamespace = getDurableObjectNamespace<ActorType>(\n env,\n actorType as ActorType\n );\n\n if (!durableObjectNamespace) {\n return new Response(\n `Durable Object namespace not found for actor type: ${actorType}`,\n { status: 500 }\n );\n }\n\n const durableObjectId = durableObjectNamespace.idFromName(actorId);\n const durableObjectStub = durableObjectNamespace.get(durableObjectId);\n\n // Parse the auth header to get the caller token\n let caller: Caller;\n try {\n caller = await getCallerFromRequest(\n request,\n actorType,\n actorId,\n env.ACTOR_KIT_SECRET\n );\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : \"Unknown error\";\n return new Response(\n `Error: ${errorMessage}. API requests must specify a valid caller in Bearer token in the Authorization header using fetch method created from 'createActorFetch' or use 'createAccessToken' directly.`,\n { status: 401 }\n );\n }\n\n // Create a unique key for the actor\n const actorKey = `${actorType}:${actorId}`;\n\n // Check if the actor has already been spawned\n if (!spawnedActors.has(actorKey)) {\n // If not, spawn it and mark it as spawned\n await durableObjectStub.spawn({\n actorType,\n actorId,\n caller,\n input: {},\n });\n spawnedActors.add(actorKey);\n }\n\n if (request.headers.get(\"Upgrade\") === \"websocket\") {\n return durableObjectStub.fetch(request);\n }\n\n if (request.method === \"GET\") {\n let searchParams: z.infer<typeof SnapshotRequestSearchSchema>;\n try {\n searchParams = SnapshotRequestSearchSchema.parse(\n Object.fromEntries(new URL(request.url).searchParams)\n );\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : \"Invalid search params\";\n return new Response(JSON.stringify({ error: errorMessage }), {\n status: 400,\n });\n }\n\n const result = await durableObjectStub.getSnapshot(caller, {\n waitForEvent: searchParams.waitForEvent\n ? AnyEventSchema.parse(JSON.parse(searchParams.waitForEvent))\n : undefined,\n waitForState: searchParams.waitForState\n ? JSON.parse(searchParams.waitForState)\n : undefined,\n timeout: searchParams.timeout,\n errorOnWaitTimeout: searchParams.errorOnWaitTimeout,\n });\n return new Response(JSON.stringify(result), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } else if (request.method === \"POST\") {\n let event: AnyEvent;\n try {\n const json = await request.json();\n event = AnyEventSchema.parse(json);\n } catch (ex: unknown) {\n const errorMessage = ex instanceof Error ? ex.message : \"Invalid JSON\";\n return new Response(JSON.stringify({ error: errorMessage }), {\n status: 400,\n });\n }\n\n durableObjectStub.send({\n ...event,\n caller,\n });\n return new Response(JSON.stringify({ success: true }));\n } else {\n return new Response(JSON.stringify({ error: \"Method not allowed\" }), {\n status: 405,\n });\n }\n };\n};\n","import { CallerType } from \"./types\";\n\nexport const HEADERS = {\n X_CALLER_ID: \"X-Caller-ID\",\n X_CALLER_TYPE: \"X-Caller-Type\",\n X_ACTOR_ID: \"X-Actor-ID\",\n X_ACTOR_TYPE: \"X-Actor-Type\",\n};\n\n/**\n * Defines the types of callers that can interact with the actor system.\n * Each type represents a different source of events with varying levels of trust and permissions.\n * Note: SYSTEM events are handled internally by Actor Kit and are not defined by the user.\n */\nexport const CallerTypes: Record<CallerType, CallerType> = {\n client: \"client\",\n system: \"system\", // Handled internally by Actor Kit\n service: \"service\",\n};\n\nexport const PERSISTED_SNAPSHOT_KEY = \"persistedSnapshot\";\n","import { DurableObject } from \"cloudflare:workers\";\nimport { compare } from \"fast-json-patch\";\nimport {\n Actor,\n AnyEventObject,\n createActor,\n EventFromLogic,\n InputFrom,\n matchesState,\n SnapshotFrom,\n StateValueFrom,\n Subscription,\n} from \"xstate\";\nimport { xstateMigrate } from \"xstate-migrate\";\nimport { z } from \"zod\";\nimport { PERSISTED_SNAPSHOT_KEY } from \"./constants\";\nimport { CallerSchema } from \"./schemas\";\nimport type {\n ActorKitInputProps,\n ActorServer,\n AnyActorKitStateMachine,\n Caller,\n CallerSnapshotFrom,\n ClientEventFrom,\n EnvFromMachine,\n MachineServerOptions,\n ServiceEventFrom,\n} from \"./types\";\nimport { assert, getCallerFromRequest } from \"./utils\";\n\nconst StorageSchema = z.object({\n actorType: z.string(),\n actorId: z.string(),\n initialCaller: CallerSchema,\n input: z.record(z.unknown()),\n});\n\nconst WebSocketAttachmentSchema = z.object({\n caller: CallerSchema,\n lastSentChecksum: z.string().optional(),\n});\n\ntype WebSocketAttachment = z.infer<typeof WebSocketAttachmentSchema>;\n\ntype ActorKitWebSocket = WebSocket & {\n serializeAttachment(value: WebSocketAttachment): void;\n deserializeAttachment(): unknown;\n};\n\ntype WebSocketResponseInit = ResponseInit & {\n webSocket: WebSocket;\n};\n\ntype ActorKitSnapshotView<TMachine extends AnyActorKitStateMachine> = {\n value: CallerSnapshotFrom<TMachine>[\"value\"];\n context: {\n public: CallerSnapshotFrom<TMachine>[\"public\"];\n private: Record<string, CallerSnapshotFrom<TMachine>[\"private\"]>;\n };\n};\n\nconst InputSearchSchema = z.object({\n input: z.string().optional(),\n});\n\nconst ParsedMessageSchema = z.string().transform((value, context) => {\n try {\n return JSON.parse(value) as unknown;\n } catch {\n context.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"Expected valid JSON payload\",\n });\n return z.NEVER;\n }\n});\n\nfunction getErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : \"Unknown error\";\n}\n\nfunction parseStoredJson<T>(value: unknown, fallbackSchema: z.ZodType<T>): T {\n const parsedString = z.string().parse(value);\n return fallbackSchema.parse(JSON.parse(parsedString));\n}\n\nexport const createMachineServer = <\n TClientEvent extends AnyEventObject,\n TServiceEvent extends AnyEventObject,\n TInputSchema extends z.ZodObject<z.ZodRawShape>,\n TMachine extends AnyActorKitStateMachine\n>({\n machine,\n schemas,\n options,\n}: {\n machine: TMachine;\n schemas: {\n clientEvent: z.ZodSchema<TClientEvent>;\n serviceEvent: z.ZodSchema<TServiceEvent>;\n inputProps: TInputSchema;\n };\n options?: MachineServerOptions;\n}): new (\n state: DurableObjectState,\n env: EnvFromMachine<TMachine>\n) => ActorServer<TMachine> =>\n class MachineServerImpl extends DurableObject implements ActorServer<TMachine> {\n actor: Actor<TMachine> | undefined;\n actorType: string | undefined;\n actorId: string | undefined;\n input: Record<string, unknown> | undefined;\n initialCaller: Caller | undefined;\n lastPersistedSnapshot: SnapshotFrom<TMachine> | null = null;\n snapshotCache = new Map<\n string,\n { snapshot: SnapshotFrom<TMachine>; timestamp: number }\n >();\n state: DurableObjectState;\n storage: DurableObjectStorage;\n attachments = new Map<WebSocket, WebSocketAttachment>();\n subscriptions = new Map<WebSocket, Subscription>();\n #sendQueues = new Map<WebSocket, Promise<void>>();\n env: EnvFromMachine<TMachine>;\n currentChecksum: string | null = null;\n\n constructor(state: DurableObjectState, env: EnvFromMachine<TMachine>) {\n super(state, env);\n this.state = state;\n this.storage = state.storage;\n this.env = env;\n\n this.state.blockConcurrencyWhile(async () => {\n const [actorType, actorId, initialCallerString, inputString] =\n await Promise.all([\n this.storage.get(\"actorType\"),\n this.storage.get(\"actorId\"),\n this.storage.get(\"initialCaller\"),\n this.storage.get(\"input\"),\n ]);\n\n if (actorType && actorId && initialCallerString && inputString) {\n try {\n const parsedData = StorageSchema.parse({\n actorType,\n actorId,\n initialCaller: parseStoredJson(initialCallerString, CallerSchema),\n input: parseStoredJson(inputString, z.record(z.unknown())),\n });\n\n this.actorType = parsedData.actorType;\n this.actorId = parsedData.actorId;\n this.initialCaller = parsedData.initialCaller;\n this.input = parsedData.input;\n\n if (options?.persisted) {\n const persistedSnapshot = await this.loadPersistedSnapshot();\n if (persistedSnapshot) {\n this.restorePersistedActor(persistedSnapshot);\n } else {\n this.#ensureActorRunning();\n }\n } else {\n this.#ensureActorRunning();\n }\n } catch {\n // Ignore corrupt startup state and wait for a fresh spawn.\n }\n }\n\n for (const socket of this.state.getWebSockets()) {\n this.#subscribeSocketToActor(socket);\n }\n });\n\n this.#startPeriodicCacheCleanup();\n }\n\n #ensureActorRunning() {\n assert(this.actorId, \"actorId is not set\");\n assert(this.actorType, \"actorType is not set\");\n assert(this.input, \"input is not set\");\n assert(this.initialCaller, \"initialCaller is not set\");\n\n if (!this.actor) {\n const input = {\n id: this.actorId,\n caller: this.initialCaller,\n env: this.env,\n storage: this.storage,\n ...this.input,\n } satisfies ActorKitInputProps;\n\n this.actor = createActor(machine, {\n input: input as InputFrom<TMachine>,\n });\n\n if (options?.persisted) {\n this.#setupStatePersistence(this.actor);\n }\n\n this.actor.start();\n }\n\n return this.actor;\n }\n\n #subscribeSocketToActor(ws: WebSocket) {\n try {\n const socket = ws as ActorKitWebSocket;\n const attachment = WebSocketAttachmentSchema.parse(\n socket.deserializeAttachment()\n );\n this.attachments.set(socket, attachment);\n this.#enqueueSendStateUpdate(socket);\n\n const subscription = this.actor?.subscribe(() => {\n this.#enqueueSendStateUpdate(socket);\n });\n\n if (subscription) {\n this.subscriptions.set(socket, subscription);\n }\n } catch {\n // Ignore malformed socket state.\n }\n }\n\n #enqueueSendStateUpdate(ws: ActorKitWebSocket) {\n const prev = this.#sendQueues.get(ws);\n const sendTask = prev\n ? prev.then(() => this.#sendStateUpdate(ws))\n : this.#sendStateUpdate(ws);\n const next = sendTask.catch(() => {\n // Errors in send are non-fatal; the next update will retry.\n });\n this.#sendQueues.set(ws, next);\n }\n\n async #sendStateUpdate(ws: ActorKitWebSocket) {\n assert(this.actor, \"actor is not running\");\n const attachment = this.attachments.get(ws);\n assert(attachment, \"Attachment missing for WebSocket\");\n\n const fullSnapshot = this.actor.getSnapshot();\n const currentChecksum = await this.#calculateChecksum(fullSnapshot);\n\n this.snapshotCache.set(currentChecksum, {\n snapshot: fullSnapshot,\n timestamp: Date.now(),\n });\n this.#scheduleSnapshotCacheCleanup(currentChecksum);\n this.currentChecksum = currentChecksum;\n\n if (attachment.lastSentChecksum === currentChecksum) {\n return;\n }\n\n const nextSnapshot = this.#createCallerSnapshot(\n fullSnapshot,\n attachment.caller.id\n );\n\n let lastSnapshot: Partial<typeof nextSnapshot> = {};\n if (attachment.lastSentChecksum) {\n const cachedSnapshot = this.snapshotCache.get(attachment.lastSentChecksum);\n if (cachedSnapshot) {\n lastSnapshot = this.#createCallerSnapshot(\n cachedSnapshot.snapshot,\n attachment.caller.id\n );\n }\n }\n\n const operations = compare(lastSnapshot, nextSnapshot);\n if (operations.length === 0) {\n return;\n }\n\n ws.send(JSON.stringify({ operations, checksum: currentChecksum }));\n attachment.lastSentChecksum = currentChecksum;\n ws.serializeAttachment(attachment);\n }\n\n #setupStatePersistence(actor: Actor<TMachine>) {\n actor.subscribe(() => {\n const fullSnapshot = actor.getSnapshot();\n this.#persistSnapshot(fullSnapshot).catch(() => {\n // Ignore persistence errors.\n });\n });\n }\n\n async #persistSnapshot(snapshot: SnapshotFrom<TMachine>) {\n if (\n this.lastPersistedSnapshot &&\n compare(this.lastPersistedSnapshot, snapshot).length === 0\n ) {\n return;\n }\n\n await this.storage.put(PERSISTED_SNAPSHOT_KEY, JSON.stringify(snapshot));\n this.lastPersistedSnapshot = snapshot;\n }\n\n async #setupActorFromRequest(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n const searchParams = InputSearchSchema.parse(\n Object.fromEntries(url.searchParams)\n );\n const pathParts = url.pathname.split(\"/\").filter(Boolean);\n const [, actorType, actorId] = pathParts;\n\n if (!actorType || !actorId) {\n return new Response(\"Invalid actor path\", { status: 400 });\n }\n\n const hasRequiredFields = Object.values(schemas.inputProps.shape).some(\n (field) => !field.isOptional()\n );\n\n if (hasRequiredFields && !searchParams.input) {\n return new Response(\"Input parameters required for initial actor setup\", {\n status: 400,\n });\n }\n\n let input: Record<string, unknown> = {};\n if (searchParams.input) {\n try {\n input = schemas.inputProps.parse(JSON.parse(searchParams.input));\n } catch (error: unknown) {\n return new Response(`Invalid input: ${getErrorMessage(error)}`, {\n status: 400,\n });\n }\n }\n\n const caller = await this.#getValidatedCaller(request, actorType, actorId);\n if (!caller) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n await this.#storeActorData(actorType, actorId, caller, input);\n this.actorType = actorType;\n this.actorId = actorId;\n this.initialCaller = caller;\n this.input = input;\n\n return null;\n }\n\n async #getValidatedCaller(\n request: Request,\n actorType: string,\n actorId: string\n ): Promise<Caller | null> {\n try {\n return await getCallerFromRequest(\n request,\n actorType,\n actorId,\n this.env.ACTOR_KIT_SECRET\n );\n } catch {\n return null;\n }\n }\n\n async #storeActorData(\n actorType: string,\n actorId: string,\n caller: Caller,\n input: Record<string, unknown>\n ) {\n await Promise.all([\n this.storage.put(\"actorType\", actorType),\n this.storage.put(\"actorId\", actorId),\n this.storage.put(\"initialCaller\", JSON.stringify(caller)),\n this.storage.put(\"input\", JSON.stringify(input)),\n ]);\n }\n\n #isActorRunning() {\n return Boolean(this.actorType);\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const clientChecksum = url.searchParams.get(\"checksum\");\n\n if (!this.#isActorRunning()) {\n const setupError = await this.#setupActorFromRequest(request);\n if (setupError) {\n return setupError;\n }\n }\n\n this.#ensureActorRunning();\n assert(this.actorType, \"actorType is not set\");\n assert(this.actorId, \"actorId is not set\");\n\n const webSocketPair = new WebSocketPair();\n const client = webSocketPair[0];\n const server = webSocketPair[1] as ActorKitWebSocket;\n\n const caller = await this.#getValidatedCaller(\n request,\n this.actorType,\n this.actorId\n );\n if (!caller) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n this.state.acceptWebSocket(server);\n server.serializeAttachment({\n caller,\n lastSentChecksum: clientChecksum ?? undefined,\n });\n this.#subscribeSocketToActor(server);\n\n return new Response(null, {\n status: 101,\n webSocket: client,\n } as WebSocketResponseInit);\n }\n\n async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {\n const attachment = this.attachments.get(ws);\n assert(attachment, \"Attachment missing for WebSocket\");\n\n const messageString =\n typeof message === \"string\"\n ? message\n : new TextDecoder().decode(message);\n const parsedMessage = ParsedMessageSchema.parse(messageString);\n\n if (attachment.caller.type === \"client\") {\n const clientEvent = schemas.clientEvent.parse(parsedMessage);\n this.send({\n ...clientEvent,\n caller: attachment.caller,\n } as ClientEventFrom<TMachine>);\n return;\n }\n\n if (attachment.caller.type === \"service\") {\n const serviceEvent = schemas.serviceEvent.parse(parsedMessage);\n this.send({\n ...serviceEvent,\n caller: attachment.caller,\n } as ServiceEventFrom<TMachine>);\n return;\n }\n\n throw new Error(`Unknown caller type: ${attachment.caller.type}`);\n }\n\n async webSocketError(_ws: WebSocket, _error: Error) {\n // No-op; the runtime closes the socket for us.\n }\n\n async webSocketClose(\n ws: WebSocket,\n code: number,\n _reason: string,\n _wasClean: boolean\n ) {\n ws.close(code, \"Durable Object is closing WebSocket\");\n const subscription = this.subscriptions.get(ws);\n if (subscription) {\n subscription.unsubscribe();\n this.subscriptions.delete(ws);\n }\n this.attachments.delete(ws);\n this.#sendQueues.delete(ws);\n }\n\n send(event: ClientEventFrom<TMachine> | ServiceEventFrom<TMachine>) {\n assert(this.actor, \"Actor is not running\");\n this.actor.send({\n ...event,\n env: this.env,\n storage: this.storage,\n });\n }\n\n async getSnapshot(\n caller: Caller,\n options?: {\n waitForEvent?: ClientEventFrom<TMachine>;\n waitForState?: StateValueFrom<TMachine>;\n timeout?: number;\n errorOnWaitTimeout?: boolean;\n }\n ): Promise<{\n checksum: string;\n snapshot: CallerSnapshotFrom<TMachine>;\n }> {\n this.#ensureActorRunning();\n\n if (!options?.waitForEvent && !options?.waitForState) {\n return await this.#getCurrentSnapshot(caller);\n }\n\n const timeoutPromise = new Promise<{\n checksum: string;\n snapshot: CallerSnapshotFrom<TMachine>;\n }>((resolve, reject) => {\n setTimeout(async () => {\n if (options.errorOnWaitTimeout !== false) {\n reject(new Error(\"Timeout waiting for event or state\"));\n } else {\n resolve(await this.#getCurrentSnapshot(caller));\n }\n }, options.timeout ?? 5000);\n });\n\n const waitPromise = new Promise<{\n checksum: string;\n snapshot: CallerSnapshotFrom<TMachine>;\n }>((resolve) => {\n const subscription = this.actor?.subscribe(async (snapshot) => {\n if (\n (options.waitForEvent &&\n this.#matchesEvent(snapshot, options.waitForEvent)) ||\n (options.waitForState &&\n this.#matchesState(snapshot, options.waitForState))\n ) {\n subscription?.unsubscribe();\n resolve(await this.#getCurrentSnapshot(caller));\n }\n });\n });\n\n return Promise.race([waitPromise, timeoutPromise]);\n }\n\n async #getCurrentSnapshot(caller: Caller) {\n const fullSnapshot = this.actor?.getSnapshot();\n assert(fullSnapshot, \"Actor snapshot is not available\");\n return {\n snapshot: this.#createCallerSnapshot(fullSnapshot, caller.id),\n checksum: await this.#calculateChecksum(fullSnapshot),\n };\n }\n\n #matchesEvent(\n _snapshot: SnapshotFrom<TMachine>,\n _event: ClientEventFrom<TMachine>\n ) {\n return true;\n }\n\n #matchesState(\n snapshot: SnapshotFrom<TMachine>,\n stateValue: StateValueFrom<TMachine>\n ) {\n return matchesState(stateValue, snapshot);\n }\n\n async #calculateChecksum(snapshot: SnapshotFrom<TMachine>) {\n const str = JSON.stringify(snapshot);\n const buffer = new TextEncoder().encode(str);\n const hash = await crypto.subtle.digest(\"SHA-256\", buffer);\n const array = new Uint8Array(hash);\n return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n }\n\n #createCallerSnapshot(\n fullSnapshot: SnapshotFrom<TMachine>,\n callerId: string\n ): CallerSnapshotFrom<TMachine> {\n const snapshot = fullSnapshot as unknown as ActorKitSnapshotView<TMachine>;\n assert(snapshot.value, \"expected value\");\n assert(snapshot.context.public, \"expected public key in context\");\n assert(snapshot.context.private, \"expected private key in context\");\n return {\n public: snapshot.context.public,\n private:\n snapshot.context.private[callerId] ??\n ({} as CallerSnapshotFrom<TMachine>[\"private\"]),\n value: snapshot.value,\n };\n }\n\n async spawn(props: {\n actorType: string;\n actorId: string;\n caller: Caller;\n input: Record<string, unknown>;\n }) {\n if (this.actorType || this.actorId || this.initialCaller) {\n return;\n }\n\n await this.#storeActorData(\n props.actorType,\n props.actorId,\n props.caller,\n props.input\n );\n this.actorType = props.actorType;\n this.actorId = props.actorId;\n this.initialCaller = props.caller;\n this.input = props.input;\n this.#ensureActorRunning();\n }\n\n #scheduleSnapshotCacheCleanup(checksum: string) {\n setTimeout(() => {\n this.#cleanupSnapshotCache(checksum);\n }, 300000);\n }\n\n #startPeriodicCacheCleanup() {\n setInterval(() => {\n const now = Date.now();\n for (const [checksum, { timestamp }] of this.snapshotCache.entries()) {\n if (now - timestamp > 300000) {\n this.snapshotCache.delete(checksum);\n }\n }\n }, 300000);\n }\n\n #cleanupSnapshotCache(checksum: string) {\n if (checksum === this.currentChecksum) {\n return;\n }\n\n const cachedData = this.snapshotCache.get(checksum);\n if (cachedData && Date.now() - cachedData.timestamp > 300000) {\n this.snapshotCache.delete(checksum);\n }\n }\n\n async loadPersistedSnapshot(): Promise<SnapshotFrom<TMachine> | null> {\n const snapshotString = await this.storage.get(PERSISTED_SNAPSHOT_KEY);\n return snapshotString\n ? (JSON.parse(z.string().parse(snapshotString)) as SnapshotFrom<TMachine>)\n : null;\n }\n\n restorePersistedActor(persistedSnapshot: SnapshotFrom<TMachine>) {\n assert(this.actorId, \"actorId is not set\");\n assert(this.actorType, \"actorType is not set\");\n assert(this.initialCaller, \"initialCaller is not set\");\n assert(this.input, \"input is not set\");\n\n const input = {\n id: this.actorId,\n caller: this.initialCaller,\n storage: this.storage,\n env: this.env,\n ...this.input,\n } as InputFrom<TMachine>;\n\n const migrations = xstateMigrate.generateMigrations(\n machine,\n persistedSnapshot,\n input\n );\n const restoredSnapshot = xstateMigrate.applyMigrations(\n persistedSnapshot,\n migrations\n );\n\n this.actor = createActor(machine, {\n snapshot: restoredSnapshot,\n input,\n });\n\n if (options?.persisted) {\n this.#setupStatePersistence(this.actor);\n }\n\n this.actor.start();\n this.actor.send({\n type: \"RESUME\",\n caller: { id: this.actorId, type: \"system\" },\n env: this.env,\n storage: this.storage,\n } as EventFromLogic<TMachine>);\n\n this.lastPersistedSnapshot = restoredSnapshot as SnapshotFrom<TMachine>;\n }\n };\n","/**\n * fromActorKit — XState callback actor for DO-to-DO communication.\n *\n * Creates an XState `fromCallback` actor that connects a parent actor-kit\n * Durable Object to a remote child actor-kit DO via WebSocket. The callback:\n *\n * 1. Spawns the remote DO (idempotent)\n * 2. Creates a JWT for the remote actor\n * 3. Opens a WebSocket via stub.fetch() with Upgrade header\n * 4. Receives JSON Patch snapshot updates → emits {TYPE}_UPDATED to parent\n * 5. Forwards events from parent → remote DO (validated via Zod schema)\n * 6. Queues events sent before WebSocket is ready\n * 7. Cleans up WebSocket on actor stop\n *\n * @example\n * ```ts\n * const sessionMachine = setup({\n * actors: {\n * zone: fromActorKit<ZoneMachine>(\"zone\"),\n * },\n * }).createMachine({\n * invoke: {\n * id: \"zone\",\n * src: \"zone\",\n * input: ({ context }) => ({\n * server: context.env.ZONE,\n * actorId: context.public.zoneId,\n * actorInput: {},\n * caller: { id: context.public.playerId, type: \"service\" as const },\n * signingKey: context.env.ACTOR_KIT_SECRET,\n * eventSchema: ZoneServiceEventSchema,\n * }),\n * },\n * on: { ZONE_UPDATED: { actions: \"syncZoneSnapshot\" } },\n * });\n * ```\n *\n * @module\n */\nimport { applyPatch } from \"fast-json-patch\";\nimport type { Operation } from \"fast-json-patch\";\nimport { produce } from \"immer\";\nimport type { CallbackLogicFunction } from \"xstate\";\nimport { fromCallback } from \"xstate\";\nimport type { z } from \"zod\";\nimport { createAccessToken } from \"./createAccessToken\";\nimport { EmittedEventSchema } from \"./schemas\";\nimport type {\n AnyActorKitStateMachine,\n Caller,\n CallerSnapshotFrom,\n} from \"./types\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Convert kebab-case to SCREAMING_SNAKE: `\"my-actor\"` → `\"MY_ACTOR\"` */\ntype KebabToScreamingSnake<S extends string> = string extends S\n ? string\n : S extends `${infer Head}-${infer Tail}`\n ? `${Uppercase<Head>}_${KebabToScreamingSnake<Tail>}`\n : Uppercase<S>;\n\n/** Events emitted back to the parent machine. */\nexport type FromActorKitEmitted<\n TActorType extends string,\n TMachine extends AnyActorKitStateMachine,\n> =\n | {\n type: `${KebabToScreamingSnake<TActorType>}_UPDATED`;\n actorType: TActorType;\n actorId: string;\n snapshot: CallerSnapshotFrom<TMachine>;\n operations: Operation[];\n }\n | {\n type: `${KebabToScreamingSnake<TActorType>}_ERROR`;\n actorType: TActorType;\n actorId: string;\n error: Error;\n };\n\n/**\n * Minimal interface for a DO namespace — compatible with both\n * `DurableObjectNamespace` (untyped) and `DurableObjectNamespace<T>` (RPC-typed).\n */\nexport interface ActorKitNamespaceLike {\n idFromName(name: string): unknown;\n get(id: unknown): {\n fetch(request: Request): Promise<Response>;\n spawn?(props: {\n actorType: string;\n actorId: string;\n caller: Caller;\n input: Record<string, unknown>;\n }): Promise<void> | void;\n [key: string]: unknown;\n };\n}\n\n/** Input the parent must provide when invoking the callback actor. */\nexport interface FromActorKitInput<\n TEventSchema extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n /** DO namespace binding for the remote actor (e.g. `env.ZONE`). */\n server: ActorKitNamespaceLike;\n /** ID of the remote actor instance. */\n actorId: string;\n /** Input props forwarded to the remote actor on spawn. */\n actorInput: Record<string, unknown>;\n /** Caller identity used for JWT signing. */\n caller: Caller;\n /** Secret key used to sign the JWT. */\n signingKey: string;\n /** Zod schema that validates events before forwarding to the remote DO. */\n eventSchema: TEventSchema;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction toScreamingSnake(s: string): string {\n return s.replace(/-/g, \"_\").toUpperCase();\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an XState callback actor that bridges a parent actor-kit DO\n * to a remote child actor-kit DO via WebSocket.\n *\n * @param actorType — kebab-case actor type name (e.g. `\"zone\"`)\n */\nexport function fromActorKit<\n TMachine extends AnyActorKitStateMachine,\n TActorType extends string = string,\n>(actorType: TActorType) {\n type TEmitted = FromActorKitEmitted<TActorType, TMachine>;\n\n const TYPE = toScreamingSnake(actorType);\n\n function emitUpdated(\n emitActorType: TActorType,\n actorId: string,\n snapshot: CallerSnapshotFrom<TMachine>,\n operations: Operation[],\n ): TEmitted {\n return {\n type: `${TYPE}_UPDATED`,\n actorType: emitActorType,\n actorId,\n snapshot,\n operations,\n } as TEmitted;\n }\n\n function emitError(\n emitActorType: TActorType,\n actorId: string,\n error: Error,\n ): TEmitted {\n return {\n type: `${TYPE}_ERROR`,\n actorType: emitActorType,\n actorId,\n error,\n } as TEmitted;\n }\n\n const callback: CallbackLogicFunction<\n Record<string, unknown> & { type: string }, // events parent can send\n TEmitted, // events sent back to parent\n FromActorKitInput // input\n > = ({ sendBack, receive, input }) => {\n let ws: WebSocket | null = null;\n const pendingEvents: string[] = [];\n\n // Async setup — spawn, auth, connect\n (async () => {\n try {\n const id = input.server.idFromName(input.actorId);\n const stub = input.server.get(id);\n\n // 1. Spawn remote actor (idempotent) via RPC\n if (stub.spawn) {\n await Promise.resolve(\n stub.spawn({\n actorType,\n actorId: input.actorId,\n caller: input.caller,\n input: input.actorInput,\n })\n );\n }\n\n // 2. Create JWT\n const accessToken = await createAccessToken({\n signingKey: input.signingKey,\n actorId: input.actorId,\n actorType,\n callerId: input.caller.id,\n callerType: input.caller.type,\n });\n\n // 3. Open WebSocket via fetch upgrade\n const response = await stub.fetch(\n new Request(\n `https://internal/api/${actorType}/${input.actorId}/?accessToken=${accessToken}`,\n { headers: { Upgrade: \"websocket\" } }\n )\n );\n\n const webSocket = response.webSocket;\n if (!webSocket) {\n throw new Error(\n `WebSocket upgrade failed for ${actorType}/${input.actorId}`\n );\n }\n\n webSocket.accept();\n ws = webSocket;\n\n // 4. Track snapshot via patches\n let currentSnapshot: CallerSnapshotFrom<TMachine> | undefined;\n\n ws.addEventListener(\"message\", (event: MessageEvent) => {\n try {\n const raw =\n typeof event.data === \"string\"\n ? event.data\n : new TextDecoder().decode(event.data);\n const data = EmittedEventSchema.parse(JSON.parse(raw));\n\n currentSnapshot = produce(\n currentSnapshot ?? ({} as CallerSnapshotFrom<TMachine>),\n (draft) => {\n applyPatch(draft, data.operations);\n }\n );\n\n sendBack(emitUpdated(actorType, input.actorId, currentSnapshot, data.operations));\n } catch (err) {\n console.error(\n `[fromActorKit:${actorType}] message error:`,\n err\n );\n }\n });\n\n ws.addEventListener(\"error\", () => {\n sendBack(emitError(actorType, input.actorId, new Error(`WebSocket error for ${actorType}/${input.actorId}`)));\n });\n\n // Handle normal close (e.g., child DO eviction/restart)\n ws.addEventListener(\"close\", (event: CloseEvent) => {\n ws = null;\n sendBack(emitError(\n actorType,\n input.actorId,\n new Error(`WebSocket closed for ${actorType}/${input.actorId}: code=${event.code} reason=${event.reason}`)\n ));\n });\n\n // 5. Flush queued events\n while (pendingEvents.length > 0) {\n ws.send(pendingEvents.shift()!);\n }\n } catch (err) {\n console.error(`[fromActorKit:${actorType}] setup error:`, err);\n sendBack(emitError(\n actorType,\n input.actorId,\n err instanceof Error\n ? err\n : new Error(`Setup failed for ${actorType}/${input.actorId}`)\n ));\n }\n })();\n\n // 6. Forward validated events from parent → remote DO\n receive((event) => {\n const result = input.eventSchema.safeParse(event);\n if (!result.success) return; // not meant for this actor\n\n const serialized = JSON.stringify(result.data);\n if (ws) {\n ws.send(serialized);\n } else {\n pendingEvents.push(serialized);\n }\n });\n\n // 7. Cleanup\n return () => {\n if (ws) {\n try {\n ws.close(1000, \"Actor stopped\");\n } catch {\n // ignore\n }\n ws = null;\n }\n };\n };\n\n return fromCallback(callback);\n}\n"],"mappings":"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