UNPKG

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

157 lines (156 loc) 5.57 kB
import { o as EmittedEventSchema } from "./schemas-DTUwr6Qg.mjs"; import { applyPatch } from "fast-json-patch"; import { produce } from "immer"; //#region src/createActorKitClient.ts /** * Creates an Actor Kit client for managing state and communication with the server. * * @template TMachine - The type of the state machine. * @param {ActorKitClientProps<TMachine>} props - Configuration options for the client. * @returns {ActorKitClient<TMachine>} An object with methods to interact with the actor. */ function createActorKitClient(props) { let currentSnapshot = props.initialSnapshot; let socket = null; let shouldReconnect = true; const listeners = /* @__PURE__ */ new Set(); const pendingEvents = []; let reconnectAttempts = 0; const maxReconnectAttempts = 5; const maxQueueSize = 100; /** * Notifies all registered listeners with the current state. */ const notifyListeners = () => { listeners.forEach((listener) => listener(currentSnapshot)); }; /** * Establishes a WebSocket connection to the Actor Kit server. * @returns {Promise<void>} A promise that resolves when the connection is established. */ const connect = async () => { shouldReconnect = true; const url = getWebSocketUrl(props); socket = new WebSocket(url); socket.addEventListener("open", () => { reconnectAttempts = 0; flushPendingEvents(); }); socket.addEventListener("message", (event) => { try { const data = EmittedEventSchema.parse(JSON.parse(typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data))); currentSnapshot = produce(currentSnapshot, (draft) => { applyPatch(draft, data.operations); }); props.onStateChange?.(currentSnapshot); notifyListeners(); } catch (error) { console.error(`[ActorKitClient] Error processing message:`, error); props.onError?.(error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown WebSocket message error")); } }); socket.addEventListener("error", (error) => { console.error(`[ActorKitClient] WebSocket error:`, error); props.onError?.(/* @__PURE__ */ new Error(`WebSocket error: ${error.type}`)); }); socket.addEventListener("close", (_event) => { if (shouldReconnect && reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; const delay = Math.min(1e3 * Math.pow(2, reconnectAttempts), 3e4); setTimeout(connect, delay); } else if (shouldReconnect) console.error(`[ActorKitClient] Max reconnection attempts reached`); }); return new Promise((resolve) => { socket.addEventListener("open", () => resolve()); }); }; /** * Closes the WebSocket connection to the Actor Kit server. */ const disconnect = () => { shouldReconnect = false; if (socket) { socket.close(); socket = null; } }; const flushPendingEvents = () => { while (socket && socket.readyState === WebSocket.OPEN && pendingEvents.length > 0) { const event = pendingEvents.shift(); if (!event) continue; socket.send(JSON.stringify(event)); } }; /** * Sends an event to the Actor Kit server. * @param {ClientEventFrom<TMachine>} event - The event to send. */ const send = (event) => { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(event)); return; } if (pendingEvents.length >= maxQueueSize) { const dropped = pendingEvents.shift(); props.onError?.(/* @__PURE__ */ new Error(`Event queue overflow: dropped event "${dropped?.type ?? "unknown"}". Queue is full at ${maxQueueSize} events while disconnected.`)); } pendingEvents.push(event); }; /** * Retrieves the current state of the actor. * @returns {CallerSnapshotFrom<TMachine>} The current state. */ const getState = () => currentSnapshot; /** * Subscribes a listener to state changes. * @param {Listener<CallerSnapshotFrom<TMachine>>} listener - The listener function to be called on state changes. * @returns {() => void} A function to unsubscribe the listener. */ const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; /** * Waits for a state condition to be met. * @param {(state: CallerSnapshotFrom<TMachine>) => boolean} predicateFn - Function that returns true when condition is met * @param {number} [timeoutMs=5000] - Maximum time to wait in milliseconds * @returns {Promise<void>} Resolves when condition is met, rejects on timeout */ const waitFor = async (predicateFn, timeoutMs = 5e3) => { if (predicateFn(currentSnapshot)) return Promise.resolve(); return new Promise((resolve, reject) => { let timeoutId = null; if (timeoutMs > 0) timeoutId = setTimeout(() => { unsubscribe(); reject(/* @__PURE__ */ new Error(`Timeout waiting for condition after ${timeoutMs}ms`)); }, timeoutMs); const unsubscribe = subscribe((state) => { if (predicateFn(state)) { if (timeoutId) clearTimeout(timeoutId); unsubscribe(); resolve(); } }); }); }; return { connect, disconnect, send, getState, subscribe, waitFor }; } function getWebSocketUrl(props) { const { host, actorId, actorType, accessToken, checksum } = props; const baseUrl = `${/^(localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(host) ? "ws" : "wss"}://${host}/api/${actorType}/${actorId}`; const params = new URLSearchParams({ accessToken }); if (checksum) params.append("checksum", checksum); return `${baseUrl}?${params.toString()}`; } //#endregion export { createActorKitClient as t }; //# sourceMappingURL=createActorKitClient-BJBupEaE.mjs.map