/** * @module tests/integration/reprojection-doors-open * @description The reprojection engine against a REAL brain on filesystem * storage: a toy secondary projection (bucket counts with its own watermark * artifact, stamp-after-data per src/utils/projectionWatermark.ts) folds the * brain's committed facts through the engine, wired with the callback-form * {@link FactLogSource} over `brain.scanFacts`. * * Proves the three doors-open rows: * (i) folding to caught-up matches ground-truth counts; * (ii) mid-fold, `find()` and `get()` still answer, and a door bump * preempts the advance at the next boundary (mechanism-pinned via * batch counts, not wall-clock); * (iii) a crash mid-fold (abandon; reopen; re-advance) resumes from the * durable stamp — never refolds from zero. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Brainy } from '../../src/index.js' import { ReprojectionEngine, type ProjectionAdapter } from '../../src/reprojection/reprojectionEngine.js' import { FactLogSource } from '../../src/reprojection/factLogSource.js' import { makeProjectionStamp, readStampedWatermark } from '../../src/utils/projectionWatermark.js' import type { CommitFact } from '../../src/db/factLog.js' /** 50 rows, 5 buckets, 10 each. */ const ROWS = 50 const BUCKETS = 5 const GROUND_TRUTH: Record = { b0: 10, b1: 10, b2: 10, b3: 10, b4: 10 } /** * The toy secondary projection: latest bucket per entity id, persisted as a * data file plus a SEPARATE stamp artifact written stamp-after-data via the * shared projectionWatermark helpers. Idempotent by construction (latest- * state per id), so at-least-once redelivery on resume is harmless. */ class BucketCountProjection implements ProjectionAdapter { readonly family = 'bucket-counts' /** Every generation this INSTANCE applied — the refold detector for (iii). */ readonly appliedGenerations: number[] = [] private latest: Map private wm: number | null private constructor( private readonly dir: string, wm: number | null, latest: Map ) { this.wm = wm this.latest = latest } /** Load from the artifact dir — data is trusted only under a valid stamp. */ static async open(dir: string): Promise { mkdirSync(dir, { recursive: true }) const stampPath = join(dir, 'stamp.json') const dataPath = join(dir, 'data.json') let wm: number | null = null if (existsSync(stampPath)) { wm = readStampedWatermark(JSON.parse(readFileSync(stampPath, 'utf8'))) } const latest = new Map( wm !== null && existsSync(dataPath) ? (JSON.parse(readFileSync(dataPath, 'utf8')) as Array<[string, string | null]>) : [] ) return new BucketCountProjection(dir, wm, latest) } /** Non-null bucket tallies from the latest-state map. */ counts(): Record { const out: Record = {} for (const bucket of this.latest.values()) { if (bucket !== null) out[bucket] = (out[bucket] ?? 0) + 1 } return out } watermark(): number | null { return this.wm } async applyBatch(facts: CommitFact[], upTo: number): Promise { for (const fact of facts) { this.appliedGenerations.push(fact.generation) for (const op of fact.ops) { if (op.kind !== 'noun') continue if (op.record === null) { this.latest.set(op.id, null) // tombstone continue } // The stored noun record nests user metadata under `.metadata`. const stored = op.record.metadata as Record | null const user = (stored?.metadata ?? stored) as Record | null const bucket = typeof user?.bucket === 'string' ? user.bucket : null this.latest.set(op.id, bucket) } } // Durability THEN stamp — the projectionWatermark law. writeFileSync(join(this.dir, 'data.json'), JSON.stringify([...this.latest])) writeFileSync(join(this.dir, 'stamp.json'), JSON.stringify(makeProjectionStamp(upTo))) this.wm = upTo } async discard(): Promise { rmSync(this.dir, { recursive: true, force: true }) } } describe('reprojection doors-open — a real brain, a toy secondary projection', () => { let brainDir: string let projRoot: string let brain: Brainy const ids: string[] = [] const openBrain = async (dir: string): Promise => { const b = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true, dimensions: 384 }) await b.init() return b } /** * The production wiring, callback form: the engine's `from` is an EXCLUSIVE * lower bound, `scanFacts` bounds are inclusive — hence `from + 1`; the * first batch is returned and the handle closed (short batches at segment * boundaries are legal — only EMPTY means caught up). */ const sourceFor = (b: Brainy): FactLogSource => new FactLogSource(async (from, limit) => { const scan = b.scanFacts({ fromGeneration: from + 1, batchSize: limit }) if (!scan) throw new Error('this brain hosts no fact log — cannot reproject') const iterator = scan.batches() try { const first = await iterator.next() return first.done ? [] : first.value.facts } finally { if (typeof iterator.return === 'function') await iterator.return(undefined) } }) beforeAll(async () => { brainDir = mkdtempSync(join(tmpdir(), 'brainy-reproj-')) projRoot = mkdtempSync(join(tmpdir(), 'brainy-reproj-artifacts-')) brain = await openBrain(brainDir) for (let i = 0; i < ROWS; i++) { ids.push( await brain.add({ data: `record ${i} filed in bucket ${i % BUCKETS}`, type: 'document', metadata: { bucket: `b${i % BUCKETS}` } }) ) } }, 240_000) afterAll(async () => { await brain?.close().catch(() => {}) rmSync(brainDir, { recursive: true, force: true }) rmSync(projRoot, { recursive: true, force: true }) }) it('(i) folds to caught-up through the engine and matches ground-truth counts', async () => { const projection = await BucketCountProjection.open(join(projRoot, 'i')) const engine = new ReprojectionEngine({ source: sourceFor(brain), batchSize: 8 }) engine.register(projection) const result = await engine.advance(projection.family, { budgetMs: 60_000 }) expect(result.status).toBe('caught-up') expect(result.watermark).toBeGreaterThanOrEqual(ROWS) // one generation per add, at least expect(result.applied).toBeGreaterThanOrEqual(ROWS) expect(engine.quarantined(projection.family)).toEqual([]) expect(projection.counts()).toEqual(GROUND_TRUTH) // The stamp on disk is the adapter's own — stamped exactly at the fold head. const reloaded = await BucketCountProjection.open(join(projRoot, 'i')) expect(reloaded.watermark()).toBe(result.watermark) expect(reloaded.counts()).toEqual(GROUND_TRUTH) }) it('(ii) doors stay open mid-fold: find() and get() answer, and a bump preempts the advance', async () => { const projection = await BucketCountProjection.open(join(projRoot, 'ii')) const engine = new ReprojectionEngine({ source: sourceFor(brain), batchSize: 4 }) engine.register(projection) const head = brain.scanFacts()!.headGeneration const inFlight = engine.advance(projection.family, { budgetMs: 60_000 }) // The read hook: foreground door traffic announces itself, then reads — // both interleave with the running fold on the same event loop. engine.doorSignal.bump() const found = await brain.find({ query: 'record filed in bucket', limit: 3 }) const got = await brain.get(ids[0]) const result = await inFlight // The doors answered mid-fold. expect(found.length).toBeGreaterThan(0) expect(got).toBeTruthy() const gotMeta = got!.metadata as Record | undefined expect((gotMeta?.bucket ?? (gotMeta?.metadata as Record)?.bucket)).toBe('b0') // THE PREEMPTION PIN — mechanism, not wall-clock: the bump landed before // the first installment boundary, so the advance yielded after exactly // one batch (≤ batchSize facts), far short of the head. expect(result.status).toBe('preempted') expect(result.applied).toBeGreaterThan(0) expect(result.applied).toBeLessThanOrEqual(4) expect(projection.appliedGenerations.length).toBe(result.applied) expect(projection.watermark()).not.toBeNull() expect(projection.watermark()!).toBeLessThan(head) // Resuming folds the remainder; nothing was lost to the preemption. const resumed = await engine.advance(projection.family, { budgetMs: 60_000 }) expect(resumed.status).toBe('caught-up') expect(projection.counts()).toEqual(GROUND_TRUTH) }) it('(iii) crash mid-fold: reopen and re-advance resumes from the stamp, never refolds from zero', async () => { const projDir = join(projRoot, 'iii') const before = await BucketCountProjection.open(projDir) const engine1 = new ReprojectionEngine({ source: sourceFor(brain), batchSize: 4 }) engine1.register(before) // A zero budget folds exactly one guaranteed batch, then stops. const partial = await engine1.advance(before.family, { budgetMs: 0 }) expect(partial.status).toBe('budget-exhausted') const stamped = before.watermark() expect(stamped).not.toBeNull() expect(stamped!).toBeGreaterThan(0) // CRASH: abandon the engine and adapter mid-fold; reopen the brain cold. await brain.close() brain = await openBrain(brainDir) const after = await BucketCountProjection.open(projDir) expect(after.watermark()).toBe(stamped) // the stamp survived the crash const engine2 = new ReprojectionEngine({ source: sourceFor(brain), batchSize: 4 }) engine2.register(after) const resumed = await engine2.advance(after.family, { budgetMs: 60_000 }) expect(resumed.status).toBe('caught-up') // NEVER REFOLDS FROM ZERO: every generation the resumed instance applied // sits strictly above the crash stamp. expect(after.appliedGenerations.length).toBeGreaterThan(0) expect(Math.min(...after.appliedGenerations)).toBeGreaterThan(stamped!) // And the combined state — durable prefix plus resumed fold — is exact. expect(after.counts()).toEqual(GROUND_TRUTH) }) })