UNPKG

@cloudflare/actors

Version:

An easier way to build with Cloudflare Durable Objects

310 lines 13 kB
import { env, DurableObject, WorkerEntrypoint } from "cloudflare:workers"; import { Storage } from "../../storage/src/index"; import { Alarms } from "../../alarms/src/index"; import { Persist, PERSISTED_VALUES, initializePersistedProperties, persistProperty } from "./persist"; export { Persist }; /** * Provide a default name value for an actor. */ const DEFAULT_ACTOR_NAME = "default"; /** * Provide a default name value for the tracking actor. */ const TRACKING_ACTOR_NAME = "_cf_actors"; /** * Base abstract class for Workers that provides common functionality and structure. * @template T - The type of the environment object that will be available to the worker */ export class Entrypoint extends WorkerEntrypoint { } /** * Extended Actor class that provides additional functionality for Durable Objects. * This class adds SQL storage capabilities and browsing functionality to the base DurableObject. * @template E - The type of the environment object that will be available to the actor */ export class Actor extends DurableObject { __studio(_) { return this.storage.__studio(_); } /** * Hook that is called whenever a @Persist decorated property is stored in the database. * Override this method to listen to persistence events. * @param key The property key that was persisted * @param value The value that was persisted */ onPersist(key, value) { // Default implementation is a no-op } /** * Set the identifier for the actor as named by the client * @param id The identifier to set */ async setIdentifier(id) { this.identifier = id; } /** * Static method to extract an ID from a request URL. Default response "default". * @param request - The incoming request * @returns The name string value defined by the client application to reference an instance */ static async nameFromRequest(request) { return DEFAULT_ACTOR_NAME; } ; /** * Static method to get an actor instance by ID * @param id - The ID of the actor to get * @returns The actor instance */ static get(id) { const stub = getActor(this, id); // This may seem repetitive from when we do this in `getActor` prior to returning the stub // but this allows classes to do `this.ctx.blockConcurrencyWhile` and log out the identifier // there. Without doing this again, that seems to fail for one reason or another. stub.setIdentifier(id); return stub; } /** * Creates a new instance of Actor. * @param ctx - The DurableObjectState for this actor * @param env - The environment object containing bindings and configuration */ constructor(ctx, env) { if (ctx && env) { super(ctx, env); this.storage = new Storage(ctx.storage); this.alarms = new Alarms(ctx, this); // Initialize the persisted values map this[PERSISTED_VALUES] = new Map(); // Move all initialization into blockConcurrencyWhile to ensure // persisted properties are loaded before any code runs ctx.blockConcurrencyWhile(async () => { // Load persisted properties await this._initializePersistedProperties(); // Call the initialize method after persisted properties are loaded await this.onInit(); }); } else { // @ts-ignore - This is handled internally by the framework super(); this.storage = new Storage(undefined); this.alarms = new Alarms(undefined, this); // Initialize the persisted values map this[PERSISTED_VALUES] = new Map(); } // Set a default identifier if none exists if (!this.identifier) { this.identifier = DEFAULT_ACTOR_NAME; } } /** * Initializes the persisted properties table and loads any stored values. * This is called during construction to ensure properties are loaded before any code uses them. * @private */ async _initializePersistedProperties() { await initializePersistedProperties(this); } /** * Persists a property value to the Durable Object storage. * @param propertyKey The name of the property to persist * @param value The value to persist * @private */ async _persistProperty(propertyKey, value) { await persistProperty(this, propertyKey, value); } /** * Abstract method that must be implemented by derived classes to handle incoming requests. * @param request - The incoming request to handle * @returns A Promise that resolves to a Response */ async fetch(request) { throw new Error('fetch() must be implemented in derived class'); } /** * Lifecycle method that is called when the actor is initialized. * @protected */ async onInit() { // Default implementation is a no-op } /** * Lifecycle method that is called when the actor is notified of an alarm. * @protected * @param alarmInfo - Information about the alarm that was triggered */ async onAlarm(alarmInfo) { // Default implementation is a no-op } /** * Execute SQL queries against the Agent's database * @template T Type of the returned rows * @param strings SQL query template strings * @param values Values to be inserted into the query * @returns Array of query results */ sql(strings, ...values) { let query = ""; try { // Construct the SQL query with placeholders query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), ""); // Execute the SQL query with the provided values return [...this.ctx.storage.sql.exec(query, ...values)]; } catch (e) { console.error(`failed to execute sql query: ${query}`, e); throw e; } } async alarm(alarmInfo) { // Call user defined onAlarm method before proceeding await this.onAlarm(alarmInfo); if (this.alarms) { return this.alarms.alarm(alarmInfo); } return; } /** * Tracks the last access time of an actor instance. * @param idString The identifier of the actor instance to track. */ async track(idString) { if (TRACKING_ACTOR_NAME === idString) { throw new Error(`Cannot track instance with same name as tracking instance, change value to differ from "${TRACKING_ACTOR_NAME}"`); } const trackingStub = getActor(this.constructor, TRACKING_ACTOR_NAME); const currentDateTime = new Date().toISOString(); await trackingStub.__studio({ type: 'query', statement: 'CREATE TABLE IF NOT EXISTS actors (identifier TEXT PRIMARY KEY, last_accessed TEXT)' }); await trackingStub.__studio({ type: 'query', statement: `INSERT INTO actors (identifier, last_accessed) VALUES (?, ?) ON CONFLICT(identifier) DO UPDATE SET last_accessed = ?`, params: [idString, currentDateTime, currentDateTime] }); } /** * Destroy the Actor by removing all actor library specific tables and state * that is associated with the actor. * @param _ - Optional configuration object * @param _.trackingInstance - Optional tracking instance name * @param _.forceEviction - When true, forces eviction of the actor from the cache * @throws Will throw an exception when forceEviction is true */ async destroy(_) { // If tracking instance is defined, delete the instance name from the tracking instance map. if (this.identifier) { try { const trackerActor = getActor(this.constructor, TRACKING_ACTOR_NAME); if (trackerActor) { await trackerActor.sql `DELETE FROM actors WHERE identifier = ${this.identifier};`; } } catch (e) { console.error(`Failed to delete actor from tracking instance: ${e instanceof Error ? e.message : 'Unknown error'}`); } } // Remove all alarms & delete all the storage await this.ctx.storage.deleteAlarm(); await this.ctx.storage.deleteAll(); if (_?.forceEviction) { // Enforce eviction of the actor. When forceEviction is true, the actor will be destroyed // and the worker will be evicted from the cache. This will throw an exception. this.ctx.abort("destroyed"); } } } /** * Static method to configure the actor. * @param options * @returns */ Actor.configuration = (request) => { return { locationHint: undefined }; }; /** * Creates a handler for a Worker or Actor. * This function can handle both class-based and function-based handlers. * @template E - The type of the environment object * @param input - The handler input (class or function) * @param opts - Optional options for integration features * @returns An ExportedHandler that can be used as a Worker */ export function handler(input, opts) { // If input is a plain function (not a class), wrap it in a simple handler if (typeof input === 'function' && !input.prototype) { return { async fetch(request, env, ctx) { const handler = input; const result = await handler(request, env, ctx); return result; } }; } // Handle existing Worker and DurableObject cases const ObjectClass = input; // Check if it's a Worker (has a no-arg constructor) if (ObjectClass && ObjectClass.prototype instanceof Entrypoint) { return { async fetch(request, env, ctx) { const instance = new ObjectClass(ctx, env); return instance.fetch(request); } }; } // For Actor classes, automatically create the worker if Actor is being used as an entrypoint if (ObjectClass.prototype instanceof Actor) { const worker = { async fetch(request, env, ctx) { try { const idString = await ObjectClass.nameFromRequest(request); // If no identifier is found or returned in `nameFromRequest` method, throw an error // to prevent attempting to access an instance that is invalid. if (idString === undefined) { return new Response(JSON.stringify({ error: "Internal Server Error", message: "Invalid actor identifier" }), { status: 500, headers: { "Content-Type": "application/json" } }); } const stub = getActor(ObjectClass, idString); // If tracking is enabled, track the current actor identifier in a separate durable object. if (opts?.track?.enabled) { try { await stub.track(idString); } catch (error) { console.error(`Failed to track actor instance: ${error instanceof Error ? error.message : 'Unknown error'}`); } } return stub.fetch(request); } catch (error) { return new Response(`Error handling request: ${error instanceof Error ? error.message : 'Unknown error'}`, { status: 500 }); } } }; return worker; } // If no class provided or it's not an Actor, return a more informative error return { async fetch(request) { return new Response("Invalid handler configuration. Please provide a valid Worker, Actor, or request handler function.", { status: 400 }); } }; } export function getActor(ActorClass, id) { const className = ActorClass.name; const envObj = env; const locationHint = ActorClass.configuration().locationHint; const bindingName = Object.keys(envObj).find(key => { const binding = env.__DURABLE_OBJECT_BINDINGS?.[key]; return key === className || binding?.class_name === className; }); if (!bindingName) { throw new Error(`No Durable Object binding found for actor class ${className}. Check update your wrangler.jsonc to match the binding "name" and "class_name" to be the same as the class name.`); } const namespace = envObj[bindingName]; const stubId = namespace.idFromName(id); const stub = namespace.get(stubId, { locationHint }); stub.setIdentifier(id); return stub; } //# sourceMappingURL=index.js.map