feat(8.0): temporal range verbs — diff, history, since(gen|Date), asOf{exclusive}, transactionLog window
asOf() answers "state AT a point"; these answer "what happened BETWEEN two
points" and "one entity's whole history", all on the existing generation records.
- Extract resolveAsOfGeneration() as the shared gen|Date→{generation,timestamp}
chokepoint (reachability + Date semantics identical everywhere). asOf()'s
inclusive path is byte-identical — db-mvcc still 25/25.
- asOf(target, { exclusive }) — strict-before (resolved−1, clamped at gen 0,
never a RangeError).
- db.since(Db | generation | Date) — overload; number/Date resolve via the shared
resolver (new DbHost.resolveGeneration); EXCLUSIVE lower bound;
since(db) === since(db.generation). Same-store guard via Db.belongsToStore.
- brain.diff(a, b) → { added, removed, modified } split by nouns/verbs. EARNS its
name: candidate set is ONLY changedBetween(gLow,gHigh), each classified by
existence at both endpoints + a key-order-insensitive value compare
(new src/db/stableEqual.ts) — a touched-but-reverted / born-and-died id is in
no bucket.
- brain.history(id, { from, to }) → every distinct version oldest→newest, each
value === asOf(version.generation).get(id); null = removal; kind auto-detected
(throws on UUID-space collision). New store helper generationsTouching().
- brain.transactionLog({ from, to, limit }) — INCLUSIVE generation/Date window
(contrast since's exclusive lower bound); limit applied last.
- Compaction policy locked: diff/since THROW GenerationCompactedError below the
horizon; history TRUNCATES to it.
Types DiffResult/HistoryVersion/EntityHistory exported from the package root.
Tests: tests/integration/db-temporal.test.ts (8 proofs incl. diff-earns-its-name,
history↔asOf cross-check, the composition proof, granularity, compaction contrast)
+ tests/unit/db/stableEqual.test.ts (6). docs/guides/snapshots-and-time-travel.md
+ RELEASES updated. 1477 unit + db-temporal 8 + db-mvcc 25 green.
This commit is contained in:
parent
373a48122d
commit
2c84f86815
10 changed files with 1069 additions and 37 deletions
|
|
@ -583,6 +583,32 @@ export class GenerationStore {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The committed generations in `(fromGen, toGen]` that touched a
|
||||
* single id, ascending. The per-id companion to {@link changedBetween} — used
|
||||
* by `history()` to walk one entity's change-points without scanning every id.
|
||||
* No all-ids index: it consults the same cached per-generation deltas.
|
||||
* @param kind - `'noun'` for entities, `'verb'` for relationships.
|
||||
* @param id - The id to track.
|
||||
* @param fromGen - Exclusive lower bound.
|
||||
* @param toGen - Inclusive upper bound.
|
||||
*/
|
||||
async generationsTouching(
|
||||
kind: 'noun' | 'verb',
|
||||
id: string,
|
||||
fromGen: number,
|
||||
toGen: number
|
||||
): Promise<number[]> {
|
||||
const result: number[] = []
|
||||
for (const gen of this.committedGens) {
|
||||
if (gen <= fromGen || gen > toGen) continue
|
||||
const delta = await this.getDelta(gen)
|
||||
const touched = kind === 'noun' ? delta.nouns : delta.verbs
|
||||
if (touched.has(id)) result.push(gen)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Assert that generation `gen` is reachable (not compacted
|
||||
* away) and within the committed range, then return it. Used by `asOf()`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue