docs(runtime): clarify streaming import cursors and byte reads
This commit is contained in:
@@ -22,8 +22,10 @@ const allowed=new Set(['documentFormatVersion','projectId','canvases','elements'
|
||||
/** 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,完成边界后外层才更新可恢复源进度。 */
|
||||
/** Each scan owns a private byte cursor; shared progress records completed boundaries. */
|
||||
/** 从指定字节位置建立私有 cursor;仅外层 scanNext 将完整结构边界写回源级进度。
|
||||
* 取消不发布业务结果,但已定位的完整 ranges 可保留用于重试,不承诺整次 read 的缓存/边界都回滚。
|
||||
* options/缓存沿用内部引用;scanner 负责定位/单值解码,不验证完整文档引用关系。 */
|
||||
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;
|
||||
@@ -41,7 +43,9 @@ function scanner(options:Options,signal:AbortSignal,start=0,cache:ScanCache={sta
|
||||
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,限制嵌套深度,避免把字符串里的分隔符当边界。 */
|
||||
/** 用 quoted/escaped/显式 stack 定位一个值范围,最多 256 层;不收集 payload 字节。
|
||||
* 此步只找词法/括号边界,primitive 或字符串内容仍可能是非法 JSON,稍后 decode 才完整解析。
|
||||
* 超大值也要扫描完才得到范围,maxRecordBytes 仅约束后续解码分配,不限制扫描总工作量。 */
|
||||
async function value():Promise<Range>{
|
||||
await spaces();const start=cursor,stack:number[]=[];let quoted=false,escaped=false;
|
||||
while(true){
|
||||
@@ -57,7 +61,9 @@ function scanner(options:Options,signal:AbortSignal,start=0,cache:ScanCache={sta
|
||||
}
|
||||
if(cursor===start)throw fault('invalid_document_import');return{start,end:cursor};
|
||||
}
|
||||
/** 对单range校验大小、fatal UTF8、JSON语法和值域,返回原值,不修改业务ID。 */
|
||||
/** 读取单 range,按指定上限控制分配,再 fatal UTF-8/JSON.parse/validJson;不重写业务 ID。
|
||||
* 默认 payload 256KiB,header 调用可传独立上限;合法 JSON 不等同元素 schema 或引用关系合法。
|
||||
* JSON.parse 本身为同步工作,扫描预算不抢占这一步;字节端口异常保持源级错误,不转成坏记录。 */
|
||||
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;
|
||||
@@ -75,7 +81,11 @@ function tombstones(value:unknown){
|
||||
/** 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进行,未扫描尾部不冒称合法。 */
|
||||
/** 浅捕获 options 后打开标准头:源最多 64MiB,单个元数据值上限 4MiB,字段名上限 1KiB。
|
||||
* 先解析整份 canvases 并验证单根/父链,再返回 canvas-first 的 ordinal 接口;此时元素/尾部可尚未读取。
|
||||
* 头顺序不满足拥有树前提时委托索引解释器,仍暴露 parserVersion=5,不能据版本推断一定渐进扫描。
|
||||
* 指纹只是上层传入身份;源字节证明/完整 SHA 与最终元素/入口校验由读取端口和导入准备层负责。
|
||||
* options.signal 用于打开阶段,返回后的每次 read 使用自己的 readSignal,不自动合并原 signal。 */
|
||||
export async function openStreamingDocumentImportSource(input:Options):Promise<ImportSource>{
|
||||
const options:Options=Object.freeze({...input});
|
||||
const {bytes,signal}=options,max=options.maxRecordBytes??256*1024;
|
||||
@@ -120,7 +130,9 @@ export async function openStreamingDocumentImportSource(input:Options):Promise<I
|
||||
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最多输出一条终结错误。
|
||||
// ranges 随请求积累元素字节范围,children 是已捕获子 canvas;ordinal 与原文件 byte offset 不同。
|
||||
// 重开源需重建这些内存范围,持久 runner 保存的是稳定 ordinal/源身份,不是这里的 Promise 链。
|
||||
// serial 只串行化范围延伸,failure 最多输出一条终结错误;请求很远的 ordinal 仍会扫描此前全部未定位项。
|
||||
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){
|
||||
@@ -146,7 +158,10 @@ export async function openStreamingDocumentImportSource(input:Options):Promise<I
|
||||
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定位;已定位范围逐记录解码,后续恢复可重读。 */
|
||||
/** ordinal read:先 children,后 ranges 中元素;源根只作为 sourceRootId 别名,不作为导入子项输出。
|
||||
* 子 canvas 可直接返回,元素定位后按独立 range 重读/解码;一个 payload 错误不抹掉其它已定位项。
|
||||
* 结构错误记终结 failure;取消/已知字节证明错误只拒绝本次,允许同 ordinal 重试。
|
||||
* EOF 仅在有效末序号返回 null,越过末尾仍拒绝,不能把任意大序号当作已完成。 */
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user