Avoid retaining processed session seqs
This commit is contained in:
@@ -49,7 +49,7 @@ Durable values need one accepted representation, not a check followed by a secon
|
||||
- `SurfaceOp` — how a surface node entered the linked list: `'append'` (normal tail append) or `{ op: 'replace', start, end }` (replace nodes from `start` through `end` inclusive — both must be valid surface node seqs; `start === end` replaces a single node). Used by compaction to shadow old nodes without deleting them.
|
||||
- `SurfaceIntent` — `{ surfaceOp: SurfaceOp; sourceEventSeqs?: number[] }`, the required third parameter to `session.append()` for surface-eligible types.
|
||||
- `SurfaceNode` — `{ seq: number; prev: number | null; next: number | null }`, one node in the surface linked list.
|
||||
- `foldSurface(events)` — replay the one canonical surface contract into detached current nodes and actual replacement ranges. The same pass rejects misplaced or malformed metadata, empty or duplicate provenance, unknown or non-earlier sources, invalid positional ranges, and replacements that fail to cite every shadowed surface node; `SurfaceManager` shares the atomic transition while retaining its incremental cache.
|
||||
- `foldSurface(events)` — replay the one canonical surface contract into detached current nodes and actual replacement ranges. The same pass rejects non-contiguous event seqs, misplaced or malformed metadata, empty or duplicate provenance, non-earlier sources, invalid positional ranges, and replacements that fail to cite every shadowed surface node; `SurfaceManager` shares the atomic transition while retaining its incremental cache.
|
||||
- `isSurfaceEvent(event)` / `isSurfaceEligibleType(type)` — the first narrows a `SessionEvent` to a fully-formed surface node (type is surface-eligible AND `surfaceOp` present); the second is the type-only check (is this one of the five `SurfaceEventType` values?), used to detect a surface-eligible event MISSING its marker — e.g. when validating a seed/load log.
|
||||
|
||||
### Request-header reconstruction (`request-header.ts`)
|
||||
|
||||
@@ -328,7 +328,7 @@ export class Session {
|
||||
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
|
||||
* circular reference, sparse array, or an exotic object such as
|
||||
* Map/Set/Date/class instance), or when the candidate violates the
|
||||
* canonical surface contract (marker shape and eligibility, unique known
|
||||
* canonical surface contract (marker shape and eligibility, unique
|
||||
* earlier provenance, positional replacement validity, and complete
|
||||
* shadowed-node coverage). One recursive pass reads, validates, and
|
||||
* copies each nested value once, so a stateful getter cannot supply one value
|
||||
|
||||
@@ -85,7 +85,6 @@ export interface SurfaceFoldResult {
|
||||
interface SurfaceFoldState {
|
||||
nodes: SurfaceNode[]
|
||||
nodeBySeq: Map<number, SurfaceNode>
|
||||
knownSeqs: Set<number>
|
||||
replaceGeneration: number
|
||||
}
|
||||
|
||||
@@ -106,7 +105,6 @@ function createFoldState(replaceGeneration = 0): SurfaceFoldState {
|
||||
return {
|
||||
nodes: [],
|
||||
nodeBySeq: new Map(),
|
||||
knownSeqs: new Set(),
|
||||
replaceGeneration,
|
||||
}
|
||||
}
|
||||
@@ -163,7 +161,6 @@ function surfaceEventOf(event: SessionEvent): SurfaceEvent | undefined {
|
||||
/** Validate provenance against prior log entries and the replacement range. */
|
||||
function assertProvenance(
|
||||
event: SurfaceEvent,
|
||||
knownSeqs: ReadonlySet<number>,
|
||||
shadowedSeqs: readonly number[],
|
||||
): void {
|
||||
const sources = event.sourceEventSeqs
|
||||
@@ -178,9 +175,6 @@ function assertProvenance(
|
||||
if (source >= event.seq) {
|
||||
throw new Error(`sourceEventSeqs must reference earlier events: ${source} >= current seq ${event.seq}`)
|
||||
}
|
||||
if (!knownSeqs.has(source)) {
|
||||
throw new Error(`sourceEventSeqs references unknown seq ${source}`)
|
||||
}
|
||||
}
|
||||
const missing = shadowedSeqs.filter(seq => !sourceSet.has(seq))
|
||||
if (missing.length > 0) {
|
||||
@@ -213,16 +207,23 @@ function replacementRange(
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate one event and prepare its atomic fold transition. */
|
||||
function planSurfaceEvent(state: SurfaceFoldState, event: SessionEvent): SurfacePlan | undefined {
|
||||
/** Validate one event at its replay boundary and prepare its atomic fold transition. */
|
||||
function planSurfaceEvent(
|
||||
state: SurfaceFoldState,
|
||||
event: SessionEvent,
|
||||
expectedSeq: number,
|
||||
): SurfacePlan | undefined {
|
||||
if (event.seq !== expectedSeq) {
|
||||
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`)
|
||||
}
|
||||
const surfaceEvent = surfaceEventOf(event)
|
||||
if (surfaceEvent === undefined) return
|
||||
if (surfaceEvent.surfaceOp === 'append') {
|
||||
assertProvenance(surfaceEvent, state.knownSeqs, [])
|
||||
assertProvenance(surfaceEvent, [])
|
||||
return { kind: 'append', seq: event.seq }
|
||||
}
|
||||
const range = replacementRange(state, surfaceEvent.surfaceOp)
|
||||
assertProvenance(surfaceEvent, state.knownSeqs, range.shadowedSeqs)
|
||||
assertProvenance(surfaceEvent, range.shadowedSeqs)
|
||||
return {
|
||||
kind: 'replace',
|
||||
seq: event.seq,
|
||||
@@ -257,8 +258,9 @@ function replaceSurface(state: SurfaceFoldState, plan: SurfaceReplacePlan): void
|
||||
function applySurfaceEvent(
|
||||
state: SurfaceFoldState,
|
||||
event: SessionEvent,
|
||||
expectedSeq: number,
|
||||
): SurfaceFoldReplacement | undefined {
|
||||
const plan = planSurfaceEvent(state, event)
|
||||
const plan = planSurfaceEvent(state, event, expectedSeq)
|
||||
if (plan?.kind === 'append') {
|
||||
const tail = state.nodes.at(-1)
|
||||
const node: SurfaceNode = { seq: plan.seq, prev: tail?.seq ?? null, next: null }
|
||||
@@ -268,7 +270,6 @@ function applySurfaceEvent(
|
||||
} else if (plan?.kind === 'replace') {
|
||||
replaceSurface(state, plan)
|
||||
}
|
||||
state.knownSeqs.add(event.seq)
|
||||
if (plan?.kind !== 'replace') return
|
||||
return {
|
||||
seq: plan.seq,
|
||||
@@ -287,14 +288,15 @@ function applySurfaceEvent(
|
||||
* @param events - session events in contiguous seq order.
|
||||
* @returns the current surface and every positional replacement.
|
||||
* @throws when any event violates the unified surface contract: metadata must
|
||||
* be well shaped and type-eligible, provenance must name unique known earlier
|
||||
* events, and a positional replacement must name and cite its complete range.
|
||||
* be well shaped and type-eligible, event seqs must be contiguous, provenance
|
||||
* must name unique earlier events, and a positional replacement must name and
|
||||
* cite its complete range.
|
||||
*/
|
||||
export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult {
|
||||
const state = createFoldState()
|
||||
const replacements: SurfaceFoldReplacement[] = []
|
||||
for (const event of events) {
|
||||
const replacement = applySurfaceEvent(state, event)
|
||||
for (const [index, event] of events.entries()) {
|
||||
const replacement = applySurfaceEvent(state, event, index)
|
||||
if (replacement !== undefined) replacements.push(replacement)
|
||||
}
|
||||
return {
|
||||
@@ -326,7 +328,7 @@ export class SurfaceManager {
|
||||
*/
|
||||
validateNext(event: SessionEvent): void {
|
||||
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
|
||||
planSurfaceEvent(this._state, event)
|
||||
planSurfaceEvent(this._state, event, this.log.length)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,7 +372,7 @@ export class SurfaceManager {
|
||||
// Index is bounded by i < this.log.length — never undefined.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const event = this.log[i]!
|
||||
applySurfaceEvent(this._state, event)
|
||||
applySurfaceEvent(this._state, event, i)
|
||||
this._lastProcessedSeq = i
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('foldSurface provenance', () => {
|
||||
['a fractional number', [provenanceEvent(0, [0.5])], /non-negative safe integers/],
|
||||
['a negative number', [provenanceEvent(0, [-1])], /non-negative safe integers/],
|
||||
['a self reference', [provenanceEvent(0, [0])], /must reference earlier events/],
|
||||
['an unknown earlier seq', [provenanceEvent(0, undefined), provenanceEvent(2, [1])], /references unknown seq 1/],
|
||||
['a non-contiguous event seq', [provenanceEvent(0, undefined), provenanceEvent(2, [1])], /seq 2 is not contiguous; expected 1/],
|
||||
['incomplete replacement coverage', [
|
||||
provenanceEvent(0, undefined),
|
||||
provenanceEvent(1, undefined),
|
||||
|
||||
Reference in New Issue
Block a user