179 lines
12 KiB
TypeScript
179 lines
12 KiB
TypeScript
/**
|
||
* 导出文档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;
|
||
}});
|
||
}
|