@genome-spy/core 0.64.0 → 0.65.0

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.
Files changed (104) hide show
  1. package/dist/bundle/{index-CCJIjehY.js → AbortablePromiseCache-CcuMrnn7.js} +22 -91
  2. package/dist/bundle/browser-txUcLy2H.js +123 -0
  3. package/dist/bundle/index-BQpbYrv4.js +1712 -0
  4. package/dist/bundle/index-BhtHKLUo.js +73 -0
  5. package/dist/bundle/index-C0llXMqm.js +280 -0
  6. package/dist/bundle/index-CCe8rnZz.js +716 -0
  7. package/dist/bundle/index-CD7FLu9x.js +269 -0
  8. package/dist/bundle/{index-C08YCM2T.js → index-D-w7Mmt9.js} +246 -126
  9. package/dist/bundle/index-D74H8TTz.js +508 -0
  10. package/dist/bundle/index-DhcU-Gk-.js +1487 -0
  11. package/dist/bundle/index.es.js +4878 -4680
  12. package/dist/bundle/index.js +151 -167
  13. package/dist/bundle/inflate-DRgHi_KK.js +1050 -0
  14. package/dist/schema.json +9 -1
  15. package/dist/src/data/collector.d.ts +7 -2
  16. package/dist/src/data/collector.d.ts.map +1 -1
  17. package/dist/src/data/collector.js +13 -2
  18. package/dist/src/data/dataFlow.d.ts +20 -42
  19. package/dist/src/data/dataFlow.d.ts.map +1 -1
  20. package/dist/src/data/dataFlow.js +57 -80
  21. package/dist/src/data/dataFlow.test.js +35 -2
  22. package/dist/src/data/flowHandle.d.ts +15 -0
  23. package/dist/src/data/flowHandle.d.ts.map +1 -0
  24. package/dist/src/data/flowHandle.js +13 -0
  25. package/dist/src/data/flowInit.d.ts +85 -0
  26. package/dist/src/data/flowInit.d.ts.map +1 -0
  27. package/dist/src/data/flowInit.js +238 -0
  28. package/dist/src/data/flowInit.test.d.ts +2 -0
  29. package/dist/src/data/flowInit.test.d.ts.map +1 -0
  30. package/dist/src/data/flowInit.test.js +413 -0
  31. package/dist/src/data/flowOptimizer.d.ts +6 -4
  32. package/dist/src/data/flowOptimizer.d.ts.map +1 -1
  33. package/dist/src/data/flowOptimizer.js +29 -14
  34. package/dist/src/data/flowOptimizer.test.js +20 -15
  35. package/dist/src/data/sources/lazy/bamSource.js +1 -1
  36. package/dist/src/data/sources/lazy/bigBedSource.js +1 -1
  37. package/dist/src/data/sources/lazy/bigWigSource.js +1 -1
  38. package/dist/src/data/sources/lazy/gff3Source.d.ts +2 -6
  39. package/dist/src/data/sources/lazy/gff3Source.d.ts.map +1 -1
  40. package/dist/src/data/sources/lazy/gff3Source.js +4 -8
  41. package/dist/src/data/sources/lazy/indexedFastaSource.d.ts.map +1 -1
  42. package/dist/src/data/sources/lazy/indexedFastaSource.js +17 -17
  43. package/dist/src/data/sources/lazy/tabixSource.js +1 -1
  44. package/dist/src/genomeSpy.d.ts +1 -1
  45. package/dist/src/genomeSpy.d.ts.map +1 -1
  46. package/dist/src/genomeSpy.js +18 -61
  47. package/dist/src/marks/mark.d.ts +1 -0
  48. package/dist/src/marks/mark.d.ts.map +1 -1
  49. package/dist/src/marks/mark.js +22 -1
  50. package/dist/src/spec/sampleView.d.ts +3 -2
  51. package/dist/src/types/viewContext.d.ts +1 -1
  52. package/dist/src/view/axisResolution.d.ts +5 -0
  53. package/dist/src/view/axisResolution.d.ts.map +1 -1
  54. package/dist/src/view/axisResolution.js +16 -1
  55. package/dist/src/view/facetView.d.ts.map +1 -1
  56. package/dist/src/view/facetView.js +1 -0
  57. package/dist/src/view/flowBuilder.d.ts +2 -2
  58. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  59. package/dist/src/view/flowBuilder.js +21 -4
  60. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  61. package/dist/src/view/gridView/gridView.js +13 -0
  62. package/dist/src/view/gridView/selectionRect.d.ts +8 -4
  63. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  64. package/dist/src/view/gridView/selectionRect.js +28 -3
  65. package/dist/src/view/gridView/selectionRect.test.d.ts +2 -0
  66. package/dist/src/view/gridView/selectionRect.test.d.ts.map +1 -0
  67. package/dist/src/view/gridView/selectionRect.test.js +87 -0
  68. package/dist/src/view/paramMediator.d.ts +2 -1
  69. package/dist/src/view/paramMediator.d.ts.map +1 -1
  70. package/dist/src/view/paramMediator.js +13 -1
  71. package/dist/src/view/paramMediator.test.js +22 -0
  72. package/dist/src/view/scaleResolution.d.ts +5 -0
  73. package/dist/src/view/scaleResolution.d.ts.map +1 -1
  74. package/dist/src/view/scaleResolution.js +10 -0
  75. package/dist/src/view/testUtils.d.ts.map +1 -1
  76. package/dist/src/view/testUtils.js +16 -4
  77. package/dist/src/view/unitView.d.ts.map +1 -1
  78. package/dist/src/view/unitView.js +58 -8
  79. package/dist/src/view/view.d.ts +17 -1
  80. package/dist/src/view/view.d.ts.map +1 -1
  81. package/dist/src/view/view.js +57 -1
  82. package/dist/src/view/viewDispose.test.d.ts +2 -0
  83. package/dist/src/view/viewDispose.test.d.ts.map +1 -0
  84. package/dist/src/view/viewDispose.test.js +110 -0
  85. package/dist/src/view/viewUtils.d.ts +4 -4
  86. package/dist/src/view/viewUtils.d.ts.map +1 -1
  87. package/dist/src/view/viewUtils.js +19 -15
  88. package/dist/src/view/viewUtils.test.d.ts +2 -0
  89. package/dist/src/view/viewUtils.test.d.ts.map +1 -0
  90. package/dist/src/view/viewUtils.test.js +87 -0
  91. package/package.json +10 -10
  92. package/dist/bundle/__vite-browser-external-C--ziKoh.js +0 -8
  93. package/dist/bundle/_commonjsHelpers-DjF3Plf2.js +0 -26
  94. package/dist/bundle/index-5ajWdKly.js +0 -1319
  95. package/dist/bundle/index-B03-Om4z.js +0 -274
  96. package/dist/bundle/index-BftNdA0O.js +0 -27
  97. package/dist/bundle/index-Bg7C4Xat.js +0 -2750
  98. package/dist/bundle/index-C3QR8Lv6.js +0 -2131
  99. package/dist/bundle/index-DTcHjAHp.js +0 -505
  100. package/dist/bundle/index-DnIkxb0L.js +0 -1025
  101. package/dist/bundle/index-Ww3TAo6_.js +0 -71
  102. package/dist/bundle/index-g8iXgW0W.js +0 -651
  103. package/dist/bundle/long-B-FASCSo.js +0 -2387
  104. package/dist/bundle/remoteFile-BuaqFGWk.js +0 -94
@@ -0,0 +1,238 @@
1
+ import UnitView from "../view/unitView.js";
2
+ import { buildDataFlow } from "../view/flowBuilder.js";
3
+ import { optimizeDataFlow } from "./flowOptimizer.js";
4
+ import { VISIT_SKIP } from "../view/view.js";
5
+ import { reconfigureScales } from "../view/scaleResolution.js";
6
+
7
+ /** @type {WeakMap<import("./sources/dataSource.js").default, Promise<void>>} */
8
+ const inFlightLoads = new WeakMap();
9
+
10
+ /**
11
+ * Deduplicate concurrent loads for shared sources without changing propagation.
12
+ *
13
+ * Data sources still propagate rows immediately during `load()`/`loadSynchronously`
14
+ * and do not retain data. This helper only prevents overlapping `load()` calls
15
+ * from running twice; collectors remain the sole in-memory cache. Once the load
16
+ * promise settles, the source may be loaded again later as usual.
17
+ *
18
+ * @param {import("./sources/dataSource.js").default} dataSource
19
+ * @returns {Promise<void>}
20
+ */
21
+ function loadDataSourceOnce(dataSource) {
22
+ const existing = inFlightLoads.get(dataSource);
23
+ if (existing) {
24
+ return existing;
25
+ }
26
+
27
+ const loadPromise = Promise.resolve()
28
+ .then(() => dataSource.load())
29
+ .finally(() => {
30
+ inFlightLoads.delete(dataSource);
31
+ });
32
+
33
+ inFlightLoads.set(dataSource, loadPromise);
34
+ return loadPromise;
35
+ }
36
+
37
+ /**
38
+ * Synchronize flow handles after data flow optimization.
39
+ *
40
+ * @param {import("../view/view.js").default} root
41
+ * @param {Map<import("./sources/dataSource.js").default, import("./sources/dataSource.js").default>} canonicalBySource
42
+ */
43
+ export function syncFlowHandles(root, canonicalBySource) {
44
+ for (const view of root.getDescendants()) {
45
+ const handle = view.flowHandle;
46
+ if (!handle) {
47
+ continue;
48
+ }
49
+
50
+ const dataSource = handle.dataSource;
51
+ if (dataSource) {
52
+ handle.dataSource = canonicalBySource.get(dataSource) ?? dataSource;
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Initializes data flow and mark wiring for a subtree without rebuilding the
59
+ * entire view hierarchy. This is the primary entry point for dynamic view
60
+ * insertion: build the subtree fully, call this, then attach the subtree to
61
+ * the live hierarchy.
62
+ *
63
+ * What it does:
64
+ * - builds/extends the dataflow graph for the subtree
65
+ * - runs flow optimization and syncs flow handles to canonical data sources
66
+ * - discovers the nearest data sources for views in the subtree
67
+ * - initializes dataflow nodes (initialize) for those sources
68
+ * - initializes mark encoders for unit views
69
+ * - queues graphics initialization (if a GL context exists)
70
+ * - wires collector observers so marks update on data arrival
71
+ *
72
+ * How to use it:
73
+ * - call after the subtree is fully constructed (post-order build)
74
+ * - do not attach the subtree to the live hierarchy until after this call
75
+ * - dispose the old subtree before replacing it to prevent observer leaks
76
+ * - follow up with finalizeSubtreeGraphics(...) once graphics promises resolve
77
+ * - reconfigure scales for the subtree when data loads complete
78
+ *
79
+ * Considerations:
80
+ * - this does not trigger data loading; callers decide when to load
81
+ * - data sources are derived by walking to the nearest ancestor source; nested
82
+ * sources should be treated as boundaries (do not walk past them)
83
+ * - only call updateGraphicsData when graphics are initialized or a GL context
84
+ * is available; headless/test contexts must avoid WebGL usage
85
+ * - loadViewSubtreeData emits a subtree-scoped "subtreeDataReady" broadcast
86
+ *
87
+ * TODO:
88
+ * - promote in-flight load caching to a persistent load-state per source
89
+ * - replace global dataLoaded usage with subtree-scoped readiness
90
+ * - integrate with async font readiness for text marks
91
+ * - unify observer wiring via a disposable registry across view types
92
+ *
93
+ * @param {import("../view/view.js").default} subtreeRoot
94
+ * @param {import("./dataFlow.js").default} flow
95
+ * @returns {{
96
+ * dataFlow: import("./dataFlow.js").default,
97
+ * unitViews: UnitView[],
98
+ * dataSources: Set<import("./sources/dataSource.js").default>,
99
+ * graphicsPromises: Promise<import("../marks/mark.js").default>[]
100
+ * }}
101
+ */
102
+ export function initializeViewSubtree(subtreeRoot, flow) {
103
+ const dataFlow = buildDataFlow(subtreeRoot, flow);
104
+ const canonicalBySource = optimizeDataFlow(dataFlow);
105
+ syncFlowHandles(subtreeRoot, canonicalBySource);
106
+ const subtreeViews = subtreeRoot.getDescendants();
107
+ const dataSources = collectViewSubtreeDataSources(subtreeViews);
108
+
109
+ // Initialize flow nodes for the sources that belong to this subtree.
110
+ for (const dataSource of dataSources) {
111
+ dataSource.visit((node) => node.initialize());
112
+ }
113
+
114
+ /** @type {UnitView[]} */
115
+ const unitViews = subtreeViews.filter((view) => view instanceof UnitView);
116
+
117
+ /** @type {Promise<import("../marks/mark.js").default>[]} */
118
+ const graphicsPromises = [];
119
+
120
+ const canInitializeGraphics = !!subtreeRoot.context.glHelper;
121
+
122
+ for (const view of unitViews) {
123
+ const mark = view.mark;
124
+ // Encoders can be initialized immediately; graphics need a GL context.
125
+ mark.initializeEncoders();
126
+ if (canInitializeGraphics) {
127
+ graphicsPromises.push(mark.initializeGraphics().then(() => mark));
128
+ }
129
+
130
+ // Wire collector completion to mark data/graphics updates.
131
+ const observer = (
132
+ /** @type {import("./collector.js").default} */ _collector
133
+ ) => {
134
+ mark.initializeData(); // does faceting
135
+ if (canInitializeGraphics) {
136
+ try {
137
+ mark.updateGraphicsData();
138
+ } catch (e) {
139
+ e.view = view;
140
+ throw e;
141
+ }
142
+ }
143
+ view.context.animator.requestRender();
144
+ };
145
+ view.registerDisposer(view.flowHandle.collector.observe(observer));
146
+ }
147
+
148
+ return {
149
+ dataFlow,
150
+ unitViews,
151
+ dataSources,
152
+ graphicsPromises,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Collects data sources needed to initialize all views in the subtree.
158
+ * This includes sources that are overridden deeper in the hierarchy.
159
+ *
160
+ * @param {import("../view/view.js").default | import("../view/view.js").default[]} subtreeRoot
161
+ * @returns {Set<import("./sources/dataSource.js").default>}
162
+ */
163
+ export function collectViewSubtreeDataSources(subtreeRoot) {
164
+ const subtreeViews = Array.isArray(subtreeRoot)
165
+ ? subtreeRoot
166
+ : subtreeRoot.getDescendants();
167
+ /** @type {Set<import("./sources/dataSource.js").default>} */
168
+ const dataSources = new Set();
169
+ for (const view of subtreeViews) {
170
+ // Walk up to the nearest view that owns a data source.
171
+ let current = view;
172
+ while (current && !current.flowHandle?.dataSource) {
173
+ current = current.dataParent;
174
+ }
175
+ if (current?.flowHandle?.dataSource) {
176
+ dataSources.add(current.flowHandle.dataSource);
177
+ }
178
+ }
179
+ return dataSources;
180
+ }
181
+
182
+ /**
183
+ * Collects the nearest data sources under a subtree root.
184
+ * These sources define data-ready boundaries for subtree-level loading.
185
+ *
186
+ * @param {import("../view/view.js").default} subtreeRoot
187
+ * @returns {Set<import("./sources/dataSource.js").default>}
188
+ */
189
+ export function collectNearestViewSubtreeDataSources(subtreeRoot) {
190
+ /** @type {Set<import("./sources/dataSource.js").default>} */
191
+ const dataSources = new Set();
192
+ subtreeRoot.visit((view) => {
193
+ if (view.flowHandle?.dataSource) {
194
+ dataSources.add(view.flowHandle.dataSource);
195
+ return VISIT_SKIP;
196
+ }
197
+ });
198
+ return dataSources;
199
+ }
200
+
201
+ /**
202
+ * Loads the nearest data sources for a subtree.
203
+ * Use the returned promise as a subtree-level "data ready" signal.
204
+ *
205
+ * @param {import("../view/view.js").default} subtreeRoot
206
+ * @param {Set<import("./sources/dataSource.js").default>} [dataSources]
207
+ * @returns {Promise<void[]>}
208
+ */
209
+ export function loadViewSubtreeData(
210
+ subtreeRoot,
211
+ dataSources = collectNearestViewSubtreeDataSources(subtreeRoot)
212
+ ) {
213
+ return Promise.all(
214
+ Array.from(dataSources).map((dataSource) =>
215
+ loadDataSourceOnce(dataSource)
216
+ )
217
+ ).then((results) => {
218
+ reconfigureScales(subtreeRoot);
219
+ broadcastSubtreeDataReady(subtreeRoot);
220
+ return results;
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Broadcasts a subtree-scoped data-ready event to views within the subtree.
226
+ *
227
+ * @param {import("../view/view.js").default} subtreeRoot
228
+ */
229
+ function broadcastSubtreeDataReady(subtreeRoot) {
230
+ /** @type {import("../view/view.js").BroadcastMessage} */
231
+ const message = {
232
+ type: /** @type {import("../genomeSpy.js").BroadcastEventType} */ (
233
+ "subtreeDataReady"
234
+ ),
235
+ payload: { subtreeRoot },
236
+ };
237
+ subtreeRoot.visit((view) => view.handleBroadcast(message));
238
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=flowInit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowInit.test.d.ts","sourceRoot":"","sources":["../../../src/data/flowInit.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,413 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+
3
+ import { createTestViewContext } from "../view/testUtils.js";
4
+ import { buildDataFlow } from "../view/flowBuilder.js";
5
+ import { optimizeDataFlow } from "./flowOptimizer.js";
6
+ import {
7
+ collectNearestViewSubtreeDataSources,
8
+ collectViewSubtreeDataSources,
9
+ initializeViewSubtree,
10
+ loadViewSubtreeData,
11
+ syncFlowHandles,
12
+ } from "./flowInit.js";
13
+
14
+ describe("flowInit", () => {
15
+ test("syncs handles to canonical data sources after merge", async () => {
16
+ const context = createTestViewContext();
17
+ context.getNamedDataFromProvider = () => [];
18
+ context.addBroadcastListener = () => undefined;
19
+ context.removeBroadcastListener = () => undefined;
20
+
21
+ /** @type {import("../spec/view.js").HConcatSpec} */
22
+ const spec = {
23
+ hconcat: [
24
+ {
25
+ data: { name: "shared" },
26
+ mark: "point",
27
+ encoding: {
28
+ x: { field: "x", type: "quantitative" },
29
+ },
30
+ },
31
+ {
32
+ data: { name: "shared" },
33
+ mark: "point",
34
+ encoding: {
35
+ x: { field: "x", type: "quantitative" },
36
+ },
37
+ },
38
+ ],
39
+ };
40
+
41
+ const root = await context.createOrImportView(spec, null, null, "root");
42
+
43
+ const flow = buildDataFlow(root, context.dataFlow);
44
+ const canonicalBySource = optimizeDataFlow(flow);
45
+ syncFlowHandles(root, canonicalBySource);
46
+
47
+ const concatRoot =
48
+ /** @type {import("../view/concatView.js").default} */ (root);
49
+ const left = concatRoot.children[0];
50
+ const right = concatRoot.children[1];
51
+
52
+ expect(left.flowHandle.dataSource).toBeDefined();
53
+ expect(right.flowHandle.dataSource).toBeDefined();
54
+ expect(left.flowHandle.dataSource).toBe(right.flowHandle.dataSource);
55
+
56
+ const sharedSources = flow.dataSources.filter(
57
+ (/** @type {import("./sources/dataSource.js").default} */ source) =>
58
+ source.identifier === "shared"
59
+ );
60
+ expect(sharedSources).toEqual([left.flowHandle.dataSource]);
61
+ });
62
+
63
+ test("initializeViewSubtree wires collector updates for subtree loads", async () => {
64
+ const context = createTestViewContext();
65
+ context.getNamedDataFromProvider = () => [];
66
+ context.addBroadcastListener = () => undefined;
67
+ context.removeBroadcastListener = () => undefined;
68
+
69
+ /** @type {import("../spec/view.js").UnitSpec} */
70
+ const spec = {
71
+ data: { values: [{ x: 1 }, { x: 2 }] },
72
+ mark: "point",
73
+ encoding: {
74
+ x: { field: "x", type: "quantitative" },
75
+ },
76
+ };
77
+
78
+ const root = await context.createOrImportView(spec, null, null, "root");
79
+ const { dataSources } = initializeViewSubtree(root, context.dataFlow);
80
+
81
+ // This guards subtree-only initialization: dynamic view rebuilds should still
82
+ // trigger mark updates when their local collectors complete.
83
+ const unitView = /** @type {import("../view/unitView.js").default} */ (
84
+ root
85
+ );
86
+ const initializeSpy = vi.spyOn(unitView.mark, "initializeData");
87
+
88
+ await Promise.all(
89
+ Array.from(dataSources).map((dataSource) => dataSource.load())
90
+ );
91
+
92
+ expect(initializeSpy).toHaveBeenCalledTimes(1);
93
+ initializeSpy.mockRestore();
94
+ });
95
+
96
+ test("disposeSubtree removes observers before rebuilding subtree", async () => {
97
+ const context = createTestViewContext();
98
+ context.getNamedDataFromProvider = () => [{ x: 1 }];
99
+ context.addBroadcastListener = () => undefined;
100
+ context.removeBroadcastListener = () => undefined;
101
+
102
+ /** @type {import("../spec/view.js").UnitSpec} */
103
+ const spec = {
104
+ data: { name: "shared" },
105
+ mark: "point",
106
+ encoding: {
107
+ x: { field: "x", type: "quantitative" },
108
+ },
109
+ };
110
+
111
+ const firstRoot = await context.createOrImportView(
112
+ spec,
113
+ null,
114
+ null,
115
+ "first"
116
+ );
117
+ const { dataSources: firstSources } = initializeViewSubtree(
118
+ firstRoot,
119
+ context.dataFlow
120
+ );
121
+
122
+ const firstUnit = /** @type {import("../view/unitView.js").default} */ (
123
+ firstRoot
124
+ );
125
+ const firstCollector = firstUnit.flowHandle.collector;
126
+ const firstInitializeSpy = vi.spyOn(firstUnit.mark, "initializeData");
127
+
128
+ await Promise.all(
129
+ Array.from(firstSources).map((dataSource) => dataSource.load())
130
+ );
131
+
132
+ expect(firstInitializeSpy).toHaveBeenCalledTimes(1);
133
+ firstInitializeSpy.mockRestore();
134
+
135
+ firstRoot.disposeSubtree();
136
+
137
+ // This prevents stale observers from firing after a subtree is rebuilt.
138
+ expect(firstCollector.observers.size).toBe(0);
139
+
140
+ const secondRoot = await context.createOrImportView(
141
+ spec,
142
+ null,
143
+ null,
144
+ "second"
145
+ );
146
+ const { dataSources: secondSources } = initializeViewSubtree(
147
+ secondRoot,
148
+ context.dataFlow
149
+ );
150
+
151
+ const secondUnit =
152
+ /** @type {import("../view/unitView.js").default} */ (secondRoot);
153
+ const secondInitializeSpy = vi.spyOn(secondUnit.mark, "initializeData");
154
+
155
+ await Promise.all(
156
+ Array.from(secondSources).map((dataSource) => dataSource.load())
157
+ );
158
+
159
+ expect(secondInitializeSpy).toHaveBeenCalledTimes(1);
160
+ secondInitializeSpy.mockRestore();
161
+ });
162
+
163
+ test("disposeSubtree prunes named source branches", async () => {
164
+ const context = createTestViewContext();
165
+ context.getNamedDataFromProvider = () => [{ x: 1 }];
166
+ context.addBroadcastListener = () => undefined;
167
+ context.removeBroadcastListener = () => undefined;
168
+
169
+ /** @type {import("../spec/view.js").UnitSpec} */
170
+ const spec = {
171
+ data: { name: "shared" },
172
+ mark: "point",
173
+ encoding: {
174
+ x: { field: "x", type: "quantitative" },
175
+ },
176
+ };
177
+
178
+ const root = await context.createOrImportView(spec, null, null, "root");
179
+ initializeViewSubtree(root, context.dataFlow);
180
+
181
+ const unitView = /** @type {import("../view/unitView.js").default} */ (
182
+ root
183
+ );
184
+ const dataSource = unitView.flowHandle.dataSource;
185
+
186
+ // This guards against stale flow branches when a subtree is disposed.
187
+ expect(context.dataFlow.dataSources).toContain(dataSource);
188
+ expect(dataSource.children.length).toBeGreaterThan(0);
189
+
190
+ root.disposeSubtree();
191
+
192
+ expect(dataSource.children.length).toBe(0);
193
+ expect(context.dataFlow.dataSources).not.toContain(dataSource);
194
+ });
195
+
196
+ test("collectNearestViewSubtreeDataSources stops at nested sources", async () => {
197
+ const context = createTestViewContext();
198
+ context.addBroadcastListener = () => undefined;
199
+ context.removeBroadcastListener = () => undefined;
200
+
201
+ /** @type {import("../spec/view.js").LayerSpec} */
202
+ const spec = {
203
+ data: { values: [{ x: 0 }] },
204
+ layer: [
205
+ {
206
+ data: { values: [{ x: 1 }] },
207
+ mark: "point",
208
+ encoding: {
209
+ x: { field: "x", type: "quantitative" },
210
+ },
211
+ },
212
+ {
213
+ mark: "point",
214
+ encoding: {
215
+ x: { field: "x", type: "quantitative" },
216
+ },
217
+ },
218
+ ],
219
+ };
220
+
221
+ const root = await context.createOrImportView(spec, null, null, "root");
222
+ initializeViewSubtree(root, context.dataFlow);
223
+
224
+ // Nearest-source semantics: a top-level source hides deeper sources.
225
+ const sources = collectNearestViewSubtreeDataSources(root);
226
+ expect(sources.size).toBe(1);
227
+
228
+ const [rootSource] = Array.from(sources);
229
+ const layerRoot =
230
+ /** @type {import("../view/layerView.js").default} */ (root);
231
+ const childWithSource = layerRoot.children[0];
232
+
233
+ expect(rootSource).toBe(layerRoot.flowHandle.dataSource);
234
+ expect(childWithSource.flowHandle.dataSource).not.toBe(rootSource);
235
+ });
236
+
237
+ test("loadViewSubtreeData only loads nearest sources", async () => {
238
+ const context = createTestViewContext();
239
+ context.addBroadcastListener = () => undefined;
240
+ context.removeBroadcastListener = () => undefined;
241
+
242
+ /** @type {import("../spec/view.js").LayerSpec} */
243
+ const spec = {
244
+ data: { values: [{ x: 0 }] },
245
+ layer: [
246
+ {
247
+ data: { values: [{ x: 1 }] },
248
+ mark: "point",
249
+ encoding: {
250
+ x: { field: "x", type: "quantitative" },
251
+ },
252
+ },
253
+ ],
254
+ };
255
+
256
+ const root = await context.createOrImportView(spec, null, null, "root");
257
+ initializeViewSubtree(root, context.dataFlow);
258
+
259
+ const layerRoot =
260
+ /** @type {import("../view/layerView.js").default} */ (root);
261
+ const rootSource = layerRoot.flowHandle.dataSource;
262
+ const childSource = layerRoot.children[0].flowHandle.dataSource;
263
+
264
+ const rootLoadSpy = vi.spyOn(rootSource, "load");
265
+ const childLoadSpy = vi.spyOn(childSource, "load");
266
+
267
+ // Data-ready should ignore nested sources.
268
+ await loadViewSubtreeData(root);
269
+
270
+ expect(rootLoadSpy).toHaveBeenCalledTimes(1);
271
+ expect(childLoadSpy).toHaveBeenCalledTimes(0);
272
+
273
+ rootLoadSpy.mockRestore();
274
+ childLoadSpy.mockRestore();
275
+ });
276
+
277
+ test("collectViewSubtreeDataSources includes nested sources", async () => {
278
+ const context = createTestViewContext();
279
+ context.addBroadcastListener = () => undefined;
280
+ context.removeBroadcastListener = () => undefined;
281
+
282
+ /** @type {import("../spec/view.js").LayerSpec} */
283
+ const spec = {
284
+ data: { values: [{ x: 0 }] },
285
+ layer: [
286
+ {
287
+ data: { values: [{ x: 1 }] },
288
+ mark: "point",
289
+ encoding: {
290
+ x: { field: "x", type: "quantitative" },
291
+ },
292
+ },
293
+ ],
294
+ };
295
+
296
+ const root = await context.createOrImportView(spec, null, null, "root");
297
+ initializeViewSubtree(root, context.dataFlow);
298
+
299
+ // Initialization needs the full set of sources, including nested ones.
300
+ const sources = collectViewSubtreeDataSources(root);
301
+ expect(sources.size).toBe(2);
302
+ });
303
+
304
+ test("collectNearestViewSubtreeDataSources returns child sources when root has none", async () => {
305
+ const context = createTestViewContext();
306
+ context.addBroadcastListener = () => undefined;
307
+ context.removeBroadcastListener = () => undefined;
308
+
309
+ /** @type {import("../spec/view.js").HConcatSpec} */
310
+ const spec = {
311
+ hconcat: [
312
+ {
313
+ data: { values: [{ x: 1 }] },
314
+ mark: "point",
315
+ encoding: {
316
+ x: { field: "x", type: "quantitative" },
317
+ },
318
+ },
319
+ {
320
+ data: { values: [{ x: 2 }] },
321
+ mark: "point",
322
+ encoding: {
323
+ x: { field: "x", type: "quantitative" },
324
+ },
325
+ },
326
+ ],
327
+ };
328
+
329
+ const root = await context.createOrImportView(spec, null, null, "root");
330
+ initializeViewSubtree(root, context.dataFlow);
331
+
332
+ // Without a root source, the nearest sources include the child sources.
333
+ // Layout decorations may add additional sources.
334
+ const sources = collectNearestViewSubtreeDataSources(root);
335
+ const concatRoot =
336
+ /** @type {import("../view/concatView.js").default} */ (root);
337
+ expect(sources.has(concatRoot.children[0].flowHandle.dataSource)).toBe(
338
+ true
339
+ );
340
+ expect(sources.has(concatRoot.children[1].flowHandle.dataSource)).toBe(
341
+ true
342
+ );
343
+ });
344
+
345
+ test("loadViewSubtreeData emits subtree data ready broadcast", async () => {
346
+ const context = createTestViewContext();
347
+ context.addBroadcastListener = () => undefined;
348
+ context.removeBroadcastListener = () => undefined;
349
+
350
+ /** @type {import("../spec/view.js").UnitSpec} */
351
+ const spec = {
352
+ data: { values: [{ x: 1 }] },
353
+ mark: "point",
354
+ encoding: {
355
+ x: { field: "x", type: "quantitative" },
356
+ },
357
+ };
358
+
359
+ const root = await context.createOrImportView(spec, null, null, "root");
360
+ initializeViewSubtree(root, context.dataFlow);
361
+
362
+ let calls = 0;
363
+ root._addBroadcastHandler("subtreeDataReady", (message) => {
364
+ calls += 1;
365
+ expect(message.payload.subtreeRoot).toBe(root);
366
+ });
367
+
368
+ await loadViewSubtreeData(root);
369
+
370
+ expect(calls).toBe(1);
371
+ });
372
+
373
+ test("loadViewSubtreeData deduplicates concurrent loads", async () => {
374
+ const context = createTestViewContext();
375
+ context.getNamedDataFromProvider = () => [{ x: 1 }];
376
+ context.addBroadcastListener = () => undefined;
377
+ context.removeBroadcastListener = () => undefined;
378
+
379
+ /** @type {import("../spec/view.js").HConcatSpec} */
380
+ const spec = {
381
+ data: { name: "shared" },
382
+ hconcat: [
383
+ {
384
+ mark: "point",
385
+ encoding: {
386
+ x: { field: "x", type: "quantitative" },
387
+ },
388
+ },
389
+ {
390
+ mark: "point",
391
+ encoding: {
392
+ x: { field: "x", type: "quantitative" },
393
+ },
394
+ },
395
+ ],
396
+ };
397
+
398
+ const root = await context.createOrImportView(spec, null, null, "root");
399
+ initializeViewSubtree(root, context.dataFlow);
400
+
401
+ const dataSource = root.flowHandle.dataSource;
402
+ const loadSpy = vi.spyOn(dataSource, "load");
403
+
404
+ // Prevent duplicate fetches when concurrent subtrees share a source.
405
+ await Promise.all([
406
+ loadViewSubtreeData(root, new Set([dataSource])),
407
+ loadViewSubtreeData(root, new Set([dataSource])),
408
+ ]);
409
+
410
+ expect(loadSpy).toHaveBeenCalledTimes(1);
411
+ loadSpy.mockRestore();
412
+ });
413
+ });
@@ -12,16 +12,18 @@ export function removeRedundantCloneTransforms(node: import("./flowNode.js").def
12
12
  export function removeRedundantCollectors(): void;
13
13
  export function combineAndPullCollectorsUp(): void;
14
14
  /**
15
- * @param {import("./dataFlow.js").default<any>} dataFlow
15
+ * @param {import("./dataFlow.js").default} dataFlow
16
+ * @returns {Map<import("./sources/dataSource.js").default, import("./sources/dataSource.js").default>}
16
17
  */
17
- export function combineIdenticalDataSources(dataFlow: import("./dataFlow.js").default<any>): void;
18
+ export function combineIdenticalDataSources(dataFlow: import("./dataFlow.js").default): Map<import("./sources/dataSource.js").default, import("./sources/dataSource.js").default>;
18
19
  /**
19
20
  *
20
21
  * @param {import("./flowNode.js").default} root
21
22
  */
22
23
  export function optimizeFlowGraph(root: import("./flowNode.js").default): void;
23
24
  /**
24
- * @param {import("./dataFlow.js").default<any>} dataFlow
25
+ * @param {import("./dataFlow.js").default} dataFlow
26
+ * @returns {Map<import("./sources/dataSource.js").default, import("./sources/dataSource.js").default>}
25
27
  */
26
- export function optimizeDataFlow(dataFlow: import("./dataFlow.js").default<any>): void;
28
+ export function optimizeDataFlow(dataFlow: import("./dataFlow.js").default): Map<import("./sources/dataSource.js").default, import("./sources/dataSource.js").default>;
27
29
  //# sourceMappingURL=flowOptimizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"flowOptimizer.d.ts","sourceRoot":"","sources":["../../../src/data/flowOptimizer.js"],"names":[],"mappings":"AAGA;;;GAGG;AACH,oCAHW,OAAO,eAAe,EAAE,OAAO,WAC/B,OAAO,eAAe,EAAE,OAAO,WAczC;AAED;;;;GAIG;AACH,qDAFW,OAAO,eAAe,EAAE,OAAO,iCAgCzC;AAED,kDAEC;AAED,mDAEC;AAmBD;;GAEG;AACH,sDAFW,OAAO,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,QAwB9C;AAED;;;GAGG;AACH,wCAFW,OAAO,eAAe,EAAE,OAAO,QASzC;AAED;;GAEG;AACH,2CAFW,OAAO,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,QAO9C"}
1
+ {"version":3,"file":"flowOptimizer.d.ts","sourceRoot":"","sources":["../../../src/data/flowOptimizer.js"],"names":[],"mappings":"AAGA;;;GAGG;AACH,oCAHW,OAAO,eAAe,EAAE,OAAO,WAC/B,OAAO,eAAe,EAAE,OAAO,WAczC;AAED;;;;GAIG;AACH,qDAFW,OAAO,eAAe,EAAE,OAAO,iCAgCzC;AAED,kDAEC;AAED,mDAEC;AAmBD;;;GAGG;AACH,sDAHW,OAAO,eAAe,EAAE,OAAO,GAC7B,GAAG,CAAC,OAAO,yBAAyB,EAAE,OAAO,EAAE,OAAO,yBAAyB,EAAE,OAAO,CAAC,CAoCrG;AAED;;;GAGG;AACH,wCAFW,OAAO,eAAe,EAAE,OAAO,QASzC;AAED;;;GAGG;AACH,2CAHW,OAAO,eAAe,EAAE,OAAO,GAC7B,GAAG,CAAC,OAAO,yBAAyB,EAAE,OAAO,EAAE,OAAO,yBAAyB,EAAE,OAAO,CAAC,CAQrG"}