feat(engine): a protected factory for the generation store — a subclass may substitute one that keeps the contract
This commit is contained in:
parent
a8c5fbf9dc
commit
f763317af7
2 changed files with 113 additions and 1 deletions
|
|
@ -1040,6 +1040,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory hook for the generation store, so an engine built on top of this
|
||||||
|
* reference implementation can substitute a `GenerationStore` that keeps
|
||||||
|
* the same behavioural contract (for example, one backed by a native
|
||||||
|
* implementation) — overriding it never changes this engine's own
|
||||||
|
* behaviour, since the default implementation is unchanged.
|
||||||
|
*/
|
||||||
|
protected createGenerationStore(storage: BaseStorage): GenerationStore {
|
||||||
|
return new GenerationStore(storage)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Brainy.
|
* Initialize Brainy.
|
||||||
*
|
*
|
||||||
|
|
@ -1297,7 +1308,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
// guarantees indexes never observe rolled-back state. Reader-mode
|
// guarantees indexes never observe rolled-back state. Reader-mode
|
||||||
// instances skip recovery (readers never write; the next writer
|
// instances skip recovery (readers never write; the next writer
|
||||||
// repairs).
|
// repairs).
|
||||||
this.generationStore = new GenerationStore(this.storage)
|
this.generationStore = this.createGenerationStore(this.storage)
|
||||||
const generationOpenResult = await step(
|
const generationOpenResult = await step(
|
||||||
'generation-store.open',
|
'generation-store.open',
|
||||||
'reading the generation manifest and committed ranges, opening the fact log and the ' +
|
'reading the generation manifest and committed ranges, opening the fact log and the ' +
|
||||||
|
|
|
||||||
101
tests/integration/generation-store-factory.test.ts
Normal file
101
tests/integration/generation-store-factory.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* @module tests/integration/generation-store-factory
|
||||||
|
* @description Pins the `createGenerationStore` protected factory hook on
|
||||||
|
* `Brainy` ({@link Brainy.createGenerationStore}). The hook exists so an
|
||||||
|
* engine built on top of this reference implementation can substitute a
|
||||||
|
* `GenerationStore` that keeps the same behavioural contract; this suite
|
||||||
|
* proves two things:
|
||||||
|
*
|
||||||
|
* 1. A subclass overriding the hook is the ONLY path that constructs the
|
||||||
|
* generation store — it is called exactly once, with the same storage
|
||||||
|
* instance `performInit` holds — and the store the brain actually uses
|
||||||
|
* is the one the override returned.
|
||||||
|
* 2. The default (non-overridden) path is unaffected — proven here by
|
||||||
|
* confirming the base class still produces a plain `GenerationStore`
|
||||||
|
* wired to `brain.storage`, and separately by running the existing
|
||||||
|
* `db-mvcc` and `brainy-core.integration` suites unmodified against this
|
||||||
|
* change (they exercise generation-store behaviour end to end).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
|
import { Brainy } from '../../src/brainy.js'
|
||||||
|
import { GenerationStore } from '../../src/db/generationStore.js'
|
||||||
|
import type { BaseStorage } from '../../src/storage/baseStorage.js'
|
||||||
|
|
||||||
|
/** Typed access to the brain's private storage + generation-store fields (test injection point). */
|
||||||
|
function internalsOf(brain: Brainy): { storage: BaseStorage; generationStore: GenerationStore } {
|
||||||
|
return brain as unknown as { storage: BaseStorage; generationStore: GenerationStore }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `GenerationStore` subclass that counts its own construction and
|
||||||
|
* remembers the storage instance it was built with, so the test can prove
|
||||||
|
* the hook is the sole construction path without mocking the module.
|
||||||
|
*/
|
||||||
|
class SpyGenerationStore extends GenerationStore {
|
||||||
|
static constructCount = 0
|
||||||
|
static lastStorage: BaseStorage | undefined
|
||||||
|
|
||||||
|
constructor(storage: BaseStorage) {
|
||||||
|
super(storage)
|
||||||
|
SpyGenerationStore.constructCount++
|
||||||
|
SpyGenerationStore.lastStorage = storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Brainy subclass overriding the factory hook — stands in for an engine built on the reference. */
|
||||||
|
class BrainyWithSpyStore extends Brainy {
|
||||||
|
hookCallCount = 0
|
||||||
|
hookStorageArg: BaseStorage | undefined
|
||||||
|
|
||||||
|
protected override createGenerationStore(storage: BaseStorage): GenerationStore {
|
||||||
|
this.hookCallCount++
|
||||||
|
this.hookStorageArg = storage
|
||||||
|
return new SpyGenerationStore(storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Brainy.createGenerationStore — protected factory hook', () => {
|
||||||
|
const brains: Brainy[] = []
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
SpyGenerationStore.constructCount = 0
|
||||||
|
SpyGenerationStore.lastStorage = undefined
|
||||||
|
for (const brain of brains.splice(0)) {
|
||||||
|
try {
|
||||||
|
await brain.close()
|
||||||
|
} catch {
|
||||||
|
// already closed by the test
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a subclass override is the sole construction path: called once, same storage instance, its store is the one the brain uses', async () => {
|
||||||
|
const brain = new BrainyWithSpyStore({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
|
||||||
|
// Called exactly once, through the hook.
|
||||||
|
expect(brain.hookCallCount).toBe(1)
|
||||||
|
expect(SpyGenerationStore.constructCount).toBe(1)
|
||||||
|
|
||||||
|
// Same storage instance the base class holds — not a copy, not a different adapter.
|
||||||
|
const { storage, generationStore } = internalsOf(brain)
|
||||||
|
expect(brain.hookStorageArg).toBe(storage)
|
||||||
|
expect(SpyGenerationStore.lastStorage).toBe(storage)
|
||||||
|
|
||||||
|
// The store the brain actually uses is the one the override returned.
|
||||||
|
expect(generationStore).toBeInstanceOf(SpyGenerationStore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('the default (non-overridden) path still produces a plain GenerationStore wired to the same storage', async () => {
|
||||||
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
|
||||||
|
const { storage, generationStore } = internalsOf(brain)
|
||||||
|
expect(generationStore).toBeInstanceOf(GenerationStore)
|
||||||
|
// The default implementation constructs from the same storage the brain holds.
|
||||||
|
expect((generationStore as unknown as { storage: BaseStorage }).storage).toBe(storage)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue