Build
Headless clients and Atom
Client.definition(contract, reducers) supplies applyEvent, an optimistic
reducer for every action, and matching Client.plugin registrations. Use an
identity reducer when an action has no optimistic behavior. Registration is
checked before resource acquisition. Root and plugin reducers receive only their
own public snapshot slot. Optional mergeSnapshot enriches newer snapshots;
mergeHistory enriches that slot without changing the cursor or settling commands.
Client.provide and providePlugin attach scoped Layers. They retain unsatisfied
Effect requirements, acquire plugins in declaration order, and finalize them in
reverse order. UI, authentication, and projection destinations remain application
concerns.
const session = yield * Client.make(definition, { persistence: { mode: "volatile" }, });const source = yield * session.open(contract.address("demo"));yield * source.ready;const result = yield * source.execute(contract.actions.set, { value: 7 });Construction requires Client.CurrentActor, Client.Transport, and Scope.
Persistent mode additionally requires ReplicaPersistence. Opening requires
Scope. Keep the session scope alive for
the authenticated actor. Close it before constructing a different actor’s session.
Effect scopes own actor, source, and lease lifetimes. Each open holds a source lease;
concurrent opens share a coordinator until the
last lease closes. Closed leases cannot dispatch, restore state, or publish messages.
Closing one lease cancels its in-flight operations while other leases keep the source
connected. Journal transactions already admitted finish atomically for that coordinator;
their command identities remain available for recovery.
source.read and source.changes expose the public replica, including the optimistic
value, authoritative position, provisional cache value, pending phases, failures,
connection state, and the last operational error.
Ordering and recovery
Section titled “Ordering and recovery”The client checks JSON through the contract’s Schema codecs. It applies contiguous
events exactly once, validates a whole batch before committing any part, and
recovers after a gap or queue overflow. Ordinary snapshots cannot rewind live
state or replace a different authority. An explicit Reset, or snapshot recovery
for an advertised ResyncRequired generation, can install a replacement authority.
Late cache loads remain provisional and cannot overwrite live state.
hydrate accepts a versioned snapshot using the same monotonic rules.
mergeObservation and applyHistory never advance the cursor or confirm commands.
messages, publishMessage, and publishPluginMessage are ephemeral and do not
enter the journal or advance a durable position. Slow view and message observers
use bounded, sliding buffers; view updates can coalesce. Durable transport frames
instead trigger disconnection and recovery if their buffer overflows.
Commands capture their Schema-encoded payload before transport I/O. The source’s
command gate serializes submissions and result lookups, including concurrent
retries. A declared rejection removes its overlay and appears in failures.
dismissFailure(id) only hides that terminal failure.
An operational ClientError retains commandId when an admitted outcome is
unknown. retry(id) first asks for the exact result. An unknown lookup can resend
the original envelope only within its known authority; expired results,
confirmed commands without a result, and ambiguous unbound commands remain
unresolved. It never allocates a replacement identity. A bare id may belong to any
source action, so retry exposes the union of their result and rejection types;
execute retains the chosen action’s exact types.
An event confirms only the authenticated (actorId, commandId) pair. Confirmation
removes the overlay while retaining ConfirmedAwaitingResult journal evidence.
A success received before its events retains its overlay until a correlated event
arrives or the authoritative cursor covers the accepted position. On authority replacement, unresolved old
commands remain visible as AuthorityChanged and require application reconciliation.
Defaults bound the runtime to 32 retained sources, 100 pending commands per source,
256 buffered frames, and eight consecutive connection attempts. Reconnection uses
250 ms exponential backoff capped at 30 seconds. Operations time out after ten
seconds. Independent 30-second snapshot probes detect missed durable progress;
automatic result reconciliation has a per-command attempt bound. Applications can
configure limits and retry; recover explicitly restarts a parked source and
its recovery budget. Applying a valid durable subscription frame resets the reconnect
budget and backoff; snapshot probes and ephemeral messages do not. Authentication
failures park instead of reconnecting forever.
Inactive sources without unresolved work may be evicted to admit a new source.
Transport and persistence
Section titled “Transport and persistence”Client.Transport is the service for the portable JSON transport port. Its snapshot, execute,
result and publish operations return typed Effects; subscription returns a Stream.
Transport implementations must include bootstrap/replay in subscription and scope
their resources to stream consumption. The subscription supplies initial authority;
snapshot requests serve recovery and periodic probes. Custom transport Layers
capture their implementation services during construction, so port methods have
no remaining Effect requirements. Use Layer.effect(Client.Transport, makeTransport)
to expose those construction requirements. The client performs its own Schema
decoding and address checks.
Client.layerRpcTransport(contract) provides this service through native Effect
RPC and requires RpcClient.Protocol. Client.rpcTransport(contract) remains
available for scoped acquisition or transport decoration. The application supplies its socket
protocol, JSON serialization, route, and credentials. Build those Layers in the
session scope, or provide them around the whole session lifetime. A protocol bound
to one route serves that route’s source address; applications hosting several
addresses can implement a routing transport. Core imports no browser, Node, Expo,
or Cloudflare implementation.
Client.CurrentActor carries { actorId }, selected by the application’s session
and provided once to its client, transport, and persistence Layers. It has no
default and does not authenticate the actor. Constructors capture that identity;
later operations use the captured owner even when called from another context.
Changing actors closes the old session scope before building a new one.
Persistence must be explicit: { mode: "volatile" } or
{ mode: "persistent" }. Failure never changes that selection. Persistent mode
reads ReplicaPersistence from context; volatile mode does not require it.
ReplicaPersistence is the actor-scoped service; its handle captures a generation
and exposes a disposable snapshot cache, transactional intent journal,
purgeSource, and wipe. Journal admission commits before transport. Storage or
capacity failure refuses new work, and unresolved evidence is never evicted.
Journal rows retain an address, reserved command id, and Schema-encoded Intent
format 1. That record contains the immutable command, local order, authority
binding, confirmation position, and any known outcome. Unrecognized formats,
source versions, codecs or inconsistent evidence are quarantined and continue
reserving their identity and capacity. Cache corruption discards only the cache.
Snapshot saves coalesce for 100 ms; session.flush reports save failure explicitly.
The journal’s first write never depends on a background save or unload callback.
memory().open supplies bounded process-local storage and requires CurrentActor.
Reuse a memory factory to retain records across scopes. ReplicaPersistence.layerMemory()
is a convenience Layer. A wipe invalidates old handles; reopen the factory to
capture the new generation. The memory adapter is not restart durability.
IndexedDb.open({ namespace }) and ExpoSqlite.open({ namespace }) acquire durable
storage in an Effect scope and require Client.CurrentActor. Their layer(options)
constructors provide ReplicaPersistence with the same identity requirement.
The client rejects a handle captured for a different actor. Durable handles add bounded oldest-write-first
snapshotCache.scan(limit) metadata. Configure maxJournalRows, maxSnapshots
and maxBytes explicitly when the defaults do not fit the application.
Adapters use separate cache/journal databases and atomic generation/revision checks.
A concurrent journal commit returns Conflict without replaying the callback.
Journal formats fail closed and retain evidence. Each actor has one bounded,
generation-tagged cache record; cache format 2 resets known format-1 cache data
without touching the journal. Malformed cache records can be replaced.
Applications own background lifecycle hooks and can await session.flush;
they need no unload write for initial journal admission. Session disposal retains
journal evidence, and logout deletion remains an explicit application policy.
See the IndexedDB and
Expo SQLite
adapter guides for physical names,
format versions, reset scope and platform proof. There is no automatic migration
from application-owned databases or namespaces. The portable Persistence helper
is available for adapter authors implementing atomic string-record stores; custom
consumers may continue implementing the smaller PersistenceHandle directly.
Atom bindings
Section titled “Atom bindings”SourceAtom.make(session) creates stable address-keyed bindings over this runtime.
replica(address) acquires a lease and returns an AsyncResult replica atom.
passiveReplica(address) returns an AsyncResult without acquiring a lease;
status(address) is also passive. Registries observe the same replica even when
several registries hold leases. Registry unmount/disposal releases its leases.
execute, retry, and recover return Effects over the same client. They acquire
a temporary lease for the operation and wait for authority. React only reads atoms
and dispatches these Effects through the application’s runtime; it owns no socket,
retry loop, cursor or second replica store.
The React board example
connects these bindings to @effect/atom-react. One RegistryProvider owns an
Atom.runtime session with native Effect RPC and IndexedDB persistence. Components
read the replica with useAtomValue and dispatch runtime.fn workflows for cards,
board titles, exact retries, and reconnection. Its ephemeral presence stream shares
the same headless client and stays outside the durable replica.