@genome-spy/core 0.68.0 → 0.69.1
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/bundle/index.es.js +12119 -10681
- package/dist/bundle/index.js +119 -119
- package/dist/schema.json +6224 -6319
- package/dist/src/data/dataFlow.d.ts.map +1 -1
- package/dist/src/data/dataFlow.js +10 -0
- package/dist/src/data/flowNode.d.ts +25 -10
- package/dist/src/data/flowNode.d.ts.map +1 -1
- package/dist/src/data/flowNode.js +66 -13
- package/dist/src/data/flowTestUtils.d.ts +2 -2
- package/dist/src/data/flowTestUtils.d.ts.map +1 -1
- package/dist/src/data/flowTestUtils.js +5 -4
- package/dist/src/data/sources/dataSource.js +2 -2
- package/dist/src/data/sources/lazy/bigBedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigBedSource.js +11 -10
- package/dist/src/data/sources/lazy/bigWigSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigWigSource.js +11 -10
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +1 -1
- package/dist/src/data/sources/lazy/tabixSource.d.ts +0 -1
- package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/tabixSource.js +41 -11
- package/dist/src/data/sources/sequenceSource.d.ts.map +1 -1
- package/dist/src/data/sources/sequenceSource.js +5 -3
- package/dist/src/data/sources/urlSource.d.ts.map +1 -1
- package/dist/src/data/sources/urlSource.js +7 -3
- package/dist/src/data/transforms/filter.d.ts +4 -4
- package/dist/src/data/transforms/filter.d.ts.map +1 -1
- package/dist/src/data/transforms/filter.js +13 -7
- package/dist/src/data/transforms/filterScoredLabels.d.ts.map +1 -1
- package/dist/src/data/transforms/filterScoredLabels.js +11 -6
- package/dist/src/data/transforms/filterScoredLabels.test.d.ts +2 -0
- package/dist/src/data/transforms/filterScoredLabels.test.d.ts.map +1 -0
- package/dist/src/data/transforms/formula.d.ts +4 -4
- package/dist/src/data/transforms/formula.d.ts.map +1 -1
- package/dist/src/data/transforms/formula.js +12 -6
- package/dist/src/data/transforms/measureText.d.ts +2 -2
- package/dist/src/data/transforms/measureText.d.ts.map +1 -1
- package/dist/src/data/transforms/measureText.js +16 -12
- package/dist/src/data/transforms/transform.d.ts +2 -2
- package/dist/src/data/transforms/transform.d.ts.map +1 -1
- package/dist/src/data/transforms/transform.js +3 -3
- package/dist/src/encoder/accessor.d.ts +8 -4
- package/dist/src/encoder/accessor.d.ts.map +1 -1
- package/dist/src/encoder/accessor.js +10 -10
- package/dist/src/encoder/encoder.js +5 -5
- package/dist/src/genome/genome.d.ts +8 -0
- package/dist/src/genome/genome.d.ts.map +1 -1
- package/dist/src/genome/genome.js +16 -1
- package/dist/src/genomeSpy/inputBindingManager.js +1 -1
- package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
- package/dist/src/genomeSpy/interactionController.js +7 -1
- package/dist/src/genomeSpy.js +1 -1
- package/dist/src/gl/glslScaleGenerator.js +1 -1
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +22 -30
- package/dist/src/marks/point.d.ts.map +1 -1
- package/dist/src/marks/point.js +4 -6
- package/dist/src/paramRuntime/expressionCompiler.d.ts +7 -0
- package/dist/src/paramRuntime/expressionCompiler.d.ts.map +1 -0
- package/dist/src/paramRuntime/expressionCompiler.js +10 -0
- package/dist/src/paramRuntime/expressionRef.d.ts +20 -0
- package/dist/src/paramRuntime/expressionRef.d.ts.map +1 -0
- package/dist/src/paramRuntime/expressionRef.js +95 -0
- package/dist/src/paramRuntime/expressionRef.test.d.ts +2 -0
- package/dist/src/paramRuntime/expressionRef.test.d.ts.map +1 -0
- package/dist/src/paramRuntime/graphRuntime.d.ts +176 -0
- package/dist/src/paramRuntime/graphRuntime.d.ts.map +1 -0
- package/dist/src/paramRuntime/graphRuntime.js +628 -0
- package/dist/src/paramRuntime/graphRuntime.test.d.ts +2 -0
- package/dist/src/paramRuntime/graphRuntime.test.d.ts.map +1 -0
- package/dist/src/paramRuntime/index.d.ts +9 -0
- package/dist/src/paramRuntime/index.d.ts.map +1 -0
- package/dist/src/paramRuntime/index.js +8 -0
- package/dist/src/paramRuntime/lifecycleRegistry.d.ts +27 -0
- package/dist/src/paramRuntime/lifecycleRegistry.d.ts.map +1 -0
- package/dist/src/paramRuntime/lifecycleRegistry.js +54 -0
- package/dist/src/paramRuntime/paramRuntime.d.ts +165 -0
- package/dist/src/paramRuntime/paramRuntime.d.ts.map +1 -0
- package/dist/src/paramRuntime/paramRuntime.js +222 -0
- package/dist/src/paramRuntime/paramRuntime.test.d.ts +2 -0
- package/dist/src/paramRuntime/paramRuntime.test.d.ts.map +1 -0
- package/dist/src/paramRuntime/paramStore.d.ts +68 -0
- package/dist/src/paramRuntime/paramStore.d.ts.map +1 -0
- package/dist/src/paramRuntime/paramStore.js +148 -0
- package/dist/src/paramRuntime/paramStore.test.d.ts +2 -0
- package/dist/src/paramRuntime/paramStore.test.d.ts.map +1 -0
- package/dist/src/paramRuntime/paramUtils.d.ts +86 -0
- package/dist/src/paramRuntime/paramUtils.d.ts.map +1 -0
- package/dist/src/paramRuntime/paramUtils.js +272 -0
- package/dist/src/paramRuntime/selectionStore.d.ts +6 -0
- package/dist/src/paramRuntime/selectionStore.d.ts.map +1 -0
- package/dist/src/paramRuntime/selectionStore.js +13 -0
- package/dist/src/paramRuntime/types.d.ts +16 -0
- package/dist/src/paramRuntime/types.d.ts.map +1 -0
- package/dist/src/paramRuntime/types.js +25 -0
- package/dist/src/paramRuntime/viewParamRuntime.d.ts +164 -0
- package/dist/src/paramRuntime/viewParamRuntime.d.ts.map +1 -0
- package/dist/src/paramRuntime/viewParamRuntime.js +443 -0
- package/dist/src/scales/scaleInstanceManager.d.ts +6 -3
- package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
- package/dist/src/scales/scaleInstanceManager.js +17 -11
- package/dist/src/scales/scaleResolution.d.ts +1 -0
- package/dist/src/scales/scaleResolution.d.ts.map +1 -1
- package/dist/src/scales/scaleResolution.js +7 -1
- package/dist/src/selection/selection.js +1 -1
- package/dist/src/spec/coreSchemaRoot.d.ts +53 -0
- package/dist/src/spec/root.d.ts +1 -1
- package/dist/src/spec/view.d.ts +114 -33
- package/dist/src/tooltip/dataTooltipHandler.d.ts +1 -1
- package/dist/src/tooltip/dataTooltipHandler.js +23 -32
- package/dist/src/tooltip/dataTooltipHandler.test.d.ts +2 -0
- package/dist/src/tooltip/dataTooltipHandler.test.d.ts.map +1 -0
- package/dist/src/tooltip/flattenDatumRows.d.ts +13 -0
- package/dist/src/tooltip/flattenDatumRows.d.ts.map +1 -0
- package/dist/src/tooltip/flattenDatumRows.js +47 -0
- package/dist/src/tooltip/flattenDatumRows.test.d.ts +2 -0
- package/dist/src/tooltip/flattenDatumRows.test.d.ts.map +1 -0
- package/dist/src/tooltip/refseqGeneTooltipHandler.d.ts +1 -1
- package/dist/src/tooltip/refseqGeneTooltipHandler.js +7 -1
- package/dist/src/tooltip/tooltipContext.d.ts +13 -0
- package/dist/src/tooltip/tooltipContext.d.ts.map +1 -0
- package/dist/src/tooltip/tooltipContext.js +543 -0
- package/dist/src/tooltip/tooltipContext.test.d.ts +2 -0
- package/dist/src/tooltip/tooltipContext.test.d.ts.map +1 -0
- package/dist/src/tooltip/tooltipHandler.d.ts +40 -1
- package/dist/src/tooltip/tooltipHandler.d.ts.map +1 -1
- package/dist/src/tooltip/tooltipHandler.ts +62 -1
- package/dist/src/types/encoder.d.ts +1 -1
- package/dist/src/utils/inputBinding.d.ts +10 -2
- package/dist/src/utils/inputBinding.d.ts.map +1 -1
- package/dist/src/utils/inputBinding.js +12 -3
- package/dist/src/view/flowBuilder.d.ts.map +1 -1
- package/dist/src/view/flowBuilder.js +12 -3
- package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
- package/dist/src/view/gridView/gridChild.js +8 -3
- package/dist/src/view/gridView/selectionRect.d.ts +6 -10
- package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
- package/dist/src/view/gridView/selectionRect.js +3 -20
- package/dist/src/view/layerView.d.ts.map +1 -1
- package/dist/src/view/layerView.js +4 -2
- package/dist/src/view/multiscale.d.ts +35 -0
- package/dist/src/view/multiscale.d.ts.map +1 -0
- package/dist/src/view/multiscale.js +233 -0
- package/dist/src/view/multiscale.test.d.ts +2 -0
- package/dist/src/view/multiscale.test.d.ts.map +1 -0
- package/dist/src/view/unitView.d.ts.map +1 -1
- package/dist/src/view/unitView.js +10 -4
- package/dist/src/view/view.d.ts +5 -4
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +223 -28
- package/dist/src/view/viewFactory.d.ts +0 -12
- package/dist/src/view/viewFactory.d.ts.map +1 -1
- package/dist/src/view/viewFactory.js +35 -24
- package/dist/src/view/viewParamRuntime.test.d.ts +2 -0
- package/dist/src/view/viewParamRuntime.test.d.ts.map +1 -0
- package/dist/src/view/viewSelectors.d.ts.map +1 -1
- package/dist/src/view/viewSelectors.js +8 -5
- package/package.json +3 -3
- package/dist/src/spec/sampleView.d.ts +0 -197
- package/dist/src/view/paramMediator.d.ts +0 -168
- package/dist/src/view/paramMediator.d.ts.map +0 -1
- package/dist/src/view/paramMediator.js +0 -545
- package/dist/src/view/paramMediator.test.d.ts +0 -2
- package/dist/src/view/paramMediator.test.d.ts.map +0 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import FlatQueue from "flatqueue";
|
|
2
|
+
|
|
3
|
+
const RUNTIME_NODE = Symbol("runtimeNode");
|
|
4
|
+
const PRIORITY_STRIDE = 1000000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {(listener: () => void) => () => void} SubscribeFn
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* id: string,
|
|
11
|
+
* rank: number,
|
|
12
|
+
* disposed: boolean,
|
|
13
|
+
* listeners: Set<() => void>,
|
|
14
|
+
* subscribe: SubscribeFn
|
|
15
|
+
* }} RuntimeNodeBase
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @template T
|
|
20
|
+
* @typedef {RuntimeNodeBase & {
|
|
21
|
+
* value: T,
|
|
22
|
+
* kind: "base" | "selection",
|
|
23
|
+
* name: string
|
|
24
|
+
* }} WritableNode
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @template T
|
|
29
|
+
* @typedef {RuntimeNodeBase & {
|
|
30
|
+
* value: T,
|
|
31
|
+
* kind: "derived",
|
|
32
|
+
* name: string,
|
|
33
|
+
* fn: () => T
|
|
34
|
+
* }} ComputedNode
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {{
|
|
39
|
+
* id: string,
|
|
40
|
+
* rank: number,
|
|
41
|
+
* disposed: boolean,
|
|
42
|
+
* fn: () => void
|
|
43
|
+
* }} EffectNode
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {import("./lifecycleRegistry.js").default | undefined} lifecycleRegistry
|
|
48
|
+
* @returns {(ownerId: string, disposer: () => void) => void}
|
|
49
|
+
*/
|
|
50
|
+
function createDisposerBinder(lifecycleRegistry) {
|
|
51
|
+
if (!lifecycleRegistry) {
|
|
52
|
+
return () => undefined;
|
|
53
|
+
} else {
|
|
54
|
+
return (ownerId, disposer) => {
|
|
55
|
+
lifecycleRegistry.addDisposer(ownerId, disposer);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @template T
|
|
62
|
+
* @param {RuntimeNodeBase & { value: T, kind: "base" | "derived" | "selection", name: string }} node
|
|
63
|
+
* @param {(value: T) => void} [setter]
|
|
64
|
+
* @returns {import("./types.js").ParamRef<T> | import("./types.js").WritableParamRef<T>}
|
|
65
|
+
*/
|
|
66
|
+
function createRef(node, setter) {
|
|
67
|
+
/**
|
|
68
|
+
* @type {import("./types.js").ParamRef<any>}
|
|
69
|
+
*/
|
|
70
|
+
const ref = {
|
|
71
|
+
id: node.id,
|
|
72
|
+
name: node.name,
|
|
73
|
+
kind: node.kind,
|
|
74
|
+
get() {
|
|
75
|
+
return node.value;
|
|
76
|
+
},
|
|
77
|
+
subscribe(listener) {
|
|
78
|
+
node.listeners.add(listener);
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
node.listeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
Object.defineProperty(ref, RUNTIME_NODE, {
|
|
87
|
+
enumerable: false,
|
|
88
|
+
configurable: false,
|
|
89
|
+
writable: false,
|
|
90
|
+
value: node,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (setter) {
|
|
94
|
+
return Object.assign(ref, {
|
|
95
|
+
set(
|
|
96
|
+
/** @type {T} */
|
|
97
|
+
value
|
|
98
|
+
) {
|
|
99
|
+
setter(value);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
return ref;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {import("./types.js").ParamRef<any>} ref
|
|
109
|
+
* @returns {RuntimeNodeBase}
|
|
110
|
+
*/
|
|
111
|
+
function getNode(ref) {
|
|
112
|
+
const node = /** @type {RuntimeNodeBase | undefined} */ (
|
|
113
|
+
/** @type {any} */ (ref)[RUNTIME_NODE]
|
|
114
|
+
);
|
|
115
|
+
if (!node) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"ParamRef is not bound to this graph runtime. Expected runtime-created ref."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return node;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {Set<() => void>} listeners
|
|
126
|
+
*/
|
|
127
|
+
function notify(listeners) {
|
|
128
|
+
for (const listener of listeners) {
|
|
129
|
+
listener();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Low-level reactive DAG runtime for parameter propagation.
|
|
135
|
+
*
|
|
136
|
+
* `GraphRuntime` is the scheduling engine behind `ParamRuntime`. It owns:
|
|
137
|
+
* 1. Writable source nodes (`base` and `selection`).
|
|
138
|
+
* 2. Derived/computed nodes with explicit dependencies.
|
|
139
|
+
* 3. Side-effect nodes that run after computed stabilization.
|
|
140
|
+
* 4. Transaction-aware batching and deterministic topological flushing.
|
|
141
|
+
*
|
|
142
|
+
* Typical usage:
|
|
143
|
+
* 1. Create writable refs using `createWritable(...)`.
|
|
144
|
+
* 2. Build derived refs with `computed(...)` from existing refs.
|
|
145
|
+
* 3. Attach side effects with `effect(...)` when external work is needed.
|
|
146
|
+
* 4. Batch writes with `runInTransaction(...)` when multiple updates belong to
|
|
147
|
+
* one logical state transition.
|
|
148
|
+
* 5. Await `whenPropagated(...)` when callers need a "graph is settled" barrier.
|
|
149
|
+
*
|
|
150
|
+
* Notes:
|
|
151
|
+
* 1. This class is intended for runtime internals; most call sites should use
|
|
152
|
+
* `ParamRuntime` / `ViewParamRuntime`.
|
|
153
|
+
* 2. Equality is referential (`!==`), so object writes must use new identities
|
|
154
|
+
* to trigger propagation.
|
|
155
|
+
*/
|
|
156
|
+
export default class GraphRuntime {
|
|
157
|
+
#nextNodeId = 1;
|
|
158
|
+
|
|
159
|
+
#nextQueueSequence = 1;
|
|
160
|
+
|
|
161
|
+
#transactionDepth = 0;
|
|
162
|
+
|
|
163
|
+
#scheduled = false;
|
|
164
|
+
|
|
165
|
+
#flushing = false;
|
|
166
|
+
|
|
167
|
+
/** @type {Set<ComputedNode<any>>} */
|
|
168
|
+
#dirtyComputeds = new Set();
|
|
169
|
+
|
|
170
|
+
/** @type {Set<EffectNode>} */
|
|
171
|
+
#dirtyEffects = new Set();
|
|
172
|
+
|
|
173
|
+
/** @type {FlatQueue<ComputedNode<any>>} */
|
|
174
|
+
#computedQueue = new FlatQueue();
|
|
175
|
+
|
|
176
|
+
/** @type {FlatQueue<EffectNode>} */
|
|
177
|
+
#effectQueue = new FlatQueue();
|
|
178
|
+
|
|
179
|
+
/** @type {Set<{resolve: () => void, reject: (error: Error) => void, abortHandler?: () => void, timeoutId?: ReturnType<typeof setTimeout>}>} */
|
|
180
|
+
#propagatedWaiters = new Set();
|
|
181
|
+
|
|
182
|
+
/** @type {(ownerId: string, disposer: () => void) => void} */
|
|
183
|
+
#bindDisposer;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Creates a graph runtime.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} [options]
|
|
189
|
+
* @param {import("./lifecycleRegistry.js").default} [options.lifecycleRegistry]
|
|
190
|
+
* Optional lifecycle owner registry. When provided, all created nodes
|
|
191
|
+
* are bound to owners and disposed automatically on owner disposal.
|
|
192
|
+
*/
|
|
193
|
+
constructor(options = {}) {
|
|
194
|
+
this.#bindDisposer = createDisposerBinder(options.lifecycleRegistry);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Registers a writable source node and returns a writable param ref.
|
|
199
|
+
*
|
|
200
|
+
* Write semantics:
|
|
201
|
+
* 1. A write triggers propagation only when `value !== currentValue`.
|
|
202
|
+
* 2. If `options.notify` is `false`, local listeners are not notified and
|
|
203
|
+
* downstream scheduling is skipped for writes to this ref.
|
|
204
|
+
* 3. Writing to a disposed ref throws.
|
|
205
|
+
*
|
|
206
|
+
* Lifecycle:
|
|
207
|
+
* 1. The node is owner-bound via `ownerId`.
|
|
208
|
+
* 2. Owner disposal marks the node disposed and clears listeners.
|
|
209
|
+
*
|
|
210
|
+
* @template T
|
|
211
|
+
* @param {string} ownerId
|
|
212
|
+
* @param {string} name
|
|
213
|
+
* @param {"base" | "selection"} kind
|
|
214
|
+
* @param {T} initialValue
|
|
215
|
+
* @param {{ notify?: boolean }} [options]
|
|
216
|
+
* @returns {import("./types.js").WritableParamRef<T>}
|
|
217
|
+
*/
|
|
218
|
+
createWritable(ownerId, name, kind, initialValue, options = {}) {
|
|
219
|
+
const nodeId = "n" + this.#nextNodeId++;
|
|
220
|
+
const notifyOnSet = options.notify ?? true;
|
|
221
|
+
|
|
222
|
+
const node = /** @type {WritableNode<T>} */ ({
|
|
223
|
+
id: nodeId,
|
|
224
|
+
name,
|
|
225
|
+
kind,
|
|
226
|
+
value: initialValue,
|
|
227
|
+
rank: 0,
|
|
228
|
+
disposed: false,
|
|
229
|
+
listeners: new Set(),
|
|
230
|
+
subscribe(
|
|
231
|
+
/** @type {() => void} */
|
|
232
|
+
listener
|
|
233
|
+
) {
|
|
234
|
+
node.listeners.add(listener);
|
|
235
|
+
return () => {
|
|
236
|
+
node.listeners.delete(listener);
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const setter = (
|
|
242
|
+
/** @type {T} */
|
|
243
|
+
value
|
|
244
|
+
) => {
|
|
245
|
+
if (node.disposed) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'Cannot set disposed parameter "' +
|
|
248
|
+
name +
|
|
249
|
+
'" (' +
|
|
250
|
+
nodeId +
|
|
251
|
+
")."
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (value !== node.value) {
|
|
256
|
+
node.value = value;
|
|
257
|
+
if (notifyOnSet) {
|
|
258
|
+
notify(node.listeners);
|
|
259
|
+
this.#scheduleFlush();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
this.#bindDisposer(ownerId, () => {
|
|
265
|
+
node.disposed = true;
|
|
266
|
+
node.listeners.clear();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return /** @type {import("./types.js").WritableParamRef<T>} */ (
|
|
270
|
+
createRef(node, setter)
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Registers a derived node whose value is computed from dependencies.
|
|
276
|
+
*
|
|
277
|
+
* Compute semantics:
|
|
278
|
+
* 1. Initial value is computed eagerly at registration time.
|
|
279
|
+
* 2. On dependency changes, recomputation is queued (deduplicated per flush).
|
|
280
|
+
* 3. Downstream listeners are notified only if computed value identity changes.
|
|
281
|
+
*
|
|
282
|
+
* Lifecycle:
|
|
283
|
+
* 1. Dependency subscriptions are created immediately.
|
|
284
|
+
* 2. Owner disposal unsubscribes dependencies and detaches the node.
|
|
285
|
+
*
|
|
286
|
+
* @template T
|
|
287
|
+
* @param {string} ownerId
|
|
288
|
+
* @param {string} name
|
|
289
|
+
* @param {import("./types.js").ParamRef<any>[]} deps
|
|
290
|
+
* @param {() => T} fn
|
|
291
|
+
* @returns {import("./types.js").ParamRef<T>}
|
|
292
|
+
*/
|
|
293
|
+
computed(ownerId, name, deps, fn) {
|
|
294
|
+
const depNodes = deps.map(getNode);
|
|
295
|
+
const maxRank = depNodes.reduce(
|
|
296
|
+
(previous, node) => Math.max(previous, node.rank),
|
|
297
|
+
0
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const nodeId = "n" + this.#nextNodeId++;
|
|
301
|
+
const node = /** @type {ComputedNode<T>} */ ({
|
|
302
|
+
id: nodeId,
|
|
303
|
+
name,
|
|
304
|
+
kind: "derived",
|
|
305
|
+
rank: maxRank + 1,
|
|
306
|
+
value: fn(),
|
|
307
|
+
disposed: false,
|
|
308
|
+
listeners: new Set(),
|
|
309
|
+
fn,
|
|
310
|
+
subscribe(
|
|
311
|
+
/** @type {() => void} */
|
|
312
|
+
listener
|
|
313
|
+
) {
|
|
314
|
+
node.listeners.add(listener);
|
|
315
|
+
return () => {
|
|
316
|
+
node.listeners.delete(listener);
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const onDependencyChange = () => {
|
|
322
|
+
if (!node.disposed) {
|
|
323
|
+
this.#enqueueComputed(node);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const unsubscribers = deps.map((dep) =>
|
|
328
|
+
dep.subscribe(onDependencyChange)
|
|
329
|
+
);
|
|
330
|
+
const dispose = () => {
|
|
331
|
+
if (node.disposed) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
node.disposed = true;
|
|
336
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
337
|
+
node.listeners.clear();
|
|
338
|
+
this.#dirtyComputeds.delete(node);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
this.#bindDisposer(ownerId, dispose);
|
|
342
|
+
|
|
343
|
+
return /** @type {import("./types.js").ParamRef<T>} */ (
|
|
344
|
+
createRef(node)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Registers an effect node that runs after computed propagation.
|
|
350
|
+
*
|
|
351
|
+
* Effect semantics:
|
|
352
|
+
* 1. Effect callbacks are queued on dependency changes.
|
|
353
|
+
* 2. Effects run after computed nodes for the same flush epoch.
|
|
354
|
+
* 3. Multiple dependency changes before a flush coalesce to one queued run.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} ownerId
|
|
357
|
+
* @param {import("./types.js").ParamRef<any>[]} deps
|
|
358
|
+
* @param {() => void} fn
|
|
359
|
+
* @returns {() => void} explicit disposer for manual teardown
|
|
360
|
+
*/
|
|
361
|
+
effect(ownerId, deps, fn) {
|
|
362
|
+
const depNodes = deps.map(getNode);
|
|
363
|
+
const maxRank = depNodes.reduce(
|
|
364
|
+
(previous, node) => Math.max(previous, node.rank),
|
|
365
|
+
0
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const nodeId = "n" + this.#nextNodeId++;
|
|
369
|
+
const node = /** @type {EffectNode} */ ({
|
|
370
|
+
id: nodeId,
|
|
371
|
+
rank: maxRank + 1,
|
|
372
|
+
disposed: false,
|
|
373
|
+
fn,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const onDependencyChange = () => {
|
|
377
|
+
if (!node.disposed) {
|
|
378
|
+
this.#enqueueEffect(node);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const unsubscribers = deps.map((dep) =>
|
|
383
|
+
dep.subscribe(onDependencyChange)
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const dispose = () => {
|
|
387
|
+
if (node.disposed) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
node.disposed = true;
|
|
392
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
393
|
+
this.#dirtyEffects.delete(node);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
this.#bindDisposer(ownerId, dispose);
|
|
397
|
+
|
|
398
|
+
return dispose;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Runs `fn` as an atomic update transaction for this runtime graph.
|
|
403
|
+
*
|
|
404
|
+
* Transaction intent:
|
|
405
|
+
* 1. Batch multiple source writes so downstream computeds/effects observe
|
|
406
|
+
* the final state for the batch, not each intermediate write.
|
|
407
|
+
* 2. Defer scheduling/flush until the outermost transaction exits.
|
|
408
|
+
* 3. Preserve deterministic propagation order by running one flush pass
|
|
409
|
+
* after the transaction boundary.
|
|
410
|
+
*
|
|
411
|
+
* Semantics:
|
|
412
|
+
* 1. Nested transactions are supported via depth counting.
|
|
413
|
+
* 2. Only the outermost transaction exit triggers scheduling.
|
|
414
|
+
* 3. If `fn` throws, the error is rethrown after transaction depth is
|
|
415
|
+
* restored; pending propagation is still scheduled from `finally`.
|
|
416
|
+
* 4. The scheduled flush runs in a microtask (`queueMicrotask`) after the
|
|
417
|
+
* outermost transaction exits.
|
|
418
|
+
* 5. This method does not force immediate synchronous propagation. Use
|
|
419
|
+
* `flushNow()` when the caller explicitly requires immediate flushing.
|
|
420
|
+
*
|
|
421
|
+
* @template T
|
|
422
|
+
* @param {() => T} fn
|
|
423
|
+
* @returns {T}
|
|
424
|
+
*/
|
|
425
|
+
runInTransaction(fn) {
|
|
426
|
+
this.#transactionDepth += 1;
|
|
427
|
+
try {
|
|
428
|
+
return fn();
|
|
429
|
+
} finally {
|
|
430
|
+
this.#transactionDepth -= 1;
|
|
431
|
+
if (this.#transactionDepth === 0) {
|
|
432
|
+
this.#scheduleFlush();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Flushes currently queued computed/effect work immediately.
|
|
439
|
+
*
|
|
440
|
+
* Behavior:
|
|
441
|
+
* 1. No-op if called while a transaction is open.
|
|
442
|
+
* 2. No-op during re-entrant flush calls.
|
|
443
|
+
* 3. Runs computeds first, then effects, until the graph reaches a fixed
|
|
444
|
+
* point for the current queued work.
|
|
445
|
+
*/
|
|
446
|
+
flushNow() {
|
|
447
|
+
if (this.#transactionDepth > 0 || this.#flushing) {
|
|
448
|
+
return;
|
|
449
|
+
} else {
|
|
450
|
+
this.#scheduled = false;
|
|
451
|
+
this.#flushing = true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
let hasWork = true;
|
|
456
|
+
while (hasWork) {
|
|
457
|
+
hasWork = false;
|
|
458
|
+
|
|
459
|
+
while (this.#computedQueue.length > 0) {
|
|
460
|
+
hasWork = true;
|
|
461
|
+
const node = this.#computedQueue.pop();
|
|
462
|
+
this.#dirtyComputeds.delete(node);
|
|
463
|
+
|
|
464
|
+
if (node.disposed) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const previous = node.value;
|
|
469
|
+
const next = node.fn();
|
|
470
|
+
if (next !== previous) {
|
|
471
|
+
node.value = next;
|
|
472
|
+
notify(node.listeners);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
while (this.#effectQueue.length > 0) {
|
|
477
|
+
hasWork = true;
|
|
478
|
+
const effectNode = this.#effectQueue.pop();
|
|
479
|
+
this.#dirtyEffects.delete(effectNode);
|
|
480
|
+
|
|
481
|
+
if (effectNode.disposed) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
effectNode.fn();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} finally {
|
|
489
|
+
this.#flushing = false;
|
|
490
|
+
this.#maybeResolveWhenPropagatedWaiters();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Returns a promise that resolves when currently pending graph propagation
|
|
496
|
+
* has completed (computed queue and effect queue are settled).
|
|
497
|
+
*
|
|
498
|
+
* This is a synchronization barrier for reactive propagation only. It does
|
|
499
|
+
* not include animation/time-based convergence semantics.
|
|
500
|
+
*
|
|
501
|
+
* @param {{ signal?: AbortSignal, timeoutMs?: number }} [options]
|
|
502
|
+
* Optional cancellation/timeout controls for waiting callers.
|
|
503
|
+
* @returns {Promise<void>}
|
|
504
|
+
*/
|
|
505
|
+
whenPropagated(options = {}) {
|
|
506
|
+
if (this.#isSettled()) {
|
|
507
|
+
return Promise.resolve();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const { signal, timeoutMs } = options;
|
|
511
|
+
if (signal?.aborted) {
|
|
512
|
+
return Promise.reject(new Error("whenPropagated aborted"));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
const waiter =
|
|
517
|
+
/** @type {{resolve: () => void, reject: (error: Error) => void, abortHandler?: () => void, timeoutId?: ReturnType<typeof setTimeout>}} */ ({
|
|
518
|
+
resolve,
|
|
519
|
+
reject,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (signal) {
|
|
523
|
+
waiter.abortHandler = () => {
|
|
524
|
+
this.#propagatedWaiters.delete(waiter);
|
|
525
|
+
reject(new Error("whenPropagated aborted"));
|
|
526
|
+
};
|
|
527
|
+
signal.addEventListener("abort", waiter.abortHandler, {
|
|
528
|
+
once: true,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (timeoutMs != null) {
|
|
533
|
+
waiter.timeoutId = setTimeout(() => {
|
|
534
|
+
this.#propagatedWaiters.delete(waiter);
|
|
535
|
+
if (waiter.abortHandler) {
|
|
536
|
+
signal?.removeEventListener(
|
|
537
|
+
"abort",
|
|
538
|
+
waiter.abortHandler
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
reject(
|
|
542
|
+
new Error(
|
|
543
|
+
"whenPropagated timeout after " + timeoutMs + " ms"
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.#propagatedWaiters.add(waiter);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @param {ComputedNode<any>} node
|
|
555
|
+
*/
|
|
556
|
+
#enqueueComputed(node) {
|
|
557
|
+
if (this.#dirtyComputeds.has(node)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
this.#dirtyComputeds.add(node);
|
|
562
|
+
this.#computedQueue.push(node, this.#computePriority(node.rank));
|
|
563
|
+
this.#scheduleFlush();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @param {EffectNode} node
|
|
568
|
+
*/
|
|
569
|
+
#enqueueEffect(node) {
|
|
570
|
+
if (this.#dirtyEffects.has(node)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
this.#dirtyEffects.add(node);
|
|
575
|
+
this.#effectQueue.push(node, this.#computePriority(node.rank));
|
|
576
|
+
this.#scheduleFlush();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @param {number} rank
|
|
581
|
+
*/
|
|
582
|
+
#computePriority(rank) {
|
|
583
|
+
const sequence = this.#nextQueueSequence % PRIORITY_STRIDE;
|
|
584
|
+
this.#nextQueueSequence += 1;
|
|
585
|
+
return rank * PRIORITY_STRIDE + sequence;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#scheduleFlush() {
|
|
589
|
+
if (this.#transactionDepth > 0 || this.#scheduled || this.#flushing) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.#scheduled = true;
|
|
594
|
+
|
|
595
|
+
queueMicrotask(() => {
|
|
596
|
+
this.flushNow();
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#isSettled() {
|
|
601
|
+
return (
|
|
602
|
+
this.#transactionDepth === 0 &&
|
|
603
|
+
!this.#scheduled &&
|
|
604
|
+
!this.#flushing &&
|
|
605
|
+
this.#computedQueue.length === 0 &&
|
|
606
|
+
this.#effectQueue.length === 0 &&
|
|
607
|
+
this.#dirtyComputeds.size === 0 &&
|
|
608
|
+
this.#dirtyEffects.size === 0
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#maybeResolveWhenPropagatedWaiters() {
|
|
613
|
+
if (!this.#isSettled()) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
for (const waiter of this.#propagatedWaiters) {
|
|
618
|
+
if (waiter.timeoutId) {
|
|
619
|
+
clearTimeout(waiter.timeoutId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
waiter.resolve();
|
|
623
|
+
}
|
|
624
|
+
this.#propagatedWaiters.clear();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export { getNode };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphRuntime.test.d.ts","sourceRoot":"","sources":["../../../src/paramRuntime/graphRuntime.test.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as GraphRuntime } from "./graphRuntime.js";
|
|
2
|
+
export { default as ParamRuntime } from "./paramRuntime.js";
|
|
3
|
+
export { default as ViewParamRuntime } from "./viewParamRuntime.js";
|
|
4
|
+
export { default as ParamStore } from "./paramStore.js";
|
|
5
|
+
export { default as LifecycleRegistry } from "./lifecycleRegistry.js";
|
|
6
|
+
export { compileExpression } from "./expressionCompiler.js";
|
|
7
|
+
export * from "./paramUtils.js";
|
|
8
|
+
export * from "./selectionStore.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/paramRuntime/index.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as GraphRuntime } from "./graphRuntime.js";
|
|
2
|
+
export { default as ParamRuntime } from "./paramRuntime.js";
|
|
3
|
+
export { default as ViewParamRuntime } from "./viewParamRuntime.js";
|
|
4
|
+
export { default as ParamStore } from "./paramStore.js";
|
|
5
|
+
export { default as LifecycleRegistry } from "./lifecycleRegistry.js";
|
|
6
|
+
export { compileExpression } from "./expressionCompiler.js";
|
|
7
|
+
export * from "./paramUtils.js";
|
|
8
|
+
export * from "./selectionStore.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks owner-bound cleanup callbacks for runtime resources.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* reactive graph nodes and expression subscriptions can outlive their creating
|
|
6
|
+
* call site unless they are disposed explicitly. The lifecycle registry gives
|
|
7
|
+
* `ParamRuntime`/`GraphRuntime` a single owner-based teardown mechanism so
|
|
8
|
+
* disposing a scope/view reliably detaches listeners and releases resources.
|
|
9
|
+
*/
|
|
10
|
+
export default class LifecycleRegistry {
|
|
11
|
+
/**
|
|
12
|
+
* @param {"view" | "mark" | "transform" | "source" | "scope"} kind
|
|
13
|
+
* @param {string} key
|
|
14
|
+
*/
|
|
15
|
+
createOwner(kind: "view" | "mark" | "transform" | "source" | "scope", key: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} ownerId
|
|
18
|
+
* @param {() => void} disposer
|
|
19
|
+
*/
|
|
20
|
+
addDisposer(ownerId: string, disposer: () => void): void;
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} ownerId
|
|
23
|
+
*/
|
|
24
|
+
disposeOwner(ownerId: string): void;
|
|
25
|
+
#private;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=lifecycleRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycleRegistry.d.ts","sourceRoot":"","sources":["../../../src/paramRuntime/lifecycleRegistry.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH;IAMI;;;OAGG;IACH,kBAHW,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,OAClD,MAAM,UAMhB;IAED;;;OAGG;IACH,qBAHW,MAAM,YACN,MAAM,IAAI,QASpB;IAED;;OAEG;IACH,sBAFW,MAAM,QAahB;;CACJ"}
|