/** * @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} — a `transact()` compare-and-swap * (`ifAtGeneration`) observed a different current generation than the * caller expected. The standard retry pattern is: re-read via * `brain.now()`, re-derive the transaction, and re-submit. * - {@link NotYetSupportedAtHistoricalGenerationError} — an index-accelerated * query (vector / hybrid / graph-traversal search) was issued against a * `Db` pinned at a generation the live indexes no longer represent. * `get()` and metadata-filter `find()` remain fully supported at pinned * generations; index-accelerated queries at historical generations require * an index rebuild from the pinned record set, which ships as a follow-up * (`asOf` rebuild). The error message names the unsupported capability and * points at the supported alternatives. * - {@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 by `brain.transact(ops, { ifAtGeneration })` when the * store's current generation does not equal the caller-supplied expectation. * This is 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. * * @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 caller expected the store to be at. */ public readonly expected: number /** The generation the store was actually at when the transaction arrived. */ public readonly actual: number /** * @param expected - The generation supplied via `ifAtGeneration`. * @param actual - The store's current generation at submission time. */ constructor(expected: number, actual: number) { super( `Generation conflict: transact() was submitted with ifAtGeneration: ${expected}, ` + `but the store is at generation ${actual}. Another write committed in between. ` + `Re-read with brain.now(), rebuild the transaction, and retry.` ) this.name = 'GenerationConflictError' this.expected = expected this.actual = actual } } /** * @description Thrown when an operation on a pinned `Db` requires * index-accelerated machinery (vector search, hybrid search, graph * traversal, cursor pagination, aggregation) that the live indexes cannot * answer for a *historical* generation. Storage-record reads (`get()`) and * metadata-filter `find()` are always supported at pinned generations. * * Rebuilding indexes from a pinned record set (the standard LSM answer for * historical index queries) is the documented `asOf`-rebuild follow-up; this * error is the honest boundary until it ships. * * @example * const db = brain.now() * await brain.transact([{ op: 'update', id, metadata: { v: 2 } }]) * await db.get(id) // ✅ pinned read — supported * await db.find({ where: { v: 1 } }) // ✅ metadata find — supported * await db.search('semantic query') // ❌ throws this error */ export class NotYetSupportedAtHistoricalGenerationError extends Error { /** The pinned generation the operation was attempted against. */ public readonly generation: number /** The capability that is not supported at historical generations. */ public readonly capability: string /** * @param capability - Human-readable name of the unsupported capability * (e.g. `'vector search'`, `'graph traversal (connected)'`). * @param generation - The Db's pinned generation. * @param currentGeneration - The store's current generation, for context. */ constructor(capability: string, generation: number, currentGeneration: number) { super( `${capability} is not yet supported at a historical generation ` + `(Db pinned at generation ${generation}; store is at ${currentGeneration}). ` + `Supported at pinned generations: get() and metadata-filter find(). ` + `For index-accelerated queries over historical state, persist() the generation ` + `you need and open it with Brainy.load(path) — the loaded snapshot rebuilds ` + `its indexes and supports the full query surface.` ) this.name = 'NotYetSupportedAtHistoricalGenerationError' 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 } }