Actors/Packages/Postgres/API reference
@sigx/actors-pg · Preview

API reference#

Exports of @sigx/actors-pg v0.11.0.

Providers#

ExportReturnsRole
pgCluster(options)ClusterProvidersmembership + directory
pgMembership(pool, options?)ClusterMembershipliveness on the database clock
pgDirectory(pool, options?)ActorDirectorysingle-activation claims
pgStorage(options)ActorStorageetag-CAS text rows
pgReminders(options)ActorRemindersSKIP LOCKED durable reminders

Schema helpers#

TypeScript
// ensurePgSchema(pool: PgPoolLike, options?: { schema?: string }): Promise<void>
// pgSchemaSql(schema?: string): string

await ensurePgSchema(pool);                  // issues the DDL under an advisory xact lock
await ensurePgSchema(pool, { schema: 'app' });

console.log(pgSchemaSql());                  // returns it, for your migration tool

Encoding helpers#

pgText(value) and pgTextDecode(value) — the text encoding used for stored values, exported for tooling that needs to read or write the tables directly.

Types#

PgQueryable, PgPoolLike, PgClusterOptions, PgStorageOptions, PgRemindersOptions.

Table layout#

All under the configured schema, sigx by default.

SQL
state(type, key, etag, state, log)                  -- state is text, log is text[]
directory(actor_id, host_id, activation_id)         -- + directory_host_id index
hosts(host_id, descriptor, expires_at)
membership_version(id, version)
reminders(type, key, name, next_due, period_ms)     -- + reminders_due index

log holds the entries appendText adds since the last full save. ensurePgSchema() adds the column to an existing schema (ALTER TABLE … ADD COLUMN IF NOT EXISTS); a migration tool carrying pgSchemaSql() must run that DDL too.

Semantics worth knowing#

Etags are client-minted UUIDs, equality-compared only. Every CAS is a single statement whose row count is the verdict — no transactions, no advisory locks.

State is text, not jsonb — deliberately. Actor state may contain NUL, which jsonb rejects outright, so the column stores the encoded string verbatim and Postgres never parses it. A top-level array state therefore cannot be silently coerced into a Postgres ARRAY either.

Directory entries carry no TTL. There is one heartbeat per host, not per activation; entry validity is the owner's liveness in the membership view, and the storage etag CAS remains the integrity floor underneath.

Membership change detection compares host signatures, not just the version counter — a host that dies silently expires on the database clock without anyone bumping a version, and views still converge.

Push is best-effort. LISTEN rides a connection checked out of the pool; if the pool cannot dedicate one, or the connection drops, the poll is the guarantee and pollMs is the propagation bound.

Reminders commit before delivery. The advance-or-delete is committed before the handler runs, which is what makes delivery at-most-once and prevents catch-up bursts after an outage. A delivery that then fails is re-armed one tick out (next_due = now() + tickMs, where tickMs is ActorRemindersContext.tickMs — the host's reminderTickMs — one statement per batch) and reported through ActorRemindersContext.undelivered; a row the actor set again meanwhile is left as the actor set it.

Next steps#