@typespec/emitter-framework 0.15.0-dev.1 → 0.15.0-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,777 @@
1
+ import { shallowReactive } from "@alloy-js/core";
2
+
3
+ export type NestedArray<T> = T | NestedArray<T>[];
4
+
5
+ export interface SCCComponent<T> {
6
+ /**
7
+ * Nested array representation of the items that belong to this component.
8
+ * Single-node components expose the item directly while cycles expose a nested array.
9
+ */
10
+ readonly value: NestedArray<T>;
11
+ /** Components that this component depends on (outgoing edges). */
12
+ readonly references: ReadonlySet<SCCComponent<T>>;
13
+ /** Components that depend on this component (incoming edges). */
14
+ readonly referencedBy: ReadonlySet<SCCComponent<T>>;
15
+ }
16
+
17
+ type Connector<T> = (item: T) => Iterable<T>;
18
+
19
+ export interface SCCSetOptions {
20
+ /**
21
+ * When true, every node reachable from an added node is automatically surfaced
22
+ * in the public `items`/`components` lists without requiring an explicit add.
23
+ */
24
+ includeReachable?: boolean;
25
+ }
26
+
27
+ interface ComponentRecord<T> {
28
+ nodes: NodeRecord<T>[];
29
+ value: NestedArray<T>;
30
+ size: number;
31
+ view: ComponentView<T>;
32
+ }
33
+
34
+ interface ComponentView<T> extends SCCComponent<T> {
35
+ readonly references: Set<SCCComponent<T>>;
36
+ readonly referencedBy: Set<SCCComponent<T>>;
37
+ }
38
+
39
+ interface NodeRecord<T> {
40
+ readonly item: T;
41
+ readonly neighbors: Set<NodeRecord<T>>;
42
+ readonly dependents: Set<NodeRecord<T>>;
43
+ added: boolean;
44
+ addedAt?: number;
45
+ component?: ComponentRecord<T>;
46
+ initialized: boolean;
47
+ }
48
+
49
+ interface RemovedComponent<T> {
50
+ component: ComponentRecord<T>;
51
+ index: number;
52
+ }
53
+
54
+ /**
55
+ * Maintains a growing directed graph and exposes its strongly connected components (SCCs).
56
+ *
57
+ * The set incrementally applies Tarjan's algorithm so newly added nodes immediately update
58
+ * the public `items` and `components` views. Both arrays are shallow reactive so observers
59
+ * can hold references without re-fetching after each mutation.
60
+ */
61
+ export class SCCSet<T> {
62
+ /**
63
+ * Flattened, topologically ordered view of every node that has been added to the set.
64
+ * Nodes appear before dependents unless they belong to the same strongly connected component.
65
+ */
66
+ public readonly items: T[];
67
+
68
+ /**
69
+ * Ordered strongly connected components that mirror `items`. Each entry exposes its members along
70
+ * with the components it depends on and the components that depend on it, enabling callers to walk
71
+ * the connectivity graph directly from any component.
72
+ */
73
+ public readonly components: SCCComponent<T>[];
74
+
75
+ readonly #nodes = new Map<T, NodeRecord<T>>();
76
+ readonly #connector: Connector<T>;
77
+ readonly #componentOrder: ComponentRecord<T>[] = [];
78
+ #addCounter = 0;
79
+ readonly #includeReachable: boolean;
80
+
81
+ /**
82
+ * Creates a new SCC set around the provided dependency connector function.
83
+ * @param connector Maps each item to the items it depends on (outgoing edges).
84
+ * @param options Controls automatic inclusion of reachable nodes.
85
+ */
86
+ constructor(connector: Connector<T>, options: SCCSetOptions = {}) {
87
+ this.#connector = connector;
88
+ this.items = shallowReactive<T[]>([]);
89
+ this.components = shallowReactive<SCCComponent<T>[]>([]);
90
+ this.#includeReachable = !!options.includeReachable;
91
+ }
92
+
93
+ /**
94
+ * Adds an item to the graph and captures its outgoing connections via the connector.
95
+ * Items can be referenced before they are added; they will only surface in the public
96
+ * views once explicitly added.
97
+ */
98
+ public add(item: T): void {
99
+ const node = this.#getOrCreateNode(item);
100
+ if (node.added) {
101
+ return;
102
+ }
103
+
104
+ node.added = true;
105
+ node.addedAt = this.#addCounter++;
106
+ this.#initializeNode(node, true);
107
+
108
+ if (this.#includeReachable) {
109
+ this.#autoAddReachable(node);
110
+ this.#recomputeAll();
111
+ } else {
112
+ this.#integrateNode(node);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Adds multiple items and recomputes SCC ordering once at the end.
118
+ */
119
+ public addAll(items: Iterable<T>): void {
120
+ const newlyAdded: NodeRecord<T>[] = [];
121
+ for (const item of items) {
122
+ const node = this.#getOrCreateNode(item);
123
+ if (node.added) {
124
+ continue;
125
+ }
126
+ node.added = true;
127
+ node.addedAt = this.#addCounter++;
128
+ this.#initializeNode(node, true);
129
+ newlyAdded.push(node);
130
+ }
131
+
132
+ if (newlyAdded.length === 0) {
133
+ return;
134
+ }
135
+
136
+ if (this.#includeReachable) {
137
+ for (const node of newlyAdded) {
138
+ this.#autoAddReachable(node);
139
+ }
140
+ }
141
+
142
+ this.#recomputeAll();
143
+ }
144
+
145
+ /**
146
+ * Recursively adds every node reachable from the starting node, initializing metadata as needed.
147
+ */
148
+ #autoAddReachable(start: NodeRecord<T>): void {
149
+ const visited = new Set<NodeRecord<T>>([start]);
150
+ const stack = [...start.neighbors];
151
+
152
+ while (stack.length > 0) {
153
+ const current = stack.pop()!;
154
+ if (visited.has(current)) {
155
+ continue;
156
+ }
157
+ visited.add(current);
158
+
159
+ if (!current.added) {
160
+ current.added = true;
161
+ current.addedAt = this.#addCounter++;
162
+ this.#initializeNode(current, true);
163
+ }
164
+
165
+ for (const neighbor of current.neighbors) {
166
+ if (!visited.has(neighbor)) {
167
+ stack.push(neighbor);
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Retrieves the cached node for the provided item or materializes a new, uninitialized record.
175
+ */
176
+ #getOrCreateNode(item: T): NodeRecord<T> {
177
+ let existing = this.#nodes.get(item);
178
+ if (!existing) {
179
+ existing = {
180
+ item,
181
+ neighbors: new Set<NodeRecord<T>>(),
182
+ dependents: new Set<NodeRecord<T>>(),
183
+ added: false,
184
+ initialized: false,
185
+ } satisfies NodeRecord<T>;
186
+ this.#nodes.set(item, existing);
187
+ }
188
+ return existing;
189
+ }
190
+
191
+ /**
192
+ * Runs the connector for the node to refresh its neighbors and dependent relationships.
193
+ * @param force When true, existing neighbor edges are cleared before recomputing.
194
+ */
195
+ #initializeNode(node: NodeRecord<T>, force = false): void {
196
+ if (!force && node.initialized) {
197
+ return;
198
+ }
199
+
200
+ if (force || node.initialized) {
201
+ for (const neighbor of node.neighbors) {
202
+ neighbor.dependents.delete(node);
203
+ }
204
+ node.neighbors.clear();
205
+ }
206
+
207
+ const dependencies = this.#connector(node.item);
208
+ node.initialized = true;
209
+ for (const dependency of dependencies) {
210
+ if (dependency === undefined) {
211
+ throw new Error(
212
+ `Connector returned undefined dependency while initializing ${String(node.item)}`,
213
+ );
214
+ }
215
+ const neighbor = this.#getOrCreateNode(dependency);
216
+ node.neighbors.add(neighbor);
217
+ neighbor.dependents.add(node);
218
+ if (!neighbor.added) {
219
+ this.#initializeNode(neighbor);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Inserts a node that was just added into the component ordering without recomputing the world.
226
+ */
227
+ #integrateNode(node: NodeRecord<T>): void {
228
+ const forward = this.#collectReachable(node, (current) => current.neighbors);
229
+ const backward = this.#collectReachable(node, (current) => current.dependents);
230
+ const candidates = new Set<NodeRecord<T>>();
231
+ for (const seen of forward) {
232
+ if (backward.has(seen)) {
233
+ candidates.add(seen);
234
+ }
235
+ }
236
+ candidates.add(node);
237
+
238
+ const orderedNodes = Array.from(candidates).sort(
239
+ (left, right) => (left.addedAt ?? 0) - (right.addedAt ?? 0),
240
+ );
241
+
242
+ const dependencyComponents = this.#collectNeighboringComponents(
243
+ orderedNodes,
244
+ (nodeRecord) => nodeRecord.neighbors,
245
+ candidates,
246
+ );
247
+ const dependentComponents = this.#collectNeighboringComponents(
248
+ orderedNodes,
249
+ (nodeRecord) => nodeRecord.dependents,
250
+ candidates,
251
+ );
252
+
253
+ const dependentClosure = this.#collectDependentComponentClosure(orderedNodes, candidates);
254
+ const sortedDependents = this.#sortComponentsTopologically(dependentClosure);
255
+
256
+ const insertIndexBeforeRemoval = this.#computeInsertIndex(
257
+ dependencyComponents,
258
+ dependentComponents,
259
+ );
260
+ const componentsToRemove = new Set<ComponentRecord<T>>();
261
+ for (const member of candidates) {
262
+ if (member.component) {
263
+ componentsToRemove.add(member.component);
264
+ }
265
+ }
266
+ for (const component of dependentClosure) {
267
+ componentsToRemove.add(component);
268
+ }
269
+
270
+ const removedComponents = this.#removeComponents(componentsToRemove);
271
+ const newComponent = this.#createComponent(orderedNodes);
272
+ const insertIndex = this.#adjustInsertIndex(insertIndexBeforeRemoval, removedComponents);
273
+ this.#insertComponent(newComponent, insertIndex);
274
+
275
+ let nextIndex = insertIndex + 1;
276
+ for (const component of sortedDependents) {
277
+ this.#insertComponent(component, nextIndex++);
278
+ }
279
+
280
+ this.#refreshComponentConnections();
281
+ }
282
+
283
+ /**
284
+ * Walks the graph in the provided direction to find all reachable, added nodes.
285
+ */
286
+ #collectReachable(
287
+ start: NodeRecord<T>,
288
+ next: (node: NodeRecord<T>) => Iterable<NodeRecord<T>>,
289
+ ): Set<NodeRecord<T>> {
290
+ const visited = new Set<NodeRecord<T>>();
291
+ const stack: NodeRecord<T>[] = [start];
292
+ while (stack.length > 0) {
293
+ const current = stack.pop()!;
294
+ if (visited.has(current) || !current.added) {
295
+ continue;
296
+ }
297
+ visited.add(current);
298
+ for (const neighbor of next(current)) {
299
+ if (neighbor.added && !visited.has(neighbor)) {
300
+ stack.push(neighbor);
301
+ }
302
+ }
303
+ }
304
+ return visited;
305
+ }
306
+
307
+ /**
308
+ * Collects components adjacent to the provided nodes that are not part of an excluded set.
309
+ */
310
+ #collectNeighboringComponents(
311
+ nodes: NodeRecord<T>[],
312
+ next: (node: NodeRecord<T>) => Iterable<NodeRecord<T>>,
313
+ excluded: Set<NodeRecord<T>>,
314
+ ): Set<ComponentRecord<T>> {
315
+ const components = new Set<ComponentRecord<T>>();
316
+ for (const node of nodes) {
317
+ for (const neighbor of next(node)) {
318
+ if (!neighbor.added || excluded.has(neighbor) || !neighbor.component) {
319
+ continue;
320
+ }
321
+ components.add(neighbor.component);
322
+ }
323
+ }
324
+ return components;
325
+ }
326
+
327
+ /**
328
+ * Computes the closure of components that depend (directly or indirectly) on the start nodes.
329
+ */
330
+ #collectDependentComponentClosure(
331
+ startNodes: NodeRecord<T>[],
332
+ excluded: Set<NodeRecord<T>>,
333
+ ): Set<ComponentRecord<T>> {
334
+ const closure = new Set<ComponentRecord<T>>();
335
+ const visited = new Set<NodeRecord<T>>();
336
+ const stack = [...startNodes];
337
+
338
+ while (stack.length > 0) {
339
+ const current = stack.pop()!;
340
+ if (visited.has(current)) {
341
+ continue;
342
+ }
343
+ visited.add(current);
344
+
345
+ for (const dependent of current.dependents) {
346
+ if (excluded.has(dependent) || visited.has(dependent)) {
347
+ continue;
348
+ }
349
+
350
+ if (dependent.added && dependent.component) {
351
+ if (!closure.has(dependent.component)) {
352
+ closure.add(dependent.component);
353
+ for (const member of dependent.component.nodes) {
354
+ stack.push(member);
355
+ }
356
+ }
357
+ } else {
358
+ stack.push(dependent);
359
+ }
360
+ }
361
+ }
362
+
363
+ return closure;
364
+ }
365
+
366
+ /**
367
+ * Sorts the provided components in topological order, falling back to insertion order on cycles.
368
+ */
369
+ #sortComponentsTopologically(components: Set<ComponentRecord<T>>): ComponentRecord<T>[] {
370
+ if (components.size === 0) {
371
+ return [];
372
+ }
373
+
374
+ const componentList = Array.from(components);
375
+ const inDegree = new Map<ComponentRecord<T>, number>();
376
+ const adjacency = new Map<ComponentRecord<T>, Set<ComponentRecord<T>>>();
377
+
378
+ for (const component of componentList) {
379
+ inDegree.set(component, 0);
380
+ adjacency.set(component, new Set());
381
+ }
382
+
383
+ for (const component of componentList) {
384
+ for (const node of component.nodes) {
385
+ for (const dependent of node.dependents) {
386
+ const dependentComponent = dependent.component;
387
+ if (!dependentComponent || dependentComponent === component) {
388
+ continue;
389
+ }
390
+ if (components.has(dependentComponent)) {
391
+ if (!adjacency.get(component)!.has(dependentComponent)) {
392
+ adjacency.get(component)!.add(dependentComponent);
393
+ inDegree.set(dependentComponent, (inDegree.get(dependentComponent) ?? 0) + 1);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+
400
+ const queue = componentList
401
+ .filter((component) => (inDegree.get(component) ?? 0) === 0)
402
+ .sort((left, right) => this.#compareComponentAddedAt(left, right));
403
+ const ordered: ComponentRecord<T>[] = [];
404
+ while (queue.length > 0) {
405
+ const current = queue.shift()!;
406
+ ordered.push(current);
407
+ for (const neighbor of adjacency.get(current) ?? []) {
408
+ const remaining = (inDegree.get(neighbor) ?? 0) - 1;
409
+ inDegree.set(neighbor, remaining);
410
+ if (remaining === 0) {
411
+ queue.push(neighbor);
412
+ queue.sort((left, right) => this.#compareComponentAddedAt(left, right));
413
+ }
414
+ }
415
+ }
416
+
417
+ if (ordered.length !== components.size) {
418
+ return componentList.sort((left, right) => this.#compareComponentAddedAt(left, right));
419
+ }
420
+
421
+ return ordered;
422
+ }
423
+
424
+ /**
425
+ * Orders components by the earliest time any of their nodes were added to the set.
426
+ */
427
+ #compareComponentAddedAt(left: ComponentRecord<T>, right: ComponentRecord<T>): number {
428
+ return this.#getComponentAddedAt(left) - this.#getComponentAddedAt(right);
429
+ }
430
+
431
+ /**
432
+ * Returns the earliest add-counter value for nodes within the component.
433
+ */
434
+ #getComponentAddedAt(component: ComponentRecord<T>): number {
435
+ return component.nodes.reduce(
436
+ (min, node) => Math.min(min, node.addedAt ?? Number.POSITIVE_INFINITY),
437
+ Number.POSITIVE_INFINITY,
438
+ );
439
+ }
440
+
441
+ /**
442
+ * Determines where a new component should be inserted so dependency ordering stays valid.
443
+ */
444
+ #computeInsertIndex(
445
+ dependencies: Set<ComponentRecord<T>>,
446
+ dependents: Set<ComponentRecord<T>>,
447
+ ): number {
448
+ const dependencyIndex = dependencies.size
449
+ ? Math.max(...Array.from(dependencies, (component) => this.#getComponentIndex(component)))
450
+ : -1;
451
+ const lowerBound = dependencyIndex + 1;
452
+ if (!dependents.size) {
453
+ if (!dependencies.size) {
454
+ return this.#componentOrder.length;
455
+ }
456
+ return Math.min(lowerBound, this.#componentOrder.length);
457
+ }
458
+
459
+ const upperBound = Math.min(
460
+ ...Array.from(dependents, (component) => this.#getComponentIndex(component)),
461
+ );
462
+ if (upperBound < lowerBound) {
463
+ return lowerBound;
464
+ }
465
+ return upperBound;
466
+ }
467
+
468
+ /**
469
+ * Retrieves the current ordering index for the component, throwing if it is unknown.
470
+ */
471
+ #getComponentIndex(component: ComponentRecord<T>): number {
472
+ const index = this.#componentOrder.indexOf(component);
473
+ if (index === -1) {
474
+ throw new Error("Component not found in order.");
475
+ }
476
+ return index;
477
+ }
478
+
479
+ /**
480
+ * Calculates the position in `items` where the first element of a component at index would live.
481
+ */
482
+ #getItemsStartIndexForInsert(targetIndex: number): number {
483
+ let start = 0;
484
+ for (let i = 0; i < targetIndex; i++) {
485
+ start += this.#componentOrder[i].size;
486
+ }
487
+ return start;
488
+ }
489
+
490
+ /**
491
+ * Convenience helper for `getItemsStartIndexForInsert` that names the intent for existing indices.
492
+ */
493
+ #getItemsStartIndexForIndex(componentIndex: number): number {
494
+ return this.#getItemsStartIndexForInsert(componentIndex);
495
+ }
496
+
497
+ /**
498
+ * Removes the specified components from both the ordering list and flattened items array.
499
+ * Returns metadata describing what was removed so later insertions can adjust their offsets.
500
+ */
501
+ #removeComponents(components: Set<ComponentRecord<T>>): RemovedComponent<T>[] {
502
+ if (components.size === 0) {
503
+ return [];
504
+ }
505
+
506
+ const indexed = Array.from(components, (component) => ({
507
+ component,
508
+ index: this.#getComponentIndex(component),
509
+ })).sort((a, b) => b.index - a.index);
510
+
511
+ const removed: RemovedComponent<T>[] = [];
512
+ for (const { component, index } of indexed) {
513
+ const startIndex = this.#getItemsStartIndexForIndex(index);
514
+ this.#componentOrder.splice(index, 1);
515
+ this.components.splice(index, 1);
516
+ this.items.splice(startIndex, component.size);
517
+ component.view.references.clear();
518
+ component.view.referencedBy.clear();
519
+ for (const node of component.nodes) {
520
+ node.component = undefined;
521
+ }
522
+ removed.push({ component, index });
523
+ }
524
+
525
+ removed.sort((a, b) => a.index - b.index);
526
+ return removed;
527
+ }
528
+
529
+ /**
530
+ * Adjusts a desired insertion point to account for previously removed components.
531
+ */
532
+ #adjustInsertIndex(desiredIndex: number, removed: RemovedComponent<T>[]): number {
533
+ let shift = 0;
534
+ for (const removedComponent of removed) {
535
+ if (removedComponent.index < desiredIndex) {
536
+ shift++;
537
+ }
538
+ }
539
+ return Math.max(0, desiredIndex - shift);
540
+ }
541
+
542
+ /**
543
+ * Inserts a component into the ordering and mirrors the change in the public lists.
544
+ */
545
+ #insertComponent(component: ComponentRecord<T>, index: number): void {
546
+ const startIndex = this.#getItemsStartIndexForInsert(index);
547
+ for (const node of component.nodes) {
548
+ node.component = component;
549
+ }
550
+ this.#componentOrder.splice(index, 0, component);
551
+ this.components.splice(index, 0, component.view);
552
+ const items = component.nodes.map((record) => record.item);
553
+ this.items.splice(startIndex, 0, ...items);
554
+ }
555
+
556
+ /**
557
+ * Builds a component record for the provided nodes and assigns the back-reference on each node.
558
+ */
559
+ #createComponent(nodes: NodeRecord<T>[]): ComponentRecord<T> {
560
+ const value = this.#createComponentValue(nodes);
561
+ const view: ComponentView<T> = {
562
+ value,
563
+ references: new Set<SCCComponent<T>>(),
564
+ referencedBy: new Set<SCCComponent<T>>(),
565
+ };
566
+ const component: ComponentRecord<T> = {
567
+ nodes,
568
+ value,
569
+ size: nodes.length,
570
+ view,
571
+ };
572
+ for (const node of nodes) {
573
+ node.component = component;
574
+ }
575
+ return component;
576
+ }
577
+
578
+ /**
579
+ * Generates the structure stored in `components` for the given nodes (item vs. nested array).
580
+ */
581
+ #createComponentValue(nodes: NodeRecord<T>[]): NestedArray<T> {
582
+ if (nodes.length === 1) {
583
+ return nodes[0].item;
584
+ }
585
+ const items = nodes.map((record) => record.item);
586
+ return shallowReactive(items) as NestedArray<T>;
587
+ }
588
+
589
+ /**
590
+ * Rebuilds the complete SCC ordering from scratch using Tarjan's algorithm and updates outputs.
591
+ */
592
+ #recomputeAll(): void {
593
+ const nodes = Array.from(this.#nodes.values()).filter((node) => node.added);
594
+ for (const node of nodes) {
595
+ node.component = undefined;
596
+ }
597
+
598
+ if (nodes.length === 0) {
599
+ for (const component of this.#componentOrder) {
600
+ component.view.references.clear();
601
+ component.view.referencedBy.clear();
602
+ }
603
+ this.#componentOrder.length = 0;
604
+ this.components.length = 0;
605
+ this.items.length = 0;
606
+ return;
607
+ }
608
+
609
+ const indexMap = new Map<NodeRecord<T>, number>();
610
+ const lowlinkMap = new Map<NodeRecord<T>, number>();
611
+ const stack: NodeRecord<T>[] = [];
612
+ const onStack = new Set<NodeRecord<T>>();
613
+ let index = 0;
614
+ const components: ComponentRecord<T>[] = [];
615
+
616
+ const stronglyConnect = (node: NodeRecord<T>): void => {
617
+ indexMap.set(node, index);
618
+ lowlinkMap.set(node, index);
619
+ index++;
620
+ stack.push(node);
621
+ onStack.add(node);
622
+
623
+ for (const neighbor of node.neighbors) {
624
+ if (!neighbor.added) {
625
+ continue;
626
+ }
627
+ if (!indexMap.has(neighbor)) {
628
+ stronglyConnect(neighbor);
629
+ lowlinkMap.set(node, Math.min(lowlinkMap.get(node)!, lowlinkMap.get(neighbor)!));
630
+ } else if (onStack.has(neighbor)) {
631
+ lowlinkMap.set(node, Math.min(lowlinkMap.get(node)!, indexMap.get(neighbor)!));
632
+ }
633
+ }
634
+
635
+ if (lowlinkMap.get(node) === indexMap.get(node)) {
636
+ const componentNodes: NodeRecord<T>[] = [];
637
+ let member: NodeRecord<T>;
638
+ do {
639
+ member = stack.pop()!;
640
+ onStack.delete(member);
641
+ componentNodes.push(member);
642
+ } while (member !== node);
643
+ componentNodes.sort((left, right) => (left.addedAt ?? 0) - (right.addedAt ?? 0));
644
+ const component = this.#createComponent(componentNodes);
645
+ components.push(component);
646
+ }
647
+ };
648
+
649
+ for (const node of nodes) {
650
+ if (!indexMap.has(node)) {
651
+ stronglyConnect(node);
652
+ }
653
+ }
654
+
655
+ const adjacency = new Map<ComponentRecord<T>, Set<ComponentRecord<T>>>();
656
+ const inDegree = new Map<ComponentRecord<T>, number>();
657
+ for (const component of components) {
658
+ adjacency.set(component, new Set());
659
+ inDegree.set(component, 0);
660
+ }
661
+
662
+ for (const component of components) {
663
+ for (const node of component.nodes) {
664
+ const dependencyComponents = new Set<ComponentRecord<T>>();
665
+ const visitedNodes = new Set<NodeRecord<T>>();
666
+ for (const neighbor of node.neighbors) {
667
+ this.#collectComponentDependencies(neighbor, dependencyComponents, visitedNodes);
668
+ }
669
+ for (const dependency of dependencyComponents) {
670
+ if (dependency === component) {
671
+ continue;
672
+ }
673
+ const dependents = adjacency.get(dependency)!;
674
+ if (!dependents.has(component)) {
675
+ dependents.add(component);
676
+ inDegree.set(component, (inDegree.get(component) ?? 0) + 1);
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ const queue = components
683
+ .filter((component) => (inDegree.get(component) ?? 0) === 0)
684
+ .sort((left, right) => this.#compareComponentAddedAt(left, right));
685
+ const orderedComponents: ComponentRecord<T>[] = [];
686
+ while (queue.length > 0) {
687
+ const current = queue.shift()!;
688
+ orderedComponents.push(current);
689
+ for (const dependent of adjacency.get(current) ?? []) {
690
+ const nextDegree = (inDegree.get(dependent) ?? 0) - 1;
691
+ inDegree.set(dependent, nextDegree);
692
+ if (nextDegree === 0) {
693
+ queue.push(dependent);
694
+ queue.sort((left, right) => this.#compareComponentAddedAt(left, right));
695
+ }
696
+ }
697
+ }
698
+
699
+ if (orderedComponents.length !== components.length) {
700
+ orderedComponents.push(
701
+ ...components
702
+ .filter((component) => !orderedComponents.includes(component))
703
+ .sort((left, right) => this.#compareComponentAddedAt(left, right)),
704
+ );
705
+ }
706
+
707
+ this.#componentOrder.splice(0, this.#componentOrder.length, ...orderedComponents);
708
+ this.components.splice(
709
+ 0,
710
+ this.components.length,
711
+ ...orderedComponents.map((component) => component.view),
712
+ );
713
+ const flatItems: T[] = [];
714
+ for (const component of orderedComponents) {
715
+ for (const node of component.nodes) {
716
+ flatItems.push(node.item);
717
+ }
718
+ }
719
+ this.items.splice(0, this.items.length, ...flatItems);
720
+
721
+ this.#refreshComponentConnections();
722
+ }
723
+
724
+ /**
725
+ * Traverses outward from a node to find components it ultimately depends on, even through
726
+ * nodes that are not yet part of the public set.
727
+ */
728
+ #collectComponentDependencies(
729
+ node: NodeRecord<T>,
730
+ collected: Set<ComponentRecord<T>>,
731
+ visited: Set<NodeRecord<T>>,
732
+ ): void {
733
+ if (visited.has(node)) {
734
+ return;
735
+ }
736
+ visited.add(node);
737
+
738
+ if (node.added) {
739
+ if (node.component) {
740
+ collected.add(node.component);
741
+ }
742
+ return;
743
+ }
744
+
745
+ for (const neighbor of node.neighbors) {
746
+ this.#collectComponentDependencies(neighbor, collected, visited);
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Updates each public component view so callers can traverse the component graph without
752
+ * recomputing edges manually.
753
+ */
754
+ #refreshComponentConnections(): void {
755
+ for (const component of this.#componentOrder) {
756
+ component.view.references.clear();
757
+ component.view.referencedBy.clear();
758
+ }
759
+
760
+ for (const component of this.#componentOrder) {
761
+ for (const node of component.nodes) {
762
+ for (const neighbor of node.neighbors) {
763
+ if (!neighbor.added) {
764
+ continue;
765
+ }
766
+ const neighborComponent = neighbor.component;
767
+ if (!neighborComponent || neighborComponent === component) {
768
+ continue;
769
+ }
770
+ const dependencyView = neighborComponent.view;
771
+ component.view.references.add(dependencyView);
772
+ dependencyView.referencedBy.add(component.view);
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }