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
+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);
}