docs(runtime): explain import proof and identity ownership
This commit is contained in:
@@ -8,10 +8,14 @@ import {fault} from '../canvas-core/values.ts';
|
||||
/** 源记录/依赖键到目标ID的只读字典,element/canvas前缀由调用者约定。 */
|
||||
type Ids=Readonly<Record<string,Id>>;
|
||||
/** One runner's last immutable ID index. Copies are deferred until allocation. */
|
||||
/** 返回带clear方法的捕获工厂,每次为一个源记录的准备提供reserve/has/finish私有上下文。 */
|
||||
/** 一个 runner 的捕获工厂,缓存最近一张可复用表及其 used Set;每次 capture 创建独立分配上下文。
|
||||
* 初始表的值域/空 ID/已有重复映射未在此完整校验,Set 只用于拦截后续 createId 与已知 ID 碰撞。
|
||||
* 不查项目文档、墓碑、其它 runner 或服务身份空间;稳定映射须由 job 校验与发布层共同保证。 */
|
||||
export function createImportIdentities(){
|
||||
let cached:{ids:Ids;used:Set<Id>}|undefined;
|
||||
/** 相同已冻结纯字符串属性表可复用used索引;其它输入先复制为无原型字典。 */
|
||||
/** 同引用命中缓存时直接复用;否则仅将冻结且描述符为字符串数据值的输入视为可复用。
|
||||
* 这是性能准入,不是完整 schema/普通对象证明;其余输入以 Object.assign 捕获到无原型字典,getter 会在此求值。
|
||||
* 首次分配前共享基表/used;写时再分离,避免每条只查既有键的记录重扫整张映射。 */
|
||||
const capture=(input:Ids,createId:()=>Id)=>{
|
||||
let identity=cached?.ids===input?cached:undefined;
|
||||
if(!identity){
|
||||
@@ -30,10 +34,12 @@ export function createImportIdentities(){
|
||||
},
|
||||
/** 判断目标ID是否已被本映射使用,不声称该ID已加入文档或已同步。 */
|
||||
has:(id:Id)=>used.has(id),
|
||||
/** 返回本次分配后的只读映射;没有新分配时保留输入身份便于下一记录复用索引。 */
|
||||
/** owned 缓冲通过 dataRecord 再捕获不可变值,后续 reserve 不改变已经返回的结果。
|
||||
* 可复用输入且无分配时沿用原表;可变/getter 输入即使未分配也返回捕获表,每次 finish 可重新捕获。
|
||||
* 返回不代表映射已落盘;runner 须与导入进度/文档同次发布和持久化。 */
|
||||
finish:():Ids=>owned?dataRecord(ids):ids,
|
||||
});
|
||||
};
|
||||
/** clear只丢弃临时复用索引,不清除已持久化job的stableIds。 */
|
||||
/** clear 只丢工厂最近索引;现有 capture 上下文仍能使用,不清已发布/持久 job.stableIds,也不回收 ID。 */
|
||||
return Object.freeze(Object.assign(capture,{clear(){cached=undefined;}}));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ const MAX_SOURCE_BYTES=64*1024*1024;
|
||||
/** version=1是块证明DTO版本,与source.integrityVersion=2的业务封存协议不是同一版本域。 */
|
||||
export type ImportReadProof={readonly version:1;readonly fileId:string;readonly byteLength:number;readonly blockBytes:16384;readonly blocks:readonly {readonly index:number;readonly sha256:string}[]};
|
||||
/** This binds only read blocks. It is not a whole-source digest or permission to send mutations. */
|
||||
/** 校验fileId/长度/固定块大小、严格递增的合法块索引及摘要;可表示只读过部分非连续块。 */
|
||||
/** 校验源身份/长度、固定块宽、严格递增的合法块索引、摘要格式及允许字段,再捕获冻结 DTO。
|
||||
* 可包含不连续的部分块,不要求完整前缀;这里不读 Blob 或验证摘要真实性,也不是整源 SHA。
|
||||
* 缺记录/非法 JSON/未知版本报解释器不可用,其它身份/结构错误报 invalid_import_read_proof。 */
|
||||
export function validateImportReadProof(value:unknown,fileId:string,byteLength:number):ImportReadProof{
|
||||
if(typeof fileId!=='string'||!fileId||!Number.isSafeInteger(byteLength)||byteLength<0||byteLength>MAX_SOURCE_BYTES)throw fault('invalid_import_read_proof');
|
||||
const proof=value as ImportReadProof;
|
||||
@@ -29,7 +31,10 @@ export function validateImportReadProof(value:unknown,fileId:string,byteLength:n
|
||||
|
||||
/** Bounded, seekable verified reads for parsers; checkpoints contain hashes, never byte buffers.
|
||||
* Restored evidence must be checked with verify() before a runner resumes business changes. */
|
||||
/** 创建可seek的验证reader;初始证明与源绑定,port.read成功后才把新块追加到本地证明。 */
|
||||
/** 建立 seek reader:hashes 保存期望摘要,resident 是按 Map 顺序淘汰的 LRU,pending 合并同块在途读取。
|
||||
* 初始 proof 仅结构校验即进入 hashes/getProof;没有 verified 标志,调用方须在恢复业务前主动 verify。
|
||||
* 最多 4 个活跃非空 range、4 个不同块的原生读取;驻留默认 4 块(最多 64),单 range 默认/最多 4MiB。
|
||||
* range 输出、在途块和 Blob 本身另占内存,residentLimit 不是整体内存硬上限;本实例不持久化证明。 */
|
||||
export function createImportReadProof(input:{bytes:Blob;fileId:string;proof?:ImportReadProof;maxResidentBlocks?:number;maxReadBytes?:number}&ImportScheduling){
|
||||
const options=Object.freeze({...input});
|
||||
const {bytes,fileId}=options,residentLimit=options.maxResidentBlocks??4,readLimit=options.maxReadBytes??4*1024*1024;
|
||||
@@ -40,13 +45,17 @@ export function createImportReadProof(input:{bytes:Blob;fileId:string;proof?:Imp
|
||||
type Block={index:number;sha256:string;data:Uint8Array};
|
||||
const resident=new Map<number,Block>(),pending=new Map<number,Promise<Block>>(),lifecycle=new AbortController();
|
||||
let closed=false,activeReads=0,proof:ImportReadProof=initial;
|
||||
/** 每次等待前后检查生命周期/取消;close是源关闭,调用者abort是停止此订阅采纳。 */
|
||||
/** 入口/成功采纳时检查 closed 与 signal,关闭优先报 import_source_closed,其次报 import_aborted。
|
||||
* 单个订阅取消只停止等待,不终止共享原生块读取;已拒绝的底层 Promise 仍可能保留自己的原始错误。 */
|
||||
const check=(signal:AbortSignal)=>{if(closed)throw fault('import_source_closed');if(signal.aborted)throw fault('import_aborted');};
|
||||
/** 单调用可取消等待共享块Promise,不强制取消可能正被其它range复用的只读底层读取。 */
|
||||
function wait<T>(task:Promise<T>,signal:AbortSignal):Promise<T>{
|
||||
check(signal);return new Promise((resolve,reject)=>{const abort=()=>{try{check(signal);}catch(error){reject(error);}};signal.addEventListener('abort',abort,{once:true});task.then(value=>{signal.removeEventListener('abort',abort);try{check(signal);resolve(value);}catch(error){reject(error);}},error=>{signal.removeEventListener('abort',abort);reject(error);});});
|
||||
}
|
||||
/** 先命中LRU或共享pending,再读块并对旧摘要;新原图与已证明块不符即源改变。 */
|
||||
/** 命中 resident 时移动到 Map 尾部作为最近使用,命中 pending 时复用同一 Promise。
|
||||
* 缺块就读完整对齐 16KiB(最后块截 EOF)并计算 SHA,可能覆盖请求 range 之外的相邻字节。
|
||||
* 比对 hashes 中旧期望后放 resident,但不在此新增持久候选 proof;range 失败也可留下可复用缓存。
|
||||
* 缓存命中不重新读 Blob/算摘要,依赖 Blob 不可变合同;淘汰只丢字节,不丢已提交的期望摘要。 */
|
||||
function block(index:number):Promise<Block>{
|
||||
const cached=resident.get(index);if(cached){resident.delete(index);resident.set(index,cached);return Promise.resolve(cached);}
|
||||
const active=pending.get(index);if(active)return active;
|
||||
@@ -61,7 +70,10 @@ export function createImportReadProof(input:{bytes:Blob;fileId:string;proof?:Imp
|
||||
})();
|
||||
pending.set(index,task);void task.finally(()=>{if(pending.get(index)===task)pending.delete(index);}).catch(()=>{});return task;
|
||||
}
|
||||
/** 精确range端口:只分配所需输出,逐块拼接并按软预算让出,最后成功才提交used块证明。 */
|
||||
/** 精确 range 端口:要求 end ≤ EOF,不自行截断;空范围在容量检查前返回,不读块/新增证明。
|
||||
* 非空范围分配独立输出并逐块拼接,每块向预算记 128 单位,最后检查取消与并发摘要一致性。
|
||||
* 只有整次范围成功才将 used 中新块加入 hashes/proof;取消/失败不提交本次部分块,也不回退其它成功读取的证明。
|
||||
* activeReads 随等待退出而归还,真实不可取消 I/O 仍占 pending 容量,不能用大量取消绕过底层上限。 */
|
||||
const port:ImportByteReader=Object.freeze({byteLength:bytes.size,async read(start:number,end:number,signal:AbortSignal){
|
||||
const combined=AbortSignal.any([signal,lifecycle.signal]);check(combined);
|
||||
if(!Number.isSafeInteger(start)||!Number.isSafeInteger(end)||start<0||end<start||end>bytes.size||end-start>readLimit)throw fault('invalid_import_byte_range');
|
||||
@@ -81,9 +93,12 @@ export function createImportReadProof(input:{bytes:Blob;fileId:string;proof?:Imp
|
||||
}finally{activeReads--;}
|
||||
}});
|
||||
return Object.freeze({port,getProof:()=>proof,
|
||||
/** 重开后验证所有已保存块;不扫描尚未读到的尾部,故不能代替完整SHA或已durable封存。 */
|
||||
/** 对调用开始时 hashes 中的索引逐块核对,不增加新证明块、不产生完整源摘要或自动持久化。
|
||||
* 重开实例缓存为空时实际重读旧块;同实例调用可命中 resident,不强制绕过缓存重新读物理字节。
|
||||
* 不核对尚未证明的尾部,也不锁住并发 range;是否已安全恢复、何时封存由引擎决定。 */
|
||||
async verify(signal:AbortSignal){const combined=AbortSignal.any([signal,lifecycle.signal]),budget=createImportBudget(combined,options);check(combined);for(const index of [...hashes.keys()]){await wait(block(index),combined);await budget.step(128);}check(combined);},
|
||||
/** 幂等关闭,清驻留字节并取消等待者;已开始的只读Blob读取可能迟到,由closed检查拒绝采纳。 */
|
||||
/** 幂等关闭并清 resident/取消等待者,不清 hashes/proof,getProof 仍可读取最后不可变快照。
|
||||
* 不等待原生 Blob 读取退出;pending 由各 Promise finally 清理,迟到读取经 closed 检查拒绝进入缓存。 */
|
||||
close(){if(closed)return;closed=true;resident.clear();lifecycle.abort();},
|
||||
/** 只读诊断:驻留/活range/pending块/证明块分别计数,不当作导入完成百分比。 */
|
||||
stats:()=>Object.freeze({residentBlocks:resident.size,activeReads,pendingBlocks:pending.size,proofBlocks:hashes.size,closed}),
|
||||
|
||||
@@ -8,20 +8,28 @@ import {importNeedsSeal,validateImportJob,type ImportJob} from './imports.ts';
|
||||
import {validateImportReadProof} from './import-read-proof.ts';
|
||||
|
||||
/** A derived durable-cut index, never an in-memory permission boolean. */
|
||||
/** 从给定job表派生fileId→SHA索引;调用者须传已可靠保存的cut,本函数不执行落盘。 */
|
||||
/** 从 integrity-v2、带 SHA 且无 integrityError 的 job 派生 fileId→SHA Map,不扫描字节或读取存储。
|
||||
* 调用方必须传已持久 cut;函数不证明其来源可靠,也不独立调用 validateImportJob 验摘要。
|
||||
* 返回实际可变 Map,若重复 fileId 后项覆盖前项;文件身份唯一性由外围约束保证。 */
|
||||
export function importSeals(jobs:ReadonlyMap<Id,ImportJob>|undefined){
|
||||
const seals=new Map<Id,string>();for(const job of jobs?.values()??[])if(job.source?.integrityVersion===2&&job.source.sha256&&!job.source.integrityError)seals.set(job.source.fileId,job.source.sha256);return seals;
|
||||
}
|
||||
/** integrity-v2任务需自身已完整验证且durable seals匹配;取消且零已提交前缀的任务可不阻塞。 */
|
||||
/** 整个 job 表的发送闸门:每个非豁免 integrity-v2 源都须无需 seal 且与 seals 中 SHA 相等。
|
||||
* 取消且 committedSourceKeys 为空者豁免;不按本次请求挑选 job,也不重验块证明/实际存储事务。
|
||||
* 其它 integrityVersion 在此跳过,是否支持/合法由 job 解释器先校验,不能把 true 当所有源都已验证。 */
|
||||
export function importsMaySend(jobs:ReadonlyMap<Id,ImportJob>|undefined,seals:ReadonlyMap<Id,string>){
|
||||
for(const job of jobs?.values()??[])if(job.source?.integrityVersion===2&&!(job.status==='cancelled'&&!job.committedSourceKeys.length)&&(importNeedsSeal(job)||seals.get(job.source.fileId)!==job.source.sha256))return false;return true;
|
||||
}
|
||||
/** Only engine-owned publication paths may advance read evidence or source verification. */
|
||||
/** 发布前保护引擎拥有的readProof/SHA/执行进度,未持owner任务ID的路径不能伪造或删除它们。 */
|
||||
/** 比较前后 job 表保护 integrity-v2 证据,owner 是引擎传入的当前任务 ID,不是外部权限令牌。
|
||||
* 同一表引用直接返回,依赖不可变状态合同;v2 job 不得删除或降级,即使 owner 匹配也不例外。
|
||||
* 每个 next v2 job 验结构且要求 proof 索引为从 0 开始的连续前缀,比通用 seek reader 更严格。
|
||||
* owner 可推进证据/执行事实,仍不能改 fileId/长度/已有 SHA 或丢改旧块;此处不重算字节或检查持久水位。 */
|
||||
export function assertImportEvidence(previous:ReadonlyMap<Id,ImportJob>|undefined,next:ReadonlyMap<Id,ImportJob>|undefined,owner:Id|undefined){
|
||||
if(previous===next)return;
|
||||
for(const[id,job]of previous??[])if(job.source?.integrityVersion===2&&!next?.has(id))throw fault('import_integrity_owned',[id]);
|
||||
/** 对比执行事实时剔除generation和源retirement生命周期字段;其它进度/身份仍要求合法owner。 */
|
||||
/** 比较执行事实时仅剔除 job.generation 和当前 source.retirement;previousFiles 等内容仍参与比较。
|
||||
* 允许普通保留流程调整这些排除字段,不在这里证明 generation/退休时钟单调,交其它发布校验处理。 */
|
||||
const execution=(job:ImportJob|undefined)=>{if(!job)return job;const{generation:_,source,...body}=job;if(!source)return body;const{retirement:__,...identity}=source;return {...body,source:identity};};
|
||||
for(const[id,job]of next??[]){const source=job.source,old=previous?.get(id)?.source;if(source?.integrityVersion!==2){if(old?.integrityVersion===2)throw fault('import_integrity_owned',[id]);continue;}
|
||||
validateImportJob(job);const proof=validateImportReadProof(source.readProof,source.fileId,source.byteLength);
|
||||
|
||||
Reference in New Issue
Block a user