Files
Endoleap/src/lib/canvas-runtime/import-groups.ts
T

59 lines
4.6 KiB
TypeScript

/**
* 依赖等待记录的批选择。以迭代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<string>, 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<string, string[]>(), reverse = new Map<string, string[]>();
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<string>(), 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;
}