/** * 依赖等待记录的批选择。以迭代Kosaraju找强连通分量(SCC),让完全收到的循环引用组原子加入, * 单独按拓扑排序丢掉所有环会误拒合法对象图;组外依赖须已提交或在本批前面选中。 */ import type { ImportLocator } from './imports.ts'; import { fault } from '../canvas-core/values.ts'; /** Ready SCCs are indivisible; source order guides each readiness scan. */ /** pending 由调用方按源顺序提供且 key 唯一;先找 SCC,再按组内/组间最早源索引扫描。 * 缺外部依赖的组会跳过,可选择其后的已就绪组;同轮后面补齐依赖也不回头重扫,留给下一次调用。 * 不修改 committed/pending,不读取源或实际提交;输出数组可变且 row 沿用输入引用。 * limits 与 locator 成本须先校验;本函数不核对真实编码字节/changes,也没有中途取消或异步让出。 */ export function selectImportGroups(pending: readonly ImportLocator[], committed: ReadonlySet, limits: {units: number; bytes: number; changes: number}): readonly (readonly ImportLocator[])[] { // byKey 用 key→原记录/源索引,edges 是依赖邻接表,reverse 是依赖→引用者表。 // 建图只含 pending 内边;外部 key 暂不入图,稍后用 committed 判定是否满足。 // 全段建图与排序同步完成,runner 应先限制 pending 大小,不能靠返回组数限制本次计算成本。 const byKey = new Map(pending.map((row, index) => [row.key, {row, index}])), edges = new Map(), reverse = new Map(); for (const row of pending) { const dependencies = row.dependencies.filter(key => byKey.has(key)); edges.set(row.key, dependencies); for (const key of dependencies) { const list = reverse.get(key) ?? []; list.push(row.key); reverse.set(key, list); } } // 第一遍显式DFS保存完成序,frame.index支持逐邻居推进,避免深链递归调用栈溢出。 const seen = new Set(), finish: string[] = []; for (const row of pending) { if (seen.has(row.key)) continue; seen.add(row.key); const stack = [{key: row.key, index: 0}]; while (stack.length) { const frame = stack.at(-1)!, neighbors = edges.get(frame.key)!; if (frame.index === neighbors.length) { finish.push(frame.key); stack.pop(); continue; } const key = neighbors[frame.index++]; if (!seen.has(key)) { seen.add(key); stack.push({key, index: 0}); } } } // 第二遍按反向完成序走反向边收集SCC,组内/组间再按最早源index排序。 seen.clear(); const components: ImportLocator[][] = []; for (let i = finish.length - 1; i >= 0; i--) { const first = finish[i]; if (seen.has(first)) continue; const stack = [first], group: ImportLocator[] = []; seen.add(first); while (stack.length) { const key = stack.pop()!; group.push(byKey.get(key)!.row); for (const next of reverse.get(key) ?? []) if (!seen.has(next)) { seen.add(next); stack.push(next); } } group.sort((a, b) => byKey.get(a.key)!.index - byKey.get(b.key)!.index); components.push(group); } components.sort((a, b) => byKey.get(a[0].key)!.index - byKey.get(b[0].key)!.index); // committed与本轮已选择组构成可用依赖;一组不能满足自己之外尚未到齐的引用。 const available = new Set(committed), chosen: ImportLocator[][] = []; let units = 0, bytes = 0, changes = 0; for (const group of components) { const internal = new Set(group.map(row => row.key)); if (!group.every(row => row.dependencies.every(key => available.has(key) || internal.has(key)))) continue; // 只有依赖就绪的组才检查硬成本;超限直接抛错,即使前面已选了组也不会返回部分结果。 // SCC 不可拆,依赖未就绪的超限组仍先等待,不能用拆组绕过循环引用完整性。 const size = group.reduce((sum, row) => sum + row.byteLength, 0), count = group.reduce((sum, row) => sum + row.changeCount, 0); if (size > limits.bytes || count > limits.changes) throw fault('import_dependency_group_capacity'); // The frame's adaptive unit budget is soft for one SCC, byte/change safety limits are hard. // 第一组可超过 units 软预算以闭合环,字节/changes 总量始终不得超限。 // 后续组放不下就继续找后面的较小可用组,不以 break 阻止其它独立组;available 只加入实际 chosen 的键。 if (units && units + group.length > limits.units || bytes + size > limits.bytes || changes + count > limits.changes) continue; chosen.push(group); units += group.length; bytes += size; changes += count; for (const row of group) available.add(row.key); } return chosen; }