@lazily-hub/lazily-js 0.4.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.
- package/.github/workflows/release.yml +48 -0
- package/README.md +356 -0
- package/package.json +64 -0
- package/src/collections.d.ts +110 -0
- package/src/collections.js +413 -0
- package/src/index.d.ts +416 -0
- package/src/index.js +1109 -0
- package/src/reactive.d.ts +43 -0
- package/src/reactive.js +531 -0
- package/src/sem-tree.d.ts +21 -0
- package/src/sem-tree.js +130 -0
- package/src/seq-crdt.d.ts +44 -0
- package/src/seq-crdt.js +364 -0
- package/src/stable-id.d.ts +45 -0
- package/src/stable-id.js +214 -0
- package/src/state-machine.d.ts +14 -0
- package/src/state-machine.js +78 -0
- package/src/state-projection.d.ts +114 -0
- package/src/state-projection.js +256 -0
- package/src/statechart.d.ts +154 -0
- package/src/statechart.js +678 -0
- package/src/text-crdt.d.ts +23 -0
- package/src/text-crdt.js +317 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ComputeFn<T> = () => T;
|
|
2
|
+
export type EffectRun = () => (() => void) | null | undefined;
|
|
3
|
+
export type EqualFn<T> = (a: T, b: T) => boolean;
|
|
4
|
+
|
|
5
|
+
export class SlotHandle<T = unknown> {
|
|
6
|
+
/** @internal */ constructor(id: number);
|
|
7
|
+
readonly id: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CellHandle<T = unknown> {
|
|
11
|
+
/** @internal */ constructor(id: number);
|
|
12
|
+
readonly id: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class EffectHandle {
|
|
16
|
+
/** @internal */ constructor(id: number);
|
|
17
|
+
readonly id: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SignalHandle<T = unknown> {
|
|
21
|
+
/** @internal */ constructor(slot: SlotHandle<T>, effect: EffectHandle);
|
|
22
|
+
readonly slot: SlotHandle<T>;
|
|
23
|
+
readonly effect: EffectHandle;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Context {
|
|
27
|
+
cell<T>(value: T): CellHandle<T>;
|
|
28
|
+
computed<T>(compute: ComputeFn<T>): SlotHandle<T>;
|
|
29
|
+
slot<T>(compute: ComputeFn<T>): SlotHandle<T>;
|
|
30
|
+
memo<T>(compute: ComputeFn<T>): SlotHandle<T>;
|
|
31
|
+
signal<T>(compute: ComputeFn<T>): SignalHandle<T>;
|
|
32
|
+
effect(run: EffectRun): EffectHandle;
|
|
33
|
+
get<T>(handle: SlotHandle<T>): T;
|
|
34
|
+
getCell<T>(handle: CellHandle<T>): T;
|
|
35
|
+
getSignal<T>(handle: SignalHandle<T>): T;
|
|
36
|
+
setCell<T>(handle: CellHandle<T>, value: T): void;
|
|
37
|
+
batch(run: () => void): void;
|
|
38
|
+
disposeEffect(handle: EffectHandle): void;
|
|
39
|
+
isEffectActive(handle: EffectHandle): boolean;
|
|
40
|
+
disposeSignal(handle: SignalHandle<unknown>): void;
|
|
41
|
+
isSignalActive(handle: SignalHandle<unknown>): boolean;
|
|
42
|
+
isSet<T>(handle: SlotHandle<T>): boolean;
|
|
43
|
+
}
|
package/src/reactive.js
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// Reactive dependency graph (lazily-spec/docs/reactive-graph.md) — the native
|
|
2
|
+
// JavaScript counterpart of lazily-kt's `Context` and lazily-rs's `Context`.
|
|
3
|
+
//
|
|
4
|
+
// The reactive family is Slot (lazy memoized derived) → Cell (mutable source)
|
|
5
|
+
// → Signal (eager derived), plus Effect (side-effecting observer). Reading a
|
|
6
|
+
// cell/slot/signal inside a computation auto-registers a dependency edge;
|
|
7
|
+
// mutating a cell invalidates dependents.
|
|
8
|
+
//
|
|
9
|
+
// - Lazy slots mark dirty on invalidation and recompute on the next read
|
|
10
|
+
// (pull-based, glitch-free: a slot always observes consistent inputs).
|
|
11
|
+
// - Cells use a `==` (PartialEq) guard: setting an equal value is a no-op.
|
|
12
|
+
// - `memo` adds a `==` guard so an equal recompute suppresses downstream.
|
|
13
|
+
// - Signals are eager: a backing memo slot plus a puller effect — the value is
|
|
14
|
+
// re-materialized by the time the invalidating `setCell`/`batch` returns.
|
|
15
|
+
// - Effects rerun after any tracked dependency invalidates.
|
|
16
|
+
|
|
17
|
+
function defaultEqual(a, b) {
|
|
18
|
+
if (a === b) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
25
|
+
if (a.length !== b.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
if (a[i] !== b[i]) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const aKeys = Object.keys(a);
|
|
36
|
+
const bKeys = Object.keys(b);
|
|
37
|
+
return (
|
|
38
|
+
aKeys.length === bKeys.length &&
|
|
39
|
+
aKeys.every((k) => Object.is(aKeys[k], bKeys[k])) &&
|
|
40
|
+
aKeys.every((k) => defaultEqual(a[k], b[k]))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// -- Handles -----------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export class SlotHandle {
|
|
47
|
+
/** @internal */ constructor(id) {
|
|
48
|
+
this.id = id;
|
|
49
|
+
Object.freeze(this);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class CellHandle {
|
|
54
|
+
/** @internal */ constructor(id) {
|
|
55
|
+
this.id = id;
|
|
56
|
+
Object.freeze(this);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class EffectHandle {
|
|
61
|
+
/** @internal */ constructor(id) {
|
|
62
|
+
this.id = id;
|
|
63
|
+
Object.freeze(this);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class SignalHandle {
|
|
68
|
+
/** @internal */ constructor(slot, effect) {
|
|
69
|
+
this.slot = slot;
|
|
70
|
+
this.effect = effect;
|
|
71
|
+
Object.freeze(this);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -- Context -----------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
class CellNode {
|
|
78
|
+
constructor(value) {
|
|
79
|
+
this.value = value;
|
|
80
|
+
this.dependents = new Set();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class SlotNode {
|
|
85
|
+
constructor(compute, memo) {
|
|
86
|
+
this.value = undefined;
|
|
87
|
+
this.hasValue = false;
|
|
88
|
+
this.memo = memo;
|
|
89
|
+
this.compute = compute;
|
|
90
|
+
this.dependencies = new Set();
|
|
91
|
+
this.dependents = new Set();
|
|
92
|
+
this.dirty = false;
|
|
93
|
+
this.forceRecompute = false;
|
|
94
|
+
this.inProgress = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class EffectNode {
|
|
99
|
+
constructor(run) {
|
|
100
|
+
this.run = run;
|
|
101
|
+
this.dependencies = new Set();
|
|
102
|
+
this.cleanup = null;
|
|
103
|
+
this.forceRun = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class Context {
|
|
108
|
+
#nodes = new Map();
|
|
109
|
+
#nextId = 1;
|
|
110
|
+
#freeIds = [];
|
|
111
|
+
#trackingStack = [];
|
|
112
|
+
#pendingEffects = [];
|
|
113
|
+
#scheduledEffects = new Set();
|
|
114
|
+
#flushingEffects = false;
|
|
115
|
+
#batchDepth = 0;
|
|
116
|
+
#batchedCells = new Set();
|
|
117
|
+
|
|
118
|
+
// -- Creation ----------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
cell(value) {
|
|
121
|
+
return new CellHandle(this.#cellAny(value));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#cellAny(value) {
|
|
125
|
+
const id = this.#allocId();
|
|
126
|
+
this.#nodes.set(id, new CellNode(value));
|
|
127
|
+
return id;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
computed(compute) {
|
|
131
|
+
return new SlotHandle(this.#slotAny(false, compute));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
slot(compute) {
|
|
135
|
+
return this.computed(compute);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
memo(compute) {
|
|
139
|
+
return new SlotHandle(this.#slotAny(true, compute));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#slotAny(memo, compute) {
|
|
143
|
+
const id = this.#allocId();
|
|
144
|
+
this.#nodes.set(id, new SlotNode(compute, memo));
|
|
145
|
+
return id;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
signal(compute) {
|
|
149
|
+
const slot = this.#slotAny(true, compute);
|
|
150
|
+
const effect = this.#effectAny(() => {
|
|
151
|
+
this.#getSlotAny(slot);
|
|
152
|
+
return null;
|
|
153
|
+
});
|
|
154
|
+
return new SignalHandle(new SlotHandle(slot), new EffectHandle(effect));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
effect(run) {
|
|
158
|
+
return new EffectHandle(this.#effectAny(run));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#effectAny(run) {
|
|
162
|
+
const id = this.#allocId();
|
|
163
|
+
const node = new EffectNode(run);
|
|
164
|
+
node.forceRun = true; // force the initial run on registration
|
|
165
|
+
this.#nodes.set(id, node);
|
|
166
|
+
this.#scheduleEffect(id, false);
|
|
167
|
+
this.#flushEffects();
|
|
168
|
+
return id;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// -- Read --------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
get(handle) {
|
|
174
|
+
return this.#getSlotAny(handle.id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getCell(handle) {
|
|
178
|
+
return this.#getCellAny(handle.id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getSignal(handle) {
|
|
182
|
+
return this.get(handle.slot);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#getSlotAny(id) {
|
|
186
|
+
const frame = this.#currentFrame();
|
|
187
|
+
if (frame !== undefined) {
|
|
188
|
+
this.#registerDependency(id, frame);
|
|
189
|
+
}
|
|
190
|
+
this.#refreshSlot(id);
|
|
191
|
+
const node = this.#nodes.get(id);
|
|
192
|
+
if (!(node instanceof SlotNode) || !node.hasValue) {
|
|
193
|
+
throw new Error(`slot ${id} has no value`);
|
|
194
|
+
}
|
|
195
|
+
return node.value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#getCellAny(id) {
|
|
199
|
+
const frame = this.#currentFrame();
|
|
200
|
+
if (frame !== undefined) {
|
|
201
|
+
this.#registerDependency(id, frame);
|
|
202
|
+
}
|
|
203
|
+
const node = this.#nodes.get(id);
|
|
204
|
+
if (!(node instanceof CellNode)) {
|
|
205
|
+
throw new Error(`get_cell on non-cell id ${id}`);
|
|
206
|
+
}
|
|
207
|
+
return node.value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// -- Write -------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
setCell(handle, value) {
|
|
213
|
+
this.#setCellAny(handle.id, value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#setCellAny(id, value) {
|
|
217
|
+
const node = this.#nodes.get(id);
|
|
218
|
+
if (!(node instanceof CellNode)) {
|
|
219
|
+
throw new Error(`set_cell on non-cell id ${id}`);
|
|
220
|
+
}
|
|
221
|
+
if (!defaultEqual(node.value, value)) {
|
|
222
|
+
node.value = value;
|
|
223
|
+
if (this.#isBatching()) {
|
|
224
|
+
this.#batchedCells.add(id);
|
|
225
|
+
} else {
|
|
226
|
+
this.#invalidateCellDependentsNow(id);
|
|
227
|
+
this.#flushEffects();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -- Batch -------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
batch(run) {
|
|
235
|
+
this.#batchDepth++;
|
|
236
|
+
try {
|
|
237
|
+
run();
|
|
238
|
+
} finally {
|
|
239
|
+
this.#finishBatch();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#finishBatch() {
|
|
244
|
+
if (this.#batchDepth <= 0) {
|
|
245
|
+
throw new Error("finishBatch without active batch");
|
|
246
|
+
}
|
|
247
|
+
this.#batchDepth--;
|
|
248
|
+
if (this.#batchDepth === 0) {
|
|
249
|
+
this.#flushBatched();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#flushBatched() {
|
|
254
|
+
const cells = [...this.#batchedCells];
|
|
255
|
+
this.#batchedCells.clear();
|
|
256
|
+
for (const id of cells) {
|
|
257
|
+
this.#invalidateCellDependentsNow(id);
|
|
258
|
+
}
|
|
259
|
+
this.#flushEffects();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#isBatching() {
|
|
263
|
+
return this.#batchDepth > 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// -- Dispose -----------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
disposeEffect(handle) {
|
|
269
|
+
const id = handle.id;
|
|
270
|
+
const idx = this.#pendingEffects.indexOf(id);
|
|
271
|
+
if (idx !== -1) {
|
|
272
|
+
this.#pendingEffects.splice(idx, 1);
|
|
273
|
+
}
|
|
274
|
+
this.#scheduledEffects.delete(id);
|
|
275
|
+
const node = this.#nodes.get(id);
|
|
276
|
+
if (!(node instanceof EffectNode)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.#nodes.delete(id);
|
|
280
|
+
this.#freeIds.push(id);
|
|
281
|
+
for (const dep of [...node.dependencies]) {
|
|
282
|
+
this.#removeDependentEdge(dep, id);
|
|
283
|
+
}
|
|
284
|
+
if (node.cleanup) {
|
|
285
|
+
node.cleanup();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
isEffectActive(handle) {
|
|
290
|
+
return this.#nodes.get(handle.id) instanceof EffectNode;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
disposeSignal(handle) {
|
|
294
|
+
this.disposeEffect(handle.effect);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
isSignalActive(handle) {
|
|
298
|
+
return this.isEffectActive(handle.effect);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
isSet(handle) {
|
|
302
|
+
const node = this.#nodes.get(handle.id);
|
|
303
|
+
if (!(node instanceof SlotNode)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
return node.hasValue && !node.dirty;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// -- Internals: id + edges --------------------------------------------
|
|
310
|
+
|
|
311
|
+
#allocId() {
|
|
312
|
+
return this.#freeIds.pop() ?? this.#nextId++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#currentFrame() {
|
|
316
|
+
return this.#trackingStack[this.#trackingStack.length - 1];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#registerDependency(depId, parentId) {
|
|
320
|
+
const dep = this.#nodes.get(depId);
|
|
321
|
+
if (dep instanceof CellNode || dep instanceof SlotNode) {
|
|
322
|
+
dep.dependents.add(parentId);
|
|
323
|
+
}
|
|
324
|
+
const parent = this.#nodes.get(parentId);
|
|
325
|
+
if (parent instanceof SlotNode || parent instanceof EffectNode) {
|
|
326
|
+
parent.dependencies.add(depId);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#removeDependentEdge(depId, parentId) {
|
|
331
|
+
const dep = this.#nodes.get(depId);
|
|
332
|
+
if (dep instanceof CellNode || dep instanceof SlotNode) {
|
|
333
|
+
dep.dependents.delete(parentId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// -- Internals: refresh / recompute (pull-based, glitch-free) ----------
|
|
338
|
+
|
|
339
|
+
#refreshSlot(id) {
|
|
340
|
+
const node = this.#nodes.get(id);
|
|
341
|
+
if (!(node instanceof SlotNode)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (node.inProgress) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`lazily: circular dependency detected at slot ${id}; a computed/memo slot depends on itself`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
node.inProgress = true;
|
|
350
|
+
try {
|
|
351
|
+
let dependencyChanged = false;
|
|
352
|
+
for (const dep of [...node.dependencies]) {
|
|
353
|
+
if (this.#nodes.get(dep) instanceof SlotNode && this.#refreshSlot(dep)) {
|
|
354
|
+
dependencyChanged = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const needsRecompute = !node.hasValue || node.forceRecompute || dependencyChanged;
|
|
358
|
+
if (!needsRecompute) {
|
|
359
|
+
node.dirty = false;
|
|
360
|
+
node.forceRecompute = false;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return this.#recomputeSlotNow(id, node);
|
|
364
|
+
} finally {
|
|
365
|
+
node.inProgress = false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#recomputeSlotNow(id, node) {
|
|
370
|
+
for (const dep of [...node.dependencies]) {
|
|
371
|
+
this.#removeDependentEdge(dep, id);
|
|
372
|
+
}
|
|
373
|
+
node.dependencies.clear();
|
|
374
|
+
this.#trackingStack.push(id);
|
|
375
|
+
let result;
|
|
376
|
+
try {
|
|
377
|
+
result = node.compute();
|
|
378
|
+
} finally {
|
|
379
|
+
this.#trackingStack.pop();
|
|
380
|
+
}
|
|
381
|
+
const unchanged = node.memo && node.hasValue && defaultEqual(node.value, result);
|
|
382
|
+
node.dirty = false;
|
|
383
|
+
node.forceRecompute = false;
|
|
384
|
+
if (unchanged) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const hadValue = node.hasValue;
|
|
388
|
+
node.value = result;
|
|
389
|
+
node.hasValue = true;
|
|
390
|
+
if (hadValue) {
|
|
391
|
+
this.#notifySlotValueChanged(id);
|
|
392
|
+
}
|
|
393
|
+
return hadValue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#notifySlotValueChanged(id) {
|
|
397
|
+
const node = this.#nodes.get(id);
|
|
398
|
+
if (!(node instanceof SlotNode)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const d of [...node.dependents]) {
|
|
402
|
+
this.#invalidateDependentFromChangedValue(d);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// -- Internals: invalidation propagation ------------------------------
|
|
407
|
+
|
|
408
|
+
#invalidateCellDependentsNow(id) {
|
|
409
|
+
const node = this.#nodes.get(id);
|
|
410
|
+
if (!(node instanceof CellNode)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
for (const d of [...node.dependents]) {
|
|
414
|
+
this.#invalidateDependentFromChangedValue(d);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#invalidateDependentFromChangedValue(id) {
|
|
419
|
+
if (this.#nodes.get(id) instanceof EffectNode) {
|
|
420
|
+
this.#scheduleEffect(id, true);
|
|
421
|
+
} else {
|
|
422
|
+
this.#markSlotDirty(id, true);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#markSlotDirty(id, force) {
|
|
427
|
+
const node = this.#nodes.get(id);
|
|
428
|
+
if (!(node instanceof SlotNode)) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const shouldPropagate = !node.dirty || (force && !node.forceRecompute);
|
|
432
|
+
node.dirty = true;
|
|
433
|
+
if (force) {
|
|
434
|
+
node.forceRecompute = true;
|
|
435
|
+
}
|
|
436
|
+
if (!shouldPropagate) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
for (const d of [...node.dependents]) {
|
|
440
|
+
if (this.#nodes.get(d) instanceof EffectNode) {
|
|
441
|
+
this.#scheduleEffect(d, false);
|
|
442
|
+
} else {
|
|
443
|
+
this.#markSlotDirty(d, false);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// -- Internals: effect scheduling / flush ------------------------------
|
|
449
|
+
|
|
450
|
+
#scheduleEffect(id, force) {
|
|
451
|
+
const node = this.#nodes.get(id);
|
|
452
|
+
if (!(node instanceof EffectNode)) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (force) {
|
|
456
|
+
node.forceRun = true;
|
|
457
|
+
}
|
|
458
|
+
if (this.#scheduledEffects.add(id)) {
|
|
459
|
+
this.#pendingEffects.push(id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#flushEffects() {
|
|
464
|
+
if (this.#flushingEffects) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.#flushingEffects = true;
|
|
468
|
+
try {
|
|
469
|
+
while (true) {
|
|
470
|
+
const id = this.#pendingEffects.shift();
|
|
471
|
+
if (id === undefined) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.#scheduledEffects.delete(id);
|
|
475
|
+
this.#runEffect(id);
|
|
476
|
+
}
|
|
477
|
+
} finally {
|
|
478
|
+
this.#flushingEffects = false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#runEffect(id) {
|
|
483
|
+
if (!this.#effectShouldRun(id)) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const node = this.#nodes.get(id);
|
|
487
|
+
if (!(node instanceof EffectNode)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const oldDeps = [...node.dependencies];
|
|
491
|
+
node.dependencies.clear();
|
|
492
|
+
const cleanup = node.cleanup;
|
|
493
|
+
node.cleanup = null;
|
|
494
|
+
node.forceRun = false;
|
|
495
|
+
for (const dep of oldDeps) {
|
|
496
|
+
this.#removeDependentEdge(dep, id);
|
|
497
|
+
}
|
|
498
|
+
if (cleanup) {
|
|
499
|
+
cleanup();
|
|
500
|
+
}
|
|
501
|
+
this.#trackingStack.push(id);
|
|
502
|
+
let nextCleanup;
|
|
503
|
+
try {
|
|
504
|
+
nextCleanup = node.run();
|
|
505
|
+
} finally {
|
|
506
|
+
this.#trackingStack.pop();
|
|
507
|
+
}
|
|
508
|
+
const current = this.#nodes.get(id);
|
|
509
|
+
if (current instanceof EffectNode) {
|
|
510
|
+
current.cleanup = typeof nextCleanup === "function" ? nextCleanup : null;
|
|
511
|
+
} else if (typeof nextCleanup === "function") {
|
|
512
|
+
nextCleanup();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
#effectShouldRun(id) {
|
|
517
|
+
const node = this.#nodes.get(id);
|
|
518
|
+
if (!(node instanceof EffectNode)) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
if (node.forceRun) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
for (const dep of node.dependencies) {
|
|
525
|
+
if (this.#nodes.get(dep) instanceof SlotNode && this.#refreshSlot(dep)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context, SlotHandle } from "./reactive.js";
|
|
2
|
+
|
|
3
|
+
export type TreeNodeSpec = {
|
|
4
|
+
id: string;
|
|
5
|
+
value: unknown;
|
|
6
|
+
children?: { order?: string[]; values?: Record<string, TreeNodeSpec> };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FoldFn<V, D> = (value: V, childDerived: D[]) => D;
|
|
10
|
+
|
|
11
|
+
export class SemTree<V = unknown, D = unknown> {
|
|
12
|
+
constructor(ctx: Context, rootSpec: TreeNodeSpec, fold: FoldFn<V, D>);
|
|
13
|
+
static build<V, D>(ctx: Context, rootSpec: TreeNodeSpec, fold: FoldFn<V, D>): SemTree<V, D>;
|
|
14
|
+
setValue(id: string, value: V): void;
|
|
15
|
+
removeChild(parentId: string, childId: string): void;
|
|
16
|
+
value(): D;
|
|
17
|
+
nodeValue(id: string): D | undefined;
|
|
18
|
+
isCached(id: string): boolean;
|
|
19
|
+
rootHandle(): SlotHandle<D>;
|
|
20
|
+
nodeHandle(id: string): SlotHandle<D> | null;
|
|
21
|
+
}
|