v0.28.0
Minor Changes
-
Commits are now crash-durable — a sealed envelope replays the exact request after a restart, on both transports. Before a batch reaches the wire, the WebSocket client seals it into one atomic outbox record: the stable idempotency key, the exact JSON-normalized ordered operations, and the source mutations it supersedes. Recovery replays the sealed request as it was sent — never re-projected from model state in the new process — and a multi-operation commit is only ever replayed whole, so an atomic A+B can’t come back later as A and B separately.
-
The stateless HTTP client replays its outbox on startup. It persists the exact request (method, path, body, idempotency key) and re-sends unacknowledged envelopes before its first new request. A definitive rejection (a 4xx other than 429) settles the envelope instead of retrying it.
-
New
commitOutboxclient option +CommitOutboxStoreinterface. Browsers default to strict IndexedDB storage when persistence is on; Node agents and workers can inject workflow-, SQLite-, or filesystem-backed storage so commits survive process restarts. The client’s transaction journal now writes with strict durability, so an acknowledged outbox record means disk-backed. -
Replay is fenced and fails closed. Reusing an idempotency key with a different request is rejected on both transports; replay is scoped to the actor and server that sealed the envelope and bounded to the server’s idempotency retention, so a stale envelope is held for review instead of re-sent; a write queued behind an ambiguous predecessor waits for it to settle rather than overtaking it; and a persistence adapter that cannot guarantee durability reports failure instead of phantom success. Generated create ids are sealed with the request, so a crash mid-create recovers the same row.
-
@abloatai/ablo/commitpublishes the transport-independent v2 commit contract. Branded identifiers, ordered operations, explicit write preconditions (unchanged_since,version_matches,claim_fence), and a discriminatedCommitReceipt—committed,conflicted(carrying the smallest current state needed to reconcile), orrejected(a typed error plus the capability a retry would need). This release publishes the contract so tooling and tests can validate against it today; the engine’s live commit path still speaks the current protocol, and the cutover to v2 ships server-side. -
New wire exports.
@abloatai/ablo/wiregains the matchingCommitRequestMessage/CommitResultMessageframes, re-exports the current transport’s operation schemas underlegacyCommit*names, and exportsBootstrapReason— the machine-readable reasons a live delta stream must resume via catch-up (too_far_behind,too_many_deltas,missing_entities,stream_gap). -
Reactive reads are typed as what they actually return: snapshot rows, not model instances.
useAbloselectors now receiveAbloReads<Schema>— model reads typed as the row’s data fields plus schema computeds, without relation accessors or model methods. Readingrow.layersinside a selector is a compile error instead of a silentundefinedat runtime; compose relations through a dedicated hook or selector instead. The newRow<'slides'>/InferRowtypes are the snapshot-row companion toModel<'slides'>, mirroring whattoReactiveSnapshot()produces. This is a type-level change only: selectors that compiled against relation accessors now fail to compile, and each of those reads was already returningundefined— the new errors point at real bugs. Runtime behavior is unchanged.
Patch Changes
-
The resume cursor advances only through the contiguous durable prefix of a delta frame. A frame spanning several local stores commits per store, and those transactions can fail independently — taking the maximum id from any successful store could permanently skip an earlier failed delta on every later catch-up.
-
Conflicting undo-scope options warn instead of being silently ignored. A scope keeps the options it was created with; the silent version of that rule could leave a surface believing it enabled stream recording when it hadn’t.
-
Docs and error copy name the real HTTP mount. REST paths are documented as
/api/v1/...— the OpenAPI server URLs carry the/apimount — and thejwt_issuer_untrustedmessage no longer points at an endpoint that doesn’t exist. -
The resume cursor advances only through the contiguous durable prefix of a delta frame. A frame spanning several local stores commits per store, and those transactions can fail independently — taking the maximum id from any successful store could permanently skip an earlier failed delta on every later catch-up.
-
Conflicting undo-scope options warn instead of being silently ignored. A scope keeps the options it was created with; the silent version of that rule could leave a surface believing it enabled stream recording when it hadn’t.
-
Docs and error copy name the real HTTP mount. REST paths are documented as
/api/v1/...— the OpenAPI server URLs carry the/apimount — and thejwt_issuer_untrustedmessage no longer points at an endpoint that doesn’t exist.