/** * @module db/errors * @description Error types for the 8.0 generational-MVCC Db API. * * Three failure modes have dedicated classes so callers can branch on them * with `instanceof` instead of string matching: * * - {@link GenerationConflictError} — the store's current generation differs * from what the caller's operation requires: a `transact()` * compare-and-swap (`ifAtGeneration`) observed a different generation than * expected, or `db.persist()` was called on a view that history has moved * past (snapshots capture current bytes, so persisting requires the view * to still be the latest generation). The standard retry pattern is: * re-read via `brain.now()`, re-derive the operation, and re-submit. * - {@link SpeculativeOverlayError} — an index-accelerated query (vector / * hybrid / graph-traversal search, cursors, aggregation) or `persist()` * was issued against a speculative `db.with()` overlay. Overlays are pure * in-memory values whose entities carry no embeddings (`with()` never * invokes the embedder), so a "full" index query over one would silently * miss the overlay's own entities — an honest error beats silently-wrong * results. Commit the operations with `brain.transact()` to get the full * query surface. Historical (non-overlay) views do NOT throw this: they * serve the full query surface via at-generation index materialization. * - {@link GenerationCompactedError} — `asOf()` asked for a generation whose * immutable records were reclaimed by `compactHistory()`. * * All three are exported from the package root (`@soulcraft/brainy`). */ /** * @description Thrown when an operation requires the store to be at a * specific generation and it is not: * * - `brain.transact(ops, { ifAtGeneration })` — the whole-store * compare-and-swap counterpart to the per-entity `ifRev` / * `RevisionConflictError` pair: it guarantees that *nothing* was committed * between the caller's read (`brain.now()`) and this transaction. The * transaction is rejected before any record is staged — the store is * untouched and the generation counter is unchanged. * - `db.persist(path)` — snapshots capture current bytes, so persisting * requires the view to still be the store's **latest** generation. A view * that history has moved past throws this error: pin with `brain.now()` * and persist before further writes. * * @example * const db = brain.now() * try { * await brain.transact(ops, { ifAtGeneration: db.generation }) * } catch (err) { * if (err instanceof GenerationConflictError) { * // Someone committed since we pinned — re-read and retry. * console.log(`expected ${err.expected}, store is at ${err.actual}`) * } * } */ export class GenerationConflictError extends Error { /** The generation the operation required the store to be at. */ public readonly expected: number /** The generation the store was actually at. */ public readonly actual: number /** * @param expected - The required generation (`ifAtGeneration`, or the * pinned generation of the view being persisted). * @param actual - The store's current generation. */ constructor(expected: number, actual: number) { super( `Generation conflict: the operation requires the store at generation ${expected}, ` + `but it is at generation ${actual}. Another write committed in between. ` + `Re-read with brain.now(), rebuild the operation, and retry.` ) this.name = 'GenerationConflictError' this.expected = expected this.actual = actual } } /** * @description Thrown when an operation on a speculative `db.with()` overlay * requires index machinery (vector / hybrid / graph-traversal search, cursor * pagination, aggregation) or durability (`persist()`). * * Why this single boundary exists: overlays are pure in-memory values — * `with()` never touches disk, the generation counter, or the embedder, so * overlay entities carry **no embeddings**. A "full" vector or hybrid query * over an overlay would silently exclude the overlay's own entities, and a * persisted overlay would be a store whose entities cannot be semantically * searched. An honest error beats silently-wrong results. Commit the * operations with `brain.transact()` to get the full query surface, or query * the overlay's base view (historical views serve the complete surface via * at-generation index materialization). * * `get()`, metadata-filter `find()`, and filter-based `related()` remain * fully supported on overlays. * * @example * const spec = await brain.now().with([{ op: 'add', type, data: 'draft' }]) * await spec.find({ where: { draft: true } }) // ✅ metadata find — supported * await spec.search('semantic query') // ❌ throws this error * await brain.transact([{ op: 'add', type, data: 'draft' }]) // → full surface */ export class SpeculativeOverlayError extends Error { /** The capability that is not supported on speculative overlays. */ public readonly capability: string /** The overlay's pinned base generation. */ public readonly generation: number /** * @param capability - Human-readable name of the unsupported capability * (e.g. `'vector search'`, `'persist'`). * @param generation - The overlay's pinned base generation. */ constructor(capability: string, generation: number) { super( `${capability} is not supported on a speculative with() overlay ` + `(pinned at base generation ${generation}). Overlays are pure in-memory ` + `values whose entities carry no embeddings, so index-accelerated reads ` + `over them would be silently incomplete. Supported on overlays: get(), ` + `metadata-filter find(), and filter-based related(). Commit the operations ` + `with brain.transact() for the full query surface, or query the base view.` ) this.name = 'SpeculativeOverlayError' this.capability = capability this.generation = generation } } /** * @description Thrown by `brain.asOf(generationOrTimestamp)` when the * requested generation's history was reclaimed by `brain.compactHistory()`. * The store records the compaction horizon (the highest reclaimed * generation) in its manifest; any generation BELOW the horizon is * unreachable — its reads would need the reclaimed before-images. The * horizon itself stays reachable, resolved from the record-sets above it. * * To keep a generation readable forever, `persist()` it to a snapshot * directory before compacting — snapshots are self-contained and unaffected * by compaction of the source store. */ export class GenerationCompactedError extends Error { /** The generation that was requested. */ public readonly requested: number /** The compaction horizon — generations < this value are unreachable. */ public readonly horizon: number /** * @param requested - The generation the caller asked for. * @param horizon - The store's current compaction horizon. */ constructor(requested: number, horizon: number) { super( `Generation ${requested} has been compacted away (compaction horizon: ${horizon}). ` + `Only generations at or above the horizon are reachable via asOf(). ` + `Use db.persist(path) before compactHistory() to keep a generation readable.` ) this.name = 'GenerationCompactedError' this.requested = requested this.horizon = horizon } }