110 lines
4.5 KiB
TypeScript
110 lines
4.5 KiB
TypeScript
/**
|
|
* Generation threading through the graph-index transaction operations.
|
|
*
|
|
* The graph time-travel contract (AddVerb/RemoveVerb gain a `generation`) is
|
|
* load-bearing for native providers: `db.asOf(g)` graph hops resolve historical
|
|
* endpoints from a per-generation edge chain the provider stamps at write time.
|
|
* Brainy's JS index ignores the value, so a behavioural test against it proves
|
|
* nothing — these tests use a spy provider to assert the operation layer:
|
|
* 1. passes the resolved generation to addVerb/removeVerb,
|
|
* 2. resolves the generation at EXECUTE time, not construction (the batch
|
|
* generation is only assigned once the commit begins executing), and
|
|
* 3. reuses one generation across an operation's forward + rollback halves
|
|
* (so an add and its undo reference the same watermark in the chain).
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
AddToGraphIndexOperation,
|
|
RemoveFromGraphIndexOperation
|
|
} from '../../../src/transaction/operations/IndexOperations.js'
|
|
import type { GraphIndexProvider } from '../../../src/plugin.js'
|
|
import type { GraphVerb } from '../../../src/coreTypes.js'
|
|
import { VerbType } from '../../../src/types/graphTypes.js'
|
|
|
|
/** A graph verb with just the fields the operations touch. */
|
|
function makeVerb(id: string): GraphVerb {
|
|
return {
|
|
id,
|
|
sourceId: 'src-uuid',
|
|
targetId: 'tgt-uuid',
|
|
type: VerbType.RelatedTo,
|
|
sourceInt: 10n,
|
|
targetInt: 20n
|
|
} as unknown as GraphVerb
|
|
}
|
|
|
|
/** Records every (method, generation) the operations push to the provider. */
|
|
function makeSpyProvider(): {
|
|
provider: GraphIndexProvider
|
|
calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }>
|
|
} {
|
|
const calls: Array<{ method: 'addVerb' | 'removeVerb'; generation: bigint; verbId: string }> = []
|
|
const provider = {
|
|
async addVerb(verb: GraphVerb, _s: bigint, _t: bigint, generation: bigint): Promise<bigint> {
|
|
calls.push({ method: 'addVerb', generation, verbId: verb.id })
|
|
return 99n // interned verb int
|
|
},
|
|
async removeVerb(verbId: string, generation: bigint): Promise<void> {
|
|
calls.push({ method: 'removeVerb', generation, verbId })
|
|
}
|
|
} as unknown as GraphIndexProvider
|
|
return { provider, calls }
|
|
}
|
|
|
|
describe('Graph index operations — generation threading', () => {
|
|
it('AddToGraphIndexOperation stamps addVerb (and its rollback removeVerb) at the resolved generation', async () => {
|
|
const { provider, calls } = makeSpyProvider()
|
|
const verb = makeVerb('verb-1')
|
|
const op = new AddToGraphIndexOperation(provider, verb, { sourceInt: 10n, targetInt: 20n }, () => 7n)
|
|
|
|
const rollback = await op.execute()
|
|
expect(calls).toEqual([{ method: 'addVerb', generation: 7n, verbId: 'verb-1' }])
|
|
|
|
await rollback()
|
|
expect(calls[1]).toEqual({ method: 'removeVerb', generation: 7n, verbId: 'verb-1' })
|
|
})
|
|
|
|
it('RemoveFromGraphIndexOperation stamps removeVerb (and its rollback addVerb) at the resolved generation', async () => {
|
|
const { provider, calls } = makeSpyProvider()
|
|
const verb = makeVerb('verb-2')
|
|
const op = new RemoveFromGraphIndexOperation(provider, verb, { sourceInt: 10n, targetInt: 20n }, () => 12n)
|
|
|
|
const rollback = await op.execute()
|
|
expect(calls).toEqual([{ method: 'removeVerb', generation: 12n, verbId: 'verb-2' }])
|
|
|
|
await rollback()
|
|
expect(calls[1]).toEqual({ method: 'addVerb', generation: 12n, verbId: 'verb-2' })
|
|
})
|
|
|
|
it('resolves the generation at EXECUTE time, not at construction', async () => {
|
|
// The generation store assigns the batch generation only once the commit
|
|
// begins executing — after the operations are built. The thunk must be read
|
|
// then, so a value that changes between construction and execute is observed
|
|
// at its execute-time value.
|
|
const { provider, calls } = makeSpyProvider()
|
|
let current = 1n
|
|
const op = new AddToGraphIndexOperation(provider, makeVerb('verb-3'), { sourceInt: 10n, targetInt: 20n }, () => current)
|
|
|
|
current = 42n // store assigns the batch generation after the op is built
|
|
await op.execute()
|
|
|
|
expect(calls[0].generation).toBe(42n)
|
|
})
|
|
|
|
it('still surfaces the provider verb int through onVerbInt after the signature change', async () => {
|
|
const { provider } = makeSpyProvider()
|
|
let seen: bigint | undefined
|
|
const op = new AddToGraphIndexOperation(
|
|
provider,
|
|
makeVerb('verb-4'),
|
|
{ sourceInt: 10n, targetInt: 20n },
|
|
() => 5n,
|
|
(verbInt) => {
|
|
seen = verbInt
|
|
}
|
|
)
|
|
|
|
await op.execute()
|
|
expect(seen).toBe(99n)
|
|
})
|
|
})
|