Files
Endoleap/src/lib/canvas-runtime/import-document-stream.ts
T

179 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 导出文档parser5:标准头先建立源树,随后每请求一项才延伸elements范围,实现先转换/加入/显示。
* 头乱序时明确回退索引解释器,再保留同一ordinal接口;不能因此声称所有文档都零预扫描。
* 范围扫描串行推进,已定位记录可独立重读;失败尾部成为明确错误记录,成功前缀不被回滚。
*/
import {readImportRange,isImportByteFailure,type ImportByteReader} from './import-byte-reader.ts';
import type {ImportLine,ImportSource} from './imports.ts';
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
import {openDocumentImportSource} from './import-document.ts';
import {fault,validJson} from '../canvas-core/values.ts';
/** 已定位元素的原字节范围;只随请求积累,不在标准头准备时扫完整elements尾部。 */
type Range={start:number;end:number};
/** 单源共享byte缓存,仅减少重复读取;各scanner仍有自己的逻辑cursor。 */
type ScanCache={start:number;bytes:Uint8Array};
/** envelope内的轻量拥有树记录,不包含入口payload或renderer状态。 */
type Canvas={id:string;parent:null|{canvasId:string;entryElementId:string}};
/** 原源/指纹、可选已验证byte端口、取消与预算;指纹由上层完整性流程管理。 */
type Options=ImportScheduling&{bytes:Blob;fingerprint:string;readBytes?:ImportByteReader;signal:AbortSignal;maxRecordBytes?:number};
/** 明确支持的format1顶层字段,重复/未知字段拒绝,不隐式猜未来格式。 */
const allowed=new Set(['documentFormatVersion','projectId','canvases','elements','tombstones']);
/** JSON语法空白字节,不用于处理字符串内容。 */
const whitespace=(byte:number)=>byte===9||byte===10||byte===13||byte===32;
/** Each scan owns its cursor. An aborted scan cannot advance the resumable source. */
/** 每次扫描从指定位置建立私有cursor,完成边界后外层才更新可恢复源进度。 */
function scanner(options:Options,signal:AbortSignal,start=0,cache:ScanCache={start:-1,bytes:new Uint8Array()}){
const {bytes}=options,budget=createImportBudget(signal,options);
let cursor=start;
/** 在读取/采纳边界检查本次scanner的signal,缓存命中也不能跳过取消。 */
const check=()=>{if(signal.aborted)throw fault('import_aborted');};
/** 共享缓存不足时精确读16KiB,不推进私有cursor,EOF返回-1。 */
async function peek(){
check();if(cursor>=bytes.size)return -1;
if(cursor<cache.start||cursor>=cache.start+cache.bytes.length){const loaded=await readImportRange(bytes,options.readBytes,cursor,cursor+16384,signal);cache.start=cursor;cache.bytes=loaded;check();}
return cache.bytes[cursor-cache.start];
}
/** 推进一个字节并返回预算可能产生的让出Promise。 */
function step(){cursor++;return budget.step();}
/** 连续消费非字符串区的合法JSON空白。 */
async function spaces(){while(whitespace(await peek()))await step();}
/** 严格匹配一个结构字节,不修补缺逗号/冒号等源损坏。 */
async function take(byte:number){await spaces();if(await peek()!==byte)throw fault('invalid_document_import');await step();}
/** 引号/转义/栈配对扫描一个完整值range,限制嵌套深度,避免把字符串里的分隔符当边界。 */
async function value():Promise<Range>{
await spaces();const start=cursor,stack:number[]=[];let quoted=false,escaped=false;
while(true){
const byte=await peek();
if(byte<0){if(quoted||stack.length)throw fault('invalid_document_import');break;}
if(!quoted&&!stack.length&&(byte===44||byte===93||byte===125||whitespace(byte)))break;
if(quoted){if(escaped)escaped=false;else if(byte===92)escaped=true;else if(byte===34)quoted=false;}
else if(byte===34)quoted=true;
else if(byte===91||byte===123){stack.push(byte);if(stack.length>256)throw fault('json_nesting_limit');}
else if(byte===93||byte===125){if(stack.pop()!==(byte===93?91:123))throw fault('invalid_document_import');}
const ended=!quoted&&!stack.length&&(byte===34||byte===93||byte===125);
await step();if(ended)break;
}
if(cursor===start)throw fault('invalid_document_import');return{start,end:cursor};
}
/** 对单range校验大小、fatal UTF8、JSON语法和值域,返回原值,不修改业务ID。 */
async function decode(range:Range,limit=options.maxRecordBytes??256*1024):Promise<any>{
check();if(range.end-range.start>limit)throw fault('record_too_large');
const data=await readImportRange(bytes,options.readBytes,range.start,range.end,signal);let text:string;
try{text=new TextDecoder('utf-8',{fatal:true}).decode(data);}catch(error){check();throw fault('invalid_utf8');}
check();try{const value=JSON.parse(text);if(!validJson(value))throw Error();return value;}catch{throw fault('invalid_json');}
}
return{peek,step,spaces,take,value,decode,position:()=>cursor};
}
/** 仅验证envelope墓碑元数据为正安全整数表,目标导入仍使用新ID/新生命周期。 */
function tombstones(value:unknown){
if(!value||typeof value!=='object'||Array.isArray(value)||Object.values(value).some(n=>!Number.isSafeInteger(n)||(n as number)<1))throw fault('invalid_document_import');
}
/** Parser 5: canonical headers open before scanning the elements tail.
* Out-of-order headers require the indexed interpreter to establish ownership first.
* Ordinals remain stable; ranges are retained only as records are requested. */
/** 打开standard header后即可返回源;真正elements/tail结构检查跟随read进行,未扫描尾部不冒称合法。 */
export async function openStreamingDocumentImportSource(input:Options):Promise<ImportSource>{
const options:Options=Object.freeze({...input});
const {bytes,signal}=options,max=options.maxRecordBytes??256*1024;
if(!(bytes instanceof Blob)||bytes.size>64*1024*1024||!options.fingerprint||!Number.isSafeInteger(max)||max<1)throw fault('invalid_import_source');
// Only range extension scans bytes; ready-record decoding uses independent slices.
// Keep one source-owned chunk across attempts instead of re-reading 16 KiB per row.
const cache:ScanCache={start:-1,bytes:new Uint8Array()},scanAt=(signal:AbortSignal,start=0)=>scanner(options,signal,start,cache);
const header=scanAt(signal),fields=new Map<string,any>();
await header.take(123);
while(true){
const key=await header.decode(await header.value(),1024);
if(typeof key!=='string'||!allowed.has(key)||fields.has(key))throw fault('invalid_document_import');
await header.take(58);
// 必需format/project/canvases还没出现则不能安全解释owner,回退全envelope索引而非猜测。
if(key==='elements'){
if(!fields.has('documentFormatVersion')||!fields.has('projectId')||!fields.has('canvases')){
const indexed=await openDocumentImportSource(options);
return Object.freeze({...indexed,parserVersion:5});
}
await header.take(91);fields.set(key,true);break;
}
fields.set(key,await header.decode(await header.value(),4*1024*1024));
await header.take(44);
}
if(fields.get('documentFormatVersion')!==1||typeof fields.get('projectId')!=='string'||!fields.get('projectId')||!Array.isArray(fields.get('canvases')))throw fault('invalid_document_import');
if(fields.has('tombstones'))tombstones(fields.get('tombstones'));
const canvases=fields.get('canvases') as Canvas[],byId=new Map<string,Canvas>();
const budget=createImportBudget(signal,options);
for(const canvas of canvases){
if(!canvas||typeof canvas.id!=='string'||!canvas.id||byId.has(canvas.id)||canvas.parent!==null&&(!canvas.parent||typeof canvas.parent.canvasId!=='string'||typeof canvas.parent.entryElementId!=='string'))throw fault('invalid_document_import');
byId.set(canvas.id,canvas);await budget.step();
}
// 验证单根和父链,rooted缓存已经验证的路径,减少深树中重复回溯工作。
const roots=canvases.filter(canvas=>canvas.parent===null);if(roots.length!==1)throw fault('invalid_document_import');
const rootId=roots[0].id,rooted=new Set([rootId]);
for(const canvas of canvases){
const path=new Set<string>();let current=canvas;
while(!rooted.has(current.id)){
if(path.has(current.id)||!current.parent)throw fault('invalid_document_import');
path.add(current.id);const parent=byId.get(current.parent.canvasId);if(!parent)throw fault('invalid_document_import');current=parent;await budget.step();
}
for(const id of path){rooted.add(id);await budget.step();}
}
const children=canvases.filter(canvas=>canvas.id!==rootId),ranges:Range[]=[];
// 源级进度只记录完整range边界/phase,serial只串行化延伸;failure最多输出一条终结错误。
let cursor=header.position(),phase:'elements'|'tail'|'done'='elements',first=true,failure:string|undefined,serial=Promise.resolve();
/** 延伸一个element range,或验证剩余envelope与EOF;只在结构边界确认后更新cursor/phase。 */
async function scanNext(readSignal:AbortSignal){
const scan=scanAt(readSignal,cursor);
if(phase==='elements'){
await scan.spaces();
if(await scan.peek()===93){if(!first)throw fault('invalid_document_import');await scan.step();cursor=scan.position();phase='tail';}
else{
const range=await scan.value();await scan.spaces();const delimiter=await scan.peek();
if(delimiter!==44&&delimiter!==93)throw fault('invalid_document_import');
await scan.step();cursor=scan.position();phase=delimiter===93?'tail':'elements';first=false;ranges.push(range);return;
}
}
// elements结束后仍检查顶层重复/未知字段、可选tombstones与实际EOF,不能略过坏尾部。
const seen=new Set(fields.keys());await scan.spaces();
while(await scan.peek()!==125){
await scan.take(44);const key=await scan.decode(await scan.value(),1024);
if(typeof key!=='string'||!allowed.has(key)||seen.has(key))throw fault('invalid_document_import');
seen.add(key);await scan.take(58);const value=await scan.decode(await scan.value(),4*1024*1024);
if(key==='tombstones')tombstones(value);
await scan.spaces();
}
await scan.take(125);await scan.spaces();if(scan.position()!==bytes.size)throw fault('invalid_document_import');
cursor=scan.position();phase='done';
}
/** ordinal read:子canvas直接返回,元素按需scan定位;已定位范围逐记录解码,后续恢复可重读。 */
async function read(position:number,readSignal:AbortSignal):Promise<ImportLine|null>{
if(readSignal.aborted)throw fault('import_aborted');
if(!Number.isSafeInteger(position)||position<0)throw fault('invalid_import_cursor');
const elementIndex=position-children.length;
// 未定位位置才推进源扫描;取消/byte证据错误抛出,其它结构失败转成尾部error记录。
while(elementIndex>=ranges.length&&phase!=='done'){
try{await scanNext(readSignal);}catch(error){if(readSignal.aborted)throw fault('import_aborted');if(isImportByteFailure(error))throw error;failure=(error as {code?:string}).code??'invalid_document_import';phase='done';}
}
const count=children.length+ranges.length;
// 失败点恰好返回一条可报告记录,下一ordinal为EOF,不能吞掉错误而宣称正常结束。
if(position>=count){
if(failure&&position===count)return{cursor:position,nextCursor:position+1,key:`record:${position}`,error:failure};
if(position===count+(failure?1:0))return null;
throw fault('invalid_import_cursor');
}
const base={cursor:position,nextCursor:position+1,key:`record:${position}`};
try{
const kind=position<children.length?'canvas':'element',record=kind==='canvas'?children[position]:await scanner(options,readSignal).decode(ranges[elementIndex]);
if(!record||typeof record!=='object'||Array.isArray(record)||typeof record.id!=='string'||!record.id)throw fault('invalid_native_import_record');
return{...base,value:{kind,key:record.id,record,sourceRootId:rootId}};
}catch(error){if(readSignal.aborted)throw fault('import_aborted');if(isImportByteFailure(error))throw error;return{...base,error:(error as {code?:string}).code??'invalid_document_import'};}
}
/** 已定位record的重读不等未来scan的锁;仅扩张ranges通过serial排队,失败不会毒化后续Promise链。 */
return Object.freeze({fingerprint:options.fingerprint,parserVersion:5,read(position:number,readSignal:AbortSignal){
// A slow future scan must not block re-reading a pending unit for commit/recovery.
if(position<children.length+ranges.length)return read(position,readSignal);
const result=serial.then(()=>read(position,readSignal));serial=result.then(()=>{},()=>{});return result;
}});
}