@mark1russell7/render-node 0.0.0-bootstrap.0 → 0.1.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/build/index.js ADDED
@@ -0,0 +1,846 @@
1
+ import { isSome, none, some } from "@mark1russell7/render-optional";
2
+ import { deps, evaluate, lit, ref } from "@mark1russell7/render-dsl";
3
+ //#region src/equality.ts
4
+ /**
5
+ * Structural equality for node values.
6
+ *
7
+ * With reference equality only, each expression that makes an object is always dirty: each evaluation
8
+ * gives a new object, and each epoch propagates it. Node values are plain data, thus the default is structural:
9
+ *
10
+ * - `Object.is` for primitives, thus `NaN` is equal to `NaN`.
11
+ * - Element by element for arrays, own key by own key for plain objects.
12
+ * - An exotic object (a class instance, a function, a React element) is equal only to itself.
13
+ * - Below `MAX_DEPTH` levels, two values are not equal. This limit also stops a cyclic value.
14
+ */
15
+ const MAX_DEPTH = 32;
16
+ const hasOwn$1 = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
17
+ /** This function tells if a value is a plain object: its prototype is `Object.prototype` or `null`. */
18
+ const isPlainObject = (v) => {
19
+ if (v === null || typeof v !== "object") return false;
20
+ const proto = Object.getPrototypeOf(v);
21
+ return proto === Object.prototype || proto === null;
22
+ };
23
+ const eq = (a, b, depth) => {
24
+ if (Object.is(a, b)) return true;
25
+ if (depth >= MAX_DEPTH) return false;
26
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
27
+ if (Array.isArray(a) || Array.isArray(b)) {
28
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
29
+ for (let i = 0; i < a.length; i++) if (!eq(a[i], b[i], depth + 1)) return false;
30
+ return true;
31
+ }
32
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
33
+ const keysA = Object.keys(a);
34
+ if (keysA.length !== Object.keys(b).length) return false;
35
+ for (const k of keysA) if (!hasOwn$1(b, k) || !eq(a[k], b[k], depth + 1)) return false;
36
+ return true;
37
+ };
38
+ /** This function compares two node values structurally. The module comment gives the rules. */
39
+ const valueEquals = (a, b) => eq(a, b, 0);
40
+ //#endregion
41
+ //#region src/ops.ts
42
+ const hasOwn = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
43
+ /**
44
+ * The default write. It keeps the old value when the new result is structurally equal, thus an equal
45
+ * object keeps its old reference. A change from a value to `none`, or from `none` to a value, is a change.
46
+ */
47
+ const defaultSplash = (value, target) => {
48
+ const prev = target.value;
49
+ if (isSome(prev) ? isSome(value) && valueEquals(prev.value, value.value) : !isSome(value)) return false;
50
+ target.value = value;
51
+ return true;
52
+ };
53
+ /** The default propagation. A change of a node makes its value readers dirty. */
54
+ const defaultFlow = (target) => target.seats;
55
+ /**
56
+ * The default resolution. It goes through slots as far as they exist, then through the own fields of the value.
57
+ * A path that ends on a node gives the value of that node. For a container, this value is the record of its slots.
58
+ * A path that stops at a container before its end reads a missing slot, thus it gives `none`.
59
+ */
60
+ const defaultDeref = (root, path, store) => {
61
+ let current = root;
62
+ let i = 0;
63
+ while (i < path.length) {
64
+ const slotId = current.slots.get(path[i]);
65
+ if (slotId === void 0) break;
66
+ const next = store.nodes.get(slotId);
67
+ if (!next) break;
68
+ current = next;
69
+ i++;
70
+ }
71
+ if (i < path.length && current.slots.size > 0) return none;
72
+ if (!isSome(current.value)) return none;
73
+ let value = current.value.value;
74
+ for (; i < path.length; i++) {
75
+ const segment = path[i];
76
+ if (value === null || typeof value !== "object" || !hasOwn(value, segment)) return none;
77
+ value = value[segment];
78
+ }
79
+ return some(value);
80
+ };
81
+ /**
82
+ * The default dependencies of a path, which agree with `defaultDeref`. The nodes that the path goes through are
83
+ * structural targets, and the node where it ends is the terminal. A path that stops at a container before its end
84
+ * reads a missing slot: it has no terminal, and the container is a structural target.
85
+ */
86
+ const defaultTargets = (root, path, store) => {
87
+ const through = [];
88
+ let current = root;
89
+ let i = 0;
90
+ for (; i < path.length; i++) {
91
+ const slotId = current.slots.get(path[i]);
92
+ const next = slotId === void 0 ? void 0 : store.nodes.get(slotId);
93
+ if (!next) break;
94
+ through.push(current);
95
+ current = next;
96
+ }
97
+ if (i < path.length && current.slots.size > 0) return {
98
+ through: [...through, current],
99
+ terminal: void 0
100
+ };
101
+ return {
102
+ through,
103
+ terminal: current
104
+ };
105
+ };
106
+ /** The default semantics of a store. */
107
+ const defaultOps = {
108
+ splash: defaultSplash,
109
+ flow: defaultFlow,
110
+ deref: defaultDeref,
111
+ targets: defaultTargets
112
+ };
113
+ //#endregion
114
+ //#region src/engine.ts
115
+ /** This function gives the internal state of a store. All stores come from `makeState`. */
116
+ const state = (store) => store;
117
+ /** This function gives the public view of a state. */
118
+ const publicStore = (s) => s;
119
+ /** This function makes the state of an empty store. */
120
+ const makeState = (nodeOps, ops) => ({
121
+ nodes: /* @__PURE__ */ new Map(),
122
+ nodeOps,
123
+ ops,
124
+ epochStats: null,
125
+ nextId: 0,
126
+ dangling: /* @__PURE__ */ new Map(),
127
+ pending: null,
128
+ batchDepth: 0,
129
+ epoch: 0
130
+ });
131
+ /** This function makes a node that is not in a store yet. */
132
+ const makeNode = (id, expr) => ({
133
+ id,
134
+ expr,
135
+ value: none,
136
+ reads: deps(expr),
137
+ seats: /* @__PURE__ */ new Set(),
138
+ seatsStructural: /* @__PURE__ */ new Set(),
139
+ seatedOn: /* @__PURE__ */ new Set(),
140
+ parent: void 0,
141
+ heldBy: /* @__PURE__ */ new Set(),
142
+ slots: /* @__PURE__ */ new Map(),
143
+ dangles: false,
144
+ mark: 0,
145
+ dirty: 0,
146
+ index: -1,
147
+ low: -1,
148
+ onStack: false,
149
+ next: null
150
+ });
151
+ /**
152
+ * This function gives an ID that no node of the store has. It uses the hint when the hint is free.
153
+ * Without a hint, the IDs have the form `n_<number>`, with a counter for each store.
154
+ */
155
+ const freshId = (s, hint) => {
156
+ if (hint !== void 0 && !s.nodes.has(hint)) return hint;
157
+ const base = hint ?? "n";
158
+ for (;;) {
159
+ const id = `${base}${hint === void 0 ? "_" : "~"}${String(s.nextId++)}`;
160
+ if (!s.nodes.has(id)) return id;
161
+ }
162
+ };
163
+ /** This function makes the `record` expression of a container: one field for each slot. */
164
+ const recordOf = (n) => {
165
+ const args = [];
166
+ for (const name of n.slots.keys()) args.push(lit(name), ref(n.id, name));
167
+ return {
168
+ tag: "app",
169
+ op: "record",
170
+ args
171
+ };
172
+ };
173
+ /** This function tells if the expression of a container is the record of its current slots. */
174
+ const isRecordOf = (n) => {
175
+ const e = n.expr;
176
+ if (e.tag !== "app" || e.op !== "record" || e.args.length !== 2 * n.slots.size) return false;
177
+ let i = 0;
178
+ for (const name of n.slots.keys()) {
179
+ const k = e.args[i++];
180
+ const r = e.args[i++];
181
+ if (k.tag !== "lit" || k.value !== name) return false;
182
+ if (r.tag !== "ref" || r.path.length !== 2 || r.path[0] !== n.id || r.path[1] !== name) return false;
183
+ }
184
+ return true;
185
+ };
186
+ /** This function points a slot at a node and records the container in `heldBy` of the node. */
187
+ const linkSlot = (container, name, child) => {
188
+ container.slots.set(name, child.id);
189
+ child.heldBy.add(container.id);
190
+ };
191
+ /** This function removes a slot. The target forgets the container when no other slot of the container points at it. */
192
+ const unlinkSlot = (s, container, name) => {
193
+ const target = container.slots.get(name);
194
+ container.slots.delete(name);
195
+ if (target === void 0) return;
196
+ for (const other of container.slots.values()) if (other === target) return;
197
+ s.nodes.get(target)?.heldBy.delete(container.id);
198
+ };
199
+ /**
200
+ * This function gives the chain of nodes that a read path goes through. The chain starts at the root
201
+ * (`path[0]`, a node ID) and follows slots as far as they exist. The last node is the terminal: the path
202
+ * reads its value. The function gives an empty chain when the root is not in the store.
203
+ */
204
+ const readTargets = (nodes, path) => {
205
+ const rootId = path[0];
206
+ const root = rootId === void 0 ? void 0 : nodes.get(rootId);
207
+ if (!root) return [];
208
+ const out = [root];
209
+ let current = root;
210
+ for (let i = 1; i < path.length; i++) {
211
+ const slotId = current.slots.get(path[i]);
212
+ const next = slotId === void 0 ? void 0 : nodes.get(slotId);
213
+ if (!next) break;
214
+ current = next;
215
+ out.push(current);
216
+ }
217
+ return out;
218
+ };
219
+ /**
220
+ * This function seats a node on the targets of its reads (the `targets` op of the store). The terminal gets a
221
+ * value seat, and each node that the path goes through gets a structural seat. A read whose root is not in the
222
+ * store waits in `dangling`. A missing slot of a container has no terminal. Thus a container cannot be on a
223
+ * false cycle with a reader of one of its missing slots.
224
+ */
225
+ const wireNode = (s, n) => {
226
+ for (const path of n.reads) {
227
+ const rootId = path[0];
228
+ const root = rootId === void 0 ? void 0 : s.nodes.get(rootId);
229
+ if (!root) {
230
+ if (rootId !== void 0) {
231
+ let waiting = s.dangling.get(rootId);
232
+ if (!waiting) s.dangling.set(rootId, waiting = /* @__PURE__ */ new Set());
233
+ waiting.add(n.id);
234
+ n.dangles = true;
235
+ }
236
+ continue;
237
+ }
238
+ const targets = s.nodeOps.targets(root, path.slice(1), publicStore(s));
239
+ for (const t of targets.through) {
240
+ const m = s.nodes.get(t.id);
241
+ if (!m || m === n) continue;
242
+ m.seatsStructural.add(n.id);
243
+ n.seatedOn.add(m.id);
244
+ }
245
+ const terminal = targets.terminal === void 0 ? void 0 : s.nodes.get(targets.terminal.id);
246
+ if (terminal && terminal !== n) {
247
+ terminal.seats.add(n.id);
248
+ n.seatedOn.add(terminal.id);
249
+ }
250
+ }
251
+ };
252
+ /** This function removes a node from each seat set that holds it, and from `dangling`. */
253
+ const unwireNode = (s, n) => {
254
+ if (n.seatedOn.size > 0) {
255
+ for (const otherId of n.seatedOn) {
256
+ const other = s.nodes.get(otherId);
257
+ if (other) {
258
+ other.seats.delete(n.id);
259
+ other.seatsStructural.delete(n.id);
260
+ }
261
+ }
262
+ n.seatedOn.clear();
263
+ }
264
+ if (n.dangles) {
265
+ for (const path of n.reads) {
266
+ const head = path[0];
267
+ const waiting = head === void 0 ? void 0 : s.dangling.get(head);
268
+ if (waiting) {
269
+ waiting.delete(n.id);
270
+ if (waiting.size === 0) s.dangling.delete(head);
271
+ }
272
+ }
273
+ n.dangles = false;
274
+ }
275
+ };
276
+ /** This function changes the expression of a node and keeps its reads correct. The caller touches the node. */
277
+ const replaceExpr = (s, n, expr) => {
278
+ unwireNode(s, n);
279
+ n.expr = expr;
280
+ n.reads = deps(expr);
281
+ };
282
+ const pending = (s) => s.pending ??= {
283
+ touched: /* @__PURE__ */ new Set(),
284
+ records: /* @__PURE__ */ new Set(),
285
+ expansions: /* @__PURE__ */ new Set()
286
+ };
287
+ /** This function marks a node for the next flush: rewire and evaluate. */
288
+ const touch = (s, id) => {
289
+ pending(s).touched.add(id);
290
+ };
291
+ /** This function marks a container: the next flush makes its record expression again. */
292
+ const touchRecord = (s, id) => {
293
+ pending(s).records.add(id);
294
+ };
295
+ /** This function marks a node for an expansion after the next epoch, when its value is settled. */
296
+ const touchExpansion = (s, id) => {
297
+ pending(s).expansions.add(id);
298
+ };
299
+ /** This function adds a node to the store. Readers that waited for its ID get rewired. */
300
+ const insertNode = (s, n) => {
301
+ s.nodes.set(n.id, n);
302
+ touch(s, n.id);
303
+ const waiting = s.dangling.get(n.id);
304
+ if (waiting) {
305
+ for (const r of waiting) touch(s, r);
306
+ s.dangling.delete(n.id);
307
+ }
308
+ };
309
+ /**
310
+ * This function expands a node with a settled value. Each field of a plain object becomes an owned
311
+ * slot, and a field that is a plain object becomes a container in turn. A field that holds one of its own
312
+ * ancestors stays a leaf, thus a cyclic object gives a finite tree. The caller flushes.
313
+ * The build uses its own stack, not the call stack, thus a deep value does not overflow it.
314
+ */
315
+ const expandNow = (s, id) => {
316
+ const n = s.nodes.get(id);
317
+ const value = n && isSome(n.value) ? n.value.value : void 0;
318
+ if (!n || n.slots.size > 0 || !isPlainObject(value)) return;
319
+ const ancestors = /* @__PURE__ */ new Set([value]);
320
+ const frames = [{
321
+ owner: n,
322
+ value,
323
+ fields: Object.entries(value),
324
+ next: 0
325
+ }];
326
+ for (let frame = frames.at(-1); frame !== void 0; frame = frames.at(-1)) {
327
+ const field = frame.fields[frame.next++];
328
+ if (field === void 0) {
329
+ frames.pop();
330
+ ancestors.delete(frame.value);
331
+ if (frame.owner !== n) touchRecord(s, frame.owner.id);
332
+ continue;
333
+ }
334
+ const [key, fieldValue] = field;
335
+ const sub = makeNode(freshId(s, `${frame.owner.id}.${key}`), lit(fieldValue));
336
+ sub.parent = frame.owner.id;
337
+ sub.value = some(fieldValue);
338
+ insertNode(s, sub);
339
+ linkSlot(frame.owner, key, sub);
340
+ if (isPlainObject(fieldValue) && !ancestors.has(fieldValue)) {
341
+ ancestors.add(fieldValue);
342
+ frames.push({
343
+ owner: sub,
344
+ value: fieldValue,
345
+ fields: Object.entries(fieldValue),
346
+ next: 0
347
+ });
348
+ }
349
+ }
350
+ touchRecord(s, n.id);
351
+ for (const r of n.seats) touch(s, r);
352
+ for (const r of n.seatsStructural) touch(s, r);
353
+ };
354
+ /** This function does the pending work at once, except inside a batch. */
355
+ const flushUnlessBatched = (s) => {
356
+ if (s.batchDepth === 0) flush(s);
357
+ };
358
+ /**
359
+ * This function makes `fn` one transaction: the pending work of all its writes becomes one epoch at the end.
360
+ * When `fn` throws, the flush still does the pending work, and the error of `fn` is the error of the batch.
361
+ */
362
+ const runBatch = (s, fn) => {
363
+ s.batchDepth++;
364
+ let result;
365
+ try {
366
+ result = fn();
367
+ } catch (error) {
368
+ s.batchDepth--;
369
+ if (s.batchDepth === 0) try {
370
+ flush(s);
371
+ } catch {}
372
+ throw error;
373
+ }
374
+ s.batchDepth--;
375
+ if (s.batchDepth === 0) flush(s);
376
+ return result;
377
+ };
378
+ /** The limit of follow-up epochs in one flush. Only a custom op that writes on each epoch reaches it. */
379
+ const MAX_FOLLOW_UPS = 1e3;
380
+ /** The error of an op during an epoch, with the dirty nodes that the epoch did not evaluate. */
381
+ var EpochFailure = class {
382
+ retry;
383
+ cause;
384
+ constructor(retry, cause) {
385
+ this.retry = retry;
386
+ this.cause = cause;
387
+ }
388
+ };
389
+ /**
390
+ * This function does the pending work. A custom op can write during an epoch. That write does not go into
391
+ * the epoch: it becomes pending, and the flush does it in a follow-up epoch. The stats join all epochs.
392
+ *
393
+ * When an op throws, the dirty nodes that the epoch did not evaluate stay pending. The next flush evaluates them,
394
+ * and the error goes to the caller. A throw before the evaluation, for example of a custom `targets` or `flow`
395
+ * op, keeps all the work of the round pending.
396
+ */
397
+ const flush = (s) => {
398
+ let stats = null;
399
+ s.batchDepth++;
400
+ try {
401
+ for (let round = 0; s.pending; round++) {
402
+ if (round > MAX_FOLLOW_UPS) {
403
+ s.pending = null;
404
+ throw new Error(`The epochs did not settle after ${String(MAX_FOLLOW_UPS)} follow-ups: an op writes on each epoch.`);
405
+ }
406
+ const p = s.pending;
407
+ s.pending = null;
408
+ let epoch;
409
+ try {
410
+ for (const id of p.records) {
411
+ const n = s.nodes.get(id);
412
+ if (!n) continue;
413
+ if (!isRecordOf(n)) replaceExpr(s, n, recordOf(n));
414
+ p.touched.add(id);
415
+ }
416
+ for (const id of p.touched) {
417
+ const n = s.nodes.get(id);
418
+ if (!n) continue;
419
+ unwireNode(s, n);
420
+ wireNode(s, n);
421
+ }
422
+ epoch = runEpoch(s, p.touched);
423
+ } catch (failure) {
424
+ if (failure instanceof EpochFailure) {
425
+ for (const id of failure.retry) touch(s, id);
426
+ for (const id of p.expansions) touchExpansion(s, id);
427
+ throw failure.cause;
428
+ }
429
+ for (const id of p.records) touchRecord(s, id);
430
+ for (const id of p.touched) touch(s, id);
431
+ for (const id of p.expansions) touchExpansion(s, id);
432
+ throw failure;
433
+ }
434
+ if (!stats) stats = epoch;
435
+ else {
436
+ for (const id of epoch.evaluated) stats.evaluated.add(id);
437
+ for (const id of epoch.changed) stats.changed.add(id);
438
+ for (const id of epoch.cyclic) stats.cyclic.add(id);
439
+ }
440
+ for (const id of p.expansions) expandNow(s, id);
441
+ }
442
+ } finally {
443
+ s.batchDepth--;
444
+ }
445
+ s.epochStats = {
446
+ evaluated: stats?.evaluated ?? /* @__PURE__ */ new Set(),
447
+ changed: stats?.changed ?? /* @__PURE__ */ new Set(),
448
+ cyclic: stats?.cyclic ?? /* @__PURE__ */ new Set(),
449
+ total: s.nodes.size
450
+ };
451
+ };
452
+ /** This function makes the resolver that the expressions of a store use: `path[0]` is a node ID. */
453
+ const resolverOf = (store) => (path) => {
454
+ const rootId = path[0];
455
+ const root = rootId === void 0 ? void 0 : store.nodes.get(rootId);
456
+ return root ? store.nodeOps.deref(root, path.length === 1 ? [] : path.slice(1), store) : none;
457
+ };
458
+ /**
459
+ * The limit of the rounds of a cycle in one epoch. Like the iterative calculation of a spreadsheet, the members
460
+ * of a cycle evaluate again until their values are stable, or until this limit.
461
+ */
462
+ const MAX_CYCLE_ROUNDS = 100;
463
+ /**
464
+ * This function gives the strongly connected components of the closure (Tarjan, without recursion).
465
+ * Tarjan gives each component after all components that it reaches. Thus the reverse list is a topological
466
+ * order of the components, and each node comes after its inputs.
467
+ */
468
+ const components = (closure) => {
469
+ const out = [];
470
+ const stack = [];
471
+ let counter = 0;
472
+ for (const start of closure) {
473
+ if (start.index !== -1) continue;
474
+ const frames = [];
475
+ const open = (v) => {
476
+ v.index = v.low = counter++;
477
+ v.onStack = true;
478
+ stack.push(v);
479
+ frames.push({
480
+ node: v,
481
+ next: 0
482
+ });
483
+ };
484
+ open(start);
485
+ while (frames.length > 0) {
486
+ const frame = frames[frames.length - 1];
487
+ const v = frame.node;
488
+ const successors = v.next;
489
+ if (frame.next < successors.length) {
490
+ const w = successors[frame.next++];
491
+ if (w.index === -1) open(w);
492
+ else if (w.onStack) v.low = Math.min(v.low, w.index);
493
+ continue;
494
+ }
495
+ frames.pop();
496
+ const parent = frames[frames.length - 1];
497
+ if (parent) parent.node.low = Math.min(parent.node.low, v.low);
498
+ if (v.low === v.index) {
499
+ const component = [];
500
+ let w;
501
+ do {
502
+ w = stack.pop();
503
+ w.onStack = false;
504
+ component.push(w);
505
+ } while (w !== v);
506
+ out.push(component);
507
+ }
508
+ }
509
+ }
510
+ return out.toReversed();
511
+ };
512
+ /**
513
+ * One epoch. The engine finds the closure of the frontier through `flow`, and it orders the strongly connected
514
+ * components of the closure topologically. Then it evaluates each dirty node in that order, thus each node
515
+ * reads settled values. A node is dirty when it is in the frontier or when one of its inputs changed.
516
+ *
517
+ * A node outside a cycle evaluates at most one time. The members of a cycle (a component with more than one
518
+ * node) evaluate again while one of them changes, up to `MAX_CYCLE_ROUNDS` rounds. The stats report them.
519
+ * The machine keeps its state in the scratch fields of the nodes, with the epoch number as a stamp.
520
+ */
521
+ const runEpoch = (s, frontier) => {
522
+ const store = publicStore(s);
523
+ const epoch = ++s.epoch;
524
+ const closure = [];
525
+ const stack = [];
526
+ const enter = (n) => {
527
+ n.mark = epoch;
528
+ n.index = -1;
529
+ n.low = -1;
530
+ n.onStack = false;
531
+ n.next = null;
532
+ stack.push(n);
533
+ };
534
+ for (const id of frontier) {
535
+ const n = s.nodes.get(id);
536
+ if (n && n.mark !== epoch) {
537
+ enter(n);
538
+ n.dirty = epoch;
539
+ }
540
+ }
541
+ while (stack.length > 0) {
542
+ const n = stack.pop();
543
+ closure.push(n);
544
+ const next = [];
545
+ for (const id of s.nodeOps.flow(n, store)) {
546
+ const m = s.nodes.get(id);
547
+ if (!m || m === n) continue;
548
+ next.push(m);
549
+ if (m.mark !== epoch) enter(m);
550
+ }
551
+ n.next = next;
552
+ }
553
+ const order = components(closure);
554
+ const resolver = resolverOf(store);
555
+ const evaluated = /* @__PURE__ */ new Set();
556
+ const changed = /* @__PURE__ */ new Set();
557
+ const cyclic = /* @__PURE__ */ new Set();
558
+ const evaluateNode = (n) => {
559
+ n.dirty = 0;
560
+ evaluated.add(n.id);
561
+ if (!s.nodeOps.splash(evaluate(n.expr, resolver, s.ops), n, store)) return false;
562
+ changed.add(n.id);
563
+ for (const m of n.next) m.dirty = epoch;
564
+ return true;
565
+ };
566
+ let position = 0;
567
+ try {
568
+ for (; position < order.length; position++) {
569
+ const component = order[position];
570
+ if (component.length === 1) {
571
+ const n = component[0];
572
+ if (n.dirty === epoch) evaluateNode(n);
573
+ continue;
574
+ }
575
+ for (const n of component) cyclic.add(n.id);
576
+ for (let round = 0; round < 100; round++) {
577
+ let any = false;
578
+ for (const n of component) if (n.dirty === epoch && evaluateNode(n)) any = true;
579
+ if (!any) break;
580
+ }
581
+ }
582
+ } catch (cause) {
583
+ const retry = /* @__PURE__ */ new Set();
584
+ for (let i = position; i < order.length; i++) for (const n of order[i]) if (n.dirty === epoch || i === position) retry.add(n.id);
585
+ for (const n of closure) n.next = null;
586
+ throw new EpochFailure(retry, cause);
587
+ }
588
+ for (const n of closure) n.next = null;
589
+ return {
590
+ evaluated,
591
+ changed,
592
+ cyclic
593
+ };
594
+ };
595
+ //#endregion
596
+ //#region src/writes.ts
597
+ /** This function makes an empty store. The options give its reactive semantics and its op registry. */
598
+ const nodeStore = (options = {}) => publicStore(makeState(options.nodeOps ?? defaultOps, options.ops ?? {}));
599
+ /**
600
+ * This function gives an independent copy of a store: the same nodes, values, slots and seats, and the same
601
+ * counters. A write to one store does not change the other. The expressions and the values are immutable,
602
+ * thus the copy shares them. The options replace the semantics or the op registry of the copy, for example the
603
+ * class semantics of a copied biblo. The function throws inside a batch, because a batch has pending work.
604
+ */
605
+ const forkStore = (store, options = {}) => {
606
+ const s = state(store);
607
+ if (s.pending !== null || s.batchDepth > 0) throw new Error("forkStore: the store is in a batch");
608
+ const copy = {
609
+ nodes: /* @__PURE__ */ new Map(),
610
+ nodeOps: options.nodeOps ?? s.nodeOps,
611
+ ops: options.ops ?? s.ops,
612
+ epochStats: s.epochStats,
613
+ nextId: s.nextId,
614
+ dangling: new Map([...s.dangling].map(([id, readers]) => [id, new Set(readers)])),
615
+ pending: null,
616
+ batchDepth: 0,
617
+ epoch: s.epoch
618
+ };
619
+ for (const [id, n] of s.nodes) {
620
+ const node = {
621
+ ...n,
622
+ seats: new Set(n.seats),
623
+ seatsStructural: new Set(n.seatsStructural),
624
+ seatedOn: new Set(n.seatedOn),
625
+ heldBy: new Set(n.heldBy),
626
+ slots: new Map(n.slots),
627
+ mark: 0,
628
+ dirty: 0,
629
+ index: -1,
630
+ low: -1,
631
+ onStack: false,
632
+ next: null
633
+ };
634
+ copy.nodes.set(id, node);
635
+ }
636
+ return publicStore(copy);
637
+ };
638
+ /** This function gives a node of the store, or `undefined`. */
639
+ const getNode = (store, id) => store.nodes.get(id);
640
+ /** This function gives the value of a node, or `none` when the store has no such node. */
641
+ const readValue = (store, id) => store.nodes.get(id)?.value ?? none;
642
+ /**
643
+ * This function makes `fn` one transaction. The writes in `fn` change the structure at once, and one epoch
644
+ * at the end evaluates all their effects. A batch inside a batch joins the outer batch.
645
+ */
646
+ const batch = (store, fn) => runBatch(state(store), fn);
647
+ /**
648
+ * This function adds a node with an expression and evaluates it. It gives the ID of the node.
649
+ * The ID is the hint when no node has it, otherwise a fresh ID. A reader that waited for this ID gets rewired.
650
+ */
651
+ const addNode = (store, expr, idHint) => {
652
+ const s = state(store);
653
+ const n = makeNode(freshId(s, idHint), expr);
654
+ insertNode(s, n);
655
+ flushUnlessBatched(s);
656
+ return n.id;
657
+ };
658
+ /**
659
+ * This function sets the expression of a node. This is the fundamental write.
660
+ * The engine rewires the reads of the node, evaluates it and propagates the change.
661
+ * A write to a container collapses it first: its own slots are removed, and the expression replaces the record.
662
+ */
663
+ const setExpr = (store, id, expr) => {
664
+ const s = state(store);
665
+ const n = s.nodes.get(id);
666
+ if (n) {
667
+ if (n.slots.size > 0) collapse(s, n);
668
+ replaceExpr(s, n, expr);
669
+ touch(s, id);
670
+ }
671
+ flushUnlessBatched(s);
672
+ };
673
+ /**
674
+ * This function writes a literal value: it is `setExpr` with `lit(value)`.
675
+ * A write to a derived node makes it an input node, like a value that a person types over a formula.
676
+ */
677
+ const setValue = (store, id, value) => {
678
+ setExpr(store, id, lit(value));
679
+ };
680
+ /** This function writes many literal values in one batch, thus in one epoch. */
681
+ const fillMany = (store, writes) => {
682
+ batch(store, () => {
683
+ for (const [id, value] of writes) setValue(store, id, value);
684
+ });
685
+ };
686
+ /**
687
+ * This function points a slot of a container at a node, or removes the slot when `childId` is `undefined`.
688
+ * This is the structural write: the container gets a new record expression, and each reader whose path goes
689
+ * through the container resolves its path again. A node that the container owns and drops is removed.
690
+ * The function does nothing when a node is missing or when the child is the container itself.
691
+ */
692
+ const setSlot = (store, parentId, name, childId, options = {}) => {
693
+ const s = state(store);
694
+ const parent = s.nodes.get(parentId);
695
+ const child = childId === void 0 ? void 0 : s.nodes.get(childId);
696
+ if (parent && (childId === void 0 || child && childId !== parentId) && parent.slots.get(name) !== childId) {
697
+ const previous = parent.slots.get(name);
698
+ if (previous !== void 0) unlinkSlot(s, parent, name);
699
+ if (child) {
700
+ linkSlot(parent, name, child);
701
+ if (options.own === true && (child.parent === void 0 || !s.nodes.has(child.parent))) child.parent = parent.id;
702
+ }
703
+ if (previous !== void 0) dropIfOwned(s, parent, previous);
704
+ touchRecord(s, parent.id);
705
+ for (const r of parent.seatsStructural) touch(s, r);
706
+ for (const r of parent.seats) touch(s, r);
707
+ }
708
+ flushUnlessBatched(s);
709
+ };
710
+ /**
711
+ * This function expands a node whose value is a plain object. Each field becomes a slot, and
712
+ * a field that is a plain object becomes a container in turn. The node becomes a container, and its slots
713
+ * are from then on the source of truth of its fields. A reader of a field gets a seat on the field node,
714
+ * thus an edit of one field makes only its own readers dirty. An expansion of a derived node keeps the current fields.
715
+ *
716
+ * Inside a batch, the expansion waits for the epoch of the batch, thus it uses the settled value.
717
+ * The function does nothing for a container or for a value that is not a plain object.
718
+ */
719
+ const expandNode = (store, id) => {
720
+ const s = state(store);
721
+ if (s.batchDepth > 0) touchExpansion(s, id);
722
+ else expandNow(s, id);
723
+ flushUnlessBatched(s);
724
+ };
725
+ /**
726
+ * This function removes a node and the slots that it owns. The containers that hold it lose the slot.
727
+ * Its readers resolve their paths again: a path to a removed node gives `none`. If a node with the same ID
728
+ * comes back later, the readers find it.
729
+ */
730
+ const removeNode = (store, id) => {
731
+ const s = state(store);
732
+ const n = s.nodes.get(id);
733
+ if (n) removeOwned(s, n);
734
+ flushUnlessBatched(s);
735
+ };
736
+ /**
737
+ * This function evaluates the full store again. It rewires each node, makes each record expression again,
738
+ * and starts one epoch in which each node evaluates one time in topological order. The operations keep the
739
+ * store consistent, thus this function is a repair tool. It does not revert a write.
740
+ */
741
+ const resolveAll = (store) => {
742
+ const s = state(store);
743
+ for (const n of s.nodes.values()) {
744
+ touch(s, n.id);
745
+ if (n.slots.size > 0) touchRecord(s, n.id);
746
+ }
747
+ flushUnlessBatched(s);
748
+ };
749
+ /** A node that the container owns and no longer holds is removed: it was a part of the container. */
750
+ const dropIfOwned = (s, container, childId) => {
751
+ const former = s.nodes.get(childId);
752
+ if (!former || former.parent !== container.id) return;
753
+ for (const target of container.slots.values()) if (target === childId) return;
754
+ removeOwned(s, former);
755
+ };
756
+ /** A container gives up its slots: the owned slot nodes are removed, and the shared ones are only released. */
757
+ const collapse = (s, n) => {
758
+ const slots = [...n.slots.values()];
759
+ for (const name of Array.from(n.slots.keys())) unlinkSlot(s, n, name);
760
+ for (const childId of slots) {
761
+ const child = s.nodes.get(childId);
762
+ if (child && child.parent === n.id) removeOwned(s, child);
763
+ }
764
+ for (const r of n.seatsStructural) touch(s, r);
765
+ };
766
+ /** This function removes a node and its owned slot subtree, and touches each reader that stays. */
767
+ const removeOwned = (s, root) => {
768
+ const doomed = [];
769
+ const doomedIds = /* @__PURE__ */ new Set();
770
+ for (const todo = [root]; todo.length > 0;) {
771
+ const n = todo.pop();
772
+ if (n === void 0 || doomedIds.has(n.id)) continue;
773
+ doomedIds.add(n.id);
774
+ doomed.push(n);
775
+ for (const childId of n.slots.values()) {
776
+ const child = s.nodes.get(childId);
777
+ if (child && child.parent === n.id) todo.push(child);
778
+ }
779
+ }
780
+ for (const n of doomed) {
781
+ for (const r of n.seatsStructural) touch(s, r);
782
+ for (const r of n.seats) touch(s, r);
783
+ for (const holderId of Array.from(n.heldBy)) {
784
+ const holder = doomedIds.has(holderId) ? void 0 : s.nodes.get(holderId);
785
+ if (!holder) continue;
786
+ for (const [name, target] of Array.from(holder.slots)) if (target === n.id) unlinkSlot(s, holder, name);
787
+ touchRecord(s, holder.id);
788
+ for (const r of holder.seatsStructural) touch(s, r);
789
+ }
790
+ }
791
+ for (const n of doomed) for (const target of n.slots.values()) if (!doomedIds.has(target)) s.nodes.get(target)?.heldBy.delete(n.id);
792
+ for (const n of doomed) {
793
+ for (const readerId of n.seats) s.nodes.get(readerId)?.seatedOn.delete(n.id);
794
+ for (const readerId of n.seatsStructural) s.nodes.get(readerId)?.seatedOn.delete(n.id);
795
+ n.seats.clear();
796
+ n.seatsStructural.clear();
797
+ }
798
+ for (const n of doomed) {
799
+ unwireNode(s, n);
800
+ s.nodes.delete(n.id);
801
+ }
802
+ };
803
+ //#endregion
804
+ //#region src/toposort.ts
805
+ /**
806
+ * This function sorts the nodes of a store by their static dependencies (Kahn's algorithm).
807
+ *
808
+ * The edges come from the `targets` op of the store, like the seat wiring.
809
+ * A reader depends on the terminal of each path and on each node that the path goes through.
810
+ * A path to a node that is not in the store adds no edge.
811
+ *
812
+ * The result has the dependencies before their readers. A node on a cycle of two or more nodes is not in the
813
+ * result. A reference of a node to itself adds no edge. The epochs of the engine use the edges of `flow`
814
+ * instead, thus this function is a tool for analysis.
815
+ */
816
+ const toposort = (store) => {
817
+ const dependedBy = /* @__PURE__ */ new Map();
818
+ const inDegree = /* @__PURE__ */ new Map();
819
+ for (const id of store.nodes.keys()) {
820
+ dependedBy.set(id, /* @__PURE__ */ new Set());
821
+ inDegree.set(id, 0);
822
+ }
823
+ for (const [id, n] of store.nodes) for (const path of n.reads) {
824
+ const root = path[0] === void 0 ? void 0 : store.nodes.get(path[0]);
825
+ if (!root) continue;
826
+ const { through, terminal } = store.nodeOps.targets(root, path.slice(1), store);
827
+ for (const target of terminal === void 0 ? through : [...through, terminal]) {
828
+ const readers = dependedBy.get(target.id);
829
+ if (target.id === id || readers.has(id)) continue;
830
+ readers.add(id);
831
+ inDegree.set(id, inDegree.get(id) + 1);
832
+ }
833
+ }
834
+ const order = [];
835
+ for (const [id, d] of inDegree) if (d === 0) order.push(id);
836
+ for (let head = 0; head < order.length; head++) for (const readerId of dependedBy.get(order[head])) {
837
+ const d = inDegree.get(readerId) - 1;
838
+ inDegree.set(readerId, d);
839
+ if (d === 0) order.push(readerId);
840
+ }
841
+ return order;
842
+ };
843
+ //#endregion
844
+ export { MAX_CYCLE_ROUNDS, addNode, batch, defaultDeref, defaultFlow, defaultOps, defaultSplash, defaultTargets, expandNode, fillMany, forkStore, getNode, isPlainObject, nodeStore, readTargets, readValue, removeNode, resolveAll, setExpr, setSlot, setValue, resolverOf as storeResolver, toposort, valueEquals };
845
+
846
+ //# sourceMappingURL=index.js.map