Skip to content

v1.0.0

Choose a tag to compare

@n8allan n8allan released this 18 Sep 21:01
· 825 commits to main since this release

Covers everything merged after v1.0.0-beta.3 (2026-09-11) up to this release (2026-09-18). Packages: @optimystic/db-core, @optimystic/db-p2p, @optimystic/db-p2p-storage-fs, -rn, -web, -ns, @optimystic/quereus-plugin-optimystic, @optimystic/quereus-plugin-crypto, @optimystic/reference-peer.

Short version: this release is mostly about writes on small networks (one to three machines) being reported correctly and not getting stuck, about closing a class of silent data corruption where a change could be applied to the wrong version of a block, and about the Quereus plugin behaving the same on phones and after a restart. It changes the peer-to-peer messages, the plugin's stored catalog, and several public types. Upgrade every machine in a network at the same time. Do not run this release and beta.3 in the same network (see 1a).


1. Upgrade notes and compatibility

Each item below says how it was checked: verified in code (read in the source diff), tested (a test in this repo covers it), or not tested.

1a. Wire protocol between peers

Mixed-version networks (a beta.3 node alongside a node on this release) are not tested anywhere. Reading the code shows at least one definite break, listed first. Upgrade all nodes together.

  1. Successful writes now carry a durability report, and new writers require it. This breaks with beta.3 coordinators. Every successful pend and commit answer now includes durability (PendSuccess.durability, CommitSuccess.durability, both required). A writer on this release that sends a write to a beta.3 coordinator gets back a success with no durability field. The writer then throws a TypeError while merging the per-coordinator answers, after the beta.3 coordinator has already saved the write. The application sees a failure for a write that landed, and it may resubmit. Verified in code (NetworkTransactor.pend/commit → mergeDurability; RepoClient does not normalise the answer). Not tested. In the other direction, a beta.3 writer ignores the extra field.
  2. New vote kind held. When a pend's blocks are reserved by a different unresolved write, a member used to vote reject with a "pending conflict … held by unresolved action(s)" reason. On a cohort of three or fewer, that reject made the write fail permanently (ValidatorRejectionError). The member now votes { type: 'held', signature, heldBy }, signed over <hash>:held:<heldBy>. A coordinator on this release treats it as "try again later" (BlocksHeldError, surfaced to the writer as a retryable conflict). Verified in code, and not tested in a mixed network:
    • A beta.3 coordinator counts a held vote as neither an approval nor a rejection. The pend fails with a generic Failed to get super-majority: … error instead of a retryable conflict.
    • A beta.3 member that checks the signatures on a record containing a held vote builds the signed text without heldBy. The signature check therefore fails, and beta.3 treats a failed check from a known key as a bad vote and lowers the sender's reputation. Inferred from beta.3's verifySignature. Not tested.
  3. Lineage question on reads (BlockGets.lineageOf → GetBlockResult.lineage). A writer whose own write was built upon before it could confirm it now asks each cohort member whether the block's current content includes that write. A beta.3 repo ignores the question and returns no lineage, which the writer reads as "unknown". The writer then reports the outcome as TornActionError with final: false ("may already be saved") rather than guessing. This fallback is designed in. Verified in code, and the mixed case is not tested. (BlockGets.floors, also new, never leaves the writer's process.)
  4. Routing key. Writers now locate a block by the raw UTF-8 bytes of its id, hashed once. Servers already did this. beta.3 writers hashed the id with sha256 first and then hashed it again, so on networks larger than one cohort they asked the wrong machines. That misrouting remains for any beta.3 writer. Nothing persisted used the old key, so no data migration is needed (verified in code). On networks where every node is in every cohort (up to clusterSize serving nodes), the encodings choose the same machines.
  5. Servers refuse blocks they are not responsible for. A coordinator on this release refuses pend, commit and cancel for a block whose cohort does not include it, and also when it cannot determine the cohort (ResponsibilityRefusalError, kind: 'not-responsible' | 'undetermined'). It also refuses to run a cluster transaction for a cohort it is not in. Reads are still served. A misrouted write is redirected using the same rule the writer uses. Over the network the refusal aborts the stream, so a remote writer sees an ordinary failed batch and retries elsewhere. How a beta.3 writer (item 4) or a beta.3 coordinator, which still puts itself into every cohort, interacts with this is not tested.
  6. Cohort membership changed. A node now belongs to a block's cohort only when it is among the nearest clusterSize serving peers. beta.3 always added itself. Nodes on this release and beta.3 nodes can therefore compute different cohorts for the same block on networks larger than clusterSize. Not tested.
  7. Conflict detection changed on members. Members no longer count a message made only of cancels, or only of commits, as a rival to another write, although a commit still conflicts with an invalidation. Members also no longer count a leftover pending record that claims an earlier revision as a rival (on four or more members, only when the newcomer built on that record's change). beta.3 members still do both, so in a mixed cohort they can still produce the tears and the stuck writes fixed in section 2. Not tested.
  8. Absence memo. A beta.3 node still remembers "this block does not exist" for up to 10 s (issue #20). Only upgraded nodes lose that memory.
  9. Block transfer encoding is unchanged on the wire. The base64 text is identical to beta.3's (a test pins exact equality with Node's Buffer output at every padding length). The new under-replication drain pushes copies over the existing block-transfer push. Whether beta.3 peers accept those pushes has not been tested.
  10. Quereus engine id is now fixed when the plugin is built (quereus@4.19.4 in this build). beta.3 read it at load time from whichever Quereus was installed. This matters only if you configure pend-time transaction validation, which no default configuration does. In that case, nodes running plugin builds compiled against different Quereus versions refuse each other's transactions with Unknown engine, even if both run the same Quereus. Verified in code and README.
  11. Tree log entries are bigger and carry a new guard. A SQL UPDATE, DELETE, REPLACE over an occupied key, or UNIQUE eviction now logs an unchanged guard holding a full copy of the entry as it was read. A delete element may now carry a guard too. The entry layout is still a positional tuple, so entries written by beta.3 still deserialize and replay (tested). How a beta.3 writer replays an entry carrying the new guard is not tested.
  12. Pends carry the base revision of each block they update (PendRequest.baseRevs). Members store it with the pending record and check it at commit and when finishing a record on a read. A beta.3 writer sends no base, so its pends fall back to the base declared at commit, as before. On cohorts of four or more members, a member also rejects a commit at vote time when the commit's declared base disagrees with the base it stored. Beta.3 members ignore the new field. Not tested in a mixed network.
  13. Multi-table commits in the plugin's default (legacy) mode now send one batched pend with a retry priority and no validation pair, where beta.3 synced each tree separately. Members configured with unvalidatablePendPolicy: 'reject' refuse these batches, just as they refused beta.3's per-tree syncs.

1b. Persisted storage

  • Block metadata gains three optional fields, all verified in code:

    • BlockMetadata.pendingRevs records the revision each pending record was made at, keyed by action id.
    • BlockMetadata.pendingBases records the revision each pending record's edits were computed against, keyed by action id.
    • BlockMetadata.lineageFloor records the lowest revision this node's current content is known to derive from locally.

    Metadata written by beta.3 lacks all three and still reads. A pending record with no stored base keeps the older rules: its commit is checked against the base the commit declares, and it is set aside by revision alone (tested). Two further consequences follow:

    • A pending record written by beta.3 has no recorded revision. It is treated as the strongest kind of reservation, and the new clean-up that removes provably dead records never touches it. A block that beta.3 left stuck therefore stays stuck after the upgrade until that write is cancelled, or a read finishes it on that node.
    • A block with no lineage floor answers "unknown" to the lineage question until its next commit.

    Not tested: beta.3 reading metadata written by this release (a downgrade). The fields are plain optional JSON.

  • The raw storage driver interface (IRawStorage) is unchanged. The storage packages (-fs, -rn, -web, -ns) write no new block format.

  • New key-value records. Each node keeps its under-replication ledger under under-replicated/<blockId> in its IKVStore. Without NodeOptions.kvStore the ledger is in memory and is lost on restart, and the node logs a node-wiring warning. The reference peer uses FileKVStore for --storage file. FileKVStore can share a base path with FileRawStorage, but its bytes then count toward getApproximateBytesUsed.

  • Quereus plugin catalog: this is a format break, and there is no migration. The plugin README documents the break.

    • A table declared without using optimystic('<uri>') now stores its rows at tree://default/<schema>/<table>, including main. In beta.3 it was tree://default/<table>. Its catalog record is now filed under a schema-qualified key; beta.3 used the bare table name.
    • After upgrading, hydrate ignores every catalog record written by beta.3 and reports { tables: 0 } for them. A re-declared table with no URI starts empty at its new location.
    • To keep reading a beta.3 table's rows, declare it with its old location: using optimystic('tree://default/Member') (tested). Tables that already named an explicit URI keep their data, but must be re-declared once because hydrate skips their old records too.
  • Plugin catalog records hold more. They now store:

    • declared type spellings;
    • CHECK constraints and foreign keys;
    • with context variables and tags;
    • full default and generated-column expressions;
    • generated-column order.

    They no longer store the session binding: transactor, keyNetwork, networkName, port, cache. Indexes and CHECKs are listed by name. A record written before this release keeps its old order until its next schema write. A record written by an older plugin lacks the new fields, so open that storage once with the DDL running (skip hydrate for that one open) to write the full record.

1c. Public API

Changed result types

  • PendSuccess.durability and CommitSuccess.durability (WriteDurability) are required. A custom ITransactor or repo must fill them in.
  • WriteDurability.quorum is one of 'full' | 'majority' | 'local' | 'unrouted', ordered from strongest to weakest. 'unrouted' means the cohort lookup failed and only this node holds the write (issue #19). The report also carries confirmed, cohort, and optionally unconfirmed, cohortPeerIds, otherCohorts and torn. Test for a complete save only with isFullyDurable(d), never by comparing quorum yourself, because a torn write can report full for its tail.
  • Collection.sync() / updateAndSync(), ICollection.sync() / updateAndSync(), Tree.replace(), Tree.sync() and Diary.append() now resolve to WriteDurability | undefined instead of void. undefined means nothing was staged, so nothing was written.
  • TransactorSource.transact() now resolves to the full CommitResult. In beta.3 success resolved to undefined, so code that tests === undefined for success must switch to result.success.
  • TransactionCoordinator.commit() still returns nothing; multi-collection durability is not reported yet.
  • requestResponse() (cohort-topic stream helper, exported from db-p2p) now resolves to Uint8Array | undefined, where undefined is the peer's "no result". The new requireReply() and NoResultReplyError cover protocols that must always answer.

Errors callers catch

  • TornActionError (db-core, new). A write's log entry is stored but its blocks could not be confirmed or finished. It carries collectionId, actionId, rev, blockIds, reason, final, detail and staleAt?:

    • reason is 'rival-holds-revision', 'completion-refused' or 'transforms-not-held'.
    • final: true means the write is not saved and cannot land, so it is safe to submit again.
    • final: false means the outcome could not be established, so the write may already be saved or may land later.

    The error message says which case applies. rival-holds-revision no longer means "nothing can land this write".

  • CoordinatorPartialCommitError (existing) is now raised for every failure after any participant was saved, including saves made by a refresh between retries. Before, those cases escaped as plain errors that looked like "nothing saved". In the plugin's default (legacy) mode, a multi-table commit that splits during the commit phase now raises this error. PartialCommitError now comes only from the fallback path, where trees cannot share one batch. Catch both.

  • SyncRetryExhaustedError (existing): the documentation now says it does not prove the write never landed, because the last attempt's log entry may be stored.

  • BlocksHeldError (db-p2p, new, carries heldBy: peer → action id). It is raised by the coordinator for held votes and reaches a writer as a retryable conflict.

  • ResponsibilityRefusalError (db-p2p, new, kind, blockIds).

  • TreeGuardRefusedError (db-core, new base class). TreeKeyTakenError now extends it, and the new TreeEntryChangedError (a row changed or removed by another writer; expected, actual) sits beside it. TreeDeleteGuardKindError (caller bug) sits outside that hierarchy.

  • The plugin renders a lost concurrent row change as concurrent modification: another writer changed or removed the row in <table> at primary key (…). It is a plain Error with cause, not a constraint error.

  • The plugin now exports PartialCommitError, CoordinatorPartialCommitError, SyncRetryExhaustedError and TornActionError from both @optimystic/quereus-plugin-optimystic and …/plugin, and they are the same classes (tested). instanceof works from either entry.

Removed or retyped

  • blockIdToBytes has been removed from db-core. Use routingKeyForBlock(blockId), which returns a branded RoutingKey.
  • IKeyNetwork.findCoordinator, findCluster and recordCoordinator now take a RoutingKey, not a Uint8Array. The bytes are the same, raw UTF-8, but a plain byte array no longer compiles.
  • RepoService: the networkManager component and the NetworkManagerLike type have been removed. Pass keyNetwork (ClusterLookup, i.e. Pick<IKeyNetwork, 'findCluster'>), or let the node attachment supply it.
  • ICoordinatorClusterSeam: getClusterPeerIds has been removed and resolveCohort added (required).
  • IBlockStorage (for custom block-storage implementations):
    • setLatest(latest, builtOnPrior, latch) gained a required builtOnPrior argument, and must be called only after promotePendingTransaction.
    • New required listPendingClaims(), pendingClaimOf() and lineageOf().
    • savePendingTransaction(actionId, transform, rev, baseRev, latch) gained a baseRev argument.
  • TreeEntryGuard<TKey, TEntry> gained a second type parameter and an unchanged kind. TreeReplaceAction is now a union of upsert and delete elements.
  • A failed Tree.replace or Diary.append now leaves nothing staged for any failure kind. Before, the staged change could ride along with the next call. Collection.act + sync keep the old behaviour.
  • Testing entry (@optimystic/db-p2p/testing): Mesh gained a required restart(node) member, so code that builds Mesh objects itself will not compile. Also new: MeshFailureConfig.onClusterDelivery, MeshNode.keyNetwork, responsibleNodes, blockIdsInCohortOf and MeshOptions.onCommittedHolders.

New exports (additive)

  • db-core:
    • routingKeyForBlock / RoutingKey;
    • isFullyDurable, durabilityRank, mergeDurability, withTornBlocks, localDurability, unroutedDurability;
    • WriteDurability, DurabilityQuorum;
    • BlockLineage, ActionLineage, judgeCohortLineage;
    • optional ITransactor.getLineage;
    • BlockFloors, answeredBlock, servedRevision;
    • structuralEquals;
    • BlockDurabilityReachedEvent / IBlockDurabilityNotifier;
    • CoordinatorOptions / PendValidationMode (types);
    • optional NetworkTransactorInit.localPeerId. Pass node.peerId: with it, a writer coordinates its own writes when its machine is one of the equally good choices.
  • db-p2p (both index and rn entries):
    • IUnderReplicationLedger, KvUnderReplicationLedger;
    • UnderReplicationDrain;
    • ResponsibilityRefusalError, RESPONSIBILITY_TTL_MS;
    • PendingClaim, isReservationAgainst;
    • BlockBaseRevs (db-core, the type of PendRequest.baseRevs);
    • StorageRepo.onBlockDurabilityReached;
    • pushBlockToPeers.
  • quereus-plugin-optimystic:
    • plugin.verifyIndexes(db, table, schema?) and its report types;
    • defaultCollectionUri, catalogKey, QualifiedTableName.

Configuration

  • NodeOptions.kvStore (new): an IKVStore or a factory for one. Supply a persistent store so the under-replication ledger survives restarts.

  • NodeOptions.underReplicationDrain (new, on by default). It pushes missing copies to cohort members once they are reachable. Turn it off with { enabled: false }.

  • Relay listen addresses. A listen address of the form <relay>/p2p/<relayId>/p2p-circuit is now handed to libp2p as a bare /p2p-circuit, and the node keeps its own reservation on each named relay. Two combinations now fail at node creation with a message naming the fix:

    • such an address together with announceAddrs: use appendAnnounceAddrs instead;
    • such an address with no circuit-relay transport configured.

    Node creation still rejects if a named relay cannot be reserved.

  • Read repair. A block missing locally now asks its cohort on every read, in every readRepairMode. The settled-absence memo is gone (issue #20). The docs for readRepairWindowMs and readRepairSampleRate no longer mention absences.

  • Rebalancing. A node now considers itself responsible for a block while it is among the block's nearest clusterSize serving peers (the same rule writers use). Expect storage per node to track clusterSize. The replication floor is now capped at clusterSize, so at clusterSize 1 or 2 a lost block can be released.

  • BlockTransferConfig.transferTimeoutMs now bounds the dial and the reply separately, so the worst case per peer is twice the value.

  • Plugin: a binding written explicitly in a table's using optimystic(…) clause now applies only to the session that runs that DDL. After a restart the table binds through the session's default_vtab_args and the plugin config. A session's default encoding never applies to a hydrated table.

1d. React Native and bundling

  • The plugin's main entry no longer loads Node file-system modules. @optimystic/quereus-plugin-optimystic used node:fs, node:url, node:path and import.meta at load time to find the Quereus version, which broke browser and Hermes builds. The version is now written into the package at build time (src/transaction/quereus-version.ts). A test fails if either plugin entry stops bundling for a browser. PartialCommitError can be imported from the root entry on every platform.

  • yarn check:rn (new, part of yarn check) bundles a host-shaped entry with Metro (React Native 0.83 toolchain) and compiles it with hermesc. It covers @optimystic/db-p2p/rn, @optimystic/db-p2p-storage-rn, and both Quereus plugin entries, which pull in Quereus itself. It checks that @optimystic/db-p2p resolves to the rn build from every importer. It does not run the bundle. Nothing in this release has been run on a device or a Hermes runtime.

  • Import no longer crashes on a phone without a TextDecoder polyfill. Our own modules used to construct a TextDecoder when they loaded. They now construct it on first use, in db-core, db-p2p and db-p2p-storage-rn. A lint rule and a load-time test guard this. The host still needs TextDecoder and structuredClone polyfills, because multiformats and cborg construct TextDecoder when they load, and db-core uses structuredClone. The db-p2p readme has the current table.

  • APIs Hermes lacks have been removed from our code:

    • AbortSignal.timeout in dispute challenges;
    • AbortSignal.any in RepoClient, on every remote block read. This also fixes a listener leak on a reused caller signal;
    • the global Buffer in block transfer;
    • an unguarded AggregateError in the logger;
    • a class static {} block that broke a downstream Metro build.

    Lint now bans Buffer, AbortSignal.timeout, AbortSignal.any, Promise.withResolvers and new DOMException in library code. AbortSignal.prototype.throwIfAborted and timer .unref() are still called and remain required host polyfills.

  • Plugin on React Native: the plugin's network transactor cannot build its own libp2p node on React Native. Register the host's node with plugin.collectionFactory.registerLibp2pNode(networkName, node, node.coordinatedRepo), as the plugin README now describes. This comes from reading the code; it has not been run on a device.

1e. Minimum Quereus version

  • The plugin is built and tested against @quereus/quereus 4.19.4. Both plugins' development range is now ^4.19.4.
  • Both plugins now declare peerDependencies and engines.quereus as ^4.19.4 (previously ^4.3.0). Why:
    • The optimystic plugin imports collectTableConstraintNames, which Quereus first exports in 4.12.0; on 4.3–4.11 it would fail to load.
    • Re-applying a schema that spells a type with an alias (int, varchar(20)) after a restart needs 4.19.4. Earlier versions produce a spurious SET DATA TYPE that Optimystic tables reject.
    • quereus-plugin-crypto has no technical need above 4.3 and was raised to keep the two plugins in lockstep.
    • If you enable pend-time validation, every node must also run the same plugin build (1a item 10).
  • quereus-plugin-optimystic still declares engines.node >=20.6.0.

2. Fixes

Correctness at small node counts (1–3 machines)

  • Writes on small networks now stay on the writer's own machine. A node is now in a block's cohort only when it is genuinely among the nearest serving peers. Given localPeerId, the writer picks its own machine when it ties with others, and every node in a small network ties. The plugin's collection factory and the reference peer pass it.
  • A machine that missed a commit no longer vetoes every later write. At three machines, a member that promised a write and then missed its commit kept a leftover pending record and refused all later writes to that block, for about 17 s each, until it happened to read the block. Pending records now record the revision they were made at, and a record for a revision the collection has moved past no longer counts as a rival. Such records are deleted once the block passes them. A test that took about 17 s to fail on the old code now passes in about 1 s. This addresses part of #18; see Known issues for what remains.
  • A "not right now" refusal is no longer a permanent failure. On a cohort of three or fewer, a member saying "another write holds these blocks" made the whole write fail as invalid (ValidatorRejectionError). It is now the retryable held vote.
  • Cancelling a refused write no longer breaks the winner's commit. On two-member cohorts, the loser's cancel counted as a competing write and knocked out the winner's save. The winner was left half-written and reported as torn.
  • A commit no longer counts as a rival to another write's pend. This fixed an intermittent tear on two-member cohorts.
  • A node no longer reads a block it just wrote as never created (#20). The 10-second "this block does not exist" memo is gone.
  • Rebalancing can release a lost block at clusterSize 1 or 2. It also now uses the same responsibility rule as writers and coordinators.
  • New tests cover real small deployments:
    • one machine alone, then a second joining, one leaving, and both restarting, over real sockets;
    • two phone-shaped machines that reach each other only through a relay;
    • a 1–5 machine sweep checked against the documented quorum rules;
    • a three-machine member that leaves mid-write and returns.
  • The design docs now state one durability bar and one minimum group size consistently.

Consistency and durability

  • Every successful write now says who holds it (#19). The four classes are full, majority, local and unrouted. The answer reaches Tree.replace, Tree.sync and Diary.append, so an app can show a change as pending until the group holds it. No acceptance rule changed.
  • A write acknowledged below full is now finished in the background. The node records which members lack each block, persistently when a kvStore is supplied, and pushes the copies when those members are reachable again, including after a restart. StorageRepo.onBlockDurabilityReached fires when a block is fully replicated.
  • A write whose log entry landed alone is no longer reported as saved. Before, the row was silently lost on every machine. The writer's retry now finishes the half-stored write at the same revision, or throws TornActionError.
  • A write reported torn that was actually saved is now reported as saved. Before, an app that resubmitted stored the row twice. The writer asks the cohort whether later content was built from its write. TornActionError.final says whether resubmitting is safe.
  • A multi-collection commit that saved some collections is reported as a partial save (CoordinatorPartialCommitError) naming what was saved, instead of "nothing saved".
  • A multi-collection commit no longer half-lands when the writer's own replica is behind. This was typical of a node that has just joined a group. A transaction touching several collections could save some and permanently lose the rest: in sereus, a joining node spent its invite and was never seated. A collection refused because the write is not yet held by enough machines is now retried until it lands, while the collections already saved stay saved. This applies to both the plugin's default commit mode and explicit transactions. The refusal text now reads commit-not-durable rather than Stale commit. A refusal that never clears is reported after the retry budget, about 21 s at defaults, as CoordinatorPartialCommitError.
  • Unique-value races in the plugin's default commit mode (#17). A write touching a table and its indexes now reserves every piece before saving any. A same-instant unique-value loser is refused with a plain UNIQUE constraint failed and leaves nothing behind (18 of 18 rounds in the test). Two table declarations over one collection id are no longer half-committed.
  • Concurrent changes to the same row are refused instead of silently corrupting an index. This covers a racing UPDATE, DELETE, REPLACE over an occupied key, primary-key move, and UNIQUE eviction. INSERT OR IGNORE / INSERT OR REPLACE races at a free key are refused as duplicates, and the sequential retry then honours the clause. A read inside a doomed transaction may now throw Query failed: … early.
  • An index lookup checks every row it returns. It could previously return a row that had moved to a different value, and = NULL seeks now return no rows. Documentation that called such leftover entries harmless has been corrected.
  • Deleting a row from a second machine no longer leaves its index entry behind forever.
  • A machine no longer caches a block older than the log entry that changed it. Before, a reader could show a stale row indefinitely (45 s observed downstream). A too-old answer is no longer kept, and the reader asks a different machine before settling for it.
  • The writer and the servers now agree on which machines hold a block (one routing encoding). Servers refuse and redirect writes for blocks they are not responsible for.
  • A commit that staged nothing now releases its transaction stamp. Before, every later stamp on that coordinator was refused.
  • Extracting one block's changes no longer shares the insert object with the original staged data.
  • A change to a block now carries the version it was written against, so a machine can no longer apply it to the wrong version (data-corruption class). Before, the edits and their base version travelled separately. Wherever they came apart, a machine could store different content from the rest of the group under the same version number, silently. Now:
    • The writer keeps each staged edit's base version fixed. If the block's content moves to a different version under a staged edit, the pending actions are re-applied against the new content before anything is sent. Before, the edit could be carried onto newer content and committed with the newer version declared, so every machine stored the same wrong content. The review of this change closed two more ways it happened, through an atomic flush and a snapshot restore, both reproduced before the fix.
    • Every pend carries each block's base version, and storage keeps it with the pending record. The commit check prefers the stored base and refuses a commit whose declaration disagrees with it.
    • A machine finishes a pending change on a read only when it holds exactly the version the change was built on. Otherwise it leaves the change pending. Before, a machine that had missed one change applied the next one over its stale copy.
    • On cohorts of four or more members, an older write's pending record is set aside only by a writer that built on it. This closes a lost update where a machine that missed a rival's pend served a fresh reader the block one change short. Two- and three-member cohorts are unchanged: there every member holds every pend, and the stricter rule would have blocked honest retries.

React Native and relays

  • Phones and browsers keep their relay slot. A relay-only node used to lose its reservation, becoming unreachable until the app restarted, whenever the relay restarted, the connection dropped, or the slot was renewed. The node now supervises the reservation itself and re-reserves in about 2 s after a relay restart (tested).
  • The Hermes and bundling fixes are listed in 1d.
  • Stopped nodes no longer keep the process alive. A 30-second timer left behind after every block transfer kept stopped nodes' processes running for 12–32 s.
  • Reactivity recovery tries the next cohort member when the first one declines, instead of giving up.

Quereus plugin and schema

  • A restarted database now rebuilds each table exactly as its declaration would. This includes types as spelled, CHECKs, defaults, generated columns, foreign keys, contexts and tags. The table connects through the current session's storage settings, not the previous session's.
  • Same-named tables in different schemas no longer share storage and rows. This is a format break; see 1b.
  • Catalog records are now byte-identical across machines at the same schema version, whatever earlier versions each machine went through.
  • A CHECK added by ALTER TABLE … ADD CONSTRAINT is now saved, so a restarted machine still enforces it. Other ALTER forms are rejected with UNSUPPORTED as before, and RENAME COLUMN still applies to the current session only. For an ADD COLUMN NOT NULL without a default on a table that has rows, the NOT NULL error is now reported before UNSUPPORTED.
  • Re-applying a schema with alias type names no longer tries to retype columns. This needs Quereus 4.19.4.
  • The plugin's commit-failure error classes are now exported from both entries.

Performance

  • Checking an unchanged table for changes before a query is cheaper. It takes 1 request instead of 7, and a table with one index takes 2 instead of 16. No block is fetched twice in one check.
  • A commit no longer deletes a pending record that is already gone. Downstream measurement: a single-machine insert fell from 99 to 88 raw storage operations, and launch from 88 to 80.
  • Freshly committed blocks are no longer pulled and pushed back to members that already hold them. On a two-member cohort, a 3-block commit caused 3 pulls and 3 pushes per member; it now causes none.
  • Routing no longer runs an extra async sha256 per lookup.
  • Costs that went up:
    • a cold schema apply makes five more cohort lookups because the absence memo is gone (still inside the #8 budget);
    • UPDATE and DELETE log entries carry a copy of the pre-write row;
    • a commit below full costs two key-value operations per block for the ledger;
    • the rebalance monitor does one cohort lookup per tracked block per pass.

3. New

  • Write durability reporting: WriteDurability, isFullyDurable, and durability returned from Tree.replace, Tree.sync and Diary.append.
  • Under-replication ledger and drain: NodeOptions.kvStore, NodeOptions.underReplicationDrain, and the block-fully-replicated event.
  • plugin.verifyIndexes(db, table, schema?): a two-way check of every secondary index against its table. It reports missing and orphaned entries (no-row, stale-value, malformed) and repairs nothing. See docs/debugging.md, "Does an index agree with its table?".
  • Relay reservation supervision for relay-only nodes.
  • ITransactor.getLineage and the lineage answer on reads.
  • Tree unchanged entry guard and structuralEquals.
  • routingKeyForBlock as the single way to turn a block id into a routing key.
  • Test harness additions: Mesh.restart, onClusterDelivery, responsibleNodes, blockIdsInCohortOf.
  • Tooling:
    • yarn check:rn;
    • yarn lint:deps, which runs Yarn constraints, a check that fails if any installed libp2p core package is on a different major version, and a check that fails if a package imports something it does not declare;
    • lint rules against APIs React Native lacks;
    • yarn test:harness now runs every harness test.
  • @libp2p/peer-id-factory has been dropped from db-core's dev dependencies. It was the last thing installing a second libp2p major version.

4. Known issues in this release

  • Two gaps remain from the version-mismatch fix above.
    • A delete stores no base version, so when the earlier of two racing writes was a delete, the lost update on four-or-more-member cohorts is still possible, as it was before.
    • On four or more members, a writer held behind a change it never saw now fails instead of overwriting. It retries on its stale copy and ends in TornActionError after about nine attempts. The error reports final: false even though the write can never land, so apps are told to read back before resubmitting, which is cautious but not exact. Two- and three-member cohorts cannot reach this path.
  • A three-machine cohort can refuse later writes after a failure mid-commit, and some leftover "write in progress" records have no clean-up. Cancelling a write needs the same quorum as the write itself, so at two or three machines, while one member is away, a refused or torn write's pending records cannot be cleared.
    • One measurement at three machines: a member dropped during the second stage of a commit, and the next writer spent about 27 s retrying before failing with SyncRetryExhaustedError ("pending conflict … unresolved rival action").
    • This release removes the leftovers whose revision the collection has moved past. Two cases are left, and both still refuse every later write to the block:
      • a record for a write whose log entry never committed and whose writer never comes back, for example a crashed client or a cancel that never arrived;
      • records left by beta.3 (1b).
      • on four or more members, a record whose writer crashed before cancelling, on a block nothing else writes to.
    • The symptom, reported independently by two consumers (#18), is repeated writes to one collection failing after about 30 s of retries.
  • Two machines, one away. A write made while one of two machines is away is sometimes accepted by the survivor alone (local) and sometimes refused as 1/2 approvals, depending on timing. Over a relay it has so far always been refused.
  • Two-member cohorts can refuse a write that both members end up holding. The write is refused as not durably stored because one member checks before the other has finished saving it. Clients then retry writes that had already succeeded.
  • A leftover entry in a uniqueness index keeps the old value reserved. Inserting a new row with that value is refused as a duplicate, permanently.
  • A refused write can leave its log entry behind, and resubmitting it logs the actions twice. Trees read correctly; only the log shows the duplicate.
  • Multi-collection transactions (TransactionCoordinator.commit) report no durability class yet.
  • Schema changes Quereus does not handle.
    • An unnamed CHECK or UNIQUE added by a later schema version is ignored by Quereus's schema diff, so upgraded databases never get the rule while fresh installs do. The fix belongs in Quereus.
    • ALTER TABLE … RENAME COLUMN is lost on restart.
    • ALTER-added UNIQUE and FOREIGN KEY constraints are not supported.
  • A pending-change read overlay exists but nothing uses it. A read can name an unfinished change to see (ActionContext.actionId) and storage knows how to answer it, but nothing in the libraries sets it, so the feature does not work end to end.
  • Nothing has been run on a phone. React Native support is checked by bundling and compiling only.
  • Mixed-version networks have not been tested (see 1a).
  • Carried from beta.3, if not already announced: the unused and non-functional pubsub (gossipsub) service was removed from every node, so node.services.pubsub is now undefined. The published types never named it, so no build breaks; code that reads it at runtime gets undefined.