feat(8.0): thread commit generation through the graph-write provider contract
Graph time-travel needs an edge's existence recorded per generation so db.asOf(g) hops resolve historically correct endpoints. The metadata layer already threads brainy's commit generation per write; the graph write path did not, leaving a versioned verb-endpoint store unable to answer "which edges existed at generation g". - GraphIndexProvider.addVerb/removeVerb gain a `generation: bigint` parameter (the same watermark the storage layer stamps onto the record). A provider with a per-generation edge chain stamps the edge at that generation; the JS baseline has no such chain and accepts-and-ignores it — graph time-travel is a native-provider capability, and the open-core path serves edges as-of-now (the one documented graph time-travel limitation). - The two graph transaction operations resolve the generation via a thunk at EXECUTE time: the generation store assigns the batch generation only once the commit begins executing, after the operations are planned. The same generation is reused for an operation's rollback half. - All graph-write call sites pass the in-flight generation. Adds a spy-provider test proving the threading, execute-time resolution, and forward/rollback generation reuse. The JS index ignores the value, so behaviour is unchanged: unit 1402/1402, db-mvcc 25/25, bigint-contract relate/unrelate 10/10.
This commit is contained in:
parent
b26d3d42b3
commit
0951fa1da0
6 changed files with 195 additions and 26 deletions
|
|
@ -283,6 +283,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* `compactHistory()` and the `Db` value type.
|
||||
*/
|
||||
private generationStore!: GenerationStore
|
||||
/**
|
||||
* Resolves the in-flight write generation as a BigInt, for stamping graph
|
||||
* edges via the {@link GraphIndexProvider} contract. Passed to every
|
||||
* `AddToGraphIndexOperation`/`RemoveFromGraphIndexOperation` and evaluated
|
||||
* when the operation executes — inside a `transact()` batch the generation
|
||||
* store has assigned the batch generation by then; for single-op writes it
|
||||
* reads the post-write watermark. The arrow body reads `generationStore`
|
||||
* lazily, so it is safe to define before `init()` assigns the store.
|
||||
*/
|
||||
private readonly graphWriteGeneration = (): bigint =>
|
||||
BigInt(this.generationStore.generation())
|
||||
/** Lazily built host surface shared by every `Db` value of this brain. */
|
||||
private _dbHost?: DbHost<T>
|
||||
/**
|
||||
|
|
@ -2188,7 +2199,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
// rollback can re-add through the BigInt addVerb contract)
|
||||
const { sourceInt, targetInt } = this.resolveVerbEndpointInts(verb)
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt)
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, this.graphWriteGeneration)
|
||||
)
|
||||
// Delete verb metadata
|
||||
tx.addOperation(
|
||||
|
|
@ -2580,6 +2591,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
tx.addOperation(
|
||||
new AddToGraphIndexOperation(
|
||||
this.graphIndex, verb, sourceInt, targetInt,
|
||||
this.graphWriteGeneration,
|
||||
(verbInt) => this.cacheVerbInt(verbInt, id)
|
||||
)
|
||||
)
|
||||
|
|
@ -2618,6 +2630,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
tx.addOperation(
|
||||
new AddToGraphIndexOperation(
|
||||
this.graphIndex, reverseVerb, targetInt, sourceInt,
|
||||
this.graphWriteGeneration,
|
||||
(verbInt) => this.cacheVerbInt(verbInt, reverseId)
|
||||
)
|
||||
)
|
||||
|
|
@ -2660,7 +2673,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (verb && endpointInts) {
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(
|
||||
this.graphIndex, verb, endpointInts.sourceInt, endpointInts.targetInt
|
||||
this.graphIndex, verb, endpointInts.sourceInt, endpointInts.targetInt, this.graphWriteGeneration
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -2785,12 +2798,13 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (typeChanged && reindexInts) {
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(
|
||||
this.graphIndex, existing, reindexInts.sourceInt, reindexInts.targetInt
|
||||
this.graphIndex, existing, reindexInts.sourceInt, reindexInts.targetInt, this.graphWriteGeneration
|
||||
)
|
||||
)
|
||||
tx.addOperation(
|
||||
new AddToGraphIndexOperation(
|
||||
this.graphIndex, verbForIndex, reindexInts.sourceInt, reindexInts.targetInt,
|
||||
this.graphWriteGeneration,
|
||||
(verbInt) => this.cacheVerbInt(verbInt, params.id)
|
||||
)
|
||||
)
|
||||
|
|
@ -4391,7 +4405,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
for (const verb of allVerbs) {
|
||||
const { sourceInt, targetInt } = this.resolveVerbEndpointInts(verb)
|
||||
tx.addOperation(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt)
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, this.graphWriteGeneration)
|
||||
)
|
||||
tx.addOperation(
|
||||
new DeleteVerbMetadataOperation(this.storage, verb.id)
|
||||
|
|
@ -5977,7 +5991,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
for (const verb of cascade.values()) {
|
||||
const { sourceInt, targetInt } = this.resolveVerbEndpointInts(verb)
|
||||
plan.operations.push(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt),
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, this.graphWriteGeneration),
|
||||
new DeleteVerbMetadataOperation(this.storage, verb.id)
|
||||
)
|
||||
plan.touchedVerbs.push(verb.id)
|
||||
|
|
@ -6103,7 +6117,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
targetId: params.to
|
||||
}),
|
||||
new SaveVerbMetadataOperation(this.storage, id, verbMetadata),
|
||||
new AddToGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, (verbInt) =>
|
||||
new AddToGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, this.graphWriteGeneration, (verbInt) =>
|
||||
this.cacheVerbInt(verbInt, id)
|
||||
)
|
||||
)
|
||||
|
|
@ -6131,7 +6145,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
targetId: params.from
|
||||
}),
|
||||
new SaveVerbMetadataOperation(this.storage, reverseId, verbMetadata),
|
||||
new AddToGraphIndexOperation(this.graphIndex, reverseVerb, targetInt, sourceInt, (verbInt) =>
|
||||
new AddToGraphIndexOperation(this.graphIndex, reverseVerb, targetInt, sourceInt, this.graphWriteGeneration, (verbInt) =>
|
||||
this.cacheVerbInt(verbInt, reverseId)
|
||||
)
|
||||
)
|
||||
|
|
@ -6161,7 +6175,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (verb) {
|
||||
const { sourceInt, targetInt } = this.resolveVerbEndpointInts(verb)
|
||||
plan.operations.push(
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt)
|
||||
new RemoveFromGraphIndexOperation(this.graphIndex, verb, sourceInt, targetInt, this.graphWriteGeneration)
|
||||
)
|
||||
}
|
||||
plan.operations.push(new DeleteVerbMetadataOperation(this.storage, id))
|
||||
|
|
|
|||
|
|
@ -646,9 +646,20 @@ export class GraphAdjacencyIndex implements GraphIndexProvider {
|
|||
* @param verb - The verb to index (endpoint UUIDs are authoritative).
|
||||
* @param sourceInt - The source entity's interned int (contract parity).
|
||||
* @param targetInt - The target entity's interned int (contract parity).
|
||||
* @param generation - The commit generation (contract parity). The JS index
|
||||
* keeps a single live adjacency view, not a per-generation edge chain, so
|
||||
* it ignores this: `db.asOf(g)` graph hops on the open-core path see edges
|
||||
* as-of-now (the one documented graph time-travel limitation — native
|
||||
* providers thread this into a versioned endpoint store for correctness).
|
||||
* @returns The interned verb int for `verb.id` (stable for the index lifetime).
|
||||
*/
|
||||
async addVerb(verb: GraphVerb, sourceInt: bigint, targetInt: bigint): Promise<bigint> {
|
||||
async addVerb(
|
||||
verb: GraphVerb,
|
||||
sourceInt: bigint,
|
||||
targetInt: bigint,
|
||||
generation: bigint
|
||||
): Promise<bigint> {
|
||||
void generation // Contract parity — no per-generation chain in the JS index.
|
||||
await this.ensureInitialized()
|
||||
return BigInt(await this.indexVerb(verb))
|
||||
}
|
||||
|
|
@ -703,9 +714,13 @@ export class GraphAdjacencyIndex implements GraphIndexProvider {
|
|||
* retained so previously returned verb ints stay resolvable via
|
||||
* {@link verbIntsToIds}.
|
||||
* @param verbId - The verb's UUID string.
|
||||
* @param generation - The commit generation (contract parity). The JS index
|
||||
* tombstones immediately rather than chaining the removal per generation,
|
||||
* so it ignores this (see {@link addVerb} for the time-travel rationale).
|
||||
* @returns Resolves once the verb no longer appears in reads.
|
||||
*/
|
||||
async removeVerb(verbId: string): Promise<void> {
|
||||
async removeVerb(verbId: string, generation: bigint): Promise<void> {
|
||||
void generation // Contract parity — no per-generation chain in the JS index.
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Load verb from cache/storage to get type info
|
||||
|
|
|
|||
|
|
@ -231,16 +231,26 @@ export interface GraphIndexProvider {
|
|||
* @param verb - The verb to index (endpoint UUIDs + derived `sourceInt`/`targetInt`).
|
||||
* @param sourceInt - The source entity's interned int.
|
||||
* @param targetInt - The target entity's interned int.
|
||||
* @param generation - Brainy's commit generation for this write — the same
|
||||
* watermark the storage layer stamps onto the record. A provider with a
|
||||
* per-generation edge chain records the edge's existence at this generation
|
||||
* so `db.asOf(g)` graph hops resolve historically correct endpoints; the
|
||||
* JS baseline has no such chain and ignores it (graph time-travel is a
|
||||
* native-provider capability — see the consistency-model doc).
|
||||
* @returns The interned verb int for `verb.id` (feeds Brainy's warm cache).
|
||||
*/
|
||||
addVerb(verb: GraphVerb, sourceInt: bigint, targetInt: bigint): Promise<bigint>
|
||||
addVerb(verb: GraphVerb, sourceInt: bigint, targetInt: bigint, generation: bigint): Promise<bigint>
|
||||
/**
|
||||
* @description Remove one verb from the index by its id string. The verb's
|
||||
* interned int stays reserved (ints are never recycled within a generation).
|
||||
* @param verbId - The verb's UUID string.
|
||||
* @param generation - Brainy's commit generation for this removal. A provider
|
||||
* with a per-generation edge chain tombstones the edge at this generation
|
||||
* (so it remains visible to `db.asOf(g)` for `g` before the removal); the
|
||||
* JS baseline removes immediately and ignores it.
|
||||
* @returns Resolves once the verb no longer appears in reads.
|
||||
*/
|
||||
removeVerb(verbId: string): Promise<void>
|
||||
removeVerb(verbId: string, generation: bigint): Promise<void>
|
||||
|
||||
rebuild(): Promise<void>
|
||||
flush(): Promise<void>
|
||||
|
|
|
|||
|
|
@ -169,6 +169,12 @@ export class RemoveFromMetadataIndexOperation implements Operation {
|
|||
* shared idMapper (`getOrAssign`) and passes them alongside the verb; the
|
||||
* provider returns the interned verb int, which is surfaced through the
|
||||
* optional `onVerbInt` callback so the coordinator can feed its warm cache.
|
||||
*
|
||||
* Generation: `generationFn` is resolved at execute time (not construction) so
|
||||
* the edge is stamped at the transaction's in-flight commit generation — which
|
||||
* the generation store only assigns once the batch begins executing. The same
|
||||
* generation is reused for the rollback removal, so an add and its undo
|
||||
* reference one watermark in a provider's per-generation edge chain.
|
||||
*/
|
||||
export class AddToGraphIndexOperation implements Operation {
|
||||
readonly name = 'AddToGraphIndex'
|
||||
|
|
@ -178,6 +184,8 @@ export class AddToGraphIndexOperation implements Operation {
|
|||
* @param verb - The verb to index (`sourceInt`/`targetInt` mirrored on it).
|
||||
* @param sourceInt - The source entity's interned int.
|
||||
* @param targetInt - The target entity's interned int.
|
||||
* @param generationFn - Resolves the commit generation to stamp this edge at,
|
||||
* evaluated when the operation executes (see class note).
|
||||
* @param onVerbInt - Optional hook invoked with the interned verb int
|
||||
* returned by the provider (feeds the coordinator's verb-int warm cache).
|
||||
*/
|
||||
|
|
@ -186,18 +194,21 @@ export class AddToGraphIndexOperation implements Operation {
|
|||
private readonly verb: GraphVerb,
|
||||
private readonly sourceInt: bigint,
|
||||
private readonly targetInt: bigint,
|
||||
private readonly generationFn: () => bigint,
|
||||
private readonly onVerbInt?: (verbInt: bigint) => void
|
||||
) {}
|
||||
|
||||
async execute(): Promise<RollbackAction> {
|
||||
// Add verb to graph index
|
||||
const verbInt = await this.index.addVerb(this.verb, this.sourceInt, this.targetInt)
|
||||
// Stamp this edge at the in-flight commit generation; reuse it for the
|
||||
// rollback so add + undo reference the same watermark.
|
||||
const generation = this.generationFn()
|
||||
const verbInt = await this.index.addVerb(this.verb, this.sourceInt, this.targetInt, generation)
|
||||
this.onVerbInt?.(verbInt)
|
||||
|
||||
// Return rollback action
|
||||
return async () => {
|
||||
// Remove verb from graph index
|
||||
await this.index.removeVerb(this.verb.id)
|
||||
await this.index.removeVerb(this.verb.id, generation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,8 +220,10 @@ export class AddToGraphIndexOperation implements Operation {
|
|||
* - Re-add verb to graph index
|
||||
*
|
||||
* 8.0 u64 contract: rollback re-adds through `addVerb(verb, sourceInt,
|
||||
* targetInt)`, so the coordinator resolves the endpoint ints up front
|
||||
* (while the entity → int mappings are guaranteed to still exist).
|
||||
* targetInt, generation)`, so the coordinator resolves the endpoint ints up
|
||||
* front (while the entity → int mappings are guaranteed to still exist). The
|
||||
* removal generation is resolved at execute time and reused for the rollback
|
||||
* re-add, so the round trip references one watermark.
|
||||
*/
|
||||
export class RemoveFromGraphIndexOperation implements Operation {
|
||||
readonly name = 'RemoveFromGraphIndex'
|
||||
|
|
@ -220,22 +233,26 @@ export class RemoveFromGraphIndexOperation implements Operation {
|
|||
* @param verb - The verb being removed (required for rollback re-add).
|
||||
* @param sourceInt - The source entity's interned int (rollback re-add).
|
||||
* @param targetInt - The target entity's interned int (rollback re-add).
|
||||
* @param generationFn - Resolves the commit generation for this removal,
|
||||
* evaluated when the operation executes.
|
||||
*/
|
||||
constructor(
|
||||
private readonly index: GraphIndexProvider,
|
||||
private readonly verb: GraphVerb, // Required for rollback
|
||||
private readonly sourceInt: bigint,
|
||||
private readonly targetInt: bigint
|
||||
private readonly targetInt: bigint,
|
||||
private readonly generationFn: () => bigint
|
||||
) {}
|
||||
|
||||
async execute(): Promise<RollbackAction> {
|
||||
// Remove verb from graph index
|
||||
await this.index.removeVerb(this.verb.id)
|
||||
// Resolve the removal generation once; reuse it for the rollback re-add.
|
||||
const generation = this.generationFn()
|
||||
await this.index.removeVerb(this.verb.id, generation)
|
||||
|
||||
// Return rollback action
|
||||
return async () => {
|
||||
// Re-add verb with original data
|
||||
await this.index.addVerb(this.verb, this.sourceInt, this.targetInt)
|
||||
await this.index.addVerb(this.verb, this.sourceInt, this.targetInt, generation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,13 @@ describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
|
|||
verb.sourceInt = sourceInt
|
||||
verb.targetInt = targetInt
|
||||
|
||||
const verbInt = await index.addVerb(verb, sourceInt, targetInt)
|
||||
// The 4th arg is the commit generation — contract parity with native
|
||||
// providers; the JS index ignores it (no per-generation edge chain).
|
||||
const verbInt = await index.addVerb(verb, sourceInt, targetInt, 1n)
|
||||
expect(typeof verbInt).toBe('bigint')
|
||||
|
||||
// Re-adding the same verb id returns the SAME interned int (stable).
|
||||
const verbIntAgain = await index.addVerb(verb, sourceInt, targetInt)
|
||||
const verbIntAgain = await index.addVerb(verb, sourceInt, targetInt, 1n)
|
||||
expect(verbIntAgain).toBe(verbInt)
|
||||
|
||||
// Both directed verb-int reads surface the int from addVerb.
|
||||
|
|
@ -88,7 +90,7 @@ describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
|
|||
const bInt = BigInt(idMapper.getOrAssign('uuid-b'))
|
||||
const verb = makeVerb(VERB_UUID_2, 'uuid-a', 'uuid-b')
|
||||
|
||||
await index.addVerb(verb, aInt, bInt)
|
||||
await index.addVerb(verb, aInt, bInt, 1n)
|
||||
|
||||
const out = await index.getNeighbors(aInt, { direction: 'out' })
|
||||
expect(out).toEqual([bInt])
|
||||
|
|
@ -113,7 +115,7 @@ describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
|
|||
const tInt = BigInt(idMapper.getOrAssign('uuid-t'))
|
||||
const verb = makeVerb(VERB_UUID_3, 'uuid-s', 'uuid-t')
|
||||
|
||||
const verbInt = await index.addVerb(verb, sInt, tInt)
|
||||
const verbInt = await index.addVerb(verb, sInt, tInt, 1n)
|
||||
// Persist the verb (vector + metadata — getVerb requires both) so
|
||||
// removeVerb can resolve its type for count updates.
|
||||
await storage.saveVerb({
|
||||
|
|
@ -129,7 +131,7 @@ describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
|
|||
createdAt: Date.now()
|
||||
})
|
||||
|
||||
await index.removeVerb(verb.id)
|
||||
await index.removeVerb(verb.id, 2n)
|
||||
|
||||
// Removed verb no longer appears in reads…
|
||||
expect(await index.getVerbIdsBySource(sInt)).toEqual([])
|
||||
|
|
|
|||
111
tests/unit/transaction/graphIndexOperations-generation.test.ts
Normal file
111
tests/unit/transaction/graphIndexOperations-generation.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* 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, 10n, 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, 10n, 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'), 10n, 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'),
|
||||
10n,
|
||||
20n,
|
||||
() => 5n,
|
||||
(verbInt) => {
|
||||
seen = verbInt
|
||||
}
|
||||
)
|
||||
|
||||
await op.execute()
|
||||
expect(seen).toBe(99n)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue