Reuse validated immutable history ranges during resource retention

This commit is contained in:
2026-11-11 01:56:43 +08:00
parent 26d1688f2f
commit 72a8e01114
5 changed files with 90 additions and 4 deletions
+26 -1
View File
@@ -5,6 +5,24 @@ type Splice<T>={readonly offset:number;readonly beforeLength:number;readonly aft
type Meta={readonly label:string;readonly groupId?:Id};
type EntryPatch={readonly id:Id;readonly before:Meta|null;readonly after:Meta|null;readonly changes:Splice<Change>;readonly operationIds:Splice<Id>};
export type HistoryTransition={readonly version:1;readonly rootRevision:number;readonly entryId?:Id;readonly before:{readonly undo:readonly Id[];readonly redo:readonly Id[]};readonly after:{readonly undo:readonly Id[];readonly redo:readonly Id[]};readonly entries:readonly EntryPatch[]};
// Resource scans revisit the same transition across many durability cuts. Cache
// only its flattened changes, never registry-dependent references or callbacks.
const transitionChanges=new WeakMap<HistoryTransition,readonly Change[]>();
const immutableData=new WeakSet<object>();
function immutablePlainData(value:unknown,depth=0):boolean{
if(depth>64)return false;
if(value===null||typeof value!=='object')return typeof value!=='function';
if(immutableData.has(value))return true;
if(!Object.isFrozen(value)||Object.getPrototypeOf(value)!==(Array.isArray(value)?Array.prototype:Object.prototype))return false;
if(Array.isArray(value))for(let i=0;i<value.length;i++)if(!Object.hasOwn(value,i))return false;
// Inspect every own descriptor, including non-enumerable properties. A frozen
// getter or a shallow-frozen container cannot establish reusable data.
for(const key of Reflect.ownKeys(value)){
const descriptor=Object.getOwnPropertyDescriptor(value,key)!;
if(!Object.hasOwn(descriptor,'value')||!immutablePlainData(descriptor.value,depth+1))return false;
}
immutableData.add(value);return true;
}
const meta=(entry:HistoryEntry|undefined):Meta|null=>{if(!entry)return null;const{id:_id,changes:_changes,operationIds:_operations,...remaining}=entry;return remaining;};
function splice<T>(before:readonly T[],after:readonly T[]):Splice<T>{
let offset=0,suffix=0;while(offset<before.length&&offset<after.length&&equal(before[offset],after[offset]))offset++;
@@ -43,4 +61,11 @@ export function applyHistoryTransition(history:HistoryState,transition:HistoryTr
const ids=[...destination.undo,...destination.redo];if(ids.length!==entries.size||ids.some(id=>!entries.has(id)))throw fault('history_transition_conflict');
return Object.freeze({undo:freeze([...destination.undo]),redo:freeze([...destination.redo]),entries:readMap(entries)});
}
export function historyTransitionChanges(transition:HistoryTransition):readonly Change[]{validateHistoryTransition(transition);return transition.entries.flatMap(patch=>[...patch.changes.before,...patch.changes.after]);}
export function historyTransitionChanges(transition:HistoryTransition):readonly Change[]{
const cached=transitionChanges.get(transition);if(cached)return cached;
validateHistoryTransition(transition);
const changes=transition.entries.flatMap(patch=>[...patch.changes.before,...patch.changes.after]);
const ownRanges=Object.hasOwn(transition,'entries')&&transition.entries.every(patch=>Object.hasOwn(patch,'changes')&&Object.hasOwn(patch.changes,'before')&&Object.hasOwn(patch.changes,'after'));
if(!ownRanges||!immutablePlainData(transition))return changes;
const result=Object.freeze(changes);transitionChanges.set(transition,result);return result;
}
+2 -1
View File
@@ -81,7 +81,8 @@ export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot
for(const entry of history.entries.values())rememberTargets(entry.changes);
let interpreted=true;
for(const record of ledger.operations.values()){
// Validate every understood transition, including resolved evidence, as before.
// Obtain validated ranges for every understood transition, including resolved
// evidence. The core may reuse only proven immutable transition data.
if(record.intent.historyTransition?.version===1)transitions.set(record,historyTransitionChanges(record.intent.historyTransition));
if(record.phase!=='resolved')rememberTargets(record.intent.changes);
if(record.intent.historyEffect&&record.intent.historyEffect.version!==1)interpreted=false;