Slice detached element recovery encoding and audit progressive canvas

This commit is contained in:
2026-11-11 04:53:07 +08:00
parent 5eabca5056
commit 2089b63e79
6 changed files with 175 additions and 14 deletions
+8 -2
View File
@@ -9,11 +9,17 @@ export function diffJournal(before:Json,after:Json):JournalPatch[]|null {
function visit(a:Json,b:Json,path:(string|number)[]){
if(a===b||patches.length>10000)return;
if(Array.isArray(a)&&Array.isArray(b)){
const common=Math.min(a.length,b.length);for(let i=0;i<common;i++)visit(a[i],b[i],[...path,i]);
const common=Math.min(a.length,b.length);for(let i=0;i<common;i++){
const previous=a[i],next=b[i];
// Shared immutable rows need no path allocation or recursive call.
if(previous!==next)visit(previous,next,[...path,i]);
}
if(a.length!==b.length)patches.push({op:'splice',path,start:common,deleteCount:a.length-common,items:b.slice(common)});
}else if(a&&b&&typeof a==='object'&&typeof b==='object'&&!Array.isArray(a)&&!Array.isArray(b)){
const left=Object.keys(a),right=Object.keys(b);
if(left.length===right.length&&left.every((key,i)=>key===right[i]))for(const key of right)visit(a[key],b[key],[...path,key]);
if(left.length===right.length&&left.every((key,i)=>key===right[i]))for(const key of right){
const previous=a[key],next=b[key];if(previous!==next)visit(previous,next,[...path,key]);
}
else patches.push({op:'set',path,value:b});
}else patches.push({op:'set',path,value:b});
}
+28 -9
View File
@@ -1,3 +1,5 @@
import {hasOwnedElementData} from '../canvas-core/element-data.ts';
import type {ElementRecord} from '../canvas-core/types.ts';
import type { Json } from '../canvas-core/types.ts';
import { fault, freeze, validJson } from '../canvas-core/values.ts';
import {isDataContainer} from '../canvas-core/data-containers.ts';
@@ -72,12 +74,20 @@ export function sealRecoveryImage(formatVersion:ImageVersion,rootRevision:number
}
type Scalar = null|string|number|boolean|undefined|symbol;
type Prepared = Scalar | Encoded | {source:object;depth:number;children:Prepared[];keys?:string[]};
type Prepared = Scalar | Encoded | {source:object;depth:number;children?:Prepared[];keys?:string[]};
// Read unknown objects synchronously, before the first suspension. Shape-owned
// containers can include external values; each such occurrence is captured now.
// Only our fresh container shape and previously proven encoded values defer.
// Detached element JSON can also defer after checking its prototype environment.
function prepareEncoding(input:unknown) {
const normalize=createEncoder(),visiting=new Set<object>();
let plainEnvironment:boolean|undefined;
const capture=(value:unknown,depth:number,key:string)=>{
try{return normalize(value,depth,key);}finally{plainEnvironment=undefined;}
};
const detached=(value:object)=>hasOwnedElementData(value as ElementRecord)&&(plainEnvironment??=(
Object.getPrototypeOf(Object.prototype)===null&&Object.getPrototypeOf(Array.prototype)===Object.prototype&&
!Object.getOwnPropertyDescriptor(Object.prototype,'toJSON')&&!Object.getOwnPropertyDescriptor(Array.prototype,'toJSON')
));
function prepare(value:unknown,depth:number,key:string):Prepared {
const kind=typeof value;
if(value===null||kind==='string'||kind==='number'||kind==='boolean'||kind==='undefined'||kind==='symbol'){
@@ -86,15 +96,18 @@ function prepareEncoding(input:unknown) {
// as well as allocation of their Encoded facts to the budgeted traversal.
return value as Scalar;
}
if(typeof value!=='object')return normalize(value,depth,key);
if(typeof value!=='object')return capture(value,depth,key);
const cached=immutable.get(value);
if(cached){if(depth+cached.height>64)throw fault('invalid_recovery_payload');return cached;}
if(!isDataContainer(value))return normalize(value,depth,key);
// Detached element JSON has no accessors, custom prototypes or mutable
// descendants. Its full walk can wait; foreign freezes cannot grant this.
if(detached(value))return {source:value,depth};
if(!isDataContainer(value))return capture(value,depth,key);
// Prototype toJSON changes must retain ordinary JSON semantics, including
// accessors. Inspect descriptors without evaluating them an extra time.
let prototype:object|null=value,hasToJSON=false;
while(prototype){if(Object.getOwnPropertyDescriptor(prototype,'toJSON')){hasToJSON=true;break;}prototype=Object.getPrototypeOf(prototype);}
if(hasToJSON)return normalize(value,depth,key);
if(hasToJSON)return capture(value,depth,key);
if(depth>64||visiting.has(value))throw fault('invalid_recovery_payload');
visiting.add(value);
try {
@@ -123,17 +136,23 @@ export async function sealRecoveryImageAsync(formatVersion:ImageVersion,rootRevi
// can reuse that fact without rebuilding the same JSON containers.
const cached=immutable.get(plan.source);
if(cached){if(depth+cached.height>64)throw fault('invalid_recovery_payload');return cached;}
if(depth>64)throw fault('invalid_recovery_payload');
let height=0,bytes=2,reusable=true,properties=0;
const array=plan.keys===undefined,result:Json[]|Record<string,Json>=array?[]:{};
for(let index=0;index<plan.children.length;index++){
const entry=plan.children[index],name=plan.keys?.[index]??String(index);
const array=Array.isArray(plan.source),keys=plan.children?plan.keys:array?undefined:Object.keys(plan.source);
const length=plan.children?.length??(array?(plan.source as unknown[]).length:keys!.length),result:Json[]|Record<string,Json>=array?[]:{};
for(let index=0;index<length;index++){
const name=keys?.[index]??String(index);
// No children array means detached JSON. Read only its immutable own data
// properties, with the prototype behavior captured during preparation.
const value=plan.children?undefined:(plan.source as Record<string,unknown>)[name];
const entry=plan.children?plan.children[index]:value&&typeof value==='object'?{source:value,depth:plan.depth+1}:value as Scalar;
const child=entry===null||typeof entry!=='object'?normalize(entry,plan.depth+1,name):'height' in entry?entry:yield* encode(entry,plan.depth+1,name);
reusable&&=child.reusable;
if(array){
if(child.value===undefined&&plan.depth+1>64)throw fault('invalid_recovery_payload');
(result as Json[]).push(child.value??null);bytes+=(index?1:0)+(child.value===undefined?4:child.bytes);height=Math.max(height,1+child.height);
}else if(child.value!==undefined){
const name=plan.keys![index];bytes+=(properties++?1:0)+scalarBytes(name)+1+child.bytes;
const name=keys![index];bytes+=(properties++?1:0)+scalarBytes(name)+1+child.bytes;
Object.defineProperty(result,name,{value:child.value,enumerable:true});height=Math.max(height,1+child.height);
}
const pause=budget.step();if(pause)yield pause;