From 63c4098e4e21e1109a70aea0cac3b54dc8bb1438 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 14:05:32 +0800 Subject: [PATCH] docs(runtime): clarify resource footprints and retention scan ownership --- src/lib/canvas-runtime/resources.ts | 40 +++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/lib/canvas-runtime/resources.ts b/src/lib/canvas-runtime/resources.ts index 7229ed6..2f330ce 100644 --- a/src/lib/canvas-runtime/resources.ts +++ b/src/lib/canvas-runtime/resources.ts @@ -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 { const result = new Set(), 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(), writes = new Set(); 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, next: ReturnType) { 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(),fieldTargets=new Set(); /** 先记需要解释 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(); - /** 每种 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=new Map())=>{ const pins=new Map>();let complete=interpreted; /** 同一 owner 只计一份保留理由;调用者得到集合,不拥有第二套可变引用计数。 */ const pin=(id:Id,owner:string)=>{const set=pins.get(id)??new Set();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(),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);