@modernrelay/orbit-core 0.2.0 → 0.13.6
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.
- package/dist/chunk-FG6TQANI.js +378 -0
- package/dist/chunk-FG6TQANI.js.map +1 -0
- package/dist/engine.d.ts +1 -1
- package/dist/{index-BPjuELfY.d.ts → index-BoQfS8jd.d.ts} +201 -4
- package/dist/index.d.ts +364 -4
- package/dist/index.js +1735 -51
- package/dist/index.js.map +1 -1
- package/dist/{clusters-ExqnvobT.d.ts → lane-SZYhcrXT.d.ts} +95 -2
- package/dist/testing.d.ts +54 -3
- package/dist/testing.js +198 -7
- package/dist/testing.js.map +1 -1
- package/dist/worker/entry.js +269 -0
- package/dist/worker/entry.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-Z7FOEASL.js +0 -141
- package/dist/chunk-Z7FOEASL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DIAGNOSTIC_SAMPLE_CAP, deriveClusters, resolveClusterCenters, clusterCentroids } from './chunk-
|
|
2
|
-
export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, clusterCentroids, deriveClusters, generateClusterCenters, resolveClusterCenters } from './chunk-
|
|
1
|
+
import { DIAGNOSTIC_SAMPLE_CAP, encodeStringTable, collectTransfers, deriveClusters, resolveClusterCenters, clusterCentroids, EnvelopeSequencer, RequestLedger } from './chunk-FG6TQANI.js';
|
|
2
|
+
export { DEFAULT_CLUSTER_CENTER_RADIUS, DEFAULT_LAYOUT_SEED, DIAGNOSTIC_SAMPLE_CAP, acceptColumnar, clusterCentroids, collectTransfers, decodeStringTable, deriveClusters, encodeStringTable, generateClusterCenters, judgeEpoch, resolveClusterCenters } from './chunk-FG6TQANI.js';
|
|
3
3
|
import { createStore } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
// src/errors.ts
|
|
@@ -96,6 +96,10 @@ function validateSnapshot(snapshot) {
|
|
|
96
96
|
record(invalidNode, `[${i}]`);
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
+
if (id.includes("\0")) {
|
|
100
|
+
record(invalidNode, `[${i}]`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
99
103
|
if (nodeIndex.has(id)) {
|
|
100
104
|
record(duplicateNode, id);
|
|
101
105
|
continue;
|
|
@@ -154,7 +158,7 @@ function validateSnapshot(snapshot) {
|
|
|
154
158
|
"invalid-node",
|
|
155
159
|
"error",
|
|
156
160
|
invalidNode,
|
|
157
|
-
`${invalidNode.count} node row(s) dropped: missing
|
|
161
|
+
`${invalidNode.count} node row(s) dropped: missing, non-string, or NUL-containing id`
|
|
158
162
|
);
|
|
159
163
|
pushDiagnostic(
|
|
160
164
|
diagnostics,
|
|
@@ -1513,6 +1517,375 @@ function neighborsOf(adj, index) {
|
|
|
1513
1517
|
}
|
|
1514
1518
|
return adj.neighbors.subarray(adj.offsets[index], adj.offsets[index + 1]);
|
|
1515
1519
|
}
|
|
1520
|
+
function buildIncidence(links, pointCount) {
|
|
1521
|
+
if (!Number.isInteger(pointCount) || pointCount < 0) {
|
|
1522
|
+
throw new RangeError(
|
|
1523
|
+
`buildIncidence: pointCount must be a non-negative integer, got ${pointCount}`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
if ((links.length & 1) !== 0) {
|
|
1527
|
+
throw new RangeError(
|
|
1528
|
+
`buildIncidence: links length must be even ([src, tgt] pairs), got ${links.length}`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const n = links.length;
|
|
1532
|
+
const offsets = new Uint32Array(pointCount + 1);
|
|
1533
|
+
for (let i = 0; i < n; i++) {
|
|
1534
|
+
const p = links[i];
|
|
1535
|
+
if (p >= pointCount) {
|
|
1536
|
+
throw new RangeError(
|
|
1537
|
+
`buildIncidence: link endpoint ${p} out of range (pointCount ${pointCount})`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
offsets[p + 1] = offsets[p + 1] + 1;
|
|
1541
|
+
}
|
|
1542
|
+
for (let i = 1; i <= pointCount; i++) {
|
|
1543
|
+
offsets[i] = offsets[i] + offsets[i - 1];
|
|
1544
|
+
}
|
|
1545
|
+
const edgeSlots = new Uint32Array(n);
|
|
1546
|
+
const cursor = offsets.slice(0, pointCount);
|
|
1547
|
+
for (let i = 0; i < n; i += 2) {
|
|
1548
|
+
const edge = i >>> 1;
|
|
1549
|
+
const a = links[i];
|
|
1550
|
+
const b = links[i + 1];
|
|
1551
|
+
const ca = cursor[a];
|
|
1552
|
+
edgeSlots[ca] = edge;
|
|
1553
|
+
cursor[a] = ca + 1;
|
|
1554
|
+
const cb = cursor[b];
|
|
1555
|
+
edgeSlots[cb] = edge;
|
|
1556
|
+
cursor[b] = cb + 1;
|
|
1557
|
+
}
|
|
1558
|
+
return { offsets, edgeSlots };
|
|
1559
|
+
}
|
|
1560
|
+
function incidentEdgesOf(inc, index) {
|
|
1561
|
+
const pointCount = inc.offsets.length - 1;
|
|
1562
|
+
if (!Number.isInteger(index) || index < 0 || index >= pointCount) {
|
|
1563
|
+
throw new RangeError(`incidentEdgesOf: index ${index} out of range (pointCount ${pointCount})`);
|
|
1564
|
+
}
|
|
1565
|
+
return inc.edgeSlots.subarray(inc.offsets[index], inc.offsets[index + 1]);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/alphaCompose.ts
|
|
1569
|
+
var IncrementalAlphaComposer = class {
|
|
1570
|
+
bufA = null;
|
|
1571
|
+
bufB = null;
|
|
1572
|
+
staleA = [];
|
|
1573
|
+
staleB = [];
|
|
1574
|
+
/** Which buffer the NEXT nextBuffer() call returns (0 = A, 1 = B). */
|
|
1575
|
+
next = 0;
|
|
1576
|
+
// --- seed key ---
|
|
1577
|
+
seededBase = null;
|
|
1578
|
+
seededCount = -1;
|
|
1579
|
+
seededDimAlpha = NaN;
|
|
1580
|
+
seededEpoch = void 0;
|
|
1581
|
+
/** Slots rewritten since the last resetStats (wave-5 gate instrument). */
|
|
1582
|
+
slotsRewritten = 0;
|
|
1583
|
+
/** Full reseeds performed (each is one O(n) masked pass over the pair). */
|
|
1584
|
+
reseeds = 0;
|
|
1585
|
+
resetStats() {
|
|
1586
|
+
this.slotsRewritten = 0;
|
|
1587
|
+
this.reseeds = 0;
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* True when the composer is seeded for exactly this (base, count,
|
|
1591
|
+
* dimAlpha, extraEpoch) tuple; otherwise both buffers are rebuilt with a
|
|
1592
|
+
* full masked pass and the stale lists reset. Object.is on the alpha
|
|
1593
|
+
* handles the NaN sentinel.
|
|
1594
|
+
*/
|
|
1595
|
+
ensureSeeded(base, count, dimAlpha, extraEpoch, alphaOf) {
|
|
1596
|
+
if (this.seededBase === base && this.seededCount === count && Object.is(this.seededDimAlpha, dimAlpha) && this.seededEpoch === extraEpoch && this.bufA !== null && this.bufB !== null) {
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
this.reseeds += 1;
|
|
1600
|
+
this.bufA = this.seedOne(base, count, alphaOf, this.bufA);
|
|
1601
|
+
this.bufB = this.seedOne(base, count, alphaOf, this.bufB);
|
|
1602
|
+
this.staleA.length = 0;
|
|
1603
|
+
this.staleB.length = 0;
|
|
1604
|
+
this.seededBase = base;
|
|
1605
|
+
this.seededCount = count;
|
|
1606
|
+
this.seededDimAlpha = dimAlpha;
|
|
1607
|
+
this.seededEpoch = extraEpoch;
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
/** Record changed slots from one mask drain (call once per drain — the
|
|
1611
|
+
* drain arrays are reused by the mask, so this copies them out). */
|
|
1612
|
+
note(slots) {
|
|
1613
|
+
for (let i = 0; i < slots.length; i += 1) {
|
|
1614
|
+
this.staleA.push(slots[i]);
|
|
1615
|
+
this.staleB.push(slots[i]);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Replay the target buffer's stale slots against the CURRENT mask state,
|
|
1620
|
+
* swap, and return it. Same output semantics as the naive composer: the
|
|
1621
|
+
* base RGB is untouched, alpha = base alpha × alphaOf(slot).
|
|
1622
|
+
*/
|
|
1623
|
+
nextBuffer(base, alphaOf) {
|
|
1624
|
+
const useA = this.next === 0;
|
|
1625
|
+
const buf = useA ? this.bufA : this.bufB;
|
|
1626
|
+
const stale = useA ? this.staleA : this.staleB;
|
|
1627
|
+
for (let i = 0; i < stale.length; i += 1) {
|
|
1628
|
+
const slot = stale[i];
|
|
1629
|
+
buf[4 * slot + 3] = base[4 * slot + 3] * alphaOf(slot);
|
|
1630
|
+
this.slotsRewritten += 1;
|
|
1631
|
+
}
|
|
1632
|
+
stale.length = 0;
|
|
1633
|
+
this.next = useA ? 1 : 0;
|
|
1634
|
+
return buf;
|
|
1635
|
+
}
|
|
1636
|
+
/** Drop the buffers entirely (scene teardown). */
|
|
1637
|
+
reset() {
|
|
1638
|
+
this.bufA = null;
|
|
1639
|
+
this.bufB = null;
|
|
1640
|
+
this.staleA.length = 0;
|
|
1641
|
+
this.staleB.length = 0;
|
|
1642
|
+
this.seededBase = null;
|
|
1643
|
+
this.seededCount = -1;
|
|
1644
|
+
this.seededDimAlpha = NaN;
|
|
1645
|
+
this.seededEpoch = void 0;
|
|
1646
|
+
this.next = 0;
|
|
1647
|
+
}
|
|
1648
|
+
seedOne(base, count, alphaOf, reuse) {
|
|
1649
|
+
const out = reuse !== null && reuse.length === base.length ? reuse : new Float32Array(base.length);
|
|
1650
|
+
out.set(base);
|
|
1651
|
+
for (let i = 0; i < count; i += 1) {
|
|
1652
|
+
const a = alphaOf(i);
|
|
1653
|
+
if (a !== 1) out[4 * i + 3] = base[4 * i + 3] * a;
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// src/perf.ts
|
|
1660
|
+
var PRESSURE_WINDOW_MS = 250;
|
|
1661
|
+
var DROPPED_FRAME_MS = 34;
|
|
1662
|
+
var SLEEP_GAP_MS = 250;
|
|
1663
|
+
var PressureSampler = class {
|
|
1664
|
+
lastFrameAt = NaN;
|
|
1665
|
+
windowStart = NaN;
|
|
1666
|
+
windowSum = 0;
|
|
1667
|
+
windowCount = 0;
|
|
1668
|
+
ewma = NaN;
|
|
1669
|
+
worst = 0;
|
|
1670
|
+
frames = 0;
|
|
1671
|
+
dropped = 0;
|
|
1672
|
+
windows = 0;
|
|
1673
|
+
idleWakeups = 0;
|
|
1674
|
+
/**
|
|
1675
|
+
* Record one onFrame tick. `settled` marks a tick that arrived while the
|
|
1676
|
+
* scene was at rest (sim settled, no pending commit work) — the idle-
|
|
1677
|
+
* wakeup counter, which reads 0 when the ADR-005 gated clock is honest.
|
|
1678
|
+
*/
|
|
1679
|
+
noteFrame(timeMs, settled) {
|
|
1680
|
+
this.frames += 1;
|
|
1681
|
+
if (settled) this.idleWakeups += 1;
|
|
1682
|
+
const prev = this.lastFrameAt;
|
|
1683
|
+
this.lastFrameAt = timeMs;
|
|
1684
|
+
if (Number.isNaN(prev)) {
|
|
1685
|
+
this.windowStart = timeMs;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const delta = timeMs - prev;
|
|
1689
|
+
if (delta < 0) {
|
|
1690
|
+
this.windowStart = timeMs;
|
|
1691
|
+
this.windowSum = 0;
|
|
1692
|
+
this.windowCount = 0;
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (delta >= SLEEP_GAP_MS) {
|
|
1696
|
+
this.closeWindow();
|
|
1697
|
+
this.windowStart = timeMs;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (delta > this.worst) this.worst = delta;
|
|
1701
|
+
if (delta >= DROPPED_FRAME_MS) this.dropped += 1;
|
|
1702
|
+
this.windowSum += delta;
|
|
1703
|
+
this.windowCount += 1;
|
|
1704
|
+
if (timeMs - this.windowStart >= PRESSURE_WINDOW_MS) {
|
|
1705
|
+
this.closeWindow();
|
|
1706
|
+
this.windowStart = timeMs;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
snapshot() {
|
|
1710
|
+
return {
|
|
1711
|
+
frameEwmaMs: this.ewma,
|
|
1712
|
+
worstFrameMs: this.worst,
|
|
1713
|
+
frames: this.frames,
|
|
1714
|
+
droppedFrames: this.dropped,
|
|
1715
|
+
windows: this.windows,
|
|
1716
|
+
idleWakeups: this.idleWakeups
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
/** Zero the accumulated counters (EWMA and frame anchor survive — the
|
|
1720
|
+
* smoothing is continuous; the counters are per-sample-period). */
|
|
1721
|
+
resetCounters() {
|
|
1722
|
+
this.worst = 0;
|
|
1723
|
+
this.frames = 0;
|
|
1724
|
+
this.dropped = 0;
|
|
1725
|
+
this.windows = 0;
|
|
1726
|
+
this.idleWakeups = 0;
|
|
1727
|
+
}
|
|
1728
|
+
closeWindow() {
|
|
1729
|
+
if (this.windowCount === 0) return;
|
|
1730
|
+
const mean = this.windowSum / this.windowCount;
|
|
1731
|
+
this.windows += 1;
|
|
1732
|
+
this.ewma = Number.isNaN(this.ewma) ? mean : this.ewma * 0.7 + mean * 0.3;
|
|
1733
|
+
this.windowSum = 0;
|
|
1734
|
+
this.windowCount = 0;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/degrade.ts
|
|
1739
|
+
var SCALE_LIMITS_DEFAULTS = Object.freeze({
|
|
1740
|
+
domLabelNodes: 1e5,
|
|
1741
|
+
pickingLinks: 25e4,
|
|
1742
|
+
histogramBatchNodes: 5e5,
|
|
1743
|
+
hysteresis: 0.1,
|
|
1744
|
+
minimumDwellMs: 1e3,
|
|
1745
|
+
resourceDegradationOrder: Object.freeze(["disable-transitions", "defer-images"])
|
|
1746
|
+
});
|
|
1747
|
+
var RESOURCE_STEPS = /* @__PURE__ */ new Set([
|
|
1748
|
+
"disable-transitions",
|
|
1749
|
+
"defer-images",
|
|
1750
|
+
"uniform-link-style"
|
|
1751
|
+
]);
|
|
1752
|
+
function resolveScaleLimits(input) {
|
|
1753
|
+
const warnings = [];
|
|
1754
|
+
const out = { ...SCALE_LIMITS_DEFAULTS };
|
|
1755
|
+
if (input === void 0) return { limits: out, warnings };
|
|
1756
|
+
const num3 = (key) => {
|
|
1757
|
+
const v = input[key];
|
|
1758
|
+
if (v === void 0) return;
|
|
1759
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[key] = v;
|
|
1760
|
+
else warnings.push(`limits.${key} must be a non-negative finite number (got ${String(v)})`);
|
|
1761
|
+
};
|
|
1762
|
+
num3("domLabelNodes");
|
|
1763
|
+
num3("pickingLinks");
|
|
1764
|
+
num3("histogramBatchNodes");
|
|
1765
|
+
num3("minimumDwellMs");
|
|
1766
|
+
if (input.hysteresis !== void 0) {
|
|
1767
|
+
const h = input.hysteresis;
|
|
1768
|
+
if (typeof h === "number" && Number.isFinite(h) && h >= 0 && h < 1) out.hysteresis = h;
|
|
1769
|
+
else warnings.push(`limits.hysteresis must be in [0, 1) (got ${String(h)})`);
|
|
1770
|
+
}
|
|
1771
|
+
if (input.resourceDegradationOrder !== void 0) {
|
|
1772
|
+
const order = input.resourceDegradationOrder;
|
|
1773
|
+
const valid = Array.isArray(order) && order.every((s) => RESOURCE_STEPS.has(s)) && new Set(order).size === order.length;
|
|
1774
|
+
if (valid) out.resourceDegradationOrder = Object.freeze([...order]);
|
|
1775
|
+
else {
|
|
1776
|
+
warnings.push(
|
|
1777
|
+
"limits.resourceDegradationOrder must be unique resource steps ('disable-transitions' | 'defer-images' | 'uniform-link-style')"
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { limits: Object.freeze(out), warnings };
|
|
1782
|
+
}
|
|
1783
|
+
var DegradeController = class {
|
|
1784
|
+
constructor(limits, now) {
|
|
1785
|
+
this.limits = limits;
|
|
1786
|
+
this.now = now;
|
|
1787
|
+
}
|
|
1788
|
+
limits;
|
|
1789
|
+
now;
|
|
1790
|
+
steps = /* @__PURE__ */ new Map();
|
|
1791
|
+
activeCache = Object.freeze([]);
|
|
1792
|
+
isEngaged(step) {
|
|
1793
|
+
return this.steps.get(step)?.engaged === true;
|
|
1794
|
+
}
|
|
1795
|
+
/** Stable frozen list for GraphPerfSnapshot.activeDegradations. */
|
|
1796
|
+
activeSteps() {
|
|
1797
|
+
return this.activeCache;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Edge-triggered count evaluation over the CURRENT visible counts.
|
|
1801
|
+
* Returns the events to emit (empty when nothing crossed its band or a
|
|
1802
|
+
* dwell is holding). Count-engaged steps disengage here; pressure- or
|
|
1803
|
+
* resource-engaged steps do NOT auto-disengage on counts — their signal
|
|
1804
|
+
* owns them (`clearPressure` / `releaseResourceSteps`).
|
|
1805
|
+
*/
|
|
1806
|
+
evaluateCounts(visible) {
|
|
1807
|
+
const events = [];
|
|
1808
|
+
const counts = [
|
|
1809
|
+
["cap-dom-labels", visible.nodes, this.limits.domLabelNodes],
|
|
1810
|
+
["defer-link-picking", visible.edges, this.limits.pickingLinks],
|
|
1811
|
+
["batch-histograms", visible.nodes, this.limits.histogramBatchNodes]
|
|
1812
|
+
];
|
|
1813
|
+
for (const [step, value, limit] of counts) {
|
|
1814
|
+
const state = this.stateOf(step);
|
|
1815
|
+
if (!state.engaged) {
|
|
1816
|
+
if (value > limit && this.dwellOver(state)) {
|
|
1817
|
+
events.push(this.transition(step, state, true, "count", visible));
|
|
1818
|
+
}
|
|
1819
|
+
} else if (state.reason === "count") {
|
|
1820
|
+
if (value < limit * (1 - this.limits.hysteresis) && this.dwellOver(state)) {
|
|
1821
|
+
events.push(this.transition(step, state, false, "count", visible));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return events;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Pressure trigger (frame or input): engage a step EARLIER than its count
|
|
1829
|
+
* hint. No-op (null) when already engaged or dwell-held.
|
|
1830
|
+
*/
|
|
1831
|
+
engageForPressure(step, reason, visible) {
|
|
1832
|
+
const state = this.stateOf(step);
|
|
1833
|
+
if (state.engaged || !this.dwellOver(state)) return null;
|
|
1834
|
+
return this.transition(step, state, true, reason, visible);
|
|
1835
|
+
}
|
|
1836
|
+
/** Release a pressure-engaged step once its signal normalizes. */
|
|
1837
|
+
clearPressure(step, visible) {
|
|
1838
|
+
const state = this.stateOf(step);
|
|
1839
|
+
if (!state.engaged) return null;
|
|
1840
|
+
if (state.reason !== "frame-pressure" && state.reason !== "input-pressure") return null;
|
|
1841
|
+
if (!this.dwellOver(state)) return null;
|
|
1842
|
+
return this.transition(step, state, false, state.reason, visible);
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Resource-admission trigger: engage the NEXT not-yet-engaged step in the
|
|
1846
|
+
* declared order. Null when the order is exhausted — the caller must then
|
|
1847
|
+
* REJECT before allocating (§17: never silently erase semantic styling).
|
|
1848
|
+
*/
|
|
1849
|
+
engageNextResourceStep(visible) {
|
|
1850
|
+
for (const step of this.limits.resourceDegradationOrder) {
|
|
1851
|
+
const state = this.stateOf(step);
|
|
1852
|
+
if (state.engaged) continue;
|
|
1853
|
+
return this.transition(step, state, true, "resource-estimate", visible);
|
|
1854
|
+
}
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
/** Release every resource-engaged step (pressure cleared / new budget). */
|
|
1858
|
+
releaseResourceSteps(visible) {
|
|
1859
|
+
const events = [];
|
|
1860
|
+
for (const step of this.limits.resourceDegradationOrder) {
|
|
1861
|
+
const state = this.stateOf(step);
|
|
1862
|
+
if (state.engaged && state.reason === "resource-estimate") {
|
|
1863
|
+
events.push(this.transition(step, state, false, "resource-estimate", visible));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return events;
|
|
1867
|
+
}
|
|
1868
|
+
stateOf(step) {
|
|
1869
|
+
let state = this.steps.get(step);
|
|
1870
|
+
if (state === void 0) {
|
|
1871
|
+
state = { engaged: false, changedAt: -Infinity, reason: null };
|
|
1872
|
+
this.steps.set(step, state);
|
|
1873
|
+
}
|
|
1874
|
+
return state;
|
|
1875
|
+
}
|
|
1876
|
+
dwellOver(state) {
|
|
1877
|
+
return this.now() - state.changedAt >= this.limits.minimumDwellMs || state.changedAt === -Infinity;
|
|
1878
|
+
}
|
|
1879
|
+
transition(step, state, engaged, reason, visible) {
|
|
1880
|
+
state.engaged = engaged;
|
|
1881
|
+
state.changedAt = this.now();
|
|
1882
|
+
state.reason = engaged ? reason : null;
|
|
1883
|
+
const active = [];
|
|
1884
|
+
for (const [s, st] of this.steps) if (st.engaged) active.push(s);
|
|
1885
|
+
this.activeCache = Object.freeze(active);
|
|
1886
|
+
return { step, engaged, reason, visible: { nodes: visible.nodes, edges: visible.edges } };
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1516
1889
|
|
|
1517
1890
|
// src/linkPick.ts
|
|
1518
1891
|
var MEDIAN_SAMPLE_CAP = 1024;
|
|
@@ -2492,8 +2865,8 @@ function buildEntries(nodes, fields) {
|
|
|
2492
2865
|
fieldValues = [];
|
|
2493
2866
|
fieldLowers = [];
|
|
2494
2867
|
const attrs = node.attrs;
|
|
2495
|
-
for (const
|
|
2496
|
-
const raw = attrs?.[
|
|
2868
|
+
for (const field2 of fields) {
|
|
2869
|
+
const raw = attrs?.[field2];
|
|
2497
2870
|
if (raw === void 0 || raw === null) continue;
|
|
2498
2871
|
const value = String(raw);
|
|
2499
2872
|
fieldValues.push(value);
|
|
@@ -2595,11 +2968,408 @@ function createLocalSearchService(getBase) {
|
|
|
2595
2968
|
};
|
|
2596
2969
|
}
|
|
2597
2970
|
|
|
2971
|
+
// src/columnar.ts
|
|
2972
|
+
function isColumnarSnapshot(input) {
|
|
2973
|
+
return input.kind === "columnar";
|
|
2974
|
+
}
|
|
2975
|
+
var isTypedLength = (arr, n) => arr !== void 0 && arr.length === n;
|
|
2976
|
+
function checkStringColumn(col, rows, where, isIds, out) {
|
|
2977
|
+
if (col === null || typeof col !== "object") {
|
|
2978
|
+
out.push({ where, problem: "not-a-string-column", detail: "column missing or not an object" });
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
if (col.kind !== "string" || !Array.isArray(col.dictionary) || !(col.codes instanceof Uint32Array)) {
|
|
2982
|
+
out.push({ where, problem: "not-a-string-column", detail: 'expected {kind:"string", dictionary, codes}' });
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
if (col.codes.length !== rows) {
|
|
2986
|
+
const detached = col.codes.length === 0 && col.codes.buffer.byteLength === 0;
|
|
2987
|
+
out.push({
|
|
2988
|
+
where,
|
|
2989
|
+
problem: "length-mismatch",
|
|
2990
|
+
detail: detached ? `codes buffer is DETACHED (a bufferOwnership:'transfer' snapshot is single-use)` : `codes.length ${col.codes.length} !== length ${rows}`
|
|
2991
|
+
});
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (col.nulls !== void 0) {
|
|
2995
|
+
if (col.nulls.length !== rows) {
|
|
2996
|
+
out.push({
|
|
2997
|
+
where,
|
|
2998
|
+
problem: "nulls-length-mismatch",
|
|
2999
|
+
detail: `nulls.length ${col.nulls.length} !== length ${rows}`
|
|
3000
|
+
});
|
|
3001
|
+
} else if (isIds) {
|
|
3002
|
+
for (let i = 0; i < rows; i++) {
|
|
3003
|
+
if (col.nulls[i] !== 0) {
|
|
3004
|
+
out.push({ where, problem: "null-id", detail: `row ${i}: ids may not be null` });
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
const dictSize = col.dictionary.length;
|
|
3011
|
+
for (let i = 0; i < rows; i++) {
|
|
3012
|
+
if (col.codes[i] >= dictSize) {
|
|
3013
|
+
out.push({
|
|
3014
|
+
where,
|
|
3015
|
+
problem: "code-out-of-range",
|
|
3016
|
+
detail: `row ${i}: code ${col.codes[i]} >= dictionary size ${dictSize}`
|
|
3017
|
+
});
|
|
3018
|
+
break;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
function checkColumn(col, rows, where, out) {
|
|
3023
|
+
if (col === null || typeof col !== "object") {
|
|
3024
|
+
out.push({ where, problem: "bad-column-kind", detail: "column missing or not an object" });
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
switch (col.kind) {
|
|
3028
|
+
case "string":
|
|
3029
|
+
checkStringColumn(col, rows, where, false, out);
|
|
3030
|
+
return;
|
|
3031
|
+
case "f64":
|
|
3032
|
+
case "i32":
|
|
3033
|
+
case "u32":
|
|
3034
|
+
case "bool": {
|
|
3035
|
+
if (!isTypedLength(col.data, rows)) {
|
|
3036
|
+
out.push({
|
|
3037
|
+
where,
|
|
3038
|
+
problem: "length-mismatch",
|
|
3039
|
+
detail: `data.length ${col.data?.length ?? "missing"} !== length ${rows}`
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
if (col.nulls !== void 0 && col.nulls.length !== rows) {
|
|
3043
|
+
out.push({
|
|
3044
|
+
where,
|
|
3045
|
+
problem: "nulls-length-mismatch",
|
|
3046
|
+
detail: `nulls.length ${col.nulls.length} !== length ${rows}`
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
default:
|
|
3052
|
+
out.push({
|
|
3053
|
+
where,
|
|
3054
|
+
problem: "bad-column-kind",
|
|
3055
|
+
detail: `unknown column kind '${String(col.kind)}'`
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
function validateColumnarStructure(snapshot) {
|
|
3060
|
+
const issues = [];
|
|
3061
|
+
const nodeRows = snapshot.nodes?.length;
|
|
3062
|
+
const edgeRows = snapshot.edges?.length;
|
|
3063
|
+
if (!Number.isInteger(nodeRows) || nodeRows < 0) {
|
|
3064
|
+
issues.push({ where: "nodes.length", problem: "bad-length", detail: String(nodeRows) });
|
|
3065
|
+
return issues;
|
|
3066
|
+
}
|
|
3067
|
+
if (!Number.isInteger(edgeRows) || edgeRows < 0) {
|
|
3068
|
+
issues.push({ where: "edges.length", problem: "bad-length", detail: String(edgeRows) });
|
|
3069
|
+
return issues;
|
|
3070
|
+
}
|
|
3071
|
+
if (snapshot.nodes === null || typeof snapshot.nodes !== "object") {
|
|
3072
|
+
issues.push({ where: "nodes", problem: "bad-length", detail: "nodes lane missing" });
|
|
3073
|
+
return issues;
|
|
3074
|
+
}
|
|
3075
|
+
if (snapshot.edges === null || typeof snapshot.edges !== "object") {
|
|
3076
|
+
issues.push({ where: "edges", problem: "bad-length", detail: "edges lane missing" });
|
|
3077
|
+
return issues;
|
|
3078
|
+
}
|
|
3079
|
+
checkStringColumn(snapshot.nodes.ids, nodeRows, "nodes.ids", true, issues);
|
|
3080
|
+
for (const [name, col] of Object.entries(snapshot.nodes.columns ?? {})) {
|
|
3081
|
+
checkColumn(col, nodeRows, `nodes.${name}`, issues);
|
|
3082
|
+
}
|
|
3083
|
+
checkStringColumn(snapshot.edges.ids, edgeRows, "edges.ids", true, issues);
|
|
3084
|
+
for (const [name, col] of Object.entries(snapshot.edges.columns ?? {})) {
|
|
3085
|
+
checkColumn(col, edgeRows, `edges.${name}`, issues);
|
|
3086
|
+
}
|
|
3087
|
+
const { source, target } = snapshot.edges;
|
|
3088
|
+
if (!(source instanceof Uint32Array) || source.length !== edgeRows) {
|
|
3089
|
+
issues.push({
|
|
3090
|
+
where: "edges.source",
|
|
3091
|
+
problem: "endpoint-length-mismatch",
|
|
3092
|
+
detail: `source.length ${source?.length ?? "missing"} !== length ${edgeRows}`
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
if (!(target instanceof Uint32Array) || target.length !== edgeRows) {
|
|
3096
|
+
issues.push({
|
|
3097
|
+
where: "edges.target",
|
|
3098
|
+
problem: "endpoint-length-mismatch",
|
|
3099
|
+
detail: `target.length ${target?.length ?? "missing"} !== length ${edgeRows}`
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
if (issues.length === 0) {
|
|
3103
|
+
for (let i = 0; i < edgeRows; i++) {
|
|
3104
|
+
if (source[i] >= nodeRows || target[i] >= nodeRows) {
|
|
3105
|
+
issues.push({
|
|
3106
|
+
where: "edges.endpoints",
|
|
3107
|
+
problem: "endpoint-out-of-range",
|
|
3108
|
+
detail: `row ${i}: (${source[i]}, ${target[i]}) with ${nodeRows} nodes`
|
|
3109
|
+
});
|
|
3110
|
+
break;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return issues;
|
|
3115
|
+
}
|
|
3116
|
+
function columnValueAt(col, i) {
|
|
3117
|
+
if (col.nulls !== void 0 && col.nulls[i] !== 0) return null;
|
|
3118
|
+
switch (col.kind) {
|
|
3119
|
+
case "string":
|
|
3120
|
+
return col.dictionary[col.codes[i]];
|
|
3121
|
+
case "bool":
|
|
3122
|
+
return col.data[i] !== 0;
|
|
3123
|
+
default:
|
|
3124
|
+
return col.data[i];
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function materializeColumnarSnapshot(snapshot) {
|
|
3128
|
+
const nodeCols = Object.entries(snapshot.nodes.columns ?? {});
|
|
3129
|
+
const nodeIds = snapshot.nodes.ids;
|
|
3130
|
+
const nodes = new Array(snapshot.nodes.length);
|
|
3131
|
+
for (let i = 0; i < snapshot.nodes.length; i++) {
|
|
3132
|
+
const attrs = {};
|
|
3133
|
+
for (const [name, col] of nodeCols) attrs[name] = columnValueAt(col, i);
|
|
3134
|
+
nodes[i] = { id: nodeIds.dictionary[nodeIds.codes[i]], attrs };
|
|
3135
|
+
}
|
|
3136
|
+
const edgeCols = Object.entries(snapshot.edges.columns ?? {});
|
|
3137
|
+
const edgeIds = snapshot.edges.ids;
|
|
3138
|
+
const { source, target } = snapshot.edges;
|
|
3139
|
+
const edges = new Array(snapshot.edges.length);
|
|
3140
|
+
for (let i = 0; i < snapshot.edges.length; i++) {
|
|
3141
|
+
const attrs = {};
|
|
3142
|
+
for (const [name, col] of edgeCols) attrs[name] = columnValueAt(col, i);
|
|
3143
|
+
edges[i] = {
|
|
3144
|
+
id: edgeIds.dictionary[edgeIds.codes[i]],
|
|
3145
|
+
source: nodes[source[i]].id,
|
|
3146
|
+
target: nodes[target[i]].id,
|
|
3147
|
+
attrs
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
return {
|
|
3151
|
+
datasetKey: snapshot.datasetKey,
|
|
3152
|
+
sourceRevision: snapshot.sourceRevision,
|
|
3153
|
+
nodes,
|
|
3154
|
+
edges
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
function buildAcceptedFromColumnar(snapshot, acceptance) {
|
|
3158
|
+
const nodeCols = Object.entries(snapshot.nodes.columns ?? {});
|
|
3159
|
+
const nodeIds = snapshot.nodes.ids;
|
|
3160
|
+
const nodes = new Array(acceptance.acceptedNodeCount);
|
|
3161
|
+
const nodeIndex = /* @__PURE__ */ new Map();
|
|
3162
|
+
let outN = 0;
|
|
3163
|
+
for (let i = 0; i < snapshot.nodes.length; i++) {
|
|
3164
|
+
if (acceptance.keepNodes[i] !== 1) continue;
|
|
3165
|
+
const attrs = {};
|
|
3166
|
+
for (const [name, col] of nodeCols) attrs[name] = columnValueAt(col, i);
|
|
3167
|
+
const id = nodeIds.dictionary[nodeIds.codes[i]];
|
|
3168
|
+
nodeIndex.set(id, outN);
|
|
3169
|
+
nodes[outN] = { id, attrs };
|
|
3170
|
+
outN += 1;
|
|
3171
|
+
}
|
|
3172
|
+
const edgeCols = Object.entries(snapshot.edges.columns ?? {});
|
|
3173
|
+
const edgeIds = snapshot.edges.ids;
|
|
3174
|
+
const { source, target } = snapshot.edges;
|
|
3175
|
+
const edges = new Array(acceptance.acceptedEdgeCount);
|
|
3176
|
+
let outE = 0;
|
|
3177
|
+
for (let e = 0; e < snapshot.edges.length; e++) {
|
|
3178
|
+
if (acceptance.keepEdges[e] !== 1) continue;
|
|
3179
|
+
const attrs = {};
|
|
3180
|
+
for (const [name, col] of edgeCols) attrs[name] = columnValueAt(col, e);
|
|
3181
|
+
edges[outE] = {
|
|
3182
|
+
id: edgeIds.dictionary[edgeIds.codes[e]],
|
|
3183
|
+
// Endpoint STRINGS resolve through the original row (a dropped
|
|
3184
|
+
// duplicate row shares its survivor's id string by construction).
|
|
3185
|
+
source: nodeIds.dictionary[nodeIds.codes[source[e]]],
|
|
3186
|
+
target: nodeIds.dictionary[nodeIds.codes[target[e]]],
|
|
3187
|
+
attrs
|
|
3188
|
+
};
|
|
3189
|
+
outE += 1;
|
|
3190
|
+
}
|
|
3191
|
+
return {
|
|
3192
|
+
datasetKey: snapshot.datasetKey,
|
|
3193
|
+
sourceRevision: snapshot.sourceRevision,
|
|
3194
|
+
nodes,
|
|
3195
|
+
edges,
|
|
3196
|
+
nodeIndex,
|
|
3197
|
+
diagnostics: acceptance.diagnostics
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
function columnarArrayBuffers(snapshot) {
|
|
3201
|
+
const buffers = /* @__PURE__ */ new Set();
|
|
3202
|
+
const add = (view) => {
|
|
3203
|
+
if (view !== void 0 && view.buffer instanceof ArrayBuffer) buffers.add(view.buffer);
|
|
3204
|
+
};
|
|
3205
|
+
const addColumn = (col) => {
|
|
3206
|
+
if (col.kind === "string") add(col.codes);
|
|
3207
|
+
else add(col.data);
|
|
3208
|
+
add(col.nulls);
|
|
3209
|
+
};
|
|
3210
|
+
addColumn(snapshot.nodes.ids);
|
|
3211
|
+
for (const col of Object.values(snapshot.nodes.columns ?? {})) addColumn(col);
|
|
3212
|
+
addColumn(snapshot.edges.ids);
|
|
3213
|
+
for (const col of Object.values(snapshot.edges.columns ?? {})) addColumn(col);
|
|
3214
|
+
add(snapshot.edges.source);
|
|
3215
|
+
add(snapshot.edges.target);
|
|
3216
|
+
return [...buffers];
|
|
3217
|
+
}
|
|
3218
|
+
function detachColumnarBuffers(snapshot) {
|
|
3219
|
+
let detached = 0;
|
|
3220
|
+
for (const buffer of columnarArrayBuffers(snapshot)) {
|
|
3221
|
+
if (buffer.byteLength === 0) continue;
|
|
3222
|
+
const transferable = buffer;
|
|
3223
|
+
if (typeof transferable.transfer === "function") {
|
|
3224
|
+
transferable.transfer();
|
|
3225
|
+
} else {
|
|
3226
|
+
structuredClone(buffer, { transfer: [buffer] });
|
|
3227
|
+
}
|
|
3228
|
+
detached += 1;
|
|
3229
|
+
}
|
|
3230
|
+
return detached;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/worker/lane.ts
|
|
3234
|
+
function defaultWorkerUrl() {
|
|
3235
|
+
return new URL("./worker/entry.js", import.meta.url);
|
|
3236
|
+
}
|
|
3237
|
+
function transportFromWorker(worker) {
|
|
3238
|
+
return {
|
|
3239
|
+
post: (envelope, transfers) => worker.postMessage(envelope, [...transfers]),
|
|
3240
|
+
onReply: (cb) => {
|
|
3241
|
+
worker.onmessage = (ev) => cb(ev.data);
|
|
3242
|
+
},
|
|
3243
|
+
onError: (cb) => {
|
|
3244
|
+
worker.onerror = (ev) => cb(ev.message !== "" ? ev.message : "worker error");
|
|
3245
|
+
worker.onmessageerror = () => cb("worker messageerror (unclonable reply)");
|
|
3246
|
+
},
|
|
3247
|
+
terminate: () => worker.terminate()
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
var WorkerLane = class {
|
|
3251
|
+
options;
|
|
3252
|
+
sequencer = new EnvelopeSequencer();
|
|
3253
|
+
ledger = new RequestLedger();
|
|
3254
|
+
pending = /* @__PURE__ */ new Map();
|
|
3255
|
+
transport = null;
|
|
3256
|
+
/** null = not yet booted; false = boot failed (permanent for this lane). */
|
|
3257
|
+
availableState = null;
|
|
3258
|
+
constructor(options = {}) {
|
|
3259
|
+
this.options = options;
|
|
3260
|
+
}
|
|
3261
|
+
available() {
|
|
3262
|
+
return this.availableState;
|
|
3263
|
+
}
|
|
3264
|
+
/** Synchronous boot probe for callers that must pick a lane NOW (the
|
|
3265
|
+
* instance's sync applyHostUpdate cannot await a rejected request). */
|
|
3266
|
+
ensureBooted() {
|
|
3267
|
+
return this.boot();
|
|
3268
|
+
}
|
|
3269
|
+
/** Boot lazily on first use. A throwing factory (no Worker global, CSP,
|
|
3270
|
+
* missing asset) marks the lane unavailable FOREVER — one diagnostic,
|
|
3271
|
+
* then the caller's main path owns every subsequent request. */
|
|
3272
|
+
boot() {
|
|
3273
|
+
if (this.availableState !== null) return this.availableState;
|
|
3274
|
+
try {
|
|
3275
|
+
let transport = this.options.transport ?? null;
|
|
3276
|
+
if (transport === null) {
|
|
3277
|
+
const factory = this.options.factory;
|
|
3278
|
+
const worker = factory !== void 0 && "create" in factory ? factory.create() : new Worker(
|
|
3279
|
+
factory !== void 0 && "url" in factory ? factory.url : defaultWorkerUrl(),
|
|
3280
|
+
{ type: "module" }
|
|
3281
|
+
);
|
|
3282
|
+
transport = transportFromWorker(worker);
|
|
3283
|
+
}
|
|
3284
|
+
transport.onReply((reply) => this.settle(reply));
|
|
3285
|
+
transport.onError?.((reason) => this.fail(reason));
|
|
3286
|
+
this.transport = transport;
|
|
3287
|
+
this.availableState = true;
|
|
3288
|
+
} catch (err) {
|
|
3289
|
+
this.availableState = false;
|
|
3290
|
+
this.options.onUnavailable?.(err instanceof Error ? err.message : String(err));
|
|
3291
|
+
}
|
|
3292
|
+
return this.availableState;
|
|
3293
|
+
}
|
|
3294
|
+
/** Async worker death (P1: a constructed-but-dead worker must strand
|
|
3295
|
+
* NOTHING): every pending request rejects as 'worker-failed' so callers
|
|
3296
|
+
* run their main-lane fallback, the lane goes permanently unavailable,
|
|
3297
|
+
* and the unavailability callback fires (the instance one-shots it). */
|
|
3298
|
+
fail(reason) {
|
|
3299
|
+
if (this.availableState === false) return;
|
|
3300
|
+
this.availableState = false;
|
|
3301
|
+
this.options.onUnavailable?.(reason);
|
|
3302
|
+
for (const [id, entry] of this.pending) {
|
|
3303
|
+
this.pending.delete(id);
|
|
3304
|
+
entry.reject(new Error("worker-failed"));
|
|
3305
|
+
}
|
|
3306
|
+
this.ledger.abortAll();
|
|
3307
|
+
this.transport?.terminate();
|
|
3308
|
+
this.transport = null;
|
|
3309
|
+
}
|
|
3310
|
+
settle(reply) {
|
|
3311
|
+
const original = this.ledger.settle(reply);
|
|
3312
|
+
if (reply.inReplyTo === void 0) return;
|
|
3313
|
+
const entry = this.pending.get(reply.inReplyTo);
|
|
3314
|
+
this.pending.delete(reply.inReplyTo);
|
|
3315
|
+
if (entry === void 0) return;
|
|
3316
|
+
if (original === null) {
|
|
3317
|
+
entry.reject(new Error("superseded"));
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3320
|
+
entry.resolve(reply);
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Send one request. Resolves with the reply envelope (op 'result' or
|
|
3324
|
+
* 'error' — protocol errors are DATA to the caller, not exceptions);
|
|
3325
|
+
* rejects only on supersession/abort/unavailability.
|
|
3326
|
+
*/
|
|
3327
|
+
request(epoch, entity, op, payload, transfers, klass, lane) {
|
|
3328
|
+
if (!this.boot()) {
|
|
3329
|
+
return Promise.reject(new Error("worker-unavailable"));
|
|
3330
|
+
}
|
|
3331
|
+
const envelope = this.sequencer.make(epoch, entity, op, payload);
|
|
3332
|
+
const signal = this.ledger.track(envelope, klass, lane);
|
|
3333
|
+
return new Promise((resolve, reject) => {
|
|
3334
|
+
if (signal.aborted) {
|
|
3335
|
+
reject(new Error("superseded"));
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
signal.addEventListener(
|
|
3339
|
+
"abort",
|
|
3340
|
+
() => {
|
|
3341
|
+
if (this.pending.delete(envelope.msgId)) reject(new Error("superseded"));
|
|
3342
|
+
},
|
|
3343
|
+
{ once: true }
|
|
3344
|
+
);
|
|
3345
|
+
this.pending.set(envelope.msgId, { resolve, reject });
|
|
3346
|
+
this.transport.post(envelope, transfers);
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
/** Abort everything in flight (epoch advance / detach / dataset swap).
|
|
3350
|
+
* The ledger's controllers fire each pending promise's abort listener;
|
|
3351
|
+
* the sweep below catches anything tracked before a listener attached —
|
|
3352
|
+
* rejects stay idempotent through the delete guard. */
|
|
3353
|
+
abortAll() {
|
|
3354
|
+
this.ledger.abortAll();
|
|
3355
|
+
for (const [id, entry] of this.pending) {
|
|
3356
|
+
this.pending.delete(id);
|
|
3357
|
+
entry.reject(new Error("aborted"));
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
terminate() {
|
|
3361
|
+
this.abortAll();
|
|
3362
|
+
this.transport?.terminate();
|
|
3363
|
+
this.transport = null;
|
|
3364
|
+
this.availableState = null;
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
|
|
2598
3368
|
// src/filter.ts
|
|
2599
|
-
function resolveFilterField(item,
|
|
2600
|
-
if (
|
|
3369
|
+
function resolveFilterField(item, field2) {
|
|
3370
|
+
if (field2 === "id") return item.id;
|
|
2601
3371
|
const attrs = item.attrs;
|
|
2602
|
-
return attrs?.[
|
|
3372
|
+
return attrs?.[field2];
|
|
2603
3373
|
}
|
|
2604
3374
|
function filterValuesEqual(a, b) {
|
|
2605
3375
|
if (typeof a === "number" && typeof b === "number") {
|
|
@@ -2769,7 +3539,7 @@ function compileSelector(selector) {
|
|
|
2769
3539
|
};
|
|
2770
3540
|
}
|
|
2771
3541
|
let current = null;
|
|
2772
|
-
const resolve = (
|
|
3542
|
+
const resolve = (field2) => current === null ? void 0 : resolveFilterField(current, field2);
|
|
2773
3543
|
return {
|
|
2774
3544
|
errors,
|
|
2775
3545
|
test(item) {
|
|
@@ -3047,6 +3817,16 @@ var MetricStore = class {
|
|
|
3047
3817
|
/** Admitted async columns by metric name; NaN encodes null. */
|
|
3048
3818
|
columns = /* @__PURE__ */ new Map();
|
|
3049
3819
|
degreePasses = 0;
|
|
3820
|
+
/** §17 telemetry: estimated bytes of metric storage held (S13-T07). */
|
|
3821
|
+
estimatedBytes() {
|
|
3822
|
+
let bytes = 0;
|
|
3823
|
+
for (const col of this.columns.values()) bytes += col.byteLength;
|
|
3824
|
+
const dc = this.degreeCache;
|
|
3825
|
+
if (dc !== null) {
|
|
3826
|
+
bytes += dc.degree.byteLength + dc.inDegree.byteLength + dc.outDegree.byteLength;
|
|
3827
|
+
}
|
|
3828
|
+
return bytes;
|
|
3829
|
+
}
|
|
3050
3830
|
/** Number of combined degree-family compute passes (test observability). */
|
|
3051
3831
|
get degreeComputePasses() {
|
|
3052
3832
|
return this.degreePasses;
|
|
@@ -3278,6 +4058,7 @@ function resolveEnginePolicy(capabilities, requested) {
|
|
|
3278
4058
|
images: imagesNative ? "native" : "placeholder",
|
|
3279
4059
|
linkPicking: capabilities.linkPicking ? "native" : "cpu-fallback",
|
|
3280
4060
|
clusterForce: clusterForceNative ? "native" : "inert",
|
|
4061
|
+
quiescence: capabilities.idleFrames === "stops" ? "stops" : "free-running",
|
|
3281
4062
|
// Defensive snapshot: the policy must not alias the caller's array.
|
|
3282
4063
|
rangedChannels: new Set(capabilities.rangeUpdates),
|
|
3283
4064
|
degradations: Object.freeze(degradations)
|
|
@@ -3302,12 +4083,27 @@ function assertCapabilityMethodParity(engine) {
|
|
|
3302
4083
|
}
|
|
3303
4084
|
function normalizeCommitForCapabilities(commit, capabilities) {
|
|
3304
4085
|
const config = commit.config;
|
|
4086
|
+
const declaredRanged = new Set(capabilities.rangeUpdates);
|
|
4087
|
+
const undeclaredPatches = commit.bufferPatches !== void 0 ? Object.keys(commit.bufferPatches).filter(
|
|
4088
|
+
(ch) => !declaredRanged.has(ch)
|
|
4089
|
+
) : [];
|
|
3305
4090
|
const dropResources = capabilities.pointImages !== true && commit.resources !== void 0;
|
|
3306
4091
|
const dropLinkArrows = capabilities.edgeArrows !== true && config !== void 0 && config.linkArrows !== void 0;
|
|
3307
4092
|
const dropCluster = capabilities.clusterForce !== true && config !== void 0 && config.cluster !== void 0;
|
|
3308
|
-
if (!dropResources && !dropLinkArrows && !dropCluster
|
|
4093
|
+
if (!dropResources && !dropLinkArrows && !dropCluster && undeclaredPatches.length === 0) {
|
|
4094
|
+
return { commit, dropped: [] };
|
|
4095
|
+
}
|
|
3309
4096
|
const dropped = [];
|
|
3310
4097
|
const next = { ...commit };
|
|
4098
|
+
if (undeclaredPatches.length > 0) {
|
|
4099
|
+
const kept = { ...next.bufferPatches };
|
|
4100
|
+
for (const ch of undeclaredPatches) {
|
|
4101
|
+
delete kept[ch];
|
|
4102
|
+
dropped.push(`bufferPatches.${ch}`);
|
|
4103
|
+
}
|
|
4104
|
+
if (Object.keys(kept).length > 0) next.bufferPatches = kept;
|
|
4105
|
+
else delete next.bufferPatches;
|
|
4106
|
+
}
|
|
3311
4107
|
if (dropResources) {
|
|
3312
4108
|
delete next.resources;
|
|
3313
4109
|
dropped.push("resources");
|
|
@@ -3660,7 +4456,7 @@ var Lane = class {
|
|
|
3660
4456
|
}
|
|
3661
4457
|
};
|
|
3662
4458
|
function newMembership(capacity) {
|
|
3663
|
-
return { flags: new Uint8Array(capacity), list: [] };
|
|
4459
|
+
return { flags: new Uint8Array(capacity), list: [], holes: 0 };
|
|
3664
4460
|
}
|
|
3665
4461
|
function growMembership(mem, capacity) {
|
|
3666
4462
|
if (capacity <= mem.flags.length) return;
|
|
@@ -3686,6 +4482,12 @@ var SoftMask = class {
|
|
|
3686
4482
|
overflowedFlag = false;
|
|
3687
4483
|
/** Total memberships currently held across all sources and lanes. */
|
|
3688
4484
|
totalHeld = 0;
|
|
4485
|
+
/** §17 O(Δ) op counters (F10-02 gate instrumentation). */
|
|
4486
|
+
statsBox = {
|
|
4487
|
+
slotsVisited: 0,
|
|
4488
|
+
zeroCrossings: 0,
|
|
4489
|
+
cascadeEdgesVisited: 0
|
|
4490
|
+
};
|
|
3689
4491
|
constructor(nodeCapacity, edgeCapacity) {
|
|
3690
4492
|
checkCapacity(nodeCapacity, "nodeCapacity");
|
|
3691
4493
|
checkCapacity(edgeCapacity, "edgeCapacity");
|
|
@@ -3780,6 +4582,14 @@ var SoftMask = class {
|
|
|
3780
4582
|
this.applyMembership(this.edgeDimLane, state.edgeDim, dimIdx);
|
|
3781
4583
|
}
|
|
3782
4584
|
},
|
|
4585
|
+
updateNodeFailures: (addHide, removeHide, crossings) => {
|
|
4586
|
+
ensureAlive();
|
|
4587
|
+
this.applyMembershipDelta(this.nodeHideLane, state.nodeHide, addHide, removeHide, crossings);
|
|
4588
|
+
},
|
|
4589
|
+
updateEdgeFailures: (addHide, removeHide) => {
|
|
4590
|
+
ensureAlive();
|
|
4591
|
+
this.applyMembershipDelta(this.edgeHideLane, state.edgeHide, addHide, removeHide);
|
|
4592
|
+
},
|
|
3783
4593
|
clear: () => {
|
|
3784
4594
|
ensureAlive();
|
|
3785
4595
|
this.clearSource(state);
|
|
@@ -3833,6 +4643,34 @@ var SoftMask = class {
|
|
|
3833
4643
|
this.cascadeSource ??= this.acquire("\xA79.1 node\u2192edge cascade");
|
|
3834
4644
|
this.cascadeSource.setEdgeFailures(failing);
|
|
3835
4645
|
}
|
|
4646
|
+
/**
|
|
4647
|
+
* O(incident-edges) delta form of the §9.1 cascade (F10-02): for each node
|
|
4648
|
+
* whose HIDE visibility crossed zero, recompute only its incident edges'
|
|
4649
|
+
* cascade state from the CURRENT node counters and apply the delta through
|
|
4650
|
+
* the same internal cascade source the full form uses — the two compose
|
|
4651
|
+
* freely (the full form re-baselines). Edges shared by two crossed nodes
|
|
4652
|
+
* are visited twice; the membership delta is idempotent, so the second
|
|
4653
|
+
* visit is an O(1) no-op. `incidence` must describe the SAME `links`
|
|
4654
|
+
* buffer (edge slot i ↔ links[2i]/[2i+1]).
|
|
4655
|
+
*/
|
|
4656
|
+
applyNodeCascadeToEdgesDelta(links, incidence, crossedNodes) {
|
|
4657
|
+
if (crossedNodes.length === 0) return;
|
|
4658
|
+
const hide = this.nodeHideLane.counters;
|
|
4659
|
+
this.cascadeSource ??= this.acquire("\xA79.1 node\u2192edge cascade");
|
|
4660
|
+
const nowFailing = [];
|
|
4661
|
+
const nowClear = [];
|
|
4662
|
+
for (let k = 0; k < crossedNodes.length; k++) {
|
|
4663
|
+
const edges = incidentEdgesOf(incidence, crossedNodes[k]);
|
|
4664
|
+
for (let j = 0; j < edges.length; j++) {
|
|
4665
|
+
const edge = edges[j];
|
|
4666
|
+
this.statsBox.cascadeEdgesVisited += 1;
|
|
4667
|
+
const failing = hide[links[edge * 2]] !== 0 || hide[links[edge * 2 + 1]] !== 0;
|
|
4668
|
+
if (failing) nowFailing.push(edge);
|
|
4669
|
+
else nowClear.push(edge);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
this.cascadeSource.updateEdgeFailures(nowFailing, nowClear);
|
|
4673
|
+
}
|
|
3836
4674
|
/**
|
|
3837
4675
|
* Drains the zero-crossing dirty lists accumulated since the previous
|
|
3838
4676
|
* drain. Only NET flips are emitted (state compared against the previous
|
|
@@ -3848,6 +4686,27 @@ var SoftMask = class {
|
|
|
3848
4686
|
edgeVisibleCount: this.edgeHideLane.zeroCount
|
|
3849
4687
|
};
|
|
3850
4688
|
}
|
|
4689
|
+
/** §17 telemetry: estimated bytes of mask storage held (S13-T07):
|
|
4690
|
+
* four counter lanes (+pending trackers) and per-source flag columns. */
|
|
4691
|
+
estimatedBytes() {
|
|
4692
|
+
let bytes = 0;
|
|
4693
|
+
for (const lane of [this.nodeHideLane, this.nodeDimLane, this.edgeHideLane, this.edgeDimLane]) {
|
|
4694
|
+
bytes += lane.counters.byteLength + lane.pending.byteLength;
|
|
4695
|
+
}
|
|
4696
|
+
for (const src of this.sources) {
|
|
4697
|
+
bytes += src.nodeHide.flags.byteLength + src.nodeDim.flags.byteLength + src.edgeHide.flags.byteLength + src.edgeDim.flags.byteLength;
|
|
4698
|
+
}
|
|
4699
|
+
return bytes;
|
|
4700
|
+
}
|
|
4701
|
+
/** §17 O(Δ) op counters (live object — snapshot before comparing). */
|
|
4702
|
+
get stats() {
|
|
4703
|
+
return this.statsBox;
|
|
4704
|
+
}
|
|
4705
|
+
resetStats() {
|
|
4706
|
+
this.statsBox.slotsVisited = 0;
|
|
4707
|
+
this.statsBox.zeroCrossings = 0;
|
|
4708
|
+
this.statsBox.cascadeEdgesVisited = 0;
|
|
4709
|
+
}
|
|
3851
4710
|
visibleNodeCount() {
|
|
3852
4711
|
return this.nodeHideLane.zeroCount;
|
|
3853
4712
|
}
|
|
@@ -3912,15 +4771,62 @@ var SoftMask = class {
|
|
|
3912
4771
|
const prev = mem.list;
|
|
3913
4772
|
for (let i = 0; i < prev.length; i++) {
|
|
3914
4773
|
const slot = prev[i];
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4774
|
+
const f = flags[slot];
|
|
4775
|
+
if ((f & 2) !== 0) continue;
|
|
4776
|
+
if ((f & 1) !== 0) this.decrement(lane, slot);
|
|
4777
|
+
flags[slot] = 0;
|
|
3919
4778
|
}
|
|
3920
4779
|
for (let i = 0; i < next.length; i++) flags[next[i]] = 1;
|
|
3921
4780
|
mem.list = next;
|
|
4781
|
+
mem.holes = 0;
|
|
4782
|
+
}
|
|
4783
|
+
/**
|
|
4784
|
+
* O(Δ) delta ops on one lane membership (F10-02). Adds and removes are
|
|
4785
|
+
* idempotent per slot (adding a member / removing a non-member is a
|
|
4786
|
+
* no-op); removed slots leave HOLES in `list` (compacted past 50%), so
|
|
4787
|
+
* replace/clear passes must honor the bit0 guard above. `crossings`, when
|
|
4788
|
+
* given, is cleared and then receives this CALL's hide zero-crossings.
|
|
4789
|
+
*/
|
|
4790
|
+
applyMembershipDelta(lane, mem, add, remove, crossings) {
|
|
4791
|
+
if (crossings !== void 0) {
|
|
4792
|
+
crossings.becameFailing.length = 0;
|
|
4793
|
+
crossings.becameClear.length = 0;
|
|
4794
|
+
}
|
|
4795
|
+
const capacity = lane.counters.length;
|
|
4796
|
+
const flags = mem.flags;
|
|
4797
|
+
const check = (slot) => {
|
|
4798
|
+
if (!Number.isInteger(slot) || slot < 0 || slot >= capacity) {
|
|
4799
|
+
throw new RangeError(`SoftMask: ${lane.label} slot ${slot} out of range [0, ${capacity})`);
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
if (add !== null) for (let i = 0; i < add.length; i++) check(add[i]);
|
|
4803
|
+
if (remove !== null) for (let i = 0; i < remove.length; i++) check(remove[i]);
|
|
4804
|
+
if (add !== null) {
|
|
4805
|
+
for (let i = 0; i < add.length; i++) {
|
|
4806
|
+
const slot = add[i];
|
|
4807
|
+
this.statsBox.slotsVisited += 1;
|
|
4808
|
+
if ((flags[slot] & 1) !== 0) continue;
|
|
4809
|
+
flags[slot] = 1;
|
|
4810
|
+
mem.list.push(slot);
|
|
4811
|
+
this.increment(lane, slot, crossings);
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
if (remove !== null) {
|
|
4815
|
+
for (let i = 0; i < remove.length; i++) {
|
|
4816
|
+
const slot = remove[i];
|
|
4817
|
+
this.statsBox.slotsVisited += 1;
|
|
4818
|
+
if ((flags[slot] & 1) === 0) continue;
|
|
4819
|
+
flags[slot] = 0;
|
|
4820
|
+
mem.holes += 1;
|
|
4821
|
+
this.decrement(lane, slot, crossings);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
if (mem.holes > mem.list.length >> 1) {
|
|
4825
|
+
mem.list = mem.list.filter((slot) => (flags[slot] & 1) !== 0);
|
|
4826
|
+
mem.holes = 0;
|
|
4827
|
+
}
|
|
3922
4828
|
}
|
|
3923
|
-
increment(lane, slot) {
|
|
4829
|
+
increment(lane, slot, crossings) {
|
|
3924
4830
|
this.totalHeld += 1;
|
|
3925
4831
|
const before = lane.counters[slot];
|
|
3926
4832
|
if (before === COUNTER_MAX) {
|
|
@@ -3930,16 +4836,20 @@ var SoftMask = class {
|
|
|
3930
4836
|
lane.counters[slot] = before + 1;
|
|
3931
4837
|
if (before === 0) {
|
|
3932
4838
|
lane.zeroCount -= 1;
|
|
4839
|
+
this.statsBox.zeroCrossings += 1;
|
|
4840
|
+
crossings?.becameFailing.push(slot);
|
|
3933
4841
|
this.markDirty(lane, slot, true);
|
|
3934
4842
|
}
|
|
3935
4843
|
}
|
|
3936
|
-
decrement(lane, slot) {
|
|
4844
|
+
decrement(lane, slot, crossings) {
|
|
3937
4845
|
this.totalHeld -= 1;
|
|
3938
4846
|
const before = lane.counters[slot];
|
|
3939
4847
|
if (before === 0) return;
|
|
3940
4848
|
lane.counters[slot] = before - 1;
|
|
3941
4849
|
if (before === 1) {
|
|
3942
4850
|
lane.zeroCount += 1;
|
|
4851
|
+
this.statsBox.zeroCrossings += 1;
|
|
4852
|
+
crossings?.becameClear.push(slot);
|
|
3943
4853
|
this.markDirty(lane, slot, false);
|
|
3944
4854
|
}
|
|
3945
4855
|
}
|
|
@@ -4355,7 +5265,15 @@ function upperBound(values, sorted, target) {
|
|
|
4355
5265
|
}
|
|
4356
5266
|
var TypedColumnCrossfilter = class {
|
|
4357
5267
|
/** Test instrumentation; see CrossfilterStats. Reset with resetStats(). */
|
|
4358
|
-
stats = {
|
|
5268
|
+
stats = {
|
|
5269
|
+
slotsWalked: 0,
|
|
5270
|
+
fullSorts: 0,
|
|
5271
|
+
permutationMerges: 0,
|
|
5272
|
+
binUpdates: 0,
|
|
5273
|
+
filteredRecomputes: 0
|
|
5274
|
+
};
|
|
5275
|
+
/** Count of dims whose filtered layer is live-maintained (F10-02). */
|
|
5276
|
+
liveDims = 0;
|
|
4359
5277
|
dims = [];
|
|
4360
5278
|
byKey = /* @__PURE__ */ new Map();
|
|
4361
5279
|
specs = [];
|
|
@@ -4377,6 +5295,29 @@ var TypedColumnCrossfilter = class {
|
|
|
4377
5295
|
this.stats.slotsWalked = 0;
|
|
4378
5296
|
this.stats.fullSorts = 0;
|
|
4379
5297
|
this.stats.permutationMerges = 0;
|
|
5298
|
+
this.stats.binUpdates = 0;
|
|
5299
|
+
this.stats.filteredRecomputes = 0;
|
|
5300
|
+
}
|
|
5301
|
+
/** §17 telemetry: estimated bytes of typed-column storage held (S13-T07).
|
|
5302
|
+
* Documented components: per-dim value/permutation/bin/code/pass arrays,
|
|
5303
|
+
* the global failure counter, and the external mask. */
|
|
5304
|
+
estimatedBytes() {
|
|
5305
|
+
let bytes = this.failCount.byteLength + (this.externalMask?.byteLength ?? 0);
|
|
5306
|
+
for (const d of this.dims) {
|
|
5307
|
+
bytes += d.pass.byteLength;
|
|
5308
|
+
if (d.kind === "categorical") {
|
|
5309
|
+
bytes += d.codes.byteLength;
|
|
5310
|
+
} else {
|
|
5311
|
+
bytes += d.values.byteLength + d.sorted.byteLength + d.slotBin.byteLength;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
return bytes;
|
|
5315
|
+
}
|
|
5316
|
+
/** F10-02 live-layer bookkeeping — the ONLY writer of `filteredLive`. */
|
|
5317
|
+
setFilteredLive(dim, live) {
|
|
5318
|
+
if (dim.filteredLive === live) return;
|
|
5319
|
+
dim.filteredLive = live;
|
|
5320
|
+
this.liveDims += live ? 1 : -1;
|
|
4380
5321
|
}
|
|
4381
5322
|
/**
|
|
4382
5323
|
* (Re)initialize columns from scratch. Clears all brushes and the external
|
|
@@ -4426,7 +5367,9 @@ var TypedColumnCrossfilter = class {
|
|
|
4426
5367
|
this.applyRangeTransition(dim, normalized.brush, delta);
|
|
4427
5368
|
}
|
|
4428
5369
|
dim.brush = normalized.brush;
|
|
4429
|
-
for (const other of this.dims)
|
|
5370
|
+
for (const other of this.dims) {
|
|
5371
|
+
if (other !== dim && !other.filteredLive) other.filteredDirty = true;
|
|
5372
|
+
}
|
|
4430
5373
|
this.revision++;
|
|
4431
5374
|
this.notify();
|
|
4432
5375
|
return delta;
|
|
@@ -4452,7 +5395,10 @@ var TypedColumnCrossfilter = class {
|
|
|
4452
5395
|
if (unchanged) return;
|
|
4453
5396
|
this.externalMask = Uint8Array.from(passSlots);
|
|
4454
5397
|
}
|
|
4455
|
-
for (const d of this.dims)
|
|
5398
|
+
for (const d of this.dims) {
|
|
5399
|
+
this.setFilteredLive(d, false);
|
|
5400
|
+
d.filteredDirty = true;
|
|
5401
|
+
}
|
|
4456
5402
|
this.notify();
|
|
4457
5403
|
}
|
|
4458
5404
|
/**
|
|
@@ -4466,6 +5412,7 @@ var TypedColumnCrossfilter = class {
|
|
|
4466
5412
|
if (dim.filteredDirty) {
|
|
4467
5413
|
this.recomputeFiltered(dim);
|
|
4468
5414
|
dim.filteredDirty = false;
|
|
5415
|
+
this.setFilteredLive(dim, true);
|
|
4469
5416
|
}
|
|
4470
5417
|
const bins = [];
|
|
4471
5418
|
const categories = [];
|
|
@@ -4530,13 +5477,16 @@ var TypedColumnCrossfilter = class {
|
|
|
4530
5477
|
this.externalMask = em;
|
|
4531
5478
|
}
|
|
4532
5479
|
this.n = newN;
|
|
5480
|
+
for (const dim of this.dims) {
|
|
5481
|
+
this.setFilteredLive(dim, false);
|
|
5482
|
+
dim.filteredDirty = true;
|
|
5483
|
+
}
|
|
4533
5484
|
for (const dim of this.dims) {
|
|
4534
5485
|
const pass = new Uint8Array(newN).fill(1);
|
|
4535
5486
|
pass.set(dim.pass);
|
|
4536
5487
|
dim.pass = pass;
|
|
4537
5488
|
if (dim.kind === "categorical") this.appendCat(dim, newNodes, oldN);
|
|
4538
5489
|
else this.appendRange(dim, newNodes, oldN);
|
|
4539
|
-
dim.filteredDirty = true;
|
|
4540
5490
|
}
|
|
4541
5491
|
for (let s = oldN; s < newN; s++) {
|
|
4542
5492
|
(this.failCount[s] === 0 ? delta.shown : delta.hidden).push(s);
|
|
@@ -4600,6 +5550,7 @@ var TypedColumnCrossfilter = class {
|
|
|
4600
5550
|
return dim;
|
|
4601
5551
|
}
|
|
4602
5552
|
buildDims(nodes, specs) {
|
|
5553
|
+
this.liveDims = 0;
|
|
4603
5554
|
const seen = /* @__PURE__ */ new Set();
|
|
4604
5555
|
const dims = [];
|
|
4605
5556
|
for (const spec of specs) {
|
|
@@ -4655,6 +5606,7 @@ var TypedColumnCrossfilter = class {
|
|
|
4655
5606
|
invalidSlots,
|
|
4656
5607
|
brush: null,
|
|
4657
5608
|
filteredDirty: true,
|
|
5609
|
+
filteredLive: false,
|
|
4658
5610
|
filteredBins: []
|
|
4659
5611
|
};
|
|
4660
5612
|
this.rebinRange(dim);
|
|
@@ -4696,6 +5648,7 @@ var TypedColumnCrossfilter = class {
|
|
|
4696
5648
|
invalidSlots,
|
|
4697
5649
|
brush: null,
|
|
4698
5650
|
filteredDirty: true,
|
|
5651
|
+
filteredLive: false,
|
|
4699
5652
|
filteredCats: []
|
|
4700
5653
|
};
|
|
4701
5654
|
}
|
|
@@ -4744,13 +5697,58 @@ var TypedColumnCrossfilter = class {
|
|
|
4744
5697
|
const next = this.failCount[slot] - 1;
|
|
4745
5698
|
this.failCount[slot] = next;
|
|
4746
5699
|
if (next === 0) delta.shown.push(slot);
|
|
5700
|
+
if (this.liveDims > 0) this.maintainFiltered(dim, slot, next, 1);
|
|
4747
5701
|
} else {
|
|
4748
5702
|
if (dim.pass[slot] === 0) return;
|
|
4749
5703
|
dim.pass[slot] = 0;
|
|
4750
5704
|
const prev = this.failCount[slot];
|
|
4751
5705
|
this.failCount[slot] = prev + 1;
|
|
4752
5706
|
if (prev === 0) delta.hidden.push(slot);
|
|
5707
|
+
if (this.liveDims > 0) this.maintainFiltered(dim, slot, prev, -1);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
/**
|
|
5711
|
+
* F10-02 inline maintenance of LIVE filtered layers, dispatched from the
|
|
5712
|
+
* one place that knows the failCount transition. `boundary` is the
|
|
5713
|
+
* other-failures picture at the interesting side of the flip (after for
|
|
5714
|
+
* shown, before for hidden):
|
|
5715
|
+
* - 0 → the slot crossed the FULLY-VISIBLE boundary: every other live
|
|
5716
|
+
* layer counts it (own layers ignore the own-dim brush, so the brushed
|
|
5717
|
+
* dim's layer is provably unchanged by its own flip);
|
|
5718
|
+
* - 1 → exactly one OTHER dim still fails the slot: only that dim's
|
|
5719
|
+
* what-if-I-cleared-mine layer flips;
|
|
5720
|
+
* - ≥2 → no layer can change.
|
|
5721
|
+
* External-mask-excluded and hygiene-invalid slots contribute nothing
|
|
5722
|
+
* either way and are skipped.
|
|
5723
|
+
*/
|
|
5724
|
+
maintainFiltered(brushed, slot, boundary, sign) {
|
|
5725
|
+
if (boundary > 1) return;
|
|
5726
|
+
const ext = this.externalMask;
|
|
5727
|
+
if (ext !== null && ext[slot] === 0) return;
|
|
5728
|
+
if (boundary === 0) {
|
|
5729
|
+
for (const d of this.dims) {
|
|
5730
|
+
if (d === brushed || !d.filteredLive) continue;
|
|
5731
|
+
this.adjustLayer(d, slot, sign);
|
|
5732
|
+
}
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5735
|
+
for (const d of this.dims) {
|
|
5736
|
+
if (d === brushed || d.pass[slot] !== 0) continue;
|
|
5737
|
+
if (d.filteredLive) this.adjustLayer(d, slot, sign);
|
|
5738
|
+
break;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
adjustLayer(d, slot, sign) {
|
|
5742
|
+
if (d.kind === "categorical") {
|
|
5743
|
+
const c = d.codes[slot];
|
|
5744
|
+
if (c < 0) return;
|
|
5745
|
+
d.filteredCats[c] = (d.filteredCats[c] ?? 0) + sign;
|
|
5746
|
+
} else {
|
|
5747
|
+
const b = d.slotBin[slot];
|
|
5748
|
+
if (b < 0) return;
|
|
5749
|
+
d.filteredBins[b] = (d.filteredBins[b] ?? 0) + sign;
|
|
4753
5750
|
}
|
|
5751
|
+
this.stats.binUpdates += 1;
|
|
4754
5752
|
}
|
|
4755
5753
|
walkSorted(dim, from, to, nowPass, delta) {
|
|
4756
5754
|
for (let i = from; i < to; i++) this.flip(dim, dim.sorted[i], nowPass, delta);
|
|
@@ -4919,6 +5917,7 @@ var TypedColumnCrossfilter = class {
|
|
|
4919
5917
|
}
|
|
4920
5918
|
// --- lazy filtered layer -----------------------------------------------------
|
|
4921
5919
|
recomputeFiltered(dim) {
|
|
5920
|
+
this.stats.filteredRecomputes += 1;
|
|
4922
5921
|
const ext = this.externalMask;
|
|
4923
5922
|
const fc = this.failCount;
|
|
4924
5923
|
if (dim.kind === "categorical") {
|
|
@@ -5250,7 +6249,8 @@ var GRAPH_THEME_DARK = Object.freeze({
|
|
|
5250
6249
|
edgeDefault: "rgba(255,255,255,0.15)",
|
|
5251
6250
|
labelFg: "#e6e9f0",
|
|
5252
6251
|
accent: "#3b82f6",
|
|
5253
|
-
mutedAlpha: 0.15
|
|
6252
|
+
mutedAlpha: 0.15,
|
|
6253
|
+
emphasisRing: "#7aa2f7"
|
|
5254
6254
|
});
|
|
5255
6255
|
var GRAPH_THEME_LIGHT = Object.freeze({
|
|
5256
6256
|
background: "#ffffff",
|
|
@@ -5258,7 +6258,8 @@ var GRAPH_THEME_LIGHT = Object.freeze({
|
|
|
5258
6258
|
edgeDefault: "rgba(15,23,42,0.18)",
|
|
5259
6259
|
labelFg: "#0f172a",
|
|
5260
6260
|
accent: "#2563eb",
|
|
5261
|
-
mutedAlpha: 0.2
|
|
6261
|
+
mutedAlpha: 0.2,
|
|
6262
|
+
emphasisRing: "#2563eb"
|
|
5262
6263
|
});
|
|
5263
6264
|
function resolveTheme(input) {
|
|
5264
6265
|
const base = input !== void 0 && input.base === "light" ? GRAPH_THEME_LIGHT : GRAPH_THEME_DARK;
|
|
@@ -5270,10 +6271,11 @@ function resolveTheme(input) {
|
|
|
5270
6271
|
if (input.labelFg !== void 0) out.labelFg = input.labelFg;
|
|
5271
6272
|
if (input.accent !== void 0) out.accent = input.accent;
|
|
5272
6273
|
if (input.mutedAlpha !== void 0) out.mutedAlpha = input.mutedAlpha;
|
|
6274
|
+
if (input.emphasisRing !== void 0) out.emphasisRing = input.emphasisRing;
|
|
5273
6275
|
return out;
|
|
5274
6276
|
}
|
|
5275
6277
|
function sameTheme(a, b) {
|
|
5276
|
-
return a.background === b.background && a.nodeDefault === b.nodeDefault && a.edgeDefault === b.edgeDefault && a.labelFg === b.labelFg && a.accent === b.accent && a.mutedAlpha === b.mutedAlpha;
|
|
6278
|
+
return a.background === b.background && a.nodeDefault === b.nodeDefault && a.edgeDefault === b.edgeDefault && a.labelFg === b.labelFg && a.accent === b.accent && a.mutedAlpha === b.mutedAlpha && a.emphasisRing === b.emphasisRing;
|
|
5277
6279
|
}
|
|
5278
6280
|
var ZOOM_STEP = 1.5;
|
|
5279
6281
|
var EMPTY_IDS = [];
|
|
@@ -5300,6 +6302,9 @@ var TIMELINE_COALESCE_WINDOW_MS = Number.MAX_SAFE_INTEGER;
|
|
|
5300
6302
|
var DEV = Boolean(import.meta.env?.DEV);
|
|
5301
6303
|
var nowMs = () => Date.now();
|
|
5302
6304
|
var DEFAULT_RGBA = [0.66, 0.66, 0.66, 1];
|
|
6305
|
+
var PERF_SAMPLE_THROTTLE_MS = 1e3;
|
|
6306
|
+
var FRAME_PRESSURE_ENGAGE_MS = 40;
|
|
6307
|
+
var FRAME_PRESSURE_CLEAR_MS = 28;
|
|
5303
6308
|
var LABEL_RERANK_THROTTLE_MS = 100;
|
|
5304
6309
|
var SIM_HOT_REFRESH_MS = 500;
|
|
5305
6310
|
function sameIds(a, b) {
|
|
@@ -5372,6 +6377,7 @@ function createGraphInstance(opts) {
|
|
|
5372
6377
|
let overlayIdSeq = 0;
|
|
5373
6378
|
let edgeIndexById = EMPTY_INDEX2;
|
|
5374
6379
|
let adjacency = null;
|
|
6380
|
+
let sceneIncidence = null;
|
|
5375
6381
|
let lastLinkWidths = null;
|
|
5376
6382
|
let scopeSpec = null;
|
|
5377
6383
|
const scopeExtraIds = /* @__PURE__ */ new Set();
|
|
@@ -5505,6 +6511,8 @@ function createGraphInstance(opts) {
|
|
|
5505
6511
|
let theme = resolveTheme(void 0);
|
|
5506
6512
|
let edgeArrows = false;
|
|
5507
6513
|
let showLinks = true;
|
|
6514
|
+
let emphasisRingOn = true;
|
|
6515
|
+
let emphasizedNodeId = null;
|
|
5508
6516
|
let nodeImage;
|
|
5509
6517
|
let labelsConfig;
|
|
5510
6518
|
const metricStore = new MetricStore();
|
|
@@ -5534,11 +6542,140 @@ function createGraphInstance(opts) {
|
|
|
5534
6542
|
const positionSubs = /* @__PURE__ */ new Set();
|
|
5535
6543
|
let currentPlacements = [];
|
|
5536
6544
|
let labelPositionCache = null;
|
|
6545
|
+
let frameCadence = 0;
|
|
6546
|
+
let quiescenceAsserted = false;
|
|
6547
|
+
let quiescenceTimer = null;
|
|
5537
6548
|
let lastHotRefreshMs = null;
|
|
5538
6549
|
let rerankTimer = null;
|
|
5539
6550
|
let lastOverloadCount = 0;
|
|
5540
6551
|
const projectScratch = [0, 0];
|
|
5541
6552
|
let dataDiags = [];
|
|
6553
|
+
let columnarDiags = [];
|
|
6554
|
+
const executionMode = opts.execution ?? "main";
|
|
6555
|
+
let workerLane = null;
|
|
6556
|
+
let workerDiags = [];
|
|
6557
|
+
let workerUnavailableReported = false;
|
|
6558
|
+
let pendingDeriveToken = 0;
|
|
6559
|
+
function workerEligible() {
|
|
6560
|
+
if (executionMode === "main") return false;
|
|
6561
|
+
if (workerLane === null) {
|
|
6562
|
+
workerLane = new WorkerLane({
|
|
6563
|
+
...opts.workerFactory !== void 0 ? { factory: opts.workerFactory } : {},
|
|
6564
|
+
onUnavailable: (reason) => {
|
|
6565
|
+
if (workerUnavailableReported) return;
|
|
6566
|
+
workerUnavailableReported = true;
|
|
6567
|
+
workerDiags = [
|
|
6568
|
+
{
|
|
6569
|
+
code: "worker-unavailable",
|
|
6570
|
+
severity: executionMode === "worker" ? "error" : "info",
|
|
6571
|
+
count: 1,
|
|
6572
|
+
sampleIds: [],
|
|
6573
|
+
message: `worker lane unavailable (${reason}) \u2014 columnar acceptance runs on the main lane`
|
|
6574
|
+
}
|
|
6575
|
+
];
|
|
6576
|
+
}
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
return workerLane.ensureBooted();
|
|
6580
|
+
}
|
|
6581
|
+
function scheduleWorkerAcceptance(columnar, deferredMetrics) {
|
|
6582
|
+
const lane = workerLane;
|
|
6583
|
+
pendingDeriveToken += 1;
|
|
6584
|
+
const token = pendingDeriveToken;
|
|
6585
|
+
const payload = {
|
|
6586
|
+
nodeIdTable: encodeStringTable(columnar.nodes.ids.dictionary),
|
|
6587
|
+
nodeIdCodes: columnar.nodes.ids.codes.slice(),
|
|
6588
|
+
nodeCount: columnar.nodes.length,
|
|
6589
|
+
edgeIdTable: encodeStringTable(columnar.edges.ids.dictionary),
|
|
6590
|
+
edgeIdCodes: columnar.edges.ids.codes.slice(),
|
|
6591
|
+
edgeSource: columnar.edges.source.slice(),
|
|
6592
|
+
edgeTarget: columnar.edges.target.slice(),
|
|
6593
|
+
edgeCount: columnar.edges.length
|
|
6594
|
+
};
|
|
6595
|
+
const transfers = collectTransfers([
|
|
6596
|
+
payload.nodeIdTable.offsets,
|
|
6597
|
+
payload.nodeIdTable.bytes,
|
|
6598
|
+
payload.nodeIdCodes,
|
|
6599
|
+
payload.edgeIdTable.offsets,
|
|
6600
|
+
payload.edgeIdTable.bytes,
|
|
6601
|
+
payload.edgeIdCodes,
|
|
6602
|
+
payload.edgeSource,
|
|
6603
|
+
payload.edgeTarget
|
|
6604
|
+
]);
|
|
6605
|
+
void lane.request(token, "scene", "derive-columnar", payload, transfers, "guaranteed", "derive").then((reply) => {
|
|
6606
|
+
if (destroyed || token !== pendingDeriveToken) return;
|
|
6607
|
+
if (reply.op !== "result") {
|
|
6608
|
+
workerDiags = [
|
|
6609
|
+
{
|
|
6610
|
+
code: "worker-unavailable",
|
|
6611
|
+
severity: "error",
|
|
6612
|
+
count: 1,
|
|
6613
|
+
sampleIds: [],
|
|
6614
|
+
message: `worker derive failed (${String(reply.payload.message)}) \u2014 snapshot dropped`
|
|
6615
|
+
}
|
|
6616
|
+
];
|
|
6617
|
+
publish({ diagnostics: composeDiagnostics() });
|
|
6618
|
+
return;
|
|
6619
|
+
}
|
|
6620
|
+
const acceptance = reply.payload;
|
|
6621
|
+
acceptanceQueue.admit(() => {
|
|
6622
|
+
if (destroyed || token !== pendingDeriveToken) return;
|
|
6623
|
+
const mutated = acceptance.keepNodes.length !== columnar.nodes.length || acceptance.keepEdges.length !== columnar.edges.length || validateColumnarStructure(columnar).length > 0;
|
|
6624
|
+
if (mutated) {
|
|
6625
|
+
columnarDiags = [
|
|
6626
|
+
{
|
|
6627
|
+
code: "invalid-columnar-snapshot",
|
|
6628
|
+
severity: "error",
|
|
6629
|
+
count: 1,
|
|
6630
|
+
sampleIds: [],
|
|
6631
|
+
message: "columnar snapshot mutated while worker acceptance was pending \u2014 rejected whole (\xA75: source coordinates are immutable; publish a new sourceRevision)"
|
|
6632
|
+
}
|
|
6633
|
+
];
|
|
6634
|
+
publish({ diagnostics: composeDiagnostics() });
|
|
6635
|
+
return;
|
|
6636
|
+
}
|
|
6637
|
+
const preAccepted = buildAcceptedFromColumnar(columnar, acceptance);
|
|
6638
|
+
applyHostUpdateInner(
|
|
6639
|
+
{
|
|
6640
|
+
data: {
|
|
6641
|
+
datasetKey: columnar.datasetKey,
|
|
6642
|
+
sourceRevision: columnar.sourceRevision,
|
|
6643
|
+
nodes: [],
|
|
6644
|
+
edges: []
|
|
6645
|
+
},
|
|
6646
|
+
...deferredMetrics !== void 0 ? { metrics: deferredMetrics } : {}
|
|
6647
|
+
},
|
|
6648
|
+
preAccepted
|
|
6649
|
+
);
|
|
6650
|
+
if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
|
|
6651
|
+
});
|
|
6652
|
+
}).catch((err) => {
|
|
6653
|
+
if (destroyed || token !== pendingDeriveToken) return;
|
|
6654
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6655
|
+
if (message !== "worker-unavailable" && message !== "worker-failed") return;
|
|
6656
|
+
acceptanceQueue.admit(() => {
|
|
6657
|
+
if (destroyed || token !== pendingDeriveToken) return;
|
|
6658
|
+
if (validateColumnarStructure(columnar).length > 0) {
|
|
6659
|
+
columnarDiags = [
|
|
6660
|
+
{
|
|
6661
|
+
code: "invalid-columnar-snapshot",
|
|
6662
|
+
severity: "error",
|
|
6663
|
+
count: 1,
|
|
6664
|
+
sampleIds: [],
|
|
6665
|
+
message: "columnar snapshot mutated while worker acceptance was pending \u2014 rejected whole (\xA75: source coordinates are immutable; publish a new sourceRevision)"
|
|
6666
|
+
}
|
|
6667
|
+
];
|
|
6668
|
+
publish({ diagnostics: composeDiagnostics() });
|
|
6669
|
+
return;
|
|
6670
|
+
}
|
|
6671
|
+
applyHostUpdateInner({
|
|
6672
|
+
data: materializeColumnarSnapshot(columnar),
|
|
6673
|
+
...deferredMetrics !== void 0 ? { metrics: deferredMetrics } : {}
|
|
6674
|
+
});
|
|
6675
|
+
if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
|
|
6676
|
+
});
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
5542
6679
|
let nodeColorDiags = [];
|
|
5543
6680
|
let nodeSizeDiags = [];
|
|
5544
6681
|
let linkColorDiags = [];
|
|
@@ -5602,6 +6739,46 @@ function createGraphInstance(opts) {
|
|
|
5602
6739
|
}
|
|
5603
6740
|
pendingHistoryDepths = null;
|
|
5604
6741
|
store.setState((prev) => ({ ...prev, ...patch }));
|
|
6742
|
+
if (patch.visible !== void 0 || patch.nodeCount !== void 0) {
|
|
6743
|
+
evaluateLadderCounts();
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
function evaluateLadderCounts() {
|
|
6747
|
+
if (destroyed) return;
|
|
6748
|
+
const vis = store.getState().visible;
|
|
6749
|
+
const events = degradeController.evaluateCounts({ nodes: vis.nodes, edges: vis.edges });
|
|
6750
|
+
for (const event of events) applyDegradeEvent(event);
|
|
6751
|
+
}
|
|
6752
|
+
function applyDegradeEvent(event) {
|
|
6753
|
+
if (event.step === "cap-dom-labels") {
|
|
6754
|
+
if (event.engaged && !capLabelsNudged) {
|
|
6755
|
+
capLabelsNudged = true;
|
|
6756
|
+
console.warn(
|
|
6757
|
+
"orbit: cap-dom-labels engaged \u2014 DOM labels are hard-capped at the label budget above limits.domLabelNodes visible nodes (\xA717). A GPU label lane (labelStrategy 'sdf') is the planned scale path (S13-T05, deferred)."
|
|
6758
|
+
);
|
|
6759
|
+
}
|
|
6760
|
+
scheduleViewportRerank();
|
|
6761
|
+
}
|
|
6762
|
+
if (event.step === "batch-histograms" && !event.engaged) {
|
|
6763
|
+
flushCrossfilterNotify();
|
|
6764
|
+
}
|
|
6765
|
+
if (event.step === "defer-images" && !event.engaged && imageRefsDeferred) {
|
|
6766
|
+
pushImageRefs();
|
|
6767
|
+
}
|
|
6768
|
+
if (event.step === "disable-transitions") {
|
|
6769
|
+
const eng = engineIfReady();
|
|
6770
|
+
if (eng !== null) {
|
|
6771
|
+
const revisions = { ...store.getState().revisions };
|
|
6772
|
+
revisions.render += 1;
|
|
6773
|
+
commitToEngine(eng, {
|
|
6774
|
+
revision: revisions.render,
|
|
6775
|
+
config: { transitionDurationMs: event.engaged ? 0 : null }
|
|
6776
|
+
});
|
|
6777
|
+
revisions.appliedRender = eng.appliedRevision();
|
|
6778
|
+
store.setState((prev) => ({ ...prev, revisions }));
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
emit("degrade", event);
|
|
5605
6782
|
}
|
|
5606
6783
|
function emitInstanceError(detail, phase, cause) {
|
|
5607
6784
|
engineDiags = [
|
|
@@ -6019,6 +7196,9 @@ function createGraphInstance(opts) {
|
|
|
6019
7196
|
return k === void 0 ? void 0 : accepted.edges[k];
|
|
6020
7197
|
}
|
|
6021
7198
|
function applyEdgeHover(edge, transitionsOnly) {
|
|
7199
|
+
if (edge !== null && degradeController.isEngaged("defer-link-picking") && store.getState().simulationRunning) {
|
|
7200
|
+
return;
|
|
7201
|
+
}
|
|
6022
7202
|
const edgeId = edge === null ? null : edge.id;
|
|
6023
7203
|
const hover = store.getState().hover;
|
|
6024
7204
|
const changed = hover.edgeId !== edgeId;
|
|
@@ -6061,6 +7241,8 @@ function createGraphInstance(opts) {
|
|
|
6061
7241
|
function composeDiagnostics() {
|
|
6062
7242
|
return [
|
|
6063
7243
|
...dataDiags,
|
|
7244
|
+
...columnarDiags,
|
|
7245
|
+
...workerDiags,
|
|
6064
7246
|
...ingestMergeDiags,
|
|
6065
7247
|
...ingestCommitDiags.map((e) => e.diag),
|
|
6066
7248
|
...nodeColorDiags,
|
|
@@ -6112,7 +7294,60 @@ function createGraphInstance(opts) {
|
|
|
6112
7294
|
baseLinkColors = null;
|
|
6113
7295
|
basePointColorsSynthesized = false;
|
|
6114
7296
|
baseLinkColorsSynthesized = false;
|
|
6115
|
-
|
|
7297
|
+
nodeAlphaComposer.reset();
|
|
7298
|
+
edgeAlphaComposer.reset();
|
|
7299
|
+
sceneIncidence = null;
|
|
7300
|
+
}
|
|
7301
|
+
const brushCrossings = { becameFailing: [], becameClear: [] };
|
|
7302
|
+
const brushSceneAdd = [];
|
|
7303
|
+
const brushSceneRemove = [];
|
|
7304
|
+
const nodeAlphaComposer = new IncrementalAlphaComposer();
|
|
7305
|
+
const edgeAlphaComposer = new IncrementalAlphaComposer();
|
|
7306
|
+
const PATCH_FULL_UPLOAD_RATIO = 0.5;
|
|
7307
|
+
function buildAlphaPatches(buf, slotLists, stride) {
|
|
7308
|
+
const slots = [];
|
|
7309
|
+
for (const list of slotLists) for (const slot of list) slots.push(slot);
|
|
7310
|
+
slots.sort((a, b) => a - b);
|
|
7311
|
+
const patches = [];
|
|
7312
|
+
let runStart = -1;
|
|
7313
|
+
let runEnd = -1;
|
|
7314
|
+
for (const slot of slots) {
|
|
7315
|
+
if (slot < runEnd) continue;
|
|
7316
|
+
if (slot === runEnd) {
|
|
7317
|
+
runEnd = slot + 1;
|
|
7318
|
+
continue;
|
|
7319
|
+
}
|
|
7320
|
+
if (runStart >= 0) {
|
|
7321
|
+
patches.push({
|
|
7322
|
+
start: runStart * stride,
|
|
7323
|
+
data: buf.subarray(runStart * stride, runEnd * stride)
|
|
7324
|
+
});
|
|
7325
|
+
}
|
|
7326
|
+
runStart = slot;
|
|
7327
|
+
runEnd = slot + 1;
|
|
7328
|
+
}
|
|
7329
|
+
if (runStart >= 0) {
|
|
7330
|
+
patches.push({
|
|
7331
|
+
start: runStart * stride,
|
|
7332
|
+
data: buf.subarray(runStart * stride, runEnd * stride)
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
return patches;
|
|
7336
|
+
}
|
|
7337
|
+
const pressureSampler = new PressureSampler();
|
|
7338
|
+
const resolvedLimits = resolveScaleLimits(opts.limits);
|
|
7339
|
+
const degradeController = new DegradeController(resolvedLimits.limits, () => nowMs());
|
|
7340
|
+
let capLabelsNudged = false;
|
|
7341
|
+
let imageRefsDeferred = false;
|
|
7342
|
+
let lastCommitMs;
|
|
7343
|
+
let lastPerfSampleAt = -Infinity;
|
|
7344
|
+
const perfCounters = {
|
|
7345
|
+
brushSlotsTranslated: 0,
|
|
7346
|
+
fullBrushRefreshes: 0,
|
|
7347
|
+
fullCascades: 0,
|
|
7348
|
+
fullNodeRecomposes: 0,
|
|
7349
|
+
fullEdgeRecomposes: 0
|
|
7350
|
+
};
|
|
6116
7351
|
function evaluateFilterMembership() {
|
|
6117
7352
|
if (softMask === null || maskFilterSource === null) return;
|
|
6118
7353
|
const model = sceneModel();
|
|
@@ -6190,6 +7425,7 @@ function createGraphInstance(opts) {
|
|
|
6190
7425
|
}
|
|
6191
7426
|
function refreshBrushMembership() {
|
|
6192
7427
|
if (softMask === null || maskBrushSource === null || scene === null) return;
|
|
7428
|
+
perfCounters.fullBrushRefreshes += 1;
|
|
6193
7429
|
const slots = [];
|
|
6194
7430
|
for (const s of crossfilterHiddenBase) {
|
|
6195
7431
|
const id = crossfilterRowIds[s];
|
|
@@ -6199,8 +7435,45 @@ function createGraphInstance(opts) {
|
|
|
6199
7435
|
}
|
|
6200
7436
|
maskBrushSource.setNodeFailures(slots, null);
|
|
6201
7437
|
}
|
|
7438
|
+
function refreshBrushMembershipDelta(delta) {
|
|
7439
|
+
if (softMask === null || maskBrushSource === null || scene === null) return false;
|
|
7440
|
+
if (groupRewrite !== null) return false;
|
|
7441
|
+
const identity = accepted !== null && sceneModel() === accepted && crossfilterRowsRef === accepted.nodes;
|
|
7442
|
+
const sceneRef = scene;
|
|
7443
|
+
const translate = (baseSlots, out) => {
|
|
7444
|
+
out.length = 0;
|
|
7445
|
+
for (let i = 0; i < baseSlots.length; i++) {
|
|
7446
|
+
const s = baseSlots[i];
|
|
7447
|
+
perfCounters.brushSlotsTranslated += 1;
|
|
7448
|
+
if (identity) {
|
|
7449
|
+
if (s < sceneRef.count) out.push(s);
|
|
7450
|
+
continue;
|
|
7451
|
+
}
|
|
7452
|
+
const id = crossfilterRowIds[s];
|
|
7453
|
+
if (id === void 0) continue;
|
|
7454
|
+
const idx = sceneRef.indexById.get(id);
|
|
7455
|
+
if (idx !== void 0) out.push(idx);
|
|
7456
|
+
}
|
|
7457
|
+
};
|
|
7458
|
+
translate(delta.hidden, brushSceneAdd);
|
|
7459
|
+
translate(delta.shown, brushSceneRemove);
|
|
7460
|
+
maskBrushSource.updateNodeFailures(brushSceneAdd, brushSceneRemove, brushCrossings);
|
|
7461
|
+
return true;
|
|
7462
|
+
}
|
|
6202
7463
|
function cascadeNodeMask() {
|
|
6203
|
-
if (softMask !== null && scene !== null)
|
|
7464
|
+
if (softMask !== null && scene !== null) {
|
|
7465
|
+
perfCounters.fullCascades += 1;
|
|
7466
|
+
softMask.applyNodeCascadeToEdges(scene.links);
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
function cascadeNodeMaskDelta() {
|
|
7470
|
+
if (softMask === null || scene === null) return;
|
|
7471
|
+
const failing = brushCrossings.becameFailing;
|
|
7472
|
+
const cleared = brushCrossings.becameClear;
|
|
7473
|
+
if (failing.length === 0 && cleared.length === 0) return;
|
|
7474
|
+
sceneIncidence ??= buildIncidence(scene.links, scene.count);
|
|
7475
|
+
const crossed = failing.length === 0 ? cleared : cleared.length === 0 ? failing : [...failing, ...cleared];
|
|
7476
|
+
softMask.applyNodeCascadeToEdgesDelta(scene.links, sceneIncidence, crossed);
|
|
6204
7477
|
}
|
|
6205
7478
|
function refreshGroupMaskMembership(hidden) {
|
|
6206
7479
|
if (softMask === null || maskGroupSource === null) return;
|
|
@@ -6289,6 +7562,8 @@ function createGraphInstance(opts) {
|
|
|
6289
7562
|
refreshGroupMaskMembership(hidden);
|
|
6290
7563
|
cascadeNodeMask();
|
|
6291
7564
|
softMask.drainDirty();
|
|
7565
|
+
nodeAlphaComposer.reset();
|
|
7566
|
+
edgeAlphaComposer.reset();
|
|
6292
7567
|
return filterDiags !== before;
|
|
6293
7568
|
}
|
|
6294
7569
|
function computeVisibleCounts() {
|
|
@@ -6332,6 +7607,7 @@ function createGraphInstance(opts) {
|
|
|
6332
7607
|
}
|
|
6333
7608
|
function composeNodeAlphaBuffer(base) {
|
|
6334
7609
|
if (softMask === null || scene === null) return base;
|
|
7610
|
+
perfCounters.fullNodeRecomposes += 1;
|
|
6335
7611
|
const out = new Float32Array(base);
|
|
6336
7612
|
const n = scene.count;
|
|
6337
7613
|
const dimAlpha = theme.mutedAlpha;
|
|
@@ -6343,6 +7619,7 @@ function createGraphInstance(opts) {
|
|
|
6343
7619
|
}
|
|
6344
7620
|
function composeEdgeAlphaBuffer(base) {
|
|
6345
7621
|
if (softMask === null && pathDimEdges === null || scene === null) return base;
|
|
7622
|
+
perfCounters.fullEdgeRecomposes += 1;
|
|
6346
7623
|
const out = new Float32Array(base);
|
|
6347
7624
|
const n = scene.linkCount;
|
|
6348
7625
|
const dimAlpha = theme.mutedAlpha;
|
|
@@ -6357,6 +7634,36 @@ function createGraphInstance(opts) {
|
|
|
6357
7634
|
}
|
|
6358
7635
|
return out;
|
|
6359
7636
|
}
|
|
7637
|
+
function composeNodeAlphaIncremental(drain) {
|
|
7638
|
+
const base = basePointColorBuffer();
|
|
7639
|
+
if (softMask === null || scene === null) return base;
|
|
7640
|
+
const mask = softMask;
|
|
7641
|
+
const dimAlpha = theme.mutedAlpha;
|
|
7642
|
+
const alphaOf = (i) => mask.nodeAlpha(i, dimAlpha);
|
|
7643
|
+
const seeded = nodeAlphaComposer.ensureSeeded(base, scene.count, dimAlpha, null, alphaOf);
|
|
7644
|
+
if (seeded) {
|
|
7645
|
+
nodeAlphaComposer.note(drain.nodes);
|
|
7646
|
+
nodeAlphaComposer.note(drain.nodesAlpha);
|
|
7647
|
+
}
|
|
7648
|
+
return nodeAlphaComposer.nextBuffer(base, alphaOf);
|
|
7649
|
+
}
|
|
7650
|
+
function composeEdgeAlphaIncremental(drain) {
|
|
7651
|
+
const base = baseLinkColorBuffer();
|
|
7652
|
+
if (softMask === null || scene === null) return base;
|
|
7653
|
+
if (pathDimEdges !== null) {
|
|
7654
|
+
edgeAlphaComposer.reset();
|
|
7655
|
+
return composeEdgeAlphaBuffer(base);
|
|
7656
|
+
}
|
|
7657
|
+
const mask = softMask;
|
|
7658
|
+
const dimAlpha = theme.mutedAlpha;
|
|
7659
|
+
const alphaOf = (k) => mask.edgeAlpha(k, dimAlpha);
|
|
7660
|
+
const seeded = edgeAlphaComposer.ensureSeeded(base, scene.linkCount, dimAlpha, null, alphaOf);
|
|
7661
|
+
if (seeded) {
|
|
7662
|
+
edgeAlphaComposer.note(drain.edges);
|
|
7663
|
+
edgeAlphaComposer.note(drain.edgesAlpha);
|
|
7664
|
+
}
|
|
7665
|
+
return edgeAlphaComposer.nextBuffer(base, alphaOf);
|
|
7666
|
+
}
|
|
6360
7667
|
function publishMaskFastPath(extraPatch, diagsChanged) {
|
|
6361
7668
|
const prev = store.getState();
|
|
6362
7669
|
const patch = { ...extraPatch };
|
|
@@ -6380,13 +7687,48 @@ function createGraphInstance(opts) {
|
|
|
6380
7687
|
};
|
|
6381
7688
|
const buffers = visDirty.nodeColor || visDirty.nodeSize ? { ...projectChannelBuffers(visDirty) ?? {} } : {};
|
|
6382
7689
|
if (visDirty.nodeColor || visDirty.nodeSize) diagsChanged = true;
|
|
7690
|
+
const maskPhaseT0 = performance.now();
|
|
7691
|
+
const rangedChannels = session !== null && session.policy !== null ? session.policy.rangedChannels : null;
|
|
7692
|
+
const bufferPatches = {};
|
|
6383
7693
|
if (nodesAffected && buffers.pointColor === void 0) {
|
|
6384
|
-
|
|
7694
|
+
const reseedsBefore = nodeAlphaComposer.reseeds;
|
|
7695
|
+
const composed = composeNodeAlphaIncremental(drain);
|
|
7696
|
+
const changed = drain.nodes.length + drain.nodesAlpha.length;
|
|
7697
|
+
if (rangedChannels !== null && rangedChannels.has("pointColor") && nodeAlphaComposer.reseeds === reseedsBefore && changed < scene.count * PATCH_FULL_UPLOAD_RATIO) {
|
|
7698
|
+
bufferPatches.pointColor = buildAlphaPatches(
|
|
7699
|
+
composed,
|
|
7700
|
+
[drain.nodes, drain.nodesAlpha],
|
|
7701
|
+
4
|
|
7702
|
+
);
|
|
7703
|
+
} else {
|
|
7704
|
+
buffers.pointColor = composed;
|
|
7705
|
+
}
|
|
6385
7706
|
}
|
|
6386
7707
|
if (edgesAffected && buffers.linkColor === void 0) {
|
|
6387
|
-
|
|
7708
|
+
const reseedsBefore = edgeAlphaComposer.reseeds;
|
|
7709
|
+
const composed = composeEdgeAlphaIncremental(drain);
|
|
7710
|
+
const changed = drain.edges.length + drain.edgesAlpha.length;
|
|
7711
|
+
if (rangedChannels !== null && rangedChannels.has("linkColor") && edgeAlphaComposer.reseeds === reseedsBefore && changed < scene.links.length / 2 * PATCH_FULL_UPLOAD_RATIO) {
|
|
7712
|
+
bufferPatches.linkColor = buildAlphaPatches(
|
|
7713
|
+
composed,
|
|
7714
|
+
[drain.edges, drain.edgesAlpha],
|
|
7715
|
+
4
|
|
7716
|
+
);
|
|
7717
|
+
} else {
|
|
7718
|
+
buffers.linkColor = composed;
|
|
7719
|
+
}
|
|
6388
7720
|
}
|
|
6389
|
-
|
|
7721
|
+
const maskUploadT0 = performance.now();
|
|
7722
|
+
const maskCommit = { revision: revisions.render, buffers };
|
|
7723
|
+
if (Object.keys(bufferPatches).length > 0) maskCommit.bufferPatches = bufferPatches;
|
|
7724
|
+
commitToEngine(eng, maskCommit);
|
|
7725
|
+
lastCommitMs = {
|
|
7726
|
+
kind: "mask",
|
|
7727
|
+
validate: 0,
|
|
7728
|
+
derive: 0,
|
|
7729
|
+
project: maskUploadT0 - maskPhaseT0,
|
|
7730
|
+
upload: performance.now() - maskUploadT0
|
|
7731
|
+
};
|
|
6390
7732
|
revisions.appliedRender = eng.appliedRevision();
|
|
6391
7733
|
}
|
|
6392
7734
|
patch.revisions = revisions;
|
|
@@ -6414,7 +7756,17 @@ function createGraphInstance(opts) {
|
|
|
6414
7756
|
crossfilterHiddenBase.clear();
|
|
6415
7757
|
}
|
|
6416
7758
|
const crossfilterFacadeListeners = /* @__PURE__ */ new Set();
|
|
7759
|
+
let crossfilterNotifyPending = false;
|
|
6417
7760
|
function notifyCrossfilterFacade() {
|
|
7761
|
+
if (degradeController.isEngaged("batch-histograms") && engineIfReady() !== null) {
|
|
7762
|
+
crossfilterNotifyPending = true;
|
|
7763
|
+
return;
|
|
7764
|
+
}
|
|
7765
|
+
for (const cb of crossfilterFacadeListeners) cb();
|
|
7766
|
+
}
|
|
7767
|
+
function flushCrossfilterNotify() {
|
|
7768
|
+
if (!crossfilterNotifyPending) return;
|
|
7769
|
+
crossfilterNotifyPending = false;
|
|
6418
7770
|
for (const cb of crossfilterFacadeListeners) cb();
|
|
6419
7771
|
}
|
|
6420
7772
|
function buildCrossfilterEngine() {
|
|
@@ -6540,9 +7892,13 @@ function createGraphInstance(opts) {
|
|
|
6540
7892
|
for (const s of delta.shown) crossfilterHiddenBase.delete(s);
|
|
6541
7893
|
if (membershipChanged && scene !== null) {
|
|
6542
7894
|
ensureMask();
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
7895
|
+
if (refreshBrushMembershipDelta(delta)) {
|
|
7896
|
+
cascadeNodeMaskDelta();
|
|
7897
|
+
} else {
|
|
7898
|
+
refreshBrushMembership();
|
|
7899
|
+
refreshGroupMaskMembership(store.getState().hiddenNodeIds);
|
|
7900
|
+
cascadeNodeMask();
|
|
7901
|
+
}
|
|
6546
7902
|
}
|
|
6547
7903
|
publishMaskFastPath(extraPatch, false);
|
|
6548
7904
|
}
|
|
@@ -6781,6 +8137,7 @@ function createGraphInstance(opts) {
|
|
|
6781
8137
|
const result = reconcileScene(scopedAccepted ?? accepted);
|
|
6782
8138
|
labelPositionCache = null;
|
|
6783
8139
|
adjacency = null;
|
|
8140
|
+
sceneIncidence = null;
|
|
6784
8141
|
structuralChange = result.structuralChange;
|
|
6785
8142
|
positionChange = result.positionChange;
|
|
6786
8143
|
}
|
|
@@ -6853,6 +8210,8 @@ function createGraphInstance(opts) {
|
|
|
6853
8210
|
revisions.scope += 1;
|
|
6854
8211
|
revisions.render += 1;
|
|
6855
8212
|
if (eng !== null) {
|
|
8213
|
+
nodeAlphaComposer.reset();
|
|
8214
|
+
edgeAlphaComposer.reset();
|
|
6856
8215
|
const buffers = {};
|
|
6857
8216
|
if (nodesAffected) buffers.pointColor = composeNodeAlphaBuffer(basePointColorBuffer());
|
|
6858
8217
|
if (edgesAffected) buffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
|
|
@@ -7047,9 +8406,10 @@ function createGraphInstance(opts) {
|
|
|
7047
8406
|
if (eng !== null && scene !== null && labelModel !== null && cfg !== void 0 && cfg.enabled !== false) {
|
|
7048
8407
|
const sceneRef = scene;
|
|
7049
8408
|
const vp = store.getState().viewport ?? eng.getViewport() ?? { zoom: 1 };
|
|
8409
|
+
const hardCap = degradeController.isEngaged("cap-dom-labels") ? LABEL_MAX_VISIBLE_DEFAULT : LABEL_MAX_VISIBLE_CAP;
|
|
7050
8410
|
const k = Math.max(
|
|
7051
8411
|
0,
|
|
7052
|
-
Math.min(Math.floor(cfg.maxVisible ?? LABEL_MAX_VISIBLE_DEFAULT),
|
|
8412
|
+
Math.min(Math.floor(cfg.maxVisible ?? LABEL_MAX_VISIBLE_DEFAULT), hardCap)
|
|
7053
8413
|
);
|
|
7054
8414
|
const clusterCandidates = clusterLabelCandidates(cfg, vp.zoom, k);
|
|
7055
8415
|
const nodeLabelsSuppressed = cfg.maxZoom !== void 0 && vp.zoom <= cfg.maxZoom;
|
|
@@ -7518,7 +8878,8 @@ function createGraphInstance(opts) {
|
|
|
7518
8878
|
defaultPointColor: theme.nodeDefault,
|
|
7519
8879
|
defaultLinkColor: theme.edgeDefault,
|
|
7520
8880
|
linkArrows: edgeArrows,
|
|
7521
|
-
renderLinks: showLinks
|
|
8881
|
+
renderLinks: showLinks,
|
|
8882
|
+
emphasisRingColor: theme.emphasisRing
|
|
7522
8883
|
};
|
|
7523
8884
|
if (simulation !== void 0) c.simulation = simulation;
|
|
7524
8885
|
if (clusterSpec !== null) {
|
|
@@ -7578,6 +8939,11 @@ function createGraphInstance(opts) {
|
|
|
7578
8939
|
}
|
|
7579
8940
|
function pushImageRefs() {
|
|
7580
8941
|
if (session === null || session.policy === null || session.policy.images !== "native") return;
|
|
8942
|
+
if (degradeController.isEngaged("defer-images")) {
|
|
8943
|
+
imageRefsDeferred = true;
|
|
8944
|
+
return;
|
|
8945
|
+
}
|
|
8946
|
+
imageRefsDeferred = false;
|
|
7581
8947
|
const model = sceneModel();
|
|
7582
8948
|
if (model === null) return;
|
|
7583
8949
|
if (nodeImage === void 0) {
|
|
@@ -7691,15 +9057,25 @@ function createGraphInstance(opts) {
|
|
|
7691
9057
|
}
|
|
7692
9058
|
eng.setPinnedIndices(indexSet.size > 0 ? [...indexSet] : null);
|
|
7693
9059
|
}
|
|
9060
|
+
function applyEmphasis(eng, index) {
|
|
9061
|
+
if (!emphasisRingOn) return;
|
|
9062
|
+
eng.setFocusedIndex(index);
|
|
9063
|
+
}
|
|
7694
9064
|
function reapplyInteractionState(eng) {
|
|
7695
9065
|
const { selection, pins, pinnedNodeIds, hover } = store.getState();
|
|
7696
9066
|
if (selection.nodeIds.length > 0 || selection.groupIds.length > 0) {
|
|
7697
9067
|
pushSelectionToEngine(eng, selection.nodeIds);
|
|
7698
9068
|
}
|
|
7699
9069
|
if (pins.size > 0 || pinnedNodeIds.size > 0) pushPinsToEngine(eng, pins);
|
|
9070
|
+
if (emphasizedNodeId !== null && scene !== null && !scene.indexById.has(emphasizedNodeId)) {
|
|
9071
|
+
emphasizedNodeId = null;
|
|
9072
|
+
}
|
|
7700
9073
|
if (hover.nodeId !== null && scene !== null) {
|
|
7701
9074
|
const idx = scene.indexById.get(hover.nodeId);
|
|
7702
|
-
if (idx !== void 0) eng
|
|
9075
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
9076
|
+
} else if (emphasizedNodeId !== null && scene !== null) {
|
|
9077
|
+
const idx = scene.indexById.get(emphasizedNodeId);
|
|
9078
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
7703
9079
|
}
|
|
7704
9080
|
}
|
|
7705
9081
|
function maybeFitView(eng) {
|
|
@@ -8441,6 +9817,7 @@ function createGraphInstance(opts) {
|
|
|
8441
9817
|
invalidateExpansionRecords();
|
|
8442
9818
|
edgeIndexById = new Map(p.merged.edges.map((e, k) => [e.id, k]));
|
|
8443
9819
|
adjacency = null;
|
|
9820
|
+
sceneIncidence = null;
|
|
8444
9821
|
acceptedAdjacency = null;
|
|
8445
9822
|
acceptedModelSeq += 1;
|
|
8446
9823
|
scopedAccepted = computeScopedAccepted();
|
|
@@ -8610,9 +9987,15 @@ function createGraphInstance(opts) {
|
|
|
8610
9987
|
if (pinsChanged || pinnedChanged || structuralChange && (nextPins.size > 0 || nextPinned.size > 0)) {
|
|
8611
9988
|
pushPinsToEngine(eng, nextPins);
|
|
8612
9989
|
}
|
|
9990
|
+
if (structuralChange && emphasizedNodeId !== null && !ingestScene.indexById.has(emphasizedNodeId)) {
|
|
9991
|
+
emphasizedNodeId = null;
|
|
9992
|
+
}
|
|
8613
9993
|
if (structuralChange && !hoverCleared && prev.hover.nodeId !== null) {
|
|
8614
9994
|
const idx = ingestScene.indexById.get(prev.hover.nodeId);
|
|
8615
|
-
if (idx !== void 0) eng
|
|
9995
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
9996
|
+
} else if (structuralChange && emphasizedNodeId !== null) {
|
|
9997
|
+
const idx = ingestScene.indexById.get(emphasizedNodeId);
|
|
9998
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
8616
9999
|
}
|
|
8617
10000
|
if (commitNeeded) maybeFitView(eng);
|
|
8618
10001
|
}
|
|
@@ -9373,6 +10756,7 @@ function createGraphInstance(opts) {
|
|
|
9373
10756
|
const result = reconcileScene(scopedAccepted ?? accepted);
|
|
9374
10757
|
labelPositionCache = null;
|
|
9375
10758
|
adjacency = null;
|
|
10759
|
+
sceneIncidence = null;
|
|
9376
10760
|
hardScopeGen += 1;
|
|
9377
10761
|
visibleGen += 1;
|
|
9378
10762
|
const filterDiagsChanged = rebuildMaskMemberships(prev.hiddenNodeIds);
|
|
@@ -9728,10 +11112,14 @@ function createGraphInstance(opts) {
|
|
|
9728
11112
|
checkRestoreAcknowledgement(update);
|
|
9729
11113
|
});
|
|
9730
11114
|
}
|
|
9731
|
-
function applyHostUpdateInner(update) {
|
|
11115
|
+
function applyHostUpdateInner(update, preAccepted) {
|
|
11116
|
+
const phaseT0 = performance.now();
|
|
11117
|
+
let phaseProjectStart = phaseT0;
|
|
9732
11118
|
const issuedModelSeq = acceptedModelSeq;
|
|
9733
11119
|
let searchIndexRejected = false;
|
|
9734
11120
|
let crossfilterRejected = false;
|
|
11121
|
+
let columnarRejected = false;
|
|
11122
|
+
let metricsDeferredToWorker = false;
|
|
9735
11123
|
const diagnosticsForced = pendingDiagnosticsRefresh;
|
|
9736
11124
|
pendingDiagnosticsRefresh = false;
|
|
9737
11125
|
if (update.filter !== void 0 && update.filter !== null) {
|
|
@@ -9772,11 +11160,42 @@ function createGraphInstance(opts) {
|
|
|
9772
11160
|
let positionChange = false;
|
|
9773
11161
|
let datasetKeyChanged = false;
|
|
9774
11162
|
let overlayIdsCleared = false;
|
|
9775
|
-
|
|
11163
|
+
let data;
|
|
11164
|
+
if (update.data !== void 0 && isColumnarSnapshot(update.data)) {
|
|
11165
|
+
const columnar = update.data;
|
|
11166
|
+
const isColumnarReplay = baseAccepted !== null && baseAccepted.datasetKey === columnar.datasetKey && baseAccepted.sourceRevision === columnar.sourceRevision;
|
|
11167
|
+
if (isColumnarReplay) {
|
|
11168
|
+
data = void 0;
|
|
11169
|
+
} else {
|
|
11170
|
+
const issues = validateColumnarStructure(columnar);
|
|
11171
|
+
if (issues.length > 0) {
|
|
11172
|
+
columnarDiags = [
|
|
11173
|
+
{
|
|
11174
|
+
code: "invalid-columnar-snapshot",
|
|
11175
|
+
severity: "error",
|
|
11176
|
+
count: issues.length,
|
|
11177
|
+
sampleIds: issues.slice(0, DIAGNOSTIC_SAMPLE_CAP).map((i) => i.where),
|
|
11178
|
+
message: `columnar snapshot rejected whole (\xA75.1): ${issues[0].where} \u2014 ${issues[0].detail}`
|
|
11179
|
+
}
|
|
11180
|
+
];
|
|
11181
|
+
columnarRejected = true;
|
|
11182
|
+
data = void 0;
|
|
11183
|
+
} else if (workerEligible()) {
|
|
11184
|
+
scheduleWorkerAcceptance(columnar, update.metrics);
|
|
11185
|
+
metricsDeferredToWorker = true;
|
|
11186
|
+
data = void 0;
|
|
11187
|
+
} else {
|
|
11188
|
+
data = materializeColumnarSnapshot(columnar);
|
|
11189
|
+
if (columnar.bufferOwnership === "transfer") detachColumnarBuffers(columnar);
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
} else {
|
|
11193
|
+
data = update.data;
|
|
11194
|
+
}
|
|
9776
11195
|
if (data !== void 0) {
|
|
9777
11196
|
const isReplay = baseAccepted !== null && baseAccepted.datasetKey === data.datasetKey && baseAccepted.sourceRevision === data.sourceRevision;
|
|
9778
11197
|
if (!isReplay) {
|
|
9779
|
-
const nextAccepted = validateSnapshot(data);
|
|
11198
|
+
const nextAccepted = preAccepted ?? validateSnapshot(data);
|
|
9780
11199
|
abortAllSessions(null, "replaced: a declarative snapshot was applied (\xA77.5)");
|
|
9781
11200
|
overlayIdsCleared = clearOverlayState();
|
|
9782
11201
|
datasetKeyChanged = baseAccepted !== null && baseAccepted.datasetKey !== data.datasetKey;
|
|
@@ -9809,8 +11228,11 @@ function createGraphInstance(opts) {
|
|
|
9809
11228
|
baseSource = "declarative";
|
|
9810
11229
|
edgeIndexById = new Map(nextAccepted.edges.map((e, k) => [e.id, k]));
|
|
9811
11230
|
adjacency = null;
|
|
11231
|
+
sceneIncidence = null;
|
|
9812
11232
|
acceptedAdjacency = null;
|
|
9813
11233
|
dataDiags = nextAccepted.diagnostics;
|
|
11234
|
+
columnarDiags = [];
|
|
11235
|
+
pendingDeriveToken += 1;
|
|
9814
11236
|
dataChanged = true;
|
|
9815
11237
|
}
|
|
9816
11238
|
}
|
|
@@ -9953,6 +11375,7 @@ function createGraphInstance(opts) {
|
|
|
9953
11375
|
const result = reconcileScene(scopedAccepted ?? accepted);
|
|
9954
11376
|
labelPositionCache = null;
|
|
9955
11377
|
adjacency = null;
|
|
11378
|
+
sceneIncidence = null;
|
|
9956
11379
|
structuralChange = result.structuralChange;
|
|
9957
11380
|
positionChange = result.positionChange;
|
|
9958
11381
|
}
|
|
@@ -9966,6 +11389,7 @@ function createGraphInstance(opts) {
|
|
|
9966
11389
|
const result = reconcileScene(scopedAccepted ?? accepted);
|
|
9967
11390
|
labelPositionCache = null;
|
|
9968
11391
|
adjacency = null;
|
|
11392
|
+
sceneIncidence = null;
|
|
9969
11393
|
structuralChange = structuralChange || result.structuralChange;
|
|
9970
11394
|
positionChange = positionChange || result.positionChange;
|
|
9971
11395
|
}
|
|
@@ -10003,7 +11427,7 @@ function createGraphInstance(opts) {
|
|
|
10003
11427
|
if (clusterDiags !== clusterDiagsBefore) groupsDiagsChanged = true;
|
|
10004
11428
|
let metricsAdmitted = null;
|
|
10005
11429
|
let metricsProcessed = false;
|
|
10006
|
-
if (update.metrics !== void 0 && update.metrics.length > 0) {
|
|
11430
|
+
if (!metricsDeferredToWorker && update.metrics !== void 0 && update.metrics.length > 0) {
|
|
10007
11431
|
metricsProcessed = true;
|
|
10008
11432
|
if (accepted === null || !ensureMetricModel()) {
|
|
10009
11433
|
metricDiags = [
|
|
@@ -10068,6 +11492,7 @@ function createGraphInstance(opts) {
|
|
|
10068
11492
|
let configPatch;
|
|
10069
11493
|
let themeChanged = false;
|
|
10070
11494
|
let mutedAlphaChanged = false;
|
|
11495
|
+
let emphasisToggled = false;
|
|
10071
11496
|
let nodeBaseInvalidated = false;
|
|
10072
11497
|
let linkBaseInvalidated = false;
|
|
10073
11498
|
{
|
|
@@ -10102,6 +11527,10 @@ function createGraphInstance(opts) {
|
|
|
10102
11527
|
linkBaseInvalidated = true;
|
|
10103
11528
|
}
|
|
10104
11529
|
}
|
|
11530
|
+
if (nextTheme.emphasisRing !== theme.emphasisRing) {
|
|
11531
|
+
c.emphasisRingColor = nextTheme.emphasisRing;
|
|
11532
|
+
any = true;
|
|
11533
|
+
}
|
|
10105
11534
|
mutedAlphaChanged = nextTheme.mutedAlpha !== theme.mutedAlpha;
|
|
10106
11535
|
if (groupRewrite !== null && (nextTheme.accent !== theme.accent || nextTheme.nodeDefault !== theme.nodeDefault)) {
|
|
10107
11536
|
dirtyNodeColor = true;
|
|
@@ -10124,6 +11553,10 @@ function createGraphInstance(opts) {
|
|
|
10124
11553
|
c.renderLinks = showLinks;
|
|
10125
11554
|
any = true;
|
|
10126
11555
|
}
|
|
11556
|
+
if (update.emphasisRing !== void 0 && update.emphasisRing !== emphasisRingOn) {
|
|
11557
|
+
emphasisRingOn = update.emphasisRing;
|
|
11558
|
+
emphasisToggled = true;
|
|
11559
|
+
}
|
|
10127
11560
|
if (clusterConfigDirty) {
|
|
10128
11561
|
c.cluster = clusterConfigPayload();
|
|
10129
11562
|
any = true;
|
|
@@ -10325,6 +11758,8 @@ function createGraphInstance(opts) {
|
|
|
10325
11758
|
const edgesAffected = drain.edges.length > 0 || drain.edgesAlpha.length > 0;
|
|
10326
11759
|
maskAffected = nodesAffected || edgesAffected;
|
|
10327
11760
|
if (maskAffected) {
|
|
11761
|
+
nodeAlphaComposer.reset();
|
|
11762
|
+
edgeAlphaComposer.reset();
|
|
10328
11763
|
maskBuffers = {};
|
|
10329
11764
|
if (nodesAffected) maskBuffers.pointColor = composeNodeAlphaBuffer(basePointColorBuffer());
|
|
10330
11765
|
if (edgesAffected) maskBuffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
|
|
@@ -10344,6 +11779,7 @@ function createGraphInstance(opts) {
|
|
|
10344
11779
|
maskBuffers.linkColor = composeEdgeAlphaBuffer(baseLinkColorBuffer());
|
|
10345
11780
|
}
|
|
10346
11781
|
}
|
|
11782
|
+
phaseProjectStart = performance.now();
|
|
10347
11783
|
let buffers = projectChannelBuffers({
|
|
10348
11784
|
nodeColor: dirtyNodeColor,
|
|
10349
11785
|
nodeSize: dirtyNodeSize,
|
|
@@ -10393,7 +11829,15 @@ function createGraphInstance(opts) {
|
|
|
10393
11829
|
simRestarted = true;
|
|
10394
11830
|
}
|
|
10395
11831
|
}
|
|
11832
|
+
const phaseUploadStart = performance.now();
|
|
10396
11833
|
commitToEngine(eng, commit);
|
|
11834
|
+
lastCommitMs = {
|
|
11835
|
+
kind: structure !== void 0 ? scopeChanged && !dataChanged ? "scope" : "model" : buffers !== void 0 ? "mask" : "config",
|
|
11836
|
+
validate: 0,
|
|
11837
|
+
derive: phaseProjectStart - phaseT0,
|
|
11838
|
+
project: phaseUploadStart - phaseProjectStart,
|
|
11839
|
+
upload: performance.now() - phaseUploadStart
|
|
11840
|
+
};
|
|
10397
11841
|
revisions.appliedRender = eng.appliedRevision();
|
|
10398
11842
|
const facade = session !== null ? session.edgePicking : null;
|
|
10399
11843
|
if (facade !== null) {
|
|
@@ -10433,7 +11877,7 @@ function createGraphInstance(opts) {
|
|
|
10433
11877
|
patch.timeline = { playingKey: null };
|
|
10434
11878
|
changed = true;
|
|
10435
11879
|
}
|
|
10436
|
-
if (dataChanged || datasetKeyChanged || buffers !== void 0 || labelRerank.diagsChanged || filterDiagsChanged || metricsProcessed || searchIndexRejected || crossfilterRejected || parallelRejected || groupsDiagsChanged || diagnosticsForced) {
|
|
11880
|
+
if (dataChanged || datasetKeyChanged || buffers !== void 0 || labelRerank.diagsChanged || filterDiagsChanged || metricsProcessed || searchIndexRejected || crossfilterRejected || columnarRejected || parallelRejected || groupsDiagsChanged || diagnosticsForced) {
|
|
10437
11881
|
patch.diagnostics = composeDiagnostics();
|
|
10438
11882
|
changed = true;
|
|
10439
11883
|
}
|
|
@@ -10502,9 +11946,26 @@ function createGraphInstance(opts) {
|
|
|
10502
11946
|
if (pinsChanged || pinnedChanged || structuralChange && (nextPins.size > 0 || nextPinned.size > 0)) {
|
|
10503
11947
|
pushPinsToEngine(eng, nextPins);
|
|
10504
11948
|
}
|
|
11949
|
+
if (structuralChange && emphasizedNodeId !== null && scene !== null && !scene.indexById.has(emphasizedNodeId)) {
|
|
11950
|
+
emphasizedNodeId = null;
|
|
11951
|
+
}
|
|
10505
11952
|
if (structuralChange && !hoverCleared && prev.hover.nodeId !== null && scene !== null) {
|
|
10506
11953
|
const idx = scene.indexById.get(prev.hover.nodeId);
|
|
10507
|
-
if (idx !== void 0) eng
|
|
11954
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
11955
|
+
} else if (structuralChange && emphasizedNodeId !== null && scene !== null) {
|
|
11956
|
+
const idx = scene.indexById.get(emphasizedNodeId);
|
|
11957
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
11958
|
+
}
|
|
11959
|
+
if (emphasisToggled) {
|
|
11960
|
+
if (!emphasisRingOn) {
|
|
11961
|
+
eng.setFocusedIndex(null);
|
|
11962
|
+
} else if (!hoverCleared && prev.hover.nodeId !== null && scene !== null) {
|
|
11963
|
+
const idx = scene.indexById.get(prev.hover.nodeId);
|
|
11964
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
11965
|
+
} else if (emphasizedNodeId !== null && scene !== null) {
|
|
11966
|
+
const idx = scene.indexById.get(emphasizedNodeId);
|
|
11967
|
+
if (idx !== void 0) applyEmphasis(eng, idx);
|
|
11968
|
+
}
|
|
10508
11969
|
}
|
|
10509
11970
|
if (commitNeeded) maybeFitView(eng);
|
|
10510
11971
|
}
|
|
@@ -10554,7 +12015,8 @@ function createGraphInstance(opts) {
|
|
|
10554
12015
|
const nodeId = node === null ? null : node.id;
|
|
10555
12016
|
const hover = store.getState().hover;
|
|
10556
12017
|
if (hover.nodeId !== nodeId) publish({ hover: { nodeId, edgeId: hover.edgeId } });
|
|
10557
|
-
|
|
12018
|
+
emphasizedNodeId = null;
|
|
12019
|
+
applyEmphasis(s.engine, node === null ? null : index);
|
|
10558
12020
|
emit("nodeHover", { node });
|
|
10559
12021
|
},
|
|
10560
12022
|
// §13 native-route edge events (§7.4 mapping). Host onLink* events are
|
|
@@ -10575,6 +12037,9 @@ function createGraphInstance(opts) {
|
|
|
10575
12037
|
emit("edgeClick", { edge });
|
|
10576
12038
|
},
|
|
10577
12039
|
onLinkHover(linkIndex) {
|
|
12040
|
+
if (linkIndex !== null && degradeController.isEngaged("defer-link-picking") && store.getState().simulationRunning) {
|
|
12041
|
+
return;
|
|
12042
|
+
}
|
|
10578
12043
|
if (!active()) return;
|
|
10579
12044
|
if (linkIndex !== null && !maskEdgeVisibleAt(linkIndex)) linkIndex = null;
|
|
10580
12045
|
const edge = linkIndex === null ? null : edgeAtLinkIndex(linkIndex) ?? null;
|
|
@@ -10616,6 +12081,32 @@ function createGraphInstance(opts) {
|
|
|
10616
12081
|
*/
|
|
10617
12082
|
onFrame(timeMs) {
|
|
10618
12083
|
if (!active()) return;
|
|
12084
|
+
frameCadence += 1;
|
|
12085
|
+
pressureSampler.noteFrame(timeMs, !store.getState().simulationRunning);
|
|
12086
|
+
flushCrossfilterNotify();
|
|
12087
|
+
if (timeMs - lastPerfSampleAt >= PERF_SAMPLE_THROTTLE_MS) {
|
|
12088
|
+
lastPerfSampleAt = timeMs;
|
|
12089
|
+
const ewma = pressureSampler.snapshot().frameEwmaMs;
|
|
12090
|
+
if (Number.isFinite(ewma)) {
|
|
12091
|
+
const vis = store.getState().visible;
|
|
12092
|
+
const visible = { nodes: vis.nodes, edges: vis.edges };
|
|
12093
|
+
if (ewma > FRAME_PRESSURE_ENGAGE_MS) {
|
|
12094
|
+
for (const step of ["cap-dom-labels", "defer-link-picking"]) {
|
|
12095
|
+
const event = degradeController.engageForPressure(step, "frame-pressure", visible);
|
|
12096
|
+
if (event !== null) applyDegradeEvent(event);
|
|
12097
|
+
}
|
|
12098
|
+
} else if (ewma < FRAME_PRESSURE_CLEAR_MS) {
|
|
12099
|
+
for (const step of ["cap-dom-labels", "defer-link-picking"]) {
|
|
12100
|
+
const event = degradeController.clearPressure(step, visible);
|
|
12101
|
+
if (event !== null) applyDegradeEvent(event);
|
|
12102
|
+
}
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
if ((listeners.get("perfSample")?.size ?? 0) > 0) {
|
|
12106
|
+
emit("perfSample", getPerfSnapshot());
|
|
12107
|
+
}
|
|
12108
|
+
pressureSampler.resetCounters();
|
|
12109
|
+
}
|
|
10619
12110
|
if (store.getState().simulationRunning && labelsEnabled()) {
|
|
10620
12111
|
if (lastHotRefreshMs === null || timeMs - lastHotRefreshMs >= SIM_HOT_REFRESH_MS || timeMs < lastHotRefreshMs) {
|
|
10621
12112
|
lastHotRefreshMs = timeMs;
|
|
@@ -10662,6 +12153,7 @@ function createGraphInstance(opts) {
|
|
|
10662
12153
|
notifyLabelSubs(positionSubs);
|
|
10663
12154
|
}
|
|
10664
12155
|
emit("simulationEnd", {});
|
|
12156
|
+
armQuiescenceAssertion();
|
|
10665
12157
|
},
|
|
10666
12158
|
onError(error) {
|
|
10667
12159
|
if (!active()) return;
|
|
@@ -10739,11 +12231,13 @@ function createGraphInstance(opts) {
|
|
|
10739
12231
|
}
|
|
10740
12232
|
function buildAndCommitFullReplay(s, restartAlpha) {
|
|
10741
12233
|
const eng = s.engine;
|
|
12234
|
+
const phaseT0 = performance.now();
|
|
10742
12235
|
const renderRevision = store.getState().revisions.render;
|
|
10743
12236
|
const replayModel = renderModel();
|
|
10744
12237
|
if (replayModel !== null) {
|
|
10745
12238
|
reconcileScene(replayModel);
|
|
10746
12239
|
const replayScene = scene;
|
|
12240
|
+
const phaseProjectStart = performance.now();
|
|
10747
12241
|
const commit = {
|
|
10748
12242
|
revision: renderRevision,
|
|
10749
12243
|
structure: {
|
|
@@ -10767,7 +12261,15 @@ function createGraphInstance(opts) {
|
|
|
10767
12261
|
commit.resources = { ...commit.resources ?? {}, pointImageIndex: syncIndex };
|
|
10768
12262
|
}
|
|
10769
12263
|
if (restartAlpha !== null) commit.restart = { alpha: restartAlpha };
|
|
12264
|
+
const phaseUploadStart = performance.now();
|
|
10770
12265
|
commitToEngine(eng, commit);
|
|
12266
|
+
lastCommitMs = {
|
|
12267
|
+
kind: "model",
|
|
12268
|
+
validate: 0,
|
|
12269
|
+
derive: phaseProjectStart - phaseT0,
|
|
12270
|
+
project: phaseUploadStart - phaseProjectStart,
|
|
12271
|
+
upload: performance.now() - phaseUploadStart
|
|
12272
|
+
};
|
|
10771
12273
|
if (s.edgePicking !== null) {
|
|
10772
12274
|
if (restartAlpha !== null) s.edgePicking.disarm();
|
|
10773
12275
|
else s.edgePicking.arm(replayScene.positions, replayScene.links);
|
|
@@ -10804,6 +12306,7 @@ function createGraphInstance(opts) {
|
|
|
10804
12306
|
const restartAlpha = layout === "force" ? 1 : null;
|
|
10805
12307
|
const committed = buildAndCommitFullReplay(s, restartAlpha);
|
|
10806
12308
|
const restarted = committed && accepted !== null && restartAlpha !== null;
|
|
12309
|
+
if (layout === "fixed") eng.pause();
|
|
10807
12310
|
publish({
|
|
10808
12311
|
status: "ready",
|
|
10809
12312
|
revisions: { ...store.getState().revisions, appliedRender: eng.appliedRevision() },
|
|
@@ -10859,6 +12362,11 @@ function createGraphInstance(opts) {
|
|
|
10859
12362
|
session = null;
|
|
10860
12363
|
mountPromise = null;
|
|
10861
12364
|
cancelRerankTimer();
|
|
12365
|
+
flushCrossfilterNotify();
|
|
12366
|
+
if (quiescenceTimer !== null) {
|
|
12367
|
+
clearTimeout(quiescenceTimer);
|
|
12368
|
+
quiescenceTimer = null;
|
|
12369
|
+
}
|
|
10862
12370
|
const timelinePatch = resetTimelineForPatch();
|
|
10863
12371
|
s.edgePicking?.destroy();
|
|
10864
12372
|
s.edgePicking = null;
|
|
@@ -10871,10 +12379,48 @@ function createGraphInstance(opts) {
|
|
|
10871
12379
|
});
|
|
10872
12380
|
rerankAndNotify();
|
|
10873
12381
|
}
|
|
12382
|
+
function armQuiescenceAssertion() {
|
|
12383
|
+
if (quiescenceAsserted || quiescenceTimer !== null || destroyed) return;
|
|
12384
|
+
const env = globalThis.process?.env?.NODE_ENV;
|
|
12385
|
+
if (env !== "development") return;
|
|
12386
|
+
const g = globalThis;
|
|
12387
|
+
if (typeof g.requestAnimationFrame !== "function") return;
|
|
12388
|
+
const armedRender = store.getState().revisions.render;
|
|
12389
|
+
quiescenceTimer = setTimeout(() => {
|
|
12390
|
+
quiescenceTimer = null;
|
|
12391
|
+
const state = store.getState();
|
|
12392
|
+
if (destroyed || state.simulationRunning || state.revisions.render !== armedRender) {
|
|
12393
|
+
return;
|
|
12394
|
+
}
|
|
12395
|
+
quiescenceAsserted = true;
|
|
12396
|
+
const native = g.requestAnimationFrame;
|
|
12397
|
+
if (typeof native !== "function") return;
|
|
12398
|
+
let registrations = 0;
|
|
12399
|
+
const wrapper = (cb) => {
|
|
12400
|
+
registrations += 1;
|
|
12401
|
+
return native.call(globalThis, cb);
|
|
12402
|
+
};
|
|
12403
|
+
g.requestAnimationFrame = wrapper;
|
|
12404
|
+
setTimeout(() => {
|
|
12405
|
+
if (g.requestAnimationFrame === wrapper) g.requestAnimationFrame = native;
|
|
12406
|
+
if (registrations > 0 && !destroyed) {
|
|
12407
|
+
console.warn(
|
|
12408
|
+
`orbit: ${registrations} requestAnimationFrame registration(s) observed over 500ms while this instance is quiescent (\xA717/S13-T06 one-frame-loop). If no app-owned animation is running, a second frame loop is leaking.`
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
}, 500);
|
|
12412
|
+
}, 2e3);
|
|
12413
|
+
}
|
|
10874
12414
|
function destroy() {
|
|
10875
12415
|
if (destroyed) return;
|
|
10876
12416
|
destroyed = true;
|
|
12417
|
+
workerLane?.terminate();
|
|
12418
|
+
workerLane = null;
|
|
10877
12419
|
stopTimelineTimer();
|
|
12420
|
+
if (quiescenceTimer !== null) {
|
|
12421
|
+
clearTimeout(quiescenceTimer);
|
|
12422
|
+
quiescenceTimer = null;
|
|
12423
|
+
}
|
|
10878
12424
|
timelinePlayingKey = null;
|
|
10879
12425
|
imagePipeline?.dispose();
|
|
10880
12426
|
imagePipeline = null;
|
|
@@ -11251,6 +12797,9 @@ function createGraphInstance(opts) {
|
|
|
11251
12797
|
} else if (wantsManualGroups && !groupsControlled) {
|
|
11252
12798
|
commitManualGroups(manualGroups.length > 0 ? manualGroups : null);
|
|
11253
12799
|
}
|
|
12800
|
+
if (state.layout.kind !== layout) {
|
|
12801
|
+
applyHostUpdateInner({ layout: state.layout.kind });
|
|
12802
|
+
}
|
|
11254
12803
|
if (state.styling !== void 0 && !needsHost) {
|
|
11255
12804
|
const patch = {};
|
|
11256
12805
|
if (state.styling.showLinks !== void 0) patch.showLinks = state.styling.showLinks;
|
|
@@ -11513,14 +13062,17 @@ function createGraphInstance(opts) {
|
|
|
11513
13062
|
}
|
|
11514
13063
|
function captureExportPin() {
|
|
11515
13064
|
if (accepted === null) return null;
|
|
11516
|
-
|
|
13065
|
+
let visibleIds = null;
|
|
13066
|
+
if (scene !== null) {
|
|
13067
|
+
const slots = visibleSlotsOf(scene, softMask);
|
|
13068
|
+
const ids = /* @__PURE__ */ new Set();
|
|
13069
|
+
for (const i of slots) ids.add(scene.idByIndex[i]);
|
|
13070
|
+
visibleIds = ids;
|
|
13071
|
+
}
|
|
13072
|
+
return { accepted, scene, visibleIds };
|
|
11517
13073
|
}
|
|
11518
13074
|
function pinnedVisibleIds(pin) {
|
|
11519
|
-
|
|
11520
|
-
const slots = visibleSlotsOf(pin.scene, pin.mask);
|
|
11521
|
-
const ids = /* @__PURE__ */ new Set();
|
|
11522
|
-
for (const i of slots) ids.add(pin.scene.idByIndex[i]);
|
|
11523
|
-
return ids;
|
|
13075
|
+
return pin.visibleIds;
|
|
11524
13076
|
}
|
|
11525
13077
|
function pinnedRowCount(pin, scope) {
|
|
11526
13078
|
if (scope === "accepted") return pin.accepted.nodes.length + pin.accepted.edges.length;
|
|
@@ -11622,7 +13174,7 @@ function createGraphInstance(opts) {
|
|
|
11622
13174
|
if (eng === null || scene === null) return EMPTY_IDS;
|
|
11623
13175
|
const idx = scene.indexById.get(id);
|
|
11624
13176
|
if (idx === void 0) return EMPTY_IDS;
|
|
11625
|
-
eng
|
|
13177
|
+
applyEmphasis(eng, idx);
|
|
11626
13178
|
if (effectiveReducedMotion()) eng.zoomToIndex?.(idx, 0);
|
|
11627
13179
|
else eng.zoomToIndex?.(idx);
|
|
11628
13180
|
const neighbors = neighborIdsOf(id);
|
|
@@ -11637,6 +13189,67 @@ function createGraphInstance(opts) {
|
|
|
11637
13189
|
}
|
|
11638
13190
|
return neighbors;
|
|
11639
13191
|
}
|
|
13192
|
+
function emphasizeNode(id) {
|
|
13193
|
+
const eng = engineIfReady();
|
|
13194
|
+
if (eng === null) return;
|
|
13195
|
+
if (id === null) {
|
|
13196
|
+
emphasizedNodeId = null;
|
|
13197
|
+
applyEmphasis(eng, null);
|
|
13198
|
+
return;
|
|
13199
|
+
}
|
|
13200
|
+
if (scene === null) return;
|
|
13201
|
+
const idx = scene.indexById.get(id);
|
|
13202
|
+
if (idx !== void 0) {
|
|
13203
|
+
emphasizedNodeId = id;
|
|
13204
|
+
applyEmphasis(eng, idx);
|
|
13205
|
+
}
|
|
13206
|
+
}
|
|
13207
|
+
function getPerfSnapshot() {
|
|
13208
|
+
const st = store.getState();
|
|
13209
|
+
const p = pressureSampler.snapshot();
|
|
13210
|
+
const snap = {
|
|
13211
|
+
at: Date.now(),
|
|
13212
|
+
nodeCount: st.nodeCount,
|
|
13213
|
+
edgeCount: st.edgeCount,
|
|
13214
|
+
visibleNodeCount: st.visible.nodes,
|
|
13215
|
+
visibleEdgeCount: st.visible.edges,
|
|
13216
|
+
estimatedCpuBytes: estimateCpuBytes(),
|
|
13217
|
+
// The acceptance queue is synchronous (depth is 0 outside a job, and a
|
|
13218
|
+
// reader inside a job observes 1); async ingestion depth arrives with
|
|
13219
|
+
// the worker lane (PR-E).
|
|
13220
|
+
queueDepth: acceptanceQueue.active ? 1 : 0,
|
|
13221
|
+
modelRevision: st.revisions.model,
|
|
13222
|
+
scopeRevision: st.revisions.scope,
|
|
13223
|
+
renderRevision: st.revisions.render,
|
|
13224
|
+
appliedRenderRevision: st.revisions.appliedRender,
|
|
13225
|
+
activeDegradations: degradeController.activeSteps(),
|
|
13226
|
+
// 'worker' iff the lane actually boots — a configured-but-unavailable
|
|
13227
|
+
// lane reports the honest 'main' (plus its worker-unavailable diag).
|
|
13228
|
+
execution: workerLane !== null && workerLane.available() === true ? "worker" : "main",
|
|
13229
|
+
rangeUpdates: session !== null && session.policy !== null ? [...session.policy.rangedChannels] : [],
|
|
13230
|
+
pressure: {
|
|
13231
|
+
frameEwmaMs: p.frameEwmaMs,
|
|
13232
|
+
droppedFrames: p.droppedFrames,
|
|
13233
|
+
idleWakeups: p.idleWakeups
|
|
13234
|
+
}
|
|
13235
|
+
};
|
|
13236
|
+
if (lastCommitMs !== void 0) snap.lastCommitMs = { ...lastCommitMs };
|
|
13237
|
+
if (scene !== null) {
|
|
13238
|
+
snap.estimatedGpuBytes = scene.count * (2 + 4 + 1) * 4 + scene.linkCount * (4 + 1 + 2) * 4;
|
|
13239
|
+
}
|
|
13240
|
+
return snap;
|
|
13241
|
+
}
|
|
13242
|
+
function estimateCpuBytes() {
|
|
13243
|
+
let bytes = 0;
|
|
13244
|
+
if (scene !== null) bytes += scene.positions.byteLength + scene.links.byteLength;
|
|
13245
|
+
if (basePointColors !== null) bytes += basePointColors.byteLength;
|
|
13246
|
+
if (baseLinkColors !== null) bytes += baseLinkColors.byteLength;
|
|
13247
|
+
if (lastLinkWidths !== null) bytes += lastLinkWidths.byteLength;
|
|
13248
|
+
bytes += metricStore.estimatedBytes();
|
|
13249
|
+
if (crossfilterEngine !== null) bytes += crossfilterEngine.estimatedBytes();
|
|
13250
|
+
if (softMask !== null) bytes += softMask.estimatedBytes();
|
|
13251
|
+
return bytes;
|
|
13252
|
+
}
|
|
11640
13253
|
function cameraZoom(factor) {
|
|
11641
13254
|
const eng = engineIfReady();
|
|
11642
13255
|
if (eng === null) return;
|
|
@@ -11709,6 +13322,7 @@ function createGraphInstance(opts) {
|
|
|
11709
13322
|
else eng.setViewport(v);
|
|
11710
13323
|
},
|
|
11711
13324
|
focusNode,
|
|
13325
|
+
emphasizeNode,
|
|
11712
13326
|
requestNodeContextMenu,
|
|
11713
13327
|
getViewState,
|
|
11714
13328
|
setViewState,
|
|
@@ -11729,6 +13343,9 @@ function createGraphInstance(opts) {
|
|
|
11729
13343
|
pickEdgeAt,
|
|
11730
13344
|
sampleEdgeHover,
|
|
11731
13345
|
sampleEdgeClick,
|
|
13346
|
+
getFrameCadence: () => frameCadence,
|
|
13347
|
+
getPerfCounters: () => perfCounters,
|
|
13348
|
+
getPerfSnapshot,
|
|
11732
13349
|
hideNodes,
|
|
11733
13350
|
showNodes,
|
|
11734
13351
|
showAll,
|
|
@@ -11933,6 +13550,73 @@ var OverviewController = class {
|
|
|
11933
13550
|
}
|
|
11934
13551
|
};
|
|
11935
13552
|
|
|
11936
|
-
|
|
13553
|
+
// src/descriptors.ts
|
|
13554
|
+
function field(path) {
|
|
13555
|
+
return path;
|
|
13556
|
+
}
|
|
13557
|
+
var TRANSFORM_OPS = /* @__PURE__ */ new Set(["identity", "number", "lowercase", "date-to-epoch-ms", "coalesce"]);
|
|
13558
|
+
function validateFieldAccessor(value) {
|
|
13559
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
13560
|
+
return { kind: "not-an-object" };
|
|
13561
|
+
}
|
|
13562
|
+
const candidate = value;
|
|
13563
|
+
if (typeof candidate.field !== "string") return { kind: "field-not-a-string" };
|
|
13564
|
+
if (candidate.transform !== void 0) {
|
|
13565
|
+
const t = candidate.transform;
|
|
13566
|
+
if (t === null || typeof t !== "object" || Array.isArray(t)) {
|
|
13567
|
+
return { kind: "transform-not-an-object" };
|
|
13568
|
+
}
|
|
13569
|
+
const op = t.op;
|
|
13570
|
+
if (typeof op !== "string" || !TRANSFORM_OPS.has(op)) {
|
|
13571
|
+
return { kind: "unknown-transform-op", op: String(op) };
|
|
13572
|
+
}
|
|
13573
|
+
if (op === "coalesce" && !("value" in t)) {
|
|
13574
|
+
return { kind: "coalesce-missing-value" };
|
|
13575
|
+
}
|
|
13576
|
+
}
|
|
13577
|
+
return null;
|
|
13578
|
+
}
|
|
13579
|
+
function isFieldAccessor(value) {
|
|
13580
|
+
return validateFieldAccessor(value) === null;
|
|
13581
|
+
}
|
|
13582
|
+
function descriptorKey(accessor) {
|
|
13583
|
+
const t = accessor.transform;
|
|
13584
|
+
if (t === void 0) return `f:${accessor.field}|identity`;
|
|
13585
|
+
if (t.op === "coalesce") return `f:${accessor.field}|coalesce:${JSON.stringify(t.value ?? null)}`;
|
|
13586
|
+
return `f:${accessor.field}|${t.op}`;
|
|
13587
|
+
}
|
|
13588
|
+
function coerceNumber(value) {
|
|
13589
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
13590
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
13591
|
+
const parsed = Number(value);
|
|
13592
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
13593
|
+
}
|
|
13594
|
+
return null;
|
|
13595
|
+
}
|
|
13596
|
+
function coerceEpochMs(value) {
|
|
13597
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
13598
|
+
if (typeof value === "string") {
|
|
13599
|
+
const parsed = Date.parse(value);
|
|
13600
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
13601
|
+
}
|
|
13602
|
+
return null;
|
|
13603
|
+
}
|
|
13604
|
+
function evaluateFieldAccessor(accessor, id, attrs) {
|
|
13605
|
+
const raw = accessor.field === "id" ? id : attrs?.[accessor.field];
|
|
13606
|
+
const t = accessor.transform;
|
|
13607
|
+
if (t === void 0 || t.op === "identity") return raw;
|
|
13608
|
+
switch (t.op) {
|
|
13609
|
+
case "number":
|
|
13610
|
+
return coerceNumber(raw);
|
|
13611
|
+
case "lowercase":
|
|
13612
|
+
return typeof raw === "string" ? raw.toLowerCase() : null;
|
|
13613
|
+
case "date-to-epoch-ms":
|
|
13614
|
+
return coerceEpochMs(raw);
|
|
13615
|
+
case "coalesce":
|
|
13616
|
+
return raw === null || raw === void 0 ? t.value : raw;
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
|
|
13620
|
+
export { ATLAS_MAX_CONCURRENT_DEFAULT, ATLAS_MAX_ENTRIES_DEFAULT, ATLAS_MAX_RETRIES_DEFAULT, AcceptanceQueue, BASE_PENDING_KEY, BRUSH_HISTORY_COALESCE_MS, CATEGORICAL_PALETTE, DEFAULT_BIN_COUNT, DIM_ALPHA_DEFAULT, DIVERGING_RANGE_DEFAULT, DomainStore, EDGE_PICK_TOLERANCE_PX, EdgePickingFacade, GRAPH_THEME_DARK, GRAPH_THEME_LIGHT, HISTORY_LIMIT_DEFAULT, HistoryKernel, INGEST_MAX_FLUSH_LATENCY_MS_DEFAULT, INGEST_MAX_PENDING_BYTES_DEFAULT, INGEST_OVERFLOW_FACTOR, ImageAtlasPipeline, LABEL_MAX_VISIBLE_CAP, LABEL_MAX_VISIBLE_DEFAULT, LinkPickIndex, META_EDGE_MAX_WIDTH, MetricStore, OVERVIEW_HOT_INTERVAL_MS, OVERVIEW_IDLE_INTERVAL_MS, OVERVIEW_SIZE_DEFAULT, OrbitOperationError, OverviewController, PHYSICAL_DEFAULT_LINK_WIDTH, PHYSICAL_DEFAULT_POINT_SIZE, PendingExpansions, Reconciler, SEARCH_CACHE_LIMIT, SEARCH_LIMIT_DEFAULT, SEARCH_SCAN_CHUNK, SEARCH_SCORE_EXACT_ID, SEARCH_SCORE_ID_PREFIX, SEARCH_SCORE_SUBSTRING, SEARCH_SCORE_TOKEN_START_BONUS, SEQUENTIAL_RANGE_DEFAULT, SUPER_NODE_MAX_SIZE, SVG_MAX_ELEMENTS_DEFAULT, SoftMask, SvgBudgetError, TIMELINE_STEP_DEFAULT, TIMELINE_TICK_MS_DEFAULT, TypedColumnCrossfilter, VIEW_STATE_VERSION, admitServiceResult, assertCapabilityMethodParity, baseFromAccepted, baseFromContribution, buildAcceptedAdjacency, buildAcceptedFromColumnar, buildAdjacency, canonicalFilterKey, canonicalJson2 as canonicalJson, canonicalScaleKey, cascadeEdges, categoricalIndex, categoricalRows, coerceNumeric, coerceNumericInto, collapseParallelEdges, columnarArrayBuffers, compileEdgeFilter, compileNodeFilter, computeNumericDomain, createGraphInstance, createLocalExpansionService, createLocalSearchService, createRequestContext, deriveGroupsByKey, descriptorKey, detachColumnarBuffers, divergingColor, escapeXml, estimateBatchBytes, evaluateFieldAccessor, evaluateFilterExpr, field, graphErrorToError, groupByDerivedId, groupSceneKey, interpolateColor, isColumnarSnapshot, isFatalGraphError, isFieldAccessor, materializeColumnarSnapshot, medianLinkWidthPx, mergeDiagnostics, mergeModel, metaEdgePublicId, metaEdgeSceneKey, metaEdgeWidthFor, neighborsOf, newContribution, newStagingTallies, nextRequestId, normalizeCommitForCapabilities, parseColor, pointSegmentDistanceSquared, projectColors, projectSizes, renderSvg, resolveEnginePolicy, resolveFilterField, resolveManualGroups, resolveScope, resolveTheme, resourceLimitFatal, rewriteGroups, sameDataRef, sameGroupBySpec, sameGroupSpecArrays, sceneGroupsOf, sceneLinkRefAt, scenePointRefAt, selectLabelCandidates, sequentialColor, sequentialSize, serviceCacheKey, sessionCommitDiagnostics, stageBatch, superNodeSizeFor, validateColumnarStructure, validateFieldAccessor, validateFilterExpr, validateGroupBySpec, validateGroupSpecs, validateSnapshot, validateViewState };
|
|
11937
13621
|
//# sourceMappingURL=index.js.map
|
|
11938
13622
|
//# sourceMappingURL=index.js.map
|