configure(options) creates a runtime and installs it as the default used by
Actor.ref(). createRuntime(options) creates an isolated runtime addressed
through runtime.ref(ActorClass, actorId). Both validate options immediately.
| Option | Default | Contract |
|---|---|---|
database |
required | A Database adapter. |
tableNamePrefix |
"solid_objects_" |
Lowercase letters, digits, and underscores; must start with a letter. |
pollingIntervalMilliseconds |
100 |
Positive durable-work polling interval. |
idlePollingIntervalMilliseconds |
1_000 |
Positive ceiling after consecutive empty polling passes. |
syncPollingIntervalMilliseconds |
50 |
Positive result-wait polling interval. |
leaseDurationMilliseconds |
30_000 |
Positive activation lease; must exceed renewal interval. |
leaseRenewalIntervalMilliseconds |
10_000 |
Positive activation renewal cadence. |
idleDeactivationTimeoutMilliseconds |
30_000 |
Non-negative hydrated activation cache lifetime; 0 disables idle retention. |
maxMailboxLength |
10_000 |
Positive maximum ready and claimed messages for one actor. |
maxPayloadBytes |
1_048_576 |
Positive byte limit for operation arguments and personalized payloads. |
maxStateBytes |
5_242_880 |
Positive persisted actor-state byte limit. |
warnStateBytes |
131_072 |
Positive persisted actor-state byte threshold that reports one warning. |
maxResultBytes |
1_048_576 |
Positive operation-result byte limit. |
maxAttempts |
5 |
Positive attempt limit evaluated on handler failure. |
maxMessagesPerActivationPass |
50 |
Positive integer turn budget before fairness yield. |
maxActivationDurationMilliseconds |
5_000 |
Positive elapsed-time budget before fairness yield. |
claimScanLimit |
100 |
Positive integer ready-actor candidates considered per global claim. |
retryDelayMilliseconds |
exponential 1-60 seconds | Receives a retry index; returns milliseconds (see below). |
The limits measure normalized JSON encoded as UTF-8. maxResultBytes also
limits each computed snapshot getter and effect-handler result. Explicit
observables() values are validated as JSON but do not currently have a
separate aggregate byte limit. This includes values wrapped in
broadcastInvalidation(): they are compared in memory even though only their
names enter the broadcast outbox. retryDelayMilliseconds should return a
non-negative finite number; an invalid application callback will fail the
affected failure path rather than schedule an invalid timestamp.
Interrupted actor messages use the existing retryDelayMilliseconds callback
on recovery. After the first claimed attempt they are immediately eligible;
after persisted attempts 2, 3, 4, ... the callback receives 1, 2, 3, ... . Its
default delays are 1, 2, 4 seconds up to 60 seconds. This is the total claim count,
including earlier handler failures, not a separate lease-loss counter. The
availability timestamp is stored transactionally in ready membership, survives
worker replacement and stale-process cleanup, and keeps the message ahead of
later messages for that actor. Other actors remain eligible. Custom delays,
including zero, remain authoritative.
Lease loss alone never creates a dead letter, including with maxAttempts: 1.
A handler failure still uses the full attempt count with maxAttempts and
passes that count to retryDelayMilliseconds, as before. Applications needing
a hard retry or elapsed-time bound must observe activation.lost and stop or
intervene. No setting or schema change is required.
maxStateBytes is a hard limit that fails the turn. warnStateBytes is a soft
threshold that keeps the turn. A commit above the threshold reports one
solid_objects.state.large instrumentation event with the actor type, the actor
ID, the byte count, and the threshold. The runtime reports it only after the
commit succeeds, so a turn that rolls back stays silent. The event holds no
application state.
The runtime measures the size only when an instrumentation callback is
configured. Throughput falls as the persisted state grows, so treat the warning
as an instruction to divide the actor. See
State size and throughput.
| Option | Default | Contract |
|---|---|---|
workerCount |
1 |
Non-negative actor workers. |
effectWorkerCount |
1 |
Non-negative effect workers. |
broadcastWorkerCount |
1 |
Non-negative broadcast workers when realtime is enabled. |
reminderSchedulerCount |
1 |
Non-negative reminder schedulers. |
processHeartbeatIntervalMilliseconds |
15_000 |
Positive persisted heartbeat cadence. |
processAliveThresholdMilliseconds |
60_000 |
Positive age after which an owner is stale. |
shutdownTimeoutMilliseconds |
15_000 |
Positive shared graceful-shutdown budget. |
supervisorRestartDelayMilliseconds |
100 |
Positive initial failed-role replacement delay. |
supervisorMaximumRestartDelayMilliseconds |
10_000 |
Positive cap no smaller than the initial delay. |
wakeUp |
"automatic" |
A name or an adapter implementing watch, notify, and close. |
logger |
console methods | Structured debug, info, warn, and error sink. |
Counts may be zero, but the complete configuration must leave at least one
runtime role enabled. Broadcast workers are started only when broadcast or
authorizeSubscription is configured.
pollingIntervalMilliseconds is the fast interval after work or a wake-up.
Consecutive empty passes double it up to
idlePollingIntervalMilliseconds. Actor workers never wait longer than
leaseRenewalIntervalMilliseconds. Set the fast and idle values equal for a
fixed cadence. A custom wake-up adapter must return true for a notification
and false for a timeout. An older adapter that returns void remains
compatible and keeps the fast cadence. Wake-ups reduce latency, while database
polling remains the correctness path.
Busy SQL workers and testing drains give the event loop a turn between passes.
In Node this lets timers and input run even with synchronous SQLite calls.
An instrumentation callback can abort runtime.run() before its next pass.
This does not interrupt a synchronous handler: responsiveness is bounded by
one pass, or by a single overlong handler that must return first.
| Option | Default | Contract |
|---|---|---|
retentionIntervalMilliseconds |
3_600_000 |
Non-negative automatic history-pruning cadence; 0 disables it. |
deadProcessCleanupIntervalMilliseconds |
60_000 |
Non-negative stale-owner recovery cadence; 0 disables it. |
messageRetentionMilliseconds |
30 days | Positive default completed-message retention. |
messageRetentionByActorType |
{} |
Positive per-type message-retention overrides. |
instanceRetentionByActorType |
{} |
Positive per-type instance-expiration opt-ins. |
processRetentionMilliseconds |
7 days | Positive stopped-process retention. |
pruneBatchSize |
1_000 |
Positive integer maximum rows rechecked per pruning transaction. |
retainedIdempotencyKeys |
64 |
Positive integer idempotency keys an actor remembers. |
retainedIdempotencyKeysBytes |
16_384 |
Positive integer serialized size those keys must fit. |
redriveBatchSize |
100 |
Positive integer rows a redrive moves per transaction. |
redriveBatchPauseMilliseconds |
50 |
Non-negative pause between redrive batches. |
administrationIdentity |
String(context) |
Names the operator recorded in an administration event. |
Automatic retention prunes messages and stopped process records. Actor instance expiration remains an explicit administration action even when a type opts in.
All authorization callbacks default to false:
authorizeMessage({ actorType, actorId, operation, arguments, authorizationContext })authorizeQuery({ actorType, actorId, operation, arguments, authorizationContext })authorizeDestroy({ actorType, actorId, authorizationContext })authorizeAdministration({ action, resource, resourceId, authorizationContext })authorizeSubscription({ actorType, actorId, authorizationContext })
Callbacks may return a boolean or promise. Authorization contexts are application-owned server values and are never persisted. See Authorization for the entry-point matrix.
instrumentation(event) is synchronous. The runtime freezes metadata-only
events, catches sink failures, and reports the sink error through logger.
broadcast(event) forwards committed realtime invalidations through an
application-owned shared transport. A receiving process calls
runtime.realtime.publish(event). It is unnecessary for a single-process
server.
DatabaseConnection.get<Row>() and all<Row>() trust the caller-supplied row
type; they do not validate or convert driver results. Type rows to the adapter's
runtime representation or normalize them at the application boundary.
| SQL value | SQLite (node:sqlite) |
PostgreSQL (pg) |
MySQL (mysql2) |
|---|---|---|---|
| ordinary integer | bigint |
number for int2/int4; bigint for int8 |
number for ordinary integer types |
| arbitrary precision integer/decimal | bigint for SQLite INTEGER |
numeric remains the driver's decimal string |
BIGINT and DECIMAL are strings |
| floating point | number |
number |
number |
| text | string |
string |
string |
| binary | Uint8Array |
Buffer |
Buffer |
| null | null |
null |
null |
RunResult.changes is always a number. lastInsertId, when an adapter
provides it, is a decimal string; SQLite and MySQL set it for nonzero generated
IDs, while PostgreSQL callers should use RETURNING with get().
sqlite({ path, timeoutMilliseconds = 5_000, lockRetryAttempts = 10, synchronous = "normal" })
uses Node's built-in node:sqlite. lockRetryAttempts is a positive integer.
The timeout controls SQLite's busy timeout; transaction acquisition retries use
short capped backoff only when no synchronous deadline is active.
A file database uses write-ahead logging (WAL). On its first access, the
adapter sets journal_mode = WAL and the synchronous level, with the same
retries as a transaction. WAL keeps two more files next to the database:
<path>-wal and <path>-shm. Committed data can be in the -wal file, so a
copy of the database file alone can miss it. To back up a database that is in
use, run VACUUM INTO '<backup path>' or use the SQLite backup command. Copy
the files only when no process has the database open. An in-memory database
(":memory:") keeps its memory journal.
synchronous is "normal" or "full":
"normal", the default, is the level that the Ruby gem gets from Active Record. A commit makes almost nofsynccall. A process crash loses no commit. A power loss or an operating system crash can lose the last commits, but the database stays consistent."full"flushes each commit to disk before the commit returns. A commit costs about onefsynccall, andnode:sqliteblocks the event loop while it waits for the disk.
postgresql(options) accepts:
| Option | Default |
|---|---|
connectionString |
required |
maximumConnections |
pg default |
idleTimeoutMilliseconds |
pg default |
connectionTimeoutMilliseconds |
pg default |
applicationName |
"solid-objects" |
onPoolError |
structured console.error |
wakeUp takes a name or an adapter, as config.cache_store does in Rails.
"automatic" is the default. It prefers SOLID_OBJECTS_REDIS_URL, then
PostgreSQL notifications, then polling. "in_process" opts out, "postgresql"
and "redis" force one, and an unknown name throws rather than polls quietly.
A requested adapter that the environment cannot provide polls instead and says
so. "postgresql" on a database with no notification channel, "redis" without
SOLID_OBJECTS_REDIS_URL, and a Redis URL without the redis package each log
solid_objects.wake_up.unavailable once and report the reason in the
capability. Only a name that does not exist is refused, because a typo cannot be
honoured at all.
Selection runs once per runtime, on first use. runtime.wakeUpCapability()
reports what it chose, whether that choice crosses processes, its measured
floor in milliseconds, and why. The doctor reports the same record, and the
polling-only warning fires on what was installed rather than on whether a
setting was set. An adapter that declares its own defaultCapability keeps it, so a
configured InProcessWakeUpAdapter still warns.
Automatic selection proves the PostgreSQL path before it chooses it. It listens
on a probe channel, sends one NOTIFY from a second connection, and waits up to
two seconds for it to arrive. A probe that does not deliver falls back to
polling and warns once, because LISTEN does not survive a transaction pooler
such as PgBouncer.
database.wakeUp(options) creates a dedicated notification adapter using the
same connection string. Its options are channelPrefix = "solid_objects",
applicationName = "solid-objects-wake-up", and onListenerError. Use a
direct or session-pooled connection because PostgreSQL LISTEN is
session-scoped.
postgresqlWakeUp({ connectionString, ...options }) constructs that adapter
without a database instance. The adapter translates portable ? parameters;
write ?? in SQL passed through DatabaseConnection when PostgreSQL needs the
literal JSON existence operator.
mysql(options) accepts:
| Option | Default |
|---|---|
connectionString |
required |
maximumConnections |
10 |
idleTimeoutMilliseconds |
60_000 |
queueLimit |
0 (unbounded driver queue) |
Counts are non-negative except maximumConnections, which is positive. Tables
use InnoDB. mysqlSql(sql) translates the limited portable conflict syntax
used by database integrations into MySQL syntax.
redisWakeUp({ url, channelPrefix = "solid_objects", connectionTimeoutMilliseconds = 1_000, onError }) uses separate lazy
publisher and subscriber clients. Connection, subscription, and publication
failures are reported and bounded; database polling continues.
Call runtime.install() before serving traffic or starting roles. Run the
supervisor with an application-owned abort signal. To shut down, abort, await
runtime.run(), then close adapters through runtime.close():
const controller = new AbortController()
const running = runtime.run(controller.signal)
controller.abort()
await running
await runtime.close()Calling close() while run() is active raises an error so database and
wake-up connections cannot disappear beneath live roles.