Files
Endoleap/src/lib/canvas-runtime/import-json-array.ts
T

62 lines
6.2 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.
/**
* 原生记录JSON数组的逐项byte解析器(parserVersion=3),不先JSON.parse整文件。
* 游标是[之后/逗号之后的字节位置,必须由runner保存合法边界;这里只校验数值范围,不重新证明任意seek位置。
* 记录级JSON/UTF8/大小错误可报告后继续;数组结构无法恢复时结束源,已发布业务前缀由runner保留。
*/
import {readImportRange,type ImportByteReader} from './import-byte-reader.ts';
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
import type {ImportSource,ImportLine} from './imports.ts';
import {fault,validJson} from '../canvas-core/values.ts';
/** Seekable JSON array reader. Cursors are byte offsets after '[' or a record comma.
* Structure errors end the source; previously published records remain explicit. */
/** 捕获 Blob/max/chunk 与返回指纹标量,按字节 offset 每次返回一条记录,缓存一个读取块。
* readBytes 与调度选项仍从原 options 读取,调用方应保持它们稳定;这里不建立源摘要/读取证明。
* 默认单条 256KiB/块 16KiB,未给 maxRecordBytes 设置总上限,也不限制源文件总长。
* 不分配业务 ID、不发布 history 或发送请求;缓存不是持久进度,合法边界由 runner 保存。 */
export function createJsonArrayImportSource(options:{bytes:Blob;fingerprint:string;readBytes?:ImportByteReader;maxRecordBytes?:number;chunkBytes?:number}&ImportScheduling):ImportSource{
const bytes=options.bytes,max=options.maxRecordBytes??256*1024,chunk=options.chunkBytes??16384;
if(!(bytes instanceof Blob)||!options.fingerprint||!Number.isSafeInteger(max)||max<1||!Number.isSafeInteger(chunk)||chunk<1||chunk>max)throw fault('invalid_import_source');
// byte缓存可跨record复用;空白按JSON四种ASCII空白处理,不误把UTF8字符串内容当分隔符。
let cache={offset:-1,value:new Uint8Array()};const whitespace=(b:number)=>[9,10,13,32].includes(b);
/** read每次返回一个ImportLine及nextCursor;EOF返回null,源级I/O失败仍抛出而不伪装坏记录。 */
return Object.freeze({fingerprint:options.fingerprint,parserVersion:3,async read(start:number,signal:AbortSignal):Promise<ImportLine|null>{
if(!Number.isSafeInteger(start)||start<0||start>bytes.size)throw fault('invalid_import_cursor');
// EOF offset 直接返回 null,不重扫先前数组结构;任意范围内 seek 也不证明是合法记录起点。
if(start>0&&start===bytes.size){if(signal.aborted)throw fault('import_aborted');return null;}
if(bytes.size===0)throw fault('invalid_json_array');
let cursor=start;const budget=createImportBudget(signal,options);
/** 按需填充定长byte缓存,不推进cursor;每次读取前后检查取消。 */
async function peek(){if(signal.aborted)throw fault('import_aborted');if(cursor>=bytes.size)return -1;if(cursor<cache.offset||cursor>=cache.offset+cache.value.length){cache={offset:cursor,value:await readImportRange(bytes,options.readBytes,cursor,cursor+chunk,signal)};if(signal.aborted)throw fault('import_aborted');}return cache.value[cursor-cache.offset];}
/** 单字节推进并记入合作预算,调用者await可在预算要求时实际让出。 */
function advance(){cursor++;return budget.step();}
/** 只跳过JSON语法空白,字符串中的空白由状态机原样收集。 */
async function spaces(){while(whitespace(await peek()))await advance();}
/** 结构失败转成一条终结错误记录,nextCursor直接EOF,不能继续猜后面的记录边界。 */
const failure=(error:string):ImportLine=>({cursor:start,nextCursor:bytes.size,key:`byte:${start}`,error});
await spaces();if(start===0){if(await peek()!==91)return failure('invalid_json_array');await advance();await spaces();}
if(await peek()===93){await advance();await spaces();if(cursor!==bytes.size)return failure('invalid_json_array');if(start!==0){/* Empty tail is valid only after a record, not a trailing comma. */return failure('invalid_json_array');}return null;}
if(await peek()===-1)return failure('incomplete_json_array');
// parts 是至多 max 个字节数值的普通数组(内存成本不等于 max 字节),stack 至多保留 256 层括号。
// quoted/escaped 只定位边界,不完整校验 JSON;超大小后仍扫描但不再收集,坏结构仍可能终结整个源。
const recordStart=cursor,parts:number[]=[],stack:number[]=[];let quoted=false,escaped=false,oversized=false;
while(true){const byte=await peek();if(byte===-1)return failure('incomplete_json_array');
if(!quoted&&stack.length===0&&(byte===44||byte===93))break;
if(cursor-recordStart<max)parts.push(byte);else oversized=true;
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===123||byte===91){stack.push(byte);if(stack.length>256)return failure('json_nesting_limit');}else if(byte===125||byte===93){if(stack.pop()!==(byte===125?123:91))return failure('invalid_json_array');}
await advance();
}
// 先确认记录后的外层分隔与尾部,再返回此记录;若尾逗号/垃圾在此处发现,本记录也返回结构失败。
// 数组内嵌 ] 已由 stack 处理;nextCursor 消费分隔和后续空白,最后一项则推进到真实 EOF。
const delimiter=await peek();await advance();
if(delimiter===93){await spaces();if(cursor!==bytes.size)return failure('invalid_json_array');}
else {await spaces();if(await peek()===93||await peek()===-1)return failure('invalid_json_array');}
const base={cursor:start,nextCursor:cursor,key:`byte:${start}`};
if(oversized)return{...base,error:'record_too_large'};
// fatal UTF-8 与 JSON.parse 在边界已确定后处理,失败可随 nextCursor 继续下一项;不做元素业务 schema 校验。
// JSON.parse 同步完成且无中途取消,记录扫描的预算不抢占解码/解析;调用方仍需在采纳结果前校验生命周期。
let text:string;try{text=new TextDecoder('utf-8',{fatal:true}).decode(new Uint8Array(parts));}catch{return{...base,error:'invalid_utf8'};}
try{const value=JSON.parse(text);return validJson(value)?{...base,value}:{...base,error:'invalid_json_value'};}catch{return{...base,error:'invalid_json'};}
}});
}