feat: canonical enumeration mode for export — storage-walked, canon-complete, with an index-drift report
This commit is contained in:
parent
999d0ebbcf
commit
4d196af41b
6 changed files with 543 additions and 20 deletions
|
|
@ -23,8 +23,12 @@
|
|||
* serve the full query surface via at-generation index materialization.
|
||||
* - {@link GenerationCompactedError} — `asOf()` asked for a generation whose
|
||||
* immutable records were reclaimed by `compactHistory()`.
|
||||
* - {@link CanonicalEnumerationUnavailableError} — `export()`'s
|
||||
* `enumeration:'canonical'` mode was called on a historical `asOf()` view or a
|
||||
* speculative `with()` overlay; the canonical storage walk only ever answers
|
||||
* "what is live right now."
|
||||
*
|
||||
* All three are exported from the package root (`@soulcraft/brainy`).
|
||||
* All are exported from the package root (`@soulcraft/brainy`).
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -160,6 +164,64 @@ export class GenerationCompactedError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Thrown by `db.export(selector, { enumeration: 'canonical' })` when
|
||||
* the `Db` it is called on is not the live, current-generation view: a historical
|
||||
* `brain.asOf(g)` pin, or a speculative `db.with()` overlay.
|
||||
*
|
||||
* Canonical enumeration mode walks the storage adapter's canonical shard layout
|
||||
* directly (`storage.getNouns()`/`getVerbs()`) instead of the metadata/graph
|
||||
* indexes — but that walk has no generation parameter, it can only ever answer
|
||||
* "what is live right now." Serving it against a historical pin would silently
|
||||
* mix generations (today's canonical records under yesterday's selector), and
|
||||
* against a speculative overlay it would silently miss the overlay's own
|
||||
* in-memory entities (which never touched storage). Both are exactly the kind of
|
||||
* silently-wrong result canonical mode exists to prevent elsewhere — so this
|
||||
* boundary throws instead.
|
||||
*
|
||||
* `enumeration: 'index'` (the default) is unaffected: it composes with
|
||||
* `asOf()`/`with()` exactly as before, via the generation-correct `find()` walk.
|
||||
*
|
||||
* @example
|
||||
* const past = await brain.asOf(g1)
|
||||
* try {
|
||||
* await past.export({}, { enumeration: 'canonical' })
|
||||
* } catch (err) {
|
||||
* if (err instanceof CanonicalEnumerationUnavailableError) {
|
||||
* // Time-travel export: use the default index-based enumeration instead.
|
||||
* await past.export({}, { enumeration: 'index' })
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export class CanonicalEnumerationUnavailableError extends Error {
|
||||
/** The view's pinned generation. */
|
||||
public readonly generation: number
|
||||
/** Why canonical mode cannot serve this view. */
|
||||
public readonly reason: 'historical' | 'overlay'
|
||||
|
||||
/**
|
||||
* @param generation - The view's pinned generation.
|
||||
* @param reason - `'historical'` (a past `asOf()` pin) or `'overlay'` (a speculative `with()`).
|
||||
*/
|
||||
constructor(generation: number, reason: 'historical' | 'overlay') {
|
||||
const what =
|
||||
reason === 'historical'
|
||||
? `a historical view pinned at generation ${generation}`
|
||||
: `a speculative with() overlay (base generation ${generation})`
|
||||
super(
|
||||
`export()'s enumeration:'canonical' requires the live, current-generation view — ` +
|
||||
`it was called on ${what}. The canonical storage walk has no generation parameter, ` +
|
||||
`so it can only answer "what is live right now"; serving it here would silently ` +
|
||||
`mix generations (historical) or miss the overlay's own in-memory entities ` +
|
||||
`(overlay). Use enumeration:'index' (the default) for a time-travel or what-if ` +
|
||||
`export, or pin brain.now() for a live canonical export.`
|
||||
)
|
||||
this.name = 'CanonicalEnumerationUnavailableError'
|
||||
this.generation = generation
|
||||
this.reason = reason
|
||||
}
|
||||
}
|
||||
|
||||
/** One entity/relationship left in an unreconciled state by a failed rollback. */
|
||||
export interface UnreconciledRecord {
|
||||
/** The entity or relationship id. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue