brainy/src
David Snelling 867939ed50 fix: atomic ifAbsent/upsert inserts + exact blob reference counts under concurrency
The fast-follow to the ifRev CAS fix: the sweep for the same check-then-act
class found two more instances, both now closed with the same discipline —
the decision runs at the serialization point that guards the apply.

add({ifAbsent}) / add({upsert}): the absence check ran before the commit
mutex, so N concurrent same-id creates could all pass it and all write — the
second silently overwriting the first, violating ifAbsent's "return the
existing id WITHOUT writing" contract and upsert's merge-never-clobber
contract. The insert leg now carries a must-be-absent commit precondition
(the same conditional-commit primitive ifRev uses), verified under the commit
mutex against the authoritative before-image. A losing caller takes its
documented resolution instead of overwriting: ifAbsent returns the existing
id with zero writes; upsert merges into the now-existing entity via update()
(shared param mapping in upsertMergeParams so the planning-time branch and
the conflict retry can never drift), with a bounded retry if a concurrent
delete lands between the conflict and the merge. The planning-time checks
remain as fast-fails. transact()'s batch-level ifAbsent/upsert keep
planning-time semantics (documented, converges to a valid entity).

BlobStorage: write()'s dedup decision (exists → increment refCount, absent →
create at 1) and delete()'s decrement-then-remove were unserialized
read-modify-writes over blob-meta:<hash>. Concurrent writes of identical
content could lose references, so a later delete removed bytes another file
still referenced (data loss), or leaked unreferenced blobs. All
reference-count-bearing mutations now serialize through a per-hash
InMemoryMutex — distinct content never contends; incrementRefCount/
decrementRefCount are documented lock-assumed internals.

Both fixes are complete by construction in-process: storage enforces
single-writer-per-directory, so the process is the whole concurrency domain.

Tests (tests/integration/ifabsent-upsert-blob-concurrency.test.ts): 8-way
ifAbsent storm advances the store by exactly one generation with _rev 1;
8-way upsert storm on an absent id yields one create + seven merges
(_rev === 8); sequential ifAbsent/upsert semantics pinned unchanged; N
identical concurrent blob writes → refCount === N; the blob survives until
the true last reference drops; interleaved write/delete storm never loses a
landed reference.
2026-07-08 15:13:26 -07:00
..
aggregation docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
cli refactor(8.0): remove dead, unreachable, and unwired modules 2026-06-24 15:18:40 -07:00
db fix: ifRev CAS is atomic — the revision check now runs under the commit mutex 2026-07-08 14:53:34 -07:00
embeddings docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
errors fix: validate where-operators and align the in-memory matcher to the documented set 2026-07-07 12:23:36 -07:00
graph fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers 2026-07-07 10:39:00 -07:00
hnsw docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
import refactor(8.0): remove dead, unreachable, and unwired modules 2026-06-24 15:18:40 -07:00
importers refactor(8.0): remove dead/deprecated code (legacy sweep) 2026-06-29 10:09:39 -07:00
indexes/columnStore docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
integrations chore(8.0): ES2023 target + drop DOM lib + downlevelIteration (config truth-up) 2026-07-01 10:58:04 -07:00
mcp refactor(8.0): remove dead, unreachable, and unwired modules 2026-06-24 15:18:40 -07:00
migration feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API 2026-06-10 15:22:47 -07:00
neural docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
patterns chore(8.0): final pre-RC1 sweep — API consistency, named errors, orphans, zero-cast codebase 2026-06-11 14:51:00 -07:00
storage fix: atomic ifAbsent/upsert inserts + exact blob reference counts under concurrency 2026-07-08 15:13:26 -07:00
streaming chore(8.0): final pre-RC1 sweep — API consistency, named errors, orphans, zero-cast codebase 2026-06-11 14:51:00 -07:00
transaction chore(8.0): modernize toolchain + position Bun as a runtime 2026-07-01 09:16:46 -07:00
triple chore(8.0): Phase A + B — purge all @deprecated APIs + cacheManager dead branches 2026-06-09 15:33:56 -07:00
types feat: guarded plugin auto-detection — installing @soulcraft/cor is the opt-in 2026-07-02 16:19:55 -07:00
universal refactor(8.0): remove dead, unreachable, and unwired modules 2026-06-24 15:18:40 -07:00
utils fix: validate where-operators and align the in-memory matcher to the documented set 2026-07-07 12:23:36 -07:00
vfs fix: recover VFS content blobs stranded by a 7→8 upgrade, in place on open 2026-07-07 12:23:36 -07:00
brainy.ts fix: atomic ifAbsent/upsert inserts + exact blob reference counts under concurrency 2026-07-08 15:13:26 -07:00
coreTypes.ts feat(8.0): auto pre-upgrade backup — hard-link snapshot before the 7.x→8.0 migration 2026-07-01 15:04:01 -07:00
index.ts docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
internals.ts docs: rename the native provider to @soulcraft/cor across public docs and JSDoc 2026-07-02 15:11:41 -07:00
plugin.ts fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers 2026-07-07 10:39:00 -07:00
setup.ts chore(8.0): collapse dead defensive guards + redundant polyfills 2026-06-09 16:46:16 -07:00