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

3 lines (2 loc) 11.2 kB
import{z as t}from"zod";import{jwtVerify as e}from"jose";import{DurableObject as s}from"cloudflare:workers";import{compare as r}from"fast-json-patch";import{createActor as o}from"xstate";import{xstateMigrate as a}from"xstate-migrate";const n=t.object({corporateProxy:t.boolean(),verifiedBot:t.boolean(),jsDetection:t.object({passed:t.boolean()}),staticResource:t.boolean(),detectionIds:t.record(t.any()),score:t.number()});t.object({ACTOR_KIT_SECRET:t.string(),ACTOR_KIT_HOST:t.string()}),t.object({longitude:t.string(),latitude:t.string(),continent:t.string(),country:t.string(),city:t.string(),timezone:t.string(),postalCode:t.string(),region:t.string(),regionCode:t.string(),metroCode:t.string(),botManagement:n});const i=t.object({id:t.string(),type:t.enum(["client","system","service"])}),c=t.object({type:t.string()});t.discriminatedUnion("type",[t.object({type:t.literal("INITIALIZE"),caller:t.object({type:t.literal("system"),id:t.string()})}),t.object({type:t.literal("CONNECT"),caller:t.object({type:t.literal("system"),id:t.string()}),connectingCaller:i}),t.object({type:t.literal("DISCONNECT"),caller:t.object({type:t.literal("system"),id:t.string()}),disconnectingCaller:i}),t.object({type:t.literal("RESUME"),caller:t.object({type:t.literal("system"),id:t.string()})}),t.object({type:t.literal("MIGRATE"),caller:t.object({type:t.literal("system"),id:t.string()}),operations:t.array(t.any())})]);const h=t.enum(["client","service","system"]),l=t.string().transform(((e,s)=>{if("anonymous"===e)return{type:"client",id:"anonymous"};const r=h.safeParse(e.split("-")[0]);if(!r.success)return r.error.issues.forEach(s.addIssue),t.NEVER;const o=r.data,a=e.substring(e.indexOf("-")+1);return t.string().uuid().safeParse(a).success?{type:o,id:a}:(s.addIssue({code:t.ZodIssueCode.custom,message:`Must be a valid uuid or 'anonymous'. Received '${a}' on value '${e}'.`}),t.NEVER)}));var p;function u(t,e){if(!t){const t=new Error(e),s=t.stack?.split("\n"),r=s&&s.length>=3?s[2]:"unknown location";throw new Error(`${e} (Assert failed at ${r?.trim()})`)}}async function d(t,s,r,o){let a;if("websocket"!==t.headers.get("Upgrade")){const e=t.headers.get("Authorization"),s=e?.split(" ")[1];u(s,"Expected authorization header to be set"),a=s}else{const e=new URLSearchParams(t.url.split("?")[1]).get("accessToken");u(e,"expected accessToken when connecting to socket"),a=e}return async function({accessToken:t,type:s,id:r,secret:o}){const a=await e(t,(new TextEncoder).encode(o));if(!a.payload.jti)throw new Error("Expected JTI on accessToken");if(a.payload.jti!==r)throw new Error(`Expected JTI on accessToken to match actor id: ${r}`);if(!a.payload.aud)throw new Error(`Expected accessToken audience to match actor type: ${s}`);if(!a.payload.sub)throw new Error("Expected accessToken to have subject");return l.parse(a.payload.sub)}({accessToken:a,type:s,id:r,secret:o})}!function(t){t[t.ERROR=0]="ERROR",t[t.WARN=1]="WARN",t[t.INFO=2]="INFO",t[t.DEBUG=3]="DEBUG"}(p||(p={}));const g=t=>{const e=new Set;return async(s,r,o)=>{const a=new URL(s.url).pathname.split("/").filter(Boolean);if(3!==a.length||"api"!==a[0])return new Response("Not Found",{status:404});const[,n,i]=a;if(!t.includes(n))return new Response(`Unknown actor type: ${n}`,{status:400});const h=function(t,e){const s=t[e.toUpperCase()];if(s&&"object"==typeof s&&"get"in s&&"idFromName"in s)return s}(r,n);if(!h)return new Response(`Durable Object namespace not found for actor type: ${n}`,{status:500});const l=h.idFromName(i),p=h.get(l);let u;try{u=await d(s,n,i,r.ACTOR_KIT_SECRET)}catch(t){return new Response(`Error: ${t.message}. API requests must specify a valid caller in Bearer token in the Authorization header using fetch method created from 'createActorFetch' or use 'createAcccessToken' directly.`,{status:401})}const g=`${n}:${i}`;if(e.has(g)||(await p.spawn({actorType:n,actorId:i,caller:u,input:{}}),e.add(g)),"websocket"===s.headers.get("Upgrade"))return p.fetch(s);if("GET"===s.method){const t=await p.getSnapshot(u);return new Response(JSON.stringify(t))}if("POST"===s.method){let t;try{const e=await s.json();t=c.parse(e)}catch(t){return new Response(JSON.stringify({error:t.message}),{status:400})}return p.send({...t,caller:u}),new Response(JSON.stringify({success:!0}))}return new Response(JSON.stringify({error:"Method not allowed"}),{status:405})}},y="persistedSnapshot",m=t.object({actorType:t.string(),actorId:t.string(),initialCaller:i,input:t.record(t.unknown())}),b=t.object({caller:i,lastSentChecksum:t.string().optional()}),S=({machine:t,schemas:e,options:n})=>class extends s{actor;actorType;actorId;input;initialCaller;lastPersistedSnapshot=null;lastSnapshotChecksum=null;snapshotCache=new Map;state;storage;attachments;subscriptions;env;currentChecksum=null;constructor(t,e,s){super(t,e),this.state=t,this.storage=t.storage,this.env=e,this.attachments=new Map,this.subscriptions=new Map,this.state.blockConcurrencyWhile((async()=>{const[t,e,s,r]=await Promise.all([this.storage.get("actorType"),this.storage.get("actorId"),this.storage.get("initialCaller"),this.storage.get("input")]);if(console.debug(`[${this.actorId}] Attempting to load actor data from storage`),t&&e&&s&&r)try{const o=m.parse({actorType:t,actorId:e,initialCaller:JSON.parse(s),input:JSON.parse(r)});if(this.actorType=o.actorType,this.actorId=o.actorId,this.initialCaller=o.initialCaller,this.input=o.input,n?.persisted){const t=await this.loadPersistedSnapshot();t?this.restorePersistedActor(t):this.#t()}else this.#t()}catch(t){console.error("Failed to parse stored data:",t)}this.state.getWebSockets().forEach((t=>{this.#e(t)}))})),this.#s()}#t(){if(u(this.actorId,"actorId is not set"),u(this.actorType,"actorType is not set"),u(this.input,"input is not set"),u(this.initialCaller,"initialCaller is not set"),!this.actor){console.debug(`[${this.actorId}] Creating new actor`);const e={id:this.actorId,caller:this.initialCaller,env:this.env,storage:this.storage,...this.input};this.actor=o(t,{input:e}),n?.persisted&&(console.debug(`[${this.actorId}] Setting up persistence for new actor`),this.#r(this.actor)),this.actor.start(),console.debug(`[${this.actorId}] New actor started`)}return this.actor}#e(t){try{const e=b.parse(t.deserializeAttachment());this.attachments.set(t,e),this.#o(t);const s=this.actor.subscribe((e=>{this.#o(t)}));this.subscriptions.set(t,s)}catch(t){console.error("Failed to subscribe WebSocket to actor:",t)}}#o(t){u(this.actor,"actor is not running");const e=this.attachments.get(t);u(e,"Attachment missing for WebSocket");const s=this.actor.getSnapshot(),o=this.#a(s);if(e.lastSentChecksum!==o){const a=this.#n(s,e.caller.id);let n={};if(e.lastSentChecksum){const t=this.snapshotCache.get(e.lastSentChecksum);t&&(n=this.#n(t.snapshot,e.caller.id))}const i=r(n,a);i.length&&(t.send(JSON.stringify({operations:i,checksum:o})),e.lastSentChecksum=o,t.serializeAttachment(e))}}#r(t){console.debug(`[${this.actorId}] Setting up state persistence`),t.subscribe((e=>{const s=t.getSnapshot();s&&this.#i(s)}))}async#i(t){try{!this.lastPersistedSnapshot||r(this.lastPersistedSnapshot,t).length>0?(console.debug(`[${this.actorId}] Persisting new snapshot`),await this.storage.put(y,JSON.stringify(t)),this.lastPersistedSnapshot=t):console.debug(`[${this.actorId}] No changes in snapshot, skipping persistence`)}catch(t){console.error(`[${this.actorId}] Error persisting snapshot:`,t)}}async fetch(t){this.#t(),u(this.actorType,"actorType is not set"),u(this.actorId,"actorId is not set");const e=new WebSocketPair,[s,r]=Object.values(e);let o;try{o=await d(t,this.actorType,this.actorId,this.env.ACTOR_KIT_SECRET)}catch(t){return new Response(`Error: ${t.message}`,{status:401})}if(!o)return new Response("Unauthorized",{status:401});const a=new URL(t.url).searchParams.get("checksum");this.state.acceptWebSocket(r);const n={caller:o,lastSentChecksum:a??void 0};return r.serializeAttachment(n),this.#e(r),new Response(null,{status:101,webSocket:s})}async webSocketMessage(t,s){const r=this.attachments.get(t);let o;u(r,"Attachment missing for WebSocket");const{caller:a}=r;if("client"===a.type){o={...e.clientEvent.parse(JSON.parse(s)),caller:a}}else{if("service"!==a.type)throw new Error(`Unknown caller type: ${a.type}`);o={...e.clientEvent.parse(JSON.parse(s)),caller:a}}this.send(o)}async webSocketError(t,e){console.error("[MachineServerImpl] WebSocket error:",e.message,e.stack)}async webSocketClose(t,e,s,r){t.close(e,"Durable Object is closing WebSocket");const o=this.subscriptions.get(t);o&&(o.unsubscribe(),this.subscriptions.delete(t)),this.attachments.delete(t)}send(t){u(this.actor,"Actor is not running"),this.actor.send({...t,env:this.env,storage:this.storage})}getSnapshot(t){u(this.actor,"Actor is not running");const e=this.actor.getSnapshot(),s=this.#a(e);this.snapshotCache.set(s,{snapshot:e,timestamp:Date.now()}),this.#c(s);return{snapshot:this.#n(e,t.id),checksum:s}}#a(t){const e=JSON.stringify(t);return this.#h(e)}#h(t){let e=0;for(let s=0;s<t.length;s++){e=(e<<5)-e+t.charCodeAt(s),e|=0}return e.toString(16)}#n(t,e){const s=t;return u(s.value,"expected value"),u(s.context.public,"expected public key in context"),u(s.context.private,"expected private key in context"),{public:s.context.public,private:s.context.private[e]||{},value:s.value}}async spawn(t){this.actorType||this.actorId||this.initialCaller||(await Promise.all([this.storage.put("actorType",t.actorType),this.storage.put("actorId",t.actorId),this.storage.put("initialCaller",JSON.stringify(t.caller)),this.storage.put("input",JSON.stringify(t.input))]).catch((t=>{console.error("Error storing actor data:",t)})),this.actorType=t.actorType,this.actorId=t.actorId,this.initialCaller=t.caller,this.input=t.input,this.#t())}#c(t){setTimeout((()=>{this.#l(t)}),3e5)}#s(){const t=3e5;setInterval((()=>{const e=Date.now();for(const[s,{timestamp:r}]of this.snapshotCache.entries())e-r>t&&this.snapshotCache.delete(s)}),t)}#l(t){if(t!==this.currentChecksum){const e=this.snapshotCache.get(t);if(e){Date.now()-e.timestamp>3e5&&this.snapshotCache.delete(t)}}}async loadPersistedSnapshot(){const t=await this.storage.get(y);return t?(console.debug(`[${this.actorId}] Loaded persisted snapshot`),JSON.parse(t)):(console.debug(`[${this.actorId}] No persisted snapshot found`),null)}restorePersistedActor(e){console.debug(`[${this.actorId}] Restoring persisted actor from `,e),u(this.actorId,"actorId is not set"),u(this.actorType,"actorType is not set"),u(this.initialCaller,"initialCaller is not set"),u(this.input,"input is not set");const s={id:this.actorId,caller:this.initialCaller,storage:this.storage,...this.input},r=a.generateMigrations(t,e,s),i=a.applyMigrations(e,r);this.actor=o(t,{snapshot:i,input:s}),n?.persisted&&(console.debug(`[${this.actorId}] Setting up persistence for restored actor`),this.#r(this.actor)),this.actor.start(),console.debug(`[${this.actorId}] Restored actor started`),this.actor.send({type:"RESUME",caller:{id:this.actorId,type:"system"},env:this.env,storage:this.storage}),console.debug(`[${this.actorId}] Sent RESUME event to restored actor`),this.lastPersistedSnapshot=i}};export{g as createActorKitRouter,S as createMachineServer}; //# sourceMappingURL=worker.js.map