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
JavaScript
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