Reuse owned table entry views during recovery encoding

This commit is contained in:
2026-11-10 14:45:18 +08:00
parent 6b3986f89f
commit 104ef3942f
5 changed files with 43 additions and 6 deletions
+8
View File
@@ -847,3 +847,11 @@ hang a second metrics read. Browser cleanup escalates only the fixture's own
process after its exit grace period. Optional heap/receipt sampling and a pinned
baseline override affect only fixture copies and are diagnostic, not product APIs
or production performance claims.
Recovery serialization memoizes entry arrays only for core-owned read-only Map
views. The core tracks membership ownership in a WeakSet; the codec's WeakMap
holds frozen pair/array containers per view. Value objects are not frozen by this
step. The existing recursive codec still checks nested mutability and depth, so
mutable values and external Maps are re-read. This is an encoding cache, not a
second state table; recovery JSON order, formats and journal checks remain the
same. Replacing a table gets a new entry view and leaves prior images unchanged.
+1 -1
View File
@@ -1,5 +1,5 @@
export * from './types.ts';
export * from './values.ts';
export {fault,reject,freeze,clone,equal,readMap,readSet,validJson} from './values.ts';
export * from './document.ts';
export * from './registry.ts';
export * from './features.ts';
+4 -1
View File
@@ -24,6 +24,9 @@ export function equal(a: unknown, b: unknown): boolean {
const left = Object.keys(a), right = Object.keys(b);
return left.length === right.length && left.every(key => Object.prototype.hasOwnProperty.call(b, key) && equal((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));
}
const ownedMapViews=new WeakSet<object>();
/** Internal provenance only; table ownership does not imply immutable values. */
export function isOwnedMapView(value:object){return ownedMapViews.has(value);}
/** A capability-safe read view: unlike TS ReadonlyMap, no .set is exposed at runtime. */
export function readMap<K, V>(input: ReadonlyMap<K, V>): ReadonlyMap<K, V> {
const map = new Map(input);
@@ -33,7 +36,7 @@ export function readMap<K, V>(input: ReadonlyMap<K, V>): ReadonlyMap<K, V> {
[Symbol.iterator]: () => map[Symbol.iterator](),
forEach: (callback, thisArg) => map.forEach((value, key) => callback.call(thisArg, value, key, view)),
};
return Object.freeze(view);
const result=Object.freeze(view);ownedMapViews.add(result);return result;
}
export function readSet<T>(input: Iterable<T>): ReadonlyValueSet<T> {
const set = new Set(input);
+16 -4
View File
@@ -14,13 +14,25 @@ import { validateHistoryReplay, replayHistoryEvidence, sameHistory } from './his
import type { RecoveryAudit } from './conflicts.ts';
import { applyHistoryTransition, validateHistoryTransition, type HistoryTransition } from '../canvas-core/history-transition.ts';
import { createDocument, validateDocument, isFactoryDocument } from '../canvas-core/document.ts';
import { fault, freeze, readMap, validJson, equal } from '../canvas-core/values.ts';
import { fault, freeze, readMap, validJson, equal, isOwnedMapView } from '../canvas-core/values.ts';
import type { EngineSnapshot, RuntimeState } from './contracts.ts';
import type { FrozenRequest, LedgerSnapshot, OperationRecord } from './ledger.ts';
import { validateRunInput, type RunRecord } from './runs.ts';
import type { ResourceRecord } from './resources.ts';
import type { RecoveryImage } from './storage-port.ts';
// Membership is stable only for core-owned read views. Never infer ownership
// from Object.freeze: a frozen native Map can still change through .set().
const entryViews=new WeakMap<object,readonly (readonly [unknown,unknown])[]>();
const noEntries=Object.freeze([]);
function mapValue<K,V>(map?:ReadonlyMap<K,V>):readonly (readonly [K,V])[]{
if(!map)return noEntries;
if(!isOwnedMapView(map))return [...map];
let value=entryViews.get(map);
if(!value){value=Object.freeze([...map].map(pair=>Object.freeze(pair)));entryViews.set(map,value);}
return value as readonly (readonly [K,V])[];
}
function encodeDocumentValue(document:Document){
// Freeze our own containers only. The codec still independently proves whether
// each nested value can be reused, including external mutable records.
@@ -35,9 +47,9 @@ function documentValue(document:Document){
export function encodeRecovery(snapshot: EngineSnapshot): RecoveryImage {
const payload = {
document: documentValue(snapshot.document),
history: { undo: snapshot.history.undo, redo: snapshot.history.redo, entries: [...snapshot.history.entries] },
operations: [...snapshot.operations],
runtime: { ...(snapshot.runtime.assetBindings?.size?{assetBindings:[...snapshot.runtime.assetBindings]}:{}), ...(snapshot.runtime.loadingRead?{loadingRead:{document:documentValue(snapshot.runtime.loadingRead.document),versions:[...snapshot.runtime.loadingRead.versions]}}:{}), ...(snapshot.runtime.loading?{loading:snapshot.runtime.loading}:{}), ...(snapshot.runtime.orphanFiles?.size?{orphanFiles:[...snapshot.runtime.orphanFiles]}:{}), imports:[...(snapshot.runtime.imports??[])], recoveries:[...(snapshot.runtime.recoveries??[])], runs: [...snapshot.runtime.runs], resources: [...snapshot.runtime.resources], resourcePins: [...snapshot.runtime.resourcePins], acknowledged: documentValue(snapshot.runtime.acknowledged), versions: [...snapshot.runtime.versions], receipts: [...snapshot.runtime.receipts], conflicts: [...snapshot.runtime.conflicts], ledger: { ...snapshot.runtime.ledger, operations: [...snapshot.runtime.ledger.operations], requests: [...snapshot.runtime.ledger.requests] } },
history: { undo: snapshot.history.undo, redo: snapshot.history.redo, entries: mapValue(snapshot.history.entries) },
operations: mapValue(snapshot.operations),
runtime: { ...(snapshot.runtime.assetBindings?.size?{assetBindings:mapValue(snapshot.runtime.assetBindings)}:{}), ...(snapshot.runtime.loadingRead?{loadingRead:{document:documentValue(snapshot.runtime.loadingRead.document),versions:mapValue(snapshot.runtime.loadingRead.versions)}}:{}), ...(snapshot.runtime.loading?{loading:snapshot.runtime.loading}:{}), ...(snapshot.runtime.orphanFiles?.size?{orphanFiles:mapValue(snapshot.runtime.orphanFiles)}:{}), imports:mapValue(snapshot.runtime.imports), recoveries:mapValue(snapshot.runtime.recoveries), runs: mapValue(snapshot.runtime.runs), resources: mapValue(snapshot.runtime.resources), resourcePins: mapValue(snapshot.runtime.resourcePins), acknowledged: documentValue(snapshot.runtime.acknowledged), versions: mapValue(snapshot.runtime.versions), receipts: mapValue(snapshot.runtime.receipts), conflicts: mapValue(snapshot.runtime.conflicts), ledger: { ...snapshot.runtime.ledger, operations: mapValue(snapshot.runtime.ledger.operations), requests: mapValue(snapshot.runtime.ledger.requests) } },
};
return sealRecoveryImage([...(snapshot.runtime.imports?.values()??[])].some(job=>job.source?.integrityVersion===2)?17:[...(snapshot.runtime.imports?.values()??[])].some(job=>job.source?.kind==='native-document'&&job.parserVersion===5)?16:[...(snapshot.runtime.imports?.values()??[])].some(job=>job.source?.kind==='native-document')?15:[...(snapshot.runtime.imports?.values()??[])].some(job=>job.source?.kind==='native-json-array')?14:snapshot.runtime.assetBindings?.size?13:snapshot.runtime.loading?(snapshot.runtime.loading.version===6?12:snapshot.runtime.loading.version===5?11:snapshot.runtime.loading.version===4?10:snapshot.runtime.loading.version===3?9:snapshot.runtime.loading.version===2?8:7):[...snapshot.operations.values()].some(intent=>intent.historyEffect)||[...snapshot.runtime.resources.values()].some(resource=>resource.sizing)?6:snapshot.runtime.orphanFiles?.size?5:[...(snapshot.runtime.imports?.values()??[])].some(job=>job.source?.retirement||job.source?.previousFiles?.length)?4:[...snapshot.runtime.resources.values()].some(resource=>resource.retirement)?3:snapshot.runtime.imports?.size ? 2 : 1,snapshot.rootRevision,snapshot.revision,payload);
}
+14
View File
@@ -14,3 +14,17 @@ test('unowned document envelopes reread mutable map membership and nested data w
test('factory table ownership does not imply deep immutability of values inherited from an external document',()=>{
const base=doc(),mutable=node('external'),source={...base,elements:new Map([['external',mutable]])},document=applyChanges(source,[]),one=encodeRecovery(snapshot(document));mutable.data.text='later';source.elements.set('new',node('new'));const two=encodeRecovery(snapshot(document,1));assert.equal(two.payload.document.elements.length,1);assert.equal(two.payload.document.elements[0].data.text,'later');assert.equal(one.payload.document.elements[0].data.text,'');assert.equal(Object.isFrozen(mutable),false);
});
import {readMap,freeze} from '../../src/lib/canvas-core/values.ts';
import * as publicCore from '../../src/lib/canvas-core/index.ts';
test('metadata roots reuse owned table entry encodings while new tables retain old image membership',()=>{
assert.equal(Object.hasOwn(publicCore,'isOwnedMapView'),false,'provenance is an internal codec optimization');
const state=snapshot(doc()),entries=readMap(new Map([['one',freeze({text:'first'})],['two',freeze({text:'second'})]]));state.operations=entries;state.history.entries=entries;state.runtime.receipts=entries;const one=encodeRecovery(state),iterate=Map.prototype[Symbol.iterator];let two;
try{Map.prototype[Symbol.iterator]=function(){if(this.size===2)throw Error('owned table enumerated again');return iterate.call(this);};two=encodeRecovery({...state,rootRevision:1});}finally{Map.prototype[Symbol.iterator]=iterate;}
assert.strictEqual(two.payload.operations,one.payload.operations);assert.strictEqual(two.payload.history.entries,one.payload.history.entries);assert.strictEqual(two.payload.runtime.receipts,one.payload.runtime.receipts);assert.strictEqual(one.payload.operations,one.payload.runtime.receipts);
const changed=readMap(new Map([...entries,['three',freeze({text:'third'})]])),three=encodeRecovery({...state,rootRevision:2,operations:changed});assert.equal(three.payload.operations.length,3);assert.equal(one.payload.operations.length,2);assert.strictEqual(three.payload.operations[0][1],one.payload.operations[0][1]);
});
test('external maps and immutable-looking maps reread membership; owned tables still reread mutable nested values',()=>{
const state=snapshot(doc()),external=Object.freeze(new Map([['one',{text:'first'}]]));state.runtime.receipts=external;const one=encodeRecovery(state);external.get('one').text='second';external.set('two',{text:'other'});const two=encodeRecovery({...state,rootRevision:1});assert.deepEqual(two.payload.runtime.receipts,[['one',{text:'second'}],['two',{text:'other'}]]);assert.equal(one.payload.runtime.receipts.length,1);assert.equal(one.payload.runtime.receipts[0][1].text,'first');assert.equal(Object.isFrozen(external.get('one')),false);
const mutable={text:'first'},shallow=Object.freeze({mutable}),owned=readMap(new Map([['one',shallow]]));state.runtime.receipts=owned;const three=encodeRecovery({...state,rootRevision:2});mutable.text='changed';const four=encodeRecovery({...state,rootRevision:3});assert.equal(three.payload.runtime.receipts[0][1].mutable.text,'first');assert.equal(four.payload.runtime.receipts[0][1].mutable.text,'changed');assert.equal(Object.isFrozen(mutable),false);
});