docs(runtime): clarify resource footprints and retention scan ownership

This commit is contained in:
2026-11-12 14:05:32 +08:00
parent 9c6f17f236
commit 63c4098e4e
+27 -13
View File
@@ -42,7 +42,9 @@ export type ResourceHandle = { readonly id: Id; readonly src: string };
/** 单资源 20 MiB 上限,与解码像素预算是不同约束。 */
export const maxResourceBytes = 20 * 1024 * 1024;
/** 从 changes 收集可能依赖的资源;这里不返回完整性证明,不能直接作为安全回收扫描。 */
/** 从变化两侧收集字符串资源值:整记录按 component.resourceFields,字段变化按所有已安装字段名的并集。
* 不调用 registry.resolve 检查版本、不校验资源值域或字节存在性,空字符串也会进入 Set;输入合法性由上层保证。
* 不解释 canvas/隐藏引用,也不返回 complete,适合保守依赖收集而非证明无引用或直接删除文件。 */
export function referencedResources(changes: readonly Change[], registry: Registry): Set<Id> {
const result = new Set<Id>(), fields = new Set([...registry.components.values()].flatMap(component => component.resourceFields ?? []));
/** 整记录按组件声明 resourceFields 收集;字段变化另用已安装字段名的保守并集。 */
@@ -61,7 +63,10 @@ export function referencedResources(changes: readonly Change[], registry: Regist
}
/** Read/write dependencies use object identities, rather than blocking every edit behind a pending image. */
/** 构造对象级 read/write 集,避免独立文本编辑被未完成图片上传全局阻塞。 */
/** 构造临时对象读写 Set:canvas:/element:/resource: 分命名空间,同一元素各字段/代次共用 element ID 键。
* owner 读取含祖先,边读取端点,上传仅写 resource 键;资源内容引用的上传依赖由 engine 另外收集。
* 不读取 intent.dependencies 或 run.action,也不获取锁/检查权限、服务版本或 before 条件;不是完整可并发证明。
* 返回可变集合供依赖规划,不保存成第二份 ledger 或持久调度状态。 */
export function operationFootprint(intent: OperationIntent, document: Document) {
const reads = new Set<string>(), writes = new Set<string>();
if (intent.operationType === 'asset.upload') { writes.add(`resource:${intent.resourceId}`); return { reads, writes }; }
@@ -92,25 +97,29 @@ export function operationFootprint(intent: OperationIntent, document: Document)
}
return { reads, writes };
}
/** 写/写、写/读或读/写相交才冲突,纯读/读可并行。 */
/** 只判给定足迹的写/写、写/读、读/写交集,纯读/读不构成该规则下的冲突。
* 不自行调度/等待;结果准确性依赖足迹覆盖所需对象,不能代替 canvas writer claim 或服务端条件提交。 */
export function conflictsWith(prior: ReturnType<typeof operationFootprint>, next: ReturnType<typeof operationFootprint>) {
return [...prior.writes].some(id => next.writes.has(id) || next.reads.has(id)) || [...prior.reads].some(id => next.writes.has(id));
}
/** One synchronous retention pass shares history interpretation across working/ACK.
* No snapshot or mutable reference-count state is retained after the caller returns. */
/** 一次保留扫描的解释器,预处理 history/ledger 可复用到 working 与 ACK;不缓存新的业务快照。 */
/** Reuse one immutable history/ledger interpretation across synchronous document scans. */
/** 预处理历史/账本后返回扫描闭包,可复用于 working 与 ACK;闭包仍持有这些输入及派生 schema/transition 引用。
* 输入需保持不可变,扫描结束不代表这些引用已释放;没有模块级新业务快照或独立可变引用计数 store。
* 每次调用重新生成 pins/complete;未知解释器降低完整性,已知 transition 形状损坏仍可能直接抛错。
* 这里只解释文档/历史/操作/explicit,ACK、显示租约、导入/run 等额外保留由 resourceRetentionPins 叠加。 */
export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot,registry:Registry){
const transitions=new Map<OperationRecord,readonly Change[]>(),fieldTargets=new Set<Id>();
/** 先记需要解释 data 字段变化的目标,再从历史整记录补齐它可能使用过的组件 schema。 */
const rememberTargets=(values:readonly Change[])=>{for(const change of values)if(change.kind==='field'&&change.field.startsWith('data.'))fieldTargets.add(change.target.id);};
// 最早未解决 sequence 决定还需保留哪些历史 transition 证据;不能只扫描当前非空 undo 栈。
// 所有 history.entries 都扫描,不只 undo/redo 栈顶;最早未解决 sequence(含上传等)确定相关 transition 前沿。
// 无未决项时 earliest=Infinity,不再将已结算 transition 增量当作待保留证据区间。
const pending=[...ledger.operations.values()].filter(record=>record.phase!=='resolved'),earliest=pending.reduce((value,record)=>Math.min(value,record.intent.sequence),Infinity);
for(const entry of history.entries.values())rememberTargets(entry.changes);
let interpreted=true;
for(const record of ledger.operations.values()){
// Obtain validated ranges for every understood transition, including resolved
// evidence. The core may reuse only proven immutable transition data.
// 所有 v1 transition 都经 helper 验结构/提取变化区间两侧,非完整历史展开;即使已 resolved 也可供 schema 索引。
// 未知 historyEffect 版本会降低完整性;未知 transition 仅在相关前沿内降低完整性,不能一概当空内容。
if(record.intent.historyTransition?.version===1)transitions.set(record,historyTransitionChanges(record.intent.historyTransition));
if(record.phase!=='resolved')rememberTargets(record.intent.changes);
if(record.intent.historyEffect&&record.intent.historyEffect.version!==1)interpreted=false;
@@ -118,7 +127,8 @@ export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot
}
// 同一 ID 的历史可能跨组件/版本,不只套用当前元素的 resourceFields。
const schemas=new Map<Id,ElementRecord[]>();
/** 每种 component/version 保留一个历史样本,只为需要解释字段变化的目标建立索引。 */
/** 对需解释字段变化的目标 ID,每种 component/version 留一个样本,跨 incarnation 保守复用历史 schema。
* 样本用于解释哪些键可能是资源,不证明当前目标仍是同一生命周期,也不复制全部历史 payload。 */
const remember=(record:ElementRecord|null)=>{if(record&&fieldTargets.has(record.id)){const list=schemas.get(record.id)??[];if(!list.some(item=>item.componentId===record.componentId&&item.dataVersion===record.dataVersion))list.push(record);schemas.set(record.id,list);}};
/** 从整记录变化的 before/after 补历史 schema,删除后的资源仍可由 history 保留。 */
const rememberChanges=(values:readonly Change[])=>{for(const change of values)if(change.kind==='element'){remember(change.before);remember(change.after);}};
@@ -127,12 +137,15 @@ export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot
for(const record of ledger.operations.values()){rememberChanges(record.intent.changes);const changes=transitions.get(record);if(changes)rememberChanges(changes);}
}
const fields=new Set([...registry.components.values()].flatMap(component=>component.resourceFields??[]));
/** 为指定 document 生成 resource ID→owner 集;explicit 输入相反,是 owner→资源 ID 列表。 */
/** 每个 document 新建资源→理由 Set;explicit 输入方向相反,为 owner→资源 ID 列表。
* complete 只表示本扫描器相关引用可解释,不证明本地字节可读、上传成功、其它 producer 无引用或允许立即回收。
* 输出 readMap/readSet 捕获成员,重复扫描之间不共享可变计数;扫描本身无异步让出/取消。 */
return(document:Document,explicit:ReadonlyMap<Id,readonly Id[]>=new Map())=>{
const pins=new Map<Id,Set<string>>();let complete=interpreted;
/** 同一 owner 只计一份保留理由;调用者得到集合,不拥有第二套可变引用计数。 */
const pin=(id:Id,owner:string)=>{const set=pins.get(id)??new Set<string>();set.add(owner);pins.set(id,set);};
/** 已知组件逐资源字段收集,字段格式或 schema 不可解释时降低完整性,不能默认为空。 */
/** 未知组件/版本或未声明 resourceFields 都令 complete=false;显式 [] 才可表示该定义无资源字段。
* 声明字段字符串作为 pin,undefined/null 视为空,非字符串非空值使解释不完整;不重算任何文件摘要。 */
const element=(record:ElementRecord|null,owner:string)=>{
if(!record)return;const component=registry.components.get(record.componentId);
if(registry.resolve(record)!=='known'||component?.resourceFields===undefined){complete=false;return;}
@@ -151,7 +164,8 @@ export function createResourcePinScan(history:HistoryState,ledger:LedgerSnapshot
}
}
}
// Keep the conservative cross-schema fallback and its original insertion order.
// 再用所有已安装资源字段的并集做保守兜底,保留可能的 before/after 字符串及原插入顺序。
// 即使当前/历史 schema 缺失已将 complete 降为 false,仍尽可能留下已知资源理由,而非只返回一个空失败表。
const refs=new Set<Id>(),collect=(record:ElementRecord|null)=>{if(record)for(const field of registry.components.get(record.componentId)?.resourceFields??[])if(typeof record.data[field]==='string')refs.add(record.data[field] as string);};
for(const change of values)if(change.kind==='element'){collect(change.before);collect(change.after);}else if(change.kind==='field'&&change.field.startsWith('data.')&&fields.has(change.field.slice(5))){if(typeof change.before==='string')refs.add(change.before);if(typeof change.after==='string')refs.add(change.after);}
for(const id of refs)pin(id,owner);