Actors/Packages/Cloudflare
@sigx/actors-cloudflare · Preview

Cloudflare#

Cloudflare already guarantees a single instance of a Durable Object globally and serializes requests to it. That is the virtual-actor contract — so the platform is the cluster.

v0.11.0 MIT

Installation#

Terminal
pnpm add @sigx/actors-cloudflare

Why it is small#

This package needs none of the machinery @sigx/actors/cluster uses to rebuild that contract: no membership heartbeats, no activation directory, no HMAC-authenticated host-to-host mount.

There is no HMAC because a Durable Object stub is not network-reachable — holding the binding is the capability grant, and guards run once at the public edge. And there is no 421 retry because ref → object id is a pure function, so a mismatch is a config bug rather than a race.

Storage#

TypeScript
const storage = durableObjectStorage(state.storage);

DO storage is strongly consistent and single-threaded per object, so the runtime's etag compare-and-set holds without a transaction.

It implements appendText, so ctx.append writes one storage key per entry instead of a full save; a full ctx.save() compacts the log, and load replays it through applyEntry. The real DurableObjectStorage needs no configuration for this. A custom DurableStorage gets the append path only if it provides the optional list({ prefix }); without it every append is a full save.

Placement#

A ref maps to its Durable Object by name — the platform is the directory — so a DO-hosted actor has no placement strategy to apply. placement on such an actor is refused:

  • At compile time, through the app-bound defineActor, when the app installs durableObjects(...) or, in an app factory, durableObjectsHosted(). Both carry never as their placement type.
  • At dispatch otherwise. A strategy tagged backend: 'cluster' — every @sigx/actors/cluster policy — throws, naming the type and the strategy. Any other declared strategy logs one __DEV__ warning per type.

Reminders#

TypeScript
const reminders = durableObjectReminders({
    storage: state.storage,
    alarms: state.storage,
    blockConcurrencyWhile: (fn) => state.blockConcurrencyWhile(fn),
});

export class ActorHost {
    async alarm() {
        await reminders.onAlarm();   // fire what is due, re-arm the rest
    }
}

The default shardedReminders() splits one table into fixed hash shards and polls it, because a host holds many actors and has to find whose reminder is due. A DO holds exactly one, so there is nothing to search and nothing to poll — reminders live in the object's own storage and the platform wakes it at the earliest due time.

The visible consequence: an alarm fires at the due time, where shardedReminders() promises only "at or after nextDue".

A dispatch that fails — a deadline, an onReminder that threw — is re-armed one reminderTickMs out and counted in HostStats.remindersUndelivered, so a failing target costs at most one attempt per tick. The rules for a reminder the actor set or cleared meanwhile are the shared ones.

Client sockets#

Browsers can reach actors over a WebSocket here, terminating in either half:

TypeScript
createWorkerHandler({ ..., socket: {} });                        // in the Worker
createWorkerHandler({ ..., socket: { terminate: 'object' } });   // in the object

Worker-terminated gives one multiplexed socket per client, reaching every actor — the right shape for a dashboard. Object-terminated gives one socket per actor, accepted with the hibernation API inside the object that owns it — the room pattern, and the mode where a disconnect actually releases the activation, because teardown happens locally instead of dying at the stub.fetch boundary.

Pair either with socketTransport() on the client. The deployment guide has the choice in full, plus the hibernation contract.

The getting-started path#

Most apps do not touch the two seams directly — createHostDurableObject() and createWorkerHandler() assemble them. See Cloudflare Workers for the full entry, the wrangler.jsonc and the three gotchas that are each worth their own callout.

Next steps#