The package exports one server entry point plus database, wake-up, and browser subpaths. TypeScript declaration files remain the source of truth for exact generic signatures; this index explains the supported role of every export.
configure(options): create the process defaultSolidObjectsRuntime.createRuntime(options): create an isolated runtime without changing the default.SolidObjectsRuntime: installation, registration, supervision, and manager owner. The normal lifecycle isinstall(),run(signal), thenclose().snapshotWithIncarnation(reference)returns the same authorized fields assnapshot(). It adds the read instance'sinstanceId,revision, andcreatedAtMsfrom that identical read. A caller can therefore fence a derived write, such as a downstream projection, against a stale or superseded actor incarnation.createdAtMsorders incarnations to the millisecond. See Limitations and non-goals for the same-millisecond boundary.Actor: base class providingref(),actorId,currentMessage,observables(),reject(),emit(),transmit(),commitAction(),schedule(),sendTo(), and protected lifecycle hooks.broadcastValue(value): mark an observable so its changed value enters the durable invalidation envelope.broadcastInvalidation(value): compare the real observable value but put only its name in the durable envelope when it changes.ObservableBroadcast: the immutable marker type returned by either helper.VERSION: running package version.reference.live: read-only live signals for an actor, enabled by thesolid-objects/signalsentry point documented below.ActorClass,ActorReference,ActorMessageSender,ActorSnapshot,ActorOperationNames,ActorQueryNames,StagedOperations, andScheduledOperations: inferred actor-class and fluent-dispatch types.SnapshotWithIncarnation: the{ snapshot, instanceId, revision, createdAtMs }shape returned bySolidObjectsRuntime.snapshotWithIncarnation.MessageReference: immutable durable message identity withid,requestId, actor identity,sequence,status(),result(), andwait().InvocationOptions,AsyncInvocationOptions,SnapshotOptions, andDestroyOptions: the options for authorization, idempotency, time, and schedule that the reference methods use.
ActorIntents, EffectIntent, CommitActionIntent, ReminderIntent,
OutboundMessageIntent, ReminderOptions, OutboundMessageOptions,
PayloadBroadcasts, and PayloadBroadcastValue describe actor-declared
transactional work and typed personalized projections.
observables() returns a flat object. Unwrapped values are invalidation-only:
their real values participate in change detection, but only their names enter
the durable envelope. Use an explicit marker when wire behavior matters:
override observables(): Record<string, unknown> {
return {
version: broadcastValue(this.document.version),
sidebar: broadcastInvalidation(this.sidebarForCurrentState()),
}
}Both values must be JSON-compatible. The runtime evaluates them after each
successful turn. An invalidation-only value takes part in change detection, but
the runtime never writes it to the broadcast outbox or the invalidation
envelope. The envelope carries its name in invalidations. A component registry
can then refresh a reauthorized endpoint, and the value stays private.
MessageReference does not retain an invocation's authorization context.
Supply authorizationContext to each status(), result(), and wait() call;
the runtime reauthorizes the persisted operation every time. Durable results
are JSON, so an operation that returns undefined or is declared void
resolves as null.
Declare named payload return shapes with a type alias rather than an
interface. PayloadBroadcastValue requires the implicit string index
signature of a JSON object, which TypeScript gives object type aliases but not
interfaces.
Snapshots return DeepReadonly, so application helpers should accept readonly
structure rather than cast it away. A helper that only needs a session ID can
preserve its useful result type with a generic boundary:
function playerForSession<PlayerType extends { sessionId: string }>(options: {
room: { readonly players: readonly PlayerType[] }
sessionId: string | null
}): PlayerType | undefined {
return options.room.players.find((player) => player.sessionId === options.sessionId)
}A reminder is one alarm per actor and name. If you schedule a name that is already armed, the runtime moves the existing alarm. It does not add a second one. A reminder is therefore safe to re-arm from a handler that can run more than once.
Without a key, that name is the operation. One actor then holds one alarm per operation. If you arm one alarm per queued item, only the last one remains:
// Wrong. Every entry overwrites the previous entry's alarm.
add({ entry }: { entry: Entry }): void {
this.entries = [...this.entries, entry]
this.schedule({ at: new Date(entry.waitUntil) }).deliver!()
}Pass key when an actor is waiting on several things at once. The key is your
own identifier for the item and names that item's alarm, so each item gets one:
add({ entry }: { entry: Entry }): void {
this.entries = [...this.entries, entry]
this.schedule({ at: new Date(entry.waitUntil), key: entry.id }).deliver!()
}Scheduling the same key again moves that item's alarm and leaves the others alone. The operation still decides which handler runs; the key only decides which alarm is which.
A key must be non-empty, and the name it composes must fit the 255 characters MySQL holds it in. That is checked on the composed name rather than the key alone, so a long operation with a short key is caught too. A key may hold colons of its own, because an actor member name cannot.
An actor that only needs to know "what is next" can still keep one alarm and
drain everything that is due when it fires. That costs one row instead of one
row per item. It also cannot strand an entry when the runtime coalesces an
occurrence. Prefer it for a large queue of interchangeable items. Prefer key
when one item needs an alarm that you can move on its own.
Every manager below is available as a property on SolidObjectsRuntime; the
class and result types are also exported for integration typing.
runtime.deadLetters/DeadLetterManager:all()and idempotentretry().runtime.reminders/ReminderManager: cursor-paginatedall()and idempotent paused-alarmresume().runtime.processes/ProcessManager: immutable roleall()and stale-ownercleanup().runtime.administration/AdministrationManager: an authorizedprocesses()query for inspecting live and stale process rows through the runtime's own database adapter.runtime.reconciliation/ReconciliationManager:active(),withoutPendingWork(),statesFor(), andorphaned()bounded reads.runtime.retention/RetentionManager:preview()and authorizedprune()for messages, instances, or processes.runtime.doctor/Doctor:run({ roundTrip })structured installation report.runtime.testing/SolidObjectsTestHelper: deterministicdrain()and explicit-timerunDueReminders(), plus dependency-orderedreset().runtime.realtime/RealtimeManager:connect(), process-localpublish(), andclose().
The manager types are DeadLetter; ReminderPage, ReminderPageOptions,
ReminderRecord, ReminderStatus, and ResumeReminderOptions;
ProcessCleanupResult, ProcessMetadata, ProcessRecord, and
ProcessShutdownState; DoctorCheck, DoctorOptions, DoctorReport, and
DoctorStatus; OrphanedReconciliationOptions,
QuietReconciliationOptions, ReconciliationInstance, ReconciliationPage,
ReconciliationPageOptions, and ReconciliationStatesOptions;
RetentionOptions, RetentionResult, and RetentionTarget; and
RunDueRemindersOptions, TestDrainOptions, and TestHelperRole.
RealtimeConnectionOptions,
RealtimeSession, and SubscriptionRequest define the server session API.
AdministrationOptions carries the application-owned authorization context
for administration calls.
The packaged solid-objects quickstart command is config-free and runs the
SQLite example shipped in the npm artifact. Every other CLI command loads the
application runtime configured through --config.
ProcessRecord.shutdownState is "running", "draining", or "stopped";
there is no separate running field. RetentionResult.count means eligible
rows for preview() and rows actually deleted for prune().
runtime.register(ActorClass): validate and register an actor definition.runtime.ref(ActorClass, actorId): register and address an actor in an isolated runtime.runtime.registerEffect(name, handler): register an at-least-once external effect handler.EffectContextcarries the stable effect ID and source identity. Success operations receive{ effectId, arguments, result }and failure operations receive{ effectId, arguments, error };argumentsis the JSON object originally staged byemit().runtime.registerCommitAction(name, handler): register a same-database fenced transaction handler.CommitActionContextincludes the activeDatabaseConnection.guardApplicationDatabase(database): return a facade that rejects writes from actor-owned execution contexts.parseSubscriptionRequest(value): validate the server-side JSON subscribe or unsubscribe request before session routing.runCli(arguments, options)andCliRunOptions: embed and configure the packaged command implementation; applications normally invoke thesolid-objectsexecutable instead.
SolidObjectsConfiguration, AuthorizationInput,
DestroyAuthorizationInput, AdministrationAuthorizationInput,
SubscriptionAuthorizationInput, BroadcastEvent, and
InstrumentationEvent type the host integration contract. JsonPrimitive,
JsonValue, JsonObject, DeepReadonly, ActorIdentifier, MessageContext,
MessageStatus, and Logger are shared types. Database,
DatabaseConnection, DatabaseFamily, and RunResult support custom database
and commit-action integration.
BroadcastEvent.observables contains changed value-broadcast projections.
BroadcastEvent.invalidations contains changed invalidation-only names. The
runtime always supplies the array; consumers should treat its absence from an
older or application-produced event as an empty array.
runtime.registerComponent(factory, { count = 1 }) adds application-owned
supervised roles. Each factory must return a LongRunningComponent:
interface LongRunningComponent {
run(signal: AbortSignal): Promise<void>
requestShutdown(): void
stopped(): boolean
stop(): void | Promise<void>
}The runtime creates count independent instances, replaces an instance whose
run() settles unexpectedly, and stops replacement before graceful shutdown.
Factories should create fresh mutable state and stop() should be idempotent.
Worker, EffectWorker, ReminderScheduler, and BroadcastWorker are
exported for test runners and hosts that intentionally operate roles outside
runtime.run(). Runtime factory methods create the same classes. Each provides
runOnce(), bounded runUntilIdle(), run(signal), requestShutdown(),
stopped(), stop(), and the inspectable
currentPollingIntervalMilliseconds. Manual roles still register process
ownership and must be stopped. Prefer runtime.run() in production and
runtime.testing in tests.
InProcessWakeUpAdapter, WakeUpAdapter, WakeUpRole, WakeUpWatch, and
WakeUpWaitOptions define the notification extension. A watch must be obtained
before checking durable state so a notification cannot fall between claim and
wait. WakeUpWatch.wait() returns true for a notification and false for a
timeout or cancellation. A legacy void result remains accepted and preserves
the fast polling cadence.
The root exports SolidObjectsError and its supported subclasses:
- policy and caller outcomes:
Unauthorized,Rejected,ActorDestroyed,SyncEnqueueTimeout,SyncTimeout,SyncInsideTransaction, andMessageFailed; - admission and payload failures:
MailboxFull,InvalidPayload,PayloadTooLarge,IdempotencyConflict,InvalidPayloadBroadcast, andUnknownPayloadBroadcast; - definition and execution failures:
InvalidActor,InvalidRejectionCode,UnknownActorType,UnknownOperation,ActorCallCycle,QueryMutatedState,StateMigrationError,ApplicationWriteForbidden,UnknownEffect, andUnknownCommitAction; - operational failures:
LostActivation,DatabaseDeadlineExceeded,UnknownDeadLetter,UnknownReminder,ReminderNotPaused, andUnsupportedDatabase.
NonRetryableError is the application subclassing point. SyncTimeoutDetails
and SyncTimeoutWaitingOn type timeout diagnostics. See
Errors and recovery before deciding what to catch.
sqlite(options): constructSQLiteDatabase.SQLiteDatabase:Databaseimplementation andclose()owner.SQLiteDatabaseOptions: path, busy timeout, and lock retry options.
sqliteWasm(options): constructSQLiteWasmDatabaseasynchronously. The first call loads the@sqlite.org/sqlite-wasmmodule.SQLiteWasmDatabase:Databaseimplementation on SQLite WASM andclose()owner. It runs in a browser and in Node. One host owns the database file; the adapter serializes access on one connection.SQLiteWasmDatabaseOptions:pathplus astoragemode."temporary"(the default) keeps data for the life of the process or page."persistent"stores data in the browser origin's OPFS through the SQLite SAH pool VFS, and fails fast where OPFS is unavailable.
Many browser tabs, one database, no visible infrastructure. Every tab
constructs the same shared database and runs an ordinary
configure → install → ref flow; the adapter hides the coordination. The
Web Locks API elects one holder per origin. The holder opens the real
SQLite WASM database; every other instance sends its SQL over a
BroadcastChannel session to the holder, through the same serialized
access queue. When the holder dies, the lock releases, the next instance
opens the pool, and the runtime's leases and fencing arbitrate the tabs'
workers exactly as they arbitrate Node processes.
sharedSqliteWasm(options): constructSharedSQLiteWasmDatabase.SharedSQLiteWasmDatabase:Databaseimplementation with arole()probe (connecting,holder, orremote) andclose().SharedSQLiteWasmDatabaseOptions:path, an optional electionname(defaults to the path), thestoragemode (persistent by default except for:memory:), and the request, retry, session-idle, and open-attempt tuning knobs.SharedDatabaseFailover: the retryable rejection an in-flight statement receives when the holder changes mid-operation. A session that has not executed a statement yet retries automatically; anything later surfaces, because replaying partially executed work is not safe.SharedDatabaseUnavailable: the rejection for a closed instance, a timed-out request, or an idle session the holder reclaimed.
A transaction that dies with its holder rolls back with the pool, which is
the same at-least-once story as a crashed Node process. Sessions that stay
idle longer than sessionIdleTimeoutMilliseconds (default 10 seconds) are
reclaimed so a dead tab cannot hold the database hostage.
postgresql(options): constructPostgreSQLDatabase.PostgreSQLDatabase: pooledDatabaseimplementation withwakeUp().PostgreSQLDatabaseOptionsandPostgreSQLDatabaseWakeUpOptions: pool and notification configuration.postgresqlWakeUp(options)andPostgreSQLWakeUpAdapter: standaloneLISTEN/NOTIFYwake-up integration.PostgreSQLWakeUpOptionsandPostgreSQLWakeUpFailure: listener options and failure callback data.
mysql(options): constructMySQLDatabase.MySQLDatabase: pooledmysql2Databaseimplementation.MySQLDatabaseOptions: pool configuration.mysqlSql(sql): translate the portable conflict syntax used by custom database integrations.
redisWakeUp(options): constructRedisWakeUpAdapter.RedisWakeUpAdapter: optional Pub/Sub latency layer.RedisWakeUpOptionsandRedisWakeUpFailure: connection, channel, timeout, and failure callback types.
SolidObjectsBrowserClient: connect, subscribe, unsubscribe, receive, and close a versioned WebSocket client without Node imports.BrowserClientOptions,ActorSubscription,InvalidationEnvelope,PayloadEnvelope, andRealtimeEnvelope: browser transport types.parseInvalidation(value)andparseRealtimeEnvelope(value): validate and deeply freeze received JSON for custom transports.SolidObjectsComponentRegistry: register keyed observable dependencies, coalesce refreshes, abort superseded work, fence application, and close.ComponentRegistration,RegisteredComponent,ComponentRefreshStrategy,ComponentRefreshRequest,ComponentRefreshResult,ComponentApplication,ComponentRefreshFailure, andComponentRegistryOptions: framework-neutral refresh contract types.
InvalidationEnvelope.observables contains values and
InvalidationEnvelope.invalidations contains names without values. The
component registry reacts to names in either location.
The wire format, trust boundary, revision rules, and component semantics are in Browser protocol.
The entry point for a runtime host inside a browser worker. An import of this module registers the browser platform: a turn-scoped context store and a browser host identity. Do not import it in the same process as the Node entry points; the last registration wins.
- Re-exports
Actor,broadcastInvalidation,broadcastValue,configure,createRuntime,SolidObjectsRuntime, andVERSIONfrom the core, andsqliteWasm,SQLiteWasmDatabase, andSQLiteWasmDatabaseOptionsfrom the WASM adapter, so a worker needs one import. - The turn-scoped context store expects serialized actor turns. One worker hosts one runtime. A page talks to that worker through messages, not through direct actor references.
- The store scopes only the synchronous part of a callback and restores
the previous scope in strict stack order, so an interleaved task never
observes another turn's scope. The cost of that isolation: after the
first
awaitinside an actor operation,currentActor(),applicationWritesForbidden(), and the database deadline read as unset. Keep guarded application-database writes in synchronous actor code or in commit actions; Node keeps fullAsyncLocalStoragepropagation. - Alarms and reminders fire only while the hosting worker is alive.
Many tabs, one runtime. Each tab starts a candidate host; the Web Locks API
elects one leader per origin. The leader starts the runtime, runs its
workers, and serves invocations from every tab over a BroadcastChannel.
When the leader's tab dies, the lock releases and the next host promotes.
startTabHost(options): join the election.TabHostOptionscarries the electionnameand astartRuntimecallback; the callback runs only on promotion, so a follower never opens the database. It returns aTabHostRuntimeHandlewith the runtime and an optionalclose.TabHost:role(),leadership()(a promise that resolves on promotion), andclose().connectTabClient(options): connect from any tab.TabClientOptionscarries the electionnameplus retry and timeout intervals.TabClient.invoke(invocation): send aTabInvocation(actorType,actorId,operation,arguments). The client retries until a leader answers; the leader enqueues with the request id as the idempotency key, so a resend applies once.TabInvocationTimeoutandTabInvocationFailed: the client-side errors.
The election needs the Web Locks API. Every current browser provides it;
Node provides navigator.locks from 24.5, so Node-side use of this module
needs a newer Node than the package floor. startTabHost fails fast with a
clear error where the API is missing.
A tab dies without a clean shutdown, so failover speed follows the fence
settings. Give the browser runtime a short leaseDurationMilliseconds and
processAliveThresholdMilliseconds (for example 750), with a
leaseRenewalIntervalMilliseconds below the lease (for example 250), so a
new leader reclaims a dead tab's activations before sync invocations time
out. When startRuntime fails, close the database in a catch block; an
open SAH pool otherwise blocks the next candidate until the worker dies.
The transactional outbox bridge between a local runtime and a server
runtime. An actor stages a transmit intent with this.transmit()
in the same transaction as its state change. The effect worker drains the
outbox with at-least-once delivery, per-actor order, and retry backoff.
-
actor.transmit(): the fluent staging surface.this.transmit().increment( { amount })stages a transmit intent that replays the operation on the server twin of the same actor, in the same transaction as the local state change. -
TRANSMIT_EFFECT: the effect name (solid-objects.transmit) underneathtransmit(). Stage it directly withemit()when the target differs from the source: the staged arguments holdoperation,arguments, and an optional targetactorTypeandactorId. -
registerTransmit(options): register the drain handler on the local runtime.RegisterTransmitOptionscarries the runtime and adelivercallback that carries aTransmitEnvelopeto the server; throw fromdeliverwhile offline and the effect retries with backoff. Give a browser runtime a generousmaxAttempts; an effect that exhausts its attempts during a long offline period lands in dead letters, andruntime.deadLetters.retryre-queues it. -
receiveTransmitEnvelope(options): idempotent server ingest.argumentsis optional in the envelope and defaults to an empty object, matching the staging side and the Ruby ingest. It enqueues an internal message withtransmit:<effectId>as the idempotency key, so a replayed envelope applies once. The host must authenticate the sender before this call; internal delivery skipsauthorizeMessage. The call belongs inside whatever route the host application gives the transmit callback to post to:import { IdempotencyConflict, InvalidPayload, receiveTransmitEnvelope } from "solid-objects" async function handleSyncRoute(request: Request): Promise<Response> { const sender = await authenticate(request) if (!sender) return new Response("Forbidden", { status: 403 }) try { await receiveTransmitEnvelope({ runtime, envelope: await request.json() }) return Response.json({}) } catch (error) { if (error instanceof InvalidPayload || error instanceof IdempotencyConflict) { return new Response(null, { status: 422 }) } throw error } }
The example uses a Fetch-style handler; any HTTP framework works. The 422 matters: it tells the sending outbox to dead-letter the effect instead of retrying it.
InvalidPayloadmarks a malformed envelope, andIdempotencyConflictmarks a replay whose arguments changed; both are permanently unappliable, and a 500 would make the outbox retry them forever. -
Per-actor order comes from an ordered drain: a claimed transmit effect transmits every undelivered envelope for its actor up to its own mailbox sequence, oldest first. A duplicate transmission is safe; the server deduplicates by effect id. Run one effect worker per local runtime for the order guarantee.
-
InvalidTransmitEnvelope: the non-retryable rejection for malformed staged arguments; the effect dead-letters instead of retrying forever.
The tab host and transmit modules are browser-safe and also run in Node.
The transmit wire contract is shared with the Ruby gem
(solid-objects-ruby#49);
compatibility/transmit-envelopes.json pins it in both repositories.
Live signals: the read-side adapter on the proposed standard JavaScript
signals API. One side-effect import enables reference.live:
import "solid-objects/signals"
const counter = Counter.ref("page-hits")
counter.live.count // a read-only signal of the broadcast observable
counter.live.snapshot // a read-only signal of the authorized snapshotOne property, three tenses: snapshot.count is one committed read,
await counter.count asks the actor now, and counter.live.count stays
current. From Lit, watch(counter.live.count) with the SignalWatcher
mixin renders it with no further glue; any consumer of the standard
signals API composes the same way.
configureLiveSignals(options): tune the lifecycle.LiveSignalsConfigurationcarrieslingerMilliseconds(default one second): how long a signal with no watchers keeps its subscription before the session closes; andretryMilliseconds(default one second): how long a still-watched signal waits before it retries a denied or failed subscription.activeLiveSubscriptionCount()andliveEntryCount(runtime): open sessions and live per-actor entries, for diagnostics and leak tests. The cache holds entries through weak references: one canonical entry per actor for as long as any proxy or signal for it is reachable, so two references to one actor can never open competing subscriptions, and an entry whose signals are all garbage-collected leaves the cache with them.ActorLiveSignalsandLiveSignal: the structural types onreference.live.LiveSignalexposes onlyget(), so the package types never require the optional peer; at runtime every signal is a standardSignal.Computed, read-only by construction.
Behavior:
- A signal subscribes its actor through an in-process
runtime.realtimesession when the first watcher arrives and closes the session after the linger when the last watcher leaves. The subscription authorizes throughauthorizeSubscriptionwith an undefined authorization context. - Value-broadcast observables set their named signals from each
envelope. Invalidation-only observables stay
undefinedby design;live.snapshotre-fetches the authorized snapshot (coalesced) on every accepted envelope, so private-value flows read from there. - Personalized payload projections arrive as
live.payloads.<name>signals. A newly watched payload name re-sends the subscription with the grown name list, and each payload keeps the independent per-name revision fence the wire protocol gives it. Payloads evaluate under the live session's authorization context. - Envelopes apply only on a monotonic revision advance for the same instance, the same fence the browser client uses.
snapshotandpayloadsare reserved names onlive; observables with those names are shadowed.signal-polyfillis an optional peer dependency. Nothing loads it until thesolid-objects/signalsentry is imported;reference.livethrows a pointer to that import otherwise.
createDashboard(options)creates an immutableSolidObjectsDashboardwith a standardfetch(request, context)entry point.createNodeDashboardHandler(options)adapts the Fetch entry point tonode:httpand Connect-compatible middleware.DashboardOptionsselects the runtime, mount path,DashboardAccess, chart library,DashboardExtensionobjects, andDashboardMiddlewarefunctions.DashboardRequestContextsupplies the existing administration authorization context and an optionalDashboardSession. Read/write access requires the session, because itsread()andwrite()methods hold the masked CSRF token across requests. Read-only modes create no CSRF state.DashboardRoute,DashboardRouteContext,DashboardPolicy,DashboardPage, andDashboardTabdefine extension pages. Every route requires a policy.DashboardRenderer,DashboardRenderInput, andDashboardMiddlewareInputdefine immutable view overrides and middleware inputs.DashboardChartLibraryselects the CDN, a self-hosted script, or disabled charts.NodeDashboardHandler,NodeDashboardHandlerOptions, andNodeDashboardRequestContextResolverdescribe the Node adapter.SolidObjectsDashboardContractis the minimal Fetch contract accepted by the Node adapter.
Mounting, authorization actions, CSRF behavior, pages, and extensions are in Operator dashboard.