/** * @module db/types * @description Shared types for the 8.0 generational-MVCC Db API: the * declarative transaction-operation union consumed by `brain.transact()`, * the options bags for `transact()` / `compactHistory()`, the persisted * record shapes of the generational record layer, and the narrow storage * contract (`GenerationStorage`) the record layer needs from an adapter. * * Persisted layout (all paths relative to the storage root): * * ``` * _system/generation.json — { generation, updatedAt } (atomic tmp+rename) * _system/manifest.json — { version, generation, committedAt, horizon } * (atomic tmp+rename — the rename IS the commit point) * _system/tx-log.jsonl — one JSON line per committed transact: * { generation, timestamp, meta? } (append-only) * _generations//tx.json — the generation-N delta: touched noun/verb ids + meta * _generations//prev/.json — immutable before-image of as of commit N * ``` * * Design note: the manifest holds the committed watermark + compaction * horizon rather than a full `id → latest record generation` map. The id * mapping is distributed across the per-generation `tx.json` deltas, which * makes each commit O(ids touched) instead of O(all ids) — see * `docs/ADR-001-generational-mvcc.md` for the full justification. */ import type { AddParams, UpdateParams, RelateParams } from '../types/brainy.types.js' // ============================================================================ // Transaction operations (brain.transact input) // ============================================================================ /** * @description Create an entity. Carries the same parameters as * `brain.add()`; supply `id` to choose the entity id (otherwise a UUID v4 is * generated and returned in the operation's planning, exactly as `add()` * does). */ export interface TxAddOperation extends AddParams { /** Discriminator. */ op: 'add' } /** * @description Update an entity. Carries the same parameters as * `brain.update()` including per-entity CAS via `ifRev` — an `ifRev` * conflict rejects the whole transaction before anything is applied. */ export interface TxUpdateOperation extends UpdateParams { /** Discriminator. */ op: 'update' } /** * @description Delete an entity (and, exactly like `brain.delete()`, every * relationship where it is source or target — the cascade is part of the * same atomic batch). */ export interface TxRemoveOperation { /** Discriminator. */ op: 'remove' /** Id of the entity to delete. */ id: string } /** * @description Create a relationship. Carries the same parameters as * `brain.relate()` (including `bidirectional`). Duplicate relationships * (same from/to/type) are deduplicated exactly as `relate()` does — the * operation becomes a no-op that resolves to the existing relationship id. */ export interface TxRelateOperation extends RelateParams { /** Discriminator. */ op: 'relate' } /** * @description Delete a relationship by id (mirror of `brain.unrelate()`). */ export interface TxUnrelateOperation { /** Discriminator. */ op: 'unrelate' /** Id of the relationship to delete. */ id: string } /** * @description One declarative operation inside a `brain.transact()` batch. * The batch executes atomically: either every operation applies and the * store advances exactly one generation, or none apply and the store is * byte-identical to its pre-transaction state. */ export type TxOperation = | TxAddOperation | TxUpdateOperation | TxRemoveOperation | TxRelateOperation | TxUnrelateOperation /** * @description Options for `brain.transact()`. */ export interface TransactOptions { /** * Transaction metadata, reified Datomic-style: recorded in * `_system/tx-log.jsonl` alongside the generation number and commit * timestamp. Use it for audit fields (author, reason, request id) — * it replaces commit messages from the pre-8.0 versioning API. */ meta?: Record /** * Whole-store compare-and-swap. When provided, the transaction commits * only if the store's current generation equals this value; otherwise it * throws `GenerationConflictError` (with `expected`/`actual`) before any * record is staged. */ ifAtGeneration?: number } /** * @description Per-operation results of a committed transaction, in input * order. `add` resolves to the created entity id, `relate` to the created * (or deduplicated existing) relationship id; `update`/`remove`/`unrelate` * resolve to the id they acted on. */ export interface TransactReceipt { /** The generation this transaction committed as. */ generation: number /** Commit timestamp (ms since epoch) recorded in the tx-log. */ timestamp: number /** Resolved id per input operation, in input order. */ ids: string[] } // ============================================================================ // Compaction // ============================================================================ /** * @description Options for `brain.compactHistory()`. When both options are * supplied a generation must satisfy *both* retention rules to be reclaimed * (the stricter retention wins). With no options, every generation not * protected by a live pin is eligible. */ export interface CompactHistoryOptions { /** * Keep at least this many of the most recent committed generations * (`0` = keep none beyond what live pins protect). */ retainGenerations?: number /** * Keep every generation committed within the last `retainMs` milliseconds. */ retainMs?: number } /** * @description Result of `brain.compactHistory()`. */ export interface CompactHistoryResult { /** Number of generation record-sets reclaimed by this call. */ removedGenerations: number /** * The new compaction horizon — the highest generation whose record-set has * been reclaimed. Generations BELOW the horizon are no longer reachable via * `asOf()` (their reads would need the reclaimed before-images); the * horizon itself stays reachable, resolved from the record-sets above it. */ horizon: number } // ============================================================================ // Db surfaces // ============================================================================ /** * @description Result of `db.since(priorDb)` — the ids touched by committed * transactions in the generation interval `(priorDb.generation, * db.generation]`. Granularity note: single-operation writes performed * outside `transact()` bump the generation counter but do not produce * generation records, so they are not reflected here (documented 8.0 * history granularity — see `docs/ADR-001-generational-mvcc.md`). */ export interface ChangedIds { /** Entity (noun) ids touched in the interval, sorted. */ nouns: string[] /** Relationship (verb) ids touched in the interval, sorted. */ verbs: string[] /** Exclusive lower bound of the interval. */ fromGeneration: number /** Inclusive upper bound of the interval. */ toGeneration: number } // ============================================================================ // Persisted record shapes // ============================================================================ /** * @description An immutable before-image of one entity or relationship, * persisted under `_generations//prev/.json` — the state of the id * immediately before commit `N` touched it. `metadata`/`vector` hold the * *raw stored objects* (exact bytes that live at the entity's canonical * storage paths); `null` means the corresponding file did not exist (for * before-images of created ids, both are `null`). Before-images are both * the crash-recovery undo log and the point-in-time read source: the state * of id X at pinned generation G is the before-image of the first committed * generation after G that touched X. */ export interface GenerationRecord { /** Whether this record images an entity (noun) or a relationship (verb). */ kind: 'noun' | 'verb' /** Raw stored metadata object, or `null` if the metadata file was absent. */ metadata: unknown | null /** Raw stored vector object, or `null` if the vector file was absent. */ vector: unknown | null } /** * @description The per-generation delta persisted at * `_generations//tx.json` — written and fsynced *before* any operation of * the transaction executes, so crash recovery always knows exactly which ids * to restore from before-images. */ export interface GenerationDelta { /** The generation this delta belongs to. */ generation: number /** Commit timestamp (ms since epoch). */ timestamp: number /** Transaction metadata (mirrored into the tx-log on commit). */ meta?: Record /** Entity ids touched by this generation. */ nouns: string[] /** Relationship ids touched by this generation. */ verbs: string[] } /** * @description Shape of `_system/manifest.json`. Replaced via atomic * tmp+rename on every commit — the rename is the commit point: a generation * directory is committed if and only if its number is ≤ `generation`. */ export interface GenerationManifest { /** Schema version of the manifest file. */ version: 1 /** Committed-transaction watermark. */ generation: number /** ISO timestamp of the most recent commit (or compaction). */ committedAt: string /** Compaction horizon — record-sets for generations ≤ this are reclaimed. */ horizon: number } /** * @description One line of `_system/tx-log.jsonl`. */ export interface TxLogEntry { /** The committed generation. */ generation: number /** Commit timestamp (ms since epoch). */ timestamp: number /** Transaction metadata, when supplied to `transact()`. */ meta?: Record } // ============================================================================ // Storage contract // ============================================================================ /** * @description The narrow storage contract the generational record layer * needs. `BaseStorage` implements it structurally (both shipped adapters — * filesystem and memory — inherit the implementation), so any * `StorageAdapter` produced by the 8.0 storage factory satisfies it. * * Raw-object methods bypass branch scoping and operate on storage-root- * relative paths (the record layer owns `_system/` + `_generations/`); * entity-raw methods read/write the canonical entity files (branch-scoped, * write-cache coherent) so before/after images capture exactly the bytes * the live read paths see. */ export interface GenerationStorage { /** Read a raw object at a storage-root-relative path (`null` if absent). */ readRawObject(path: string): Promise /** Write a raw object at a storage-root-relative path (atomic tmp+rename on disk). */ writeRawObject(path: string, data: any): Promise /** Delete a raw object (no-op if absent). */ deleteRawObject(path: string): Promise /** List raw object paths under a prefix (normalized, `.gz`-stripped). */ listRawObjects(prefix: string): Promise /** Remove every object under a prefix (and the directory itself on disk). */ removeRawPrefix(prefix: string): Promise /** Durability barrier: fsync the given object paths (no-op in memory). */ syncRawObjects(paths: string[]): Promise /** Read an entity's raw stored metadata+vector objects. */ readNounRaw(id: string): Promise<{ metadata: any | null; vector: any | null }> /** Restore an entity's raw stored objects (`null` part ⇒ delete that file). */ writeNounRaw(id: string, record: { metadata: any | null; vector: any | null }): Promise /** Read a relationship's raw stored metadata+vector objects. */ readVerbRaw(id: string): Promise<{ metadata: any | null; vector: any | null }> /** Restore a relationship's raw stored objects (`null` part ⇒ delete that file). */ writeVerbRaw(id: string, record: { metadata: any | null; vector: any | null }): Promise /** Append one line to `_system/tx-log.jsonl`. */ appendTxLogLine(line: string): Promise /** Read all lines of `_system/tx-log.jsonl` (empty array if absent). */ readTxLogLines(): Promise /** * Register the generation-bump hook invoked on every entity-visible * single-operation write (see `BaseStorage.setGenerationBumpHook`). */ setGenerationBumpHook(hook: (() => void) | undefined): void /** Snapshot the entire store to a directory (hard-link farm on disk). */ snapshotToDirectory(targetPath: string): Promise /** Replace the entire store's contents from a snapshot directory. */ restoreFromDirectory(sourcePath: string): Promise }