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 • 10.2 kB
Source Map (JSON)
{"version":3,"file":"createActorKitClient-BJBupEaE.mjs","names":[],"sources":["../src/createActorKitClient.ts"],"sourcesContent":["import { applyPatch } from \"fast-json-patch\";\nimport { produce } from \"immer\";\n\nimport { EmittedEventSchema } from \"./schemas\";\nimport type {\n ActorKitClient,\n AnyActorKitStateMachine,\n CallerSnapshotFrom,\n ClientEventFrom,\n} from \"./types\";\n\nexport type ActorKitClientProps<TMachine extends AnyActorKitStateMachine> = {\n host: string;\n actorType: string;\n actorId: string;\n checksum: string;\n accessToken: string;\n initialSnapshot: CallerSnapshotFrom<TMachine>;\n onStateChange?: (newState: CallerSnapshotFrom<TMachine>) => void;\n onError?: (error: Error) => void;\n};\n\ntype Listener<T> = (state: T) => void;\n\n/**\n * Creates an Actor Kit client for managing state and communication with the server.\n *\n * @template TMachine - The type of the state machine.\n * @param {ActorKitClientProps<TMachine>} props - Configuration options for the client.\n * @returns {ActorKitClient<TMachine>} An object with methods to interact with the actor.\n */\nexport function createActorKitClient<TMachine extends AnyActorKitStateMachine>(\n props: ActorKitClientProps<TMachine>\n): ActorKitClient<TMachine> {\n let currentSnapshot = props.initialSnapshot;\n let socket: WebSocket | null = null;\n\n let shouldReconnect = true;\n const listeners: Set<Listener<CallerSnapshotFrom<TMachine>>> = new Set();\n const pendingEvents: ClientEventFrom<TMachine>[] = [];\n let reconnectAttempts = 0;\n const maxReconnectAttempts = 5;\n const maxQueueSize = 100;\n\n /**\n * Notifies all registered listeners with the current state.\n */\n const notifyListeners = () => {\n listeners.forEach((listener) => listener(currentSnapshot));\n };\n\n /**\n * Establishes a WebSocket connection to the Actor Kit server.\n * @returns {Promise<void>} A promise that resolves when the connection is established.\n */\n const connect = async () => {\n shouldReconnect = true;\n const url = getWebSocketUrl(props);\n\n socket = new WebSocket(url);\n\n\n socket.addEventListener(\"open\", () => {\n reconnectAttempts = 0;\n flushPendingEvents();\n });\n\n socket.addEventListener(\"message\", (event: MessageEvent) => {\n try {\n const data = EmittedEventSchema.parse(JSON.parse(\n typeof event.data === \"string\"\n ? event.data\n : new TextDecoder().decode(event.data)\n ));\n\n currentSnapshot = produce(currentSnapshot, (draft) => {\n applyPatch(draft, data.operations);\n });\n\n props.onStateChange?.(currentSnapshot);\n notifyListeners();\n } catch (error: unknown) {\n console.error(`[ActorKitClient] Error processing message:`, error);\n props.onError?.(\n error instanceof Error\n ? error\n : new Error(\"Unknown WebSocket message error\")\n );\n }\n });\n\n socket.addEventListener(\"error\", (error: Event) => {\n console.error(`[ActorKitClient] WebSocket error:`, error);\n props.onError?.(new Error(`WebSocket error: ${error.type}`));\n });\n\n // todo, how do we reconnect when a user returns to the tab\n // later after it's disconnected\n\n socket.addEventListener(\"close\", (_event) => {\n \n\n // Implement reconnection logic\n if (shouldReconnect && reconnectAttempts < maxReconnectAttempts) {\n reconnectAttempts++;\n const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);\n setTimeout(connect, delay);\n } else if (shouldReconnect) {\n console.error(`[ActorKitClient] Max reconnection attempts reached`);\n }\n });\n\n return new Promise<void>((resolve) => {\n socket!.addEventListener(\"open\", () => resolve());\n });\n };\n\n /**\n * Closes the WebSocket connection to the Actor Kit server.\n */\n const disconnect = () => {\n shouldReconnect = false;\n if (socket) {\n socket.close();\n socket = null;\n }\n\n };\n\n const flushPendingEvents = () => {\n while (socket && socket.readyState === WebSocket.OPEN && pendingEvents.length > 0) {\n const event = pendingEvents.shift();\n if (!event) {\n continue;\n }\n socket.send(JSON.stringify(event));\n }\n };\n\n /**\n * Sends an event to the Actor Kit server.\n * @param {ClientEventFrom<TMachine>} event - The event to send.\n */\n const send = (event: ClientEventFrom<TMachine>) => {\n if (socket && socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify(event));\n return;\n }\n\n // Queue the event for delivery when connection is (re)established\n if (pendingEvents.length >= maxQueueSize) {\n const dropped = pendingEvents.shift();\n props.onError?.(\n new Error(\n `Event queue overflow: dropped event \"${dropped?.type ?? \"unknown\"}\". ` +\n `Queue is full at ${maxQueueSize} events while disconnected.`\n )\n );\n }\n pendingEvents.push(event);\n };\n\n /**\n * Retrieves the current state of the actor.\n * @returns {CallerSnapshotFrom<TMachine>} The current state.\n */\n const getState = () => currentSnapshot;\n\n /**\n * Subscribes a listener to state changes.\n * @param {Listener<CallerSnapshotFrom<TMachine>>} listener - The listener function to be called on state changes.\n * @returns {() => void} A function to unsubscribe the listener.\n */\n const subscribe = (listener: Listener<CallerSnapshotFrom<TMachine>>) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n };\n\n /**\n * Waits for a state condition to be met.\n * @param {(state: CallerSnapshotFrom<TMachine>) => boolean} predicateFn - Function that returns true when condition is met\n * @param {number} [timeoutMs=5000] - Maximum time to wait in milliseconds\n * @returns {Promise<void>} Resolves when condition is met, rejects on timeout\n */\n const waitFor = async (\n predicateFn: (state: CallerSnapshotFrom<TMachine>) => boolean,\n timeoutMs: number = 5000\n ): Promise<void> => {\n // Check if condition is already met\n if (predicateFn(currentSnapshot)) {\n return Promise.resolve();\n }\n return new Promise((resolve, reject) => {\n let timeoutId: number | null = null;\n\n // Set up timeout to reject if condition isn't met in time\n if (timeoutMs > 0) {\n timeoutId = setTimeout(() => {\n unsubscribe();\n reject(\n new Error(`Timeout waiting for condition after ${timeoutMs}ms`)\n );\n }, timeoutMs);\n }\n\n // Subscribe to state changes\n const unsubscribe = subscribe((state) => {\n if (predicateFn(state)) {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n unsubscribe();\n resolve();\n }\n });\n });\n };\n\n return {\n connect,\n disconnect,\n send,\n getState,\n subscribe,\n waitFor,\n };\n}\n\nfunction getWebSocketUrl(\n props: ActorKitClientProps<AnyActorKitStateMachine>\n): string {\n const { host, actorId, actorType, accessToken, checksum } = props;\n\n // Determine protocol (ws or wss)\n const protocol =\n /^(localhost|127\\.0\\.0\\.1|192\\.168\\.|10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.)/.test(\n host\n )\n ? \"ws\"\n : \"wss\";\n\n // Construct base URL\n const baseUrl = `${protocol}://${host}/api/${actorType}/${actorId}`;\n\n // Add query parameters\n const params = new URLSearchParams({ accessToken });\n if (checksum) params.append(\"checksum\", checksum);\n\n const finalUrl = `${baseUrl}?${params.toString()}`;\n\n return finalUrl;\n}\n"],"mappings":"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"}