Atlas 0.5.0
Released 2026-10-19 · v0.5.0 on GitHub · install with "atlas": "github:AndreyDAraya/atlas#v0.5.0"
Delivery that keeps up under load and survives the database going away: workers spend a few statements per second when idle, deliver entries in batches, spread over every worker instance, and a lost connection no longer kills the process. Found by the MallFeed backend stress audit (an idle instance with 105 consumers issued ~840 statements/s and saturated a 10-connection pool; backlogs never drained; one instance took every lease; a Postgres restart crashed the process; DB errors signed users out).
Breaking
Section titled “Breaking”- Express: an
identifythat throws answers 503store-failed, not 401. Onlyunauthenticated()(exported byatlas/express) answers 401; any other throw is an outage, reported toonError, so a client never drops a valid token because the session store failed. Migration: throwunauthenticated()whereidentifyrejects credentials (expired, bad signature, revoked), or passidentifyErrors: 'unauthenticated'to keep the 0.4 answer while migrating. (fdb8147) - Conflicts retry 5 times, with a wait. A conflicting command or delivery is retried after a jittered exponential wait
(
retry: { attempts: 5, baseMs: 10, maxMs: 200 }by default), soconflictnow carries{ attempts: 5 }and arrives up to ~150 ms later. Migration:createRuntime({ retry: { attempts: 3, baseMs: 0 } })restores 0.4. (9de7f82)
- Unchecked record reads. A record read with
checked: falseloads the record without expecting its version at the commit, so a write to it between the decision and the commit is no conflict. For advisory checks whose authority lives elsewhere (a stock pre-check at checkout while the reservation reaction decides for real): concurrent checkouts of one product no longer conflict with the reservations of the ones before them. In a delivery batch an unchecked load stays out of the batch cache, so a checked read of the same record still expects its version. Found by the MallFeed stress audit (S5: 40–76%409with 25–100 concurrent checkouts of one variant). (1e5536e) - Delivery batches. Consecutive entries a consumer projects or reacts to commit in one transaction with one cursor
advance, exactly once and in order:
createRuntime({ delivery: { projections: 100, reactions: 20, subscribers: 10 } })(the defaults; 1 restores one entry per transaction). Reactions in a batch see the records the ones before them wrote. A batch that conflicts or is refused commits nothing and falls back to one entry at a time. Entries that cannot share a transaction go alone (pastmaxDepth, not liftable, a sent command nobody owns, a reaction that reads a view). Edge subscribers resolve each fact in turn and advance once per batch. (db332c3) - Fair lease spread. With a store that implements the new optional
Store.holders()(memory and Postgres do), each worker instance holds at mostceil(consumers / live instances running the same consumers)and hands the excess over within arenewMs; failover is unchanged. Instances hold a presence lease(instance) <hash> <owner>inatlas_leases(no schema change). (21d29bf) createRuntime({ workerStore }): the workers use this store (deliveries, timers, leases), requests usestore; with Postgres, a second pool on the same database isolates delivery from request connections. (aa9efde)start({ idleMs })(default 1000): the outbox check backs off frompollMsto it while nothing changes. (54f5cc0)retryDelay(attempt, policy, random?), and the typesRetryPolicy,DeliveryBatch,RuntimeOptions,WorkerOptionsfromatlas/runtime;unauthenticated()andisUnauthenticated()fromatlas/express. (9de7f82, db332c3, fdb8147)
Changed
Section titled “Changed”- One outbox check per instance. Workers no longer poll per consumer, and a commit no longer wakes every consumer: one
check per instance releases due timers, reads the head and the types appended since, and wakes only the consumers that
take them; the others skip ahead in memory. A consumer holding its lease keeps its cursor in memory; a cursor that only
skipped entries is written within 5 s or on
stop(), so a lag monitor may see it trail by up to 5 s. Timers fire withinidleMsafter a quiet spell (waspollMs); a commit in another process is seen withinpollMs..idleMs. (54f5cc0)
- Postgres: a connection lost while the adapter held a client (Postgres restarted, a terminated backend) emitted an
unhandled
errorevent and killed the process (node-pg’s pool drops its listener while a client is checked out). Every checkout now listens until release and destroys a broken client; the operation fails (store-failed, or a worker retry with backoff). Apps still attachpool.on('error', ...)for idle clients. (ee35ab5) await: 'views'(andAtlas-Await) answered either in a few ms or at the 2 s timeout withawaited: falseunder the new outbox check; 0.4.1 answered in ~250 ms. Three causes, each fixed: a lane whose skipped progress found its cursor moved (adrain()in the same process, or another runtime) sleptrenewMs(10 s) with no cursor, and the outbox check never wakes such a lane, so it now reads the cursor again at once; a local commit still hidden behind an older open transaction (any session on the server) let the check back off towardidleMs, so it now keepspollMsuntil the head moves (5 s at most); and the wait required each view’s consumer to pass entries it does not take, which a worker skips without writing its cursor for up to 5 s, so it now waits only for the entries the consumer takes.
Performance
Section titled “Performance”MallFeed-shaped micro-benchmark (106 consumers: 50 projections, 50 reactions, 5 edge subscribers, 1 command owner; Postgres 17 in Docker, 2 vCPU, shared with other load; pool of 10; counting every statement):
| 0.4.1 | 0.5.0 | |
|---|---|---|
| idle statements/s | 1,590 | 26 (about 11 of them lease renewals) |
| drain 20,000 facts (7 consumers take them) | 96.7 s, 986k statements | 24.2 s, 325k statements |
Upgrade
Section titled “Upgrade”Run migrate() as before (nothing new in the schema). Throw unauthenticated() from identify for bad credentials.
Optional: give the workers their own pool (workerStore), and run N identical worker instances: they now share the load.
Custom stores: implement holders() to get the fair spread (the contract suite covers it); without it the first instance
takes every free lease, as in 0.4.
From CHANGELOG.md. All versions: Releases.