@ersbeth/picoflow 2.3.2 → 3.0.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 (70) hide show
  1. package/README.md +34 -5
  2. package/SKILL.md +130 -16
  3. package/dist/index.d.ts +767 -0
  4. package/dist/index.js +544 -0
  5. package/dist/inspect.d.ts +252 -0
  6. package/dist/inspect.js +300 -0
  7. package/dist/shared.d.ts +361 -0
  8. package/dist/shared.js +1496 -0
  9. package/dist/shared2.d.ts +93 -0
  10. package/dist/shared2.js +59 -0
  11. package/dist/shared3.d.ts +54 -0
  12. package/dist/shared3.js +17 -0
  13. package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
  14. package/dist/solid.js +63 -0
  15. package/package.json +37 -25
  16. package/dist/picoflow.js +0 -1283
  17. package/dist/types/converters/index.d.ts +0 -1
  18. package/dist/types/core/api/base/flowConfig.d.ts +0 -17
  19. package/dist/types/core/api/base/flowDisposable.d.ts +0 -40
  20. package/dist/types/core/api/base/flowErrors.d.ts +0 -1
  21. package/dist/types/core/api/base/flowObservable.d.ts +0 -26
  22. package/dist/types/core/api/base/flowPrimitiveOptions.d.ts +0 -14
  23. package/dist/types/core/api/base/flowSubscribable.d.ts +0 -78
  24. package/dist/types/core/api/base/flowTracker.d.ts +0 -7
  25. package/dist/types/core/api/base/index.d.ts +0 -7
  26. package/dist/types/core/api/index.d.ts +0 -2
  27. package/dist/types/core/api/nodes/async/flowConstantAsync.d.ts +0 -32
  28. package/dist/types/core/api/nodes/async/flowDerivationAsync.d.ts +0 -37
  29. package/dist/types/core/api/nodes/async/flowStateAsync.d.ts +0 -42
  30. package/dist/types/core/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
  31. package/dist/types/core/api/nodes/async/index.d.ts +0 -4
  32. package/dist/types/core/api/nodes/collections/flowArray.d.ts +0 -138
  33. package/dist/types/core/api/nodes/collections/flowMap.d.ts +0 -100
  34. package/dist/types/core/api/nodes/collections/index.d.ts +0 -2
  35. package/dist/types/core/api/nodes/flowEffect.d.ts +0 -28
  36. package/dist/types/core/api/nodes/flowSignal.d.ts +0 -26
  37. package/dist/types/core/api/nodes/flowValue.d.ts +0 -35
  38. package/dist/types/core/api/nodes/index.d.ts +0 -7
  39. package/dist/types/core/api/nodes/sync/flowConstant.d.ts +0 -30
  40. package/dist/types/core/api/nodes/sync/flowDerivation.d.ts +0 -37
  41. package/dist/types/core/api/nodes/sync/flowState.d.ts +0 -40
  42. package/dist/types/core/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
  43. package/dist/types/core/api/nodes/sync/index.d.ts +0 -4
  44. package/dist/types/core/api/nodes/utils.d.ts +0 -22
  45. package/dist/types/core/base/dependenciesRegistry.d.ts +0 -1
  46. package/dist/types/core/base/dependentsRegistry.d.ts +0 -1
  47. package/dist/types/core/base/disposable.d.ts +0 -15
  48. package/dist/types/core/base/errors.d.ts +0 -31
  49. package/dist/types/core/base/executionStack.d.ts +0 -31
  50. package/dist/types/core/base/index.d.ts +0 -8
  51. package/dist/types/core/base/node.d.ts +0 -27
  52. package/dist/types/core/base/observable.d.ts +0 -34
  53. package/dist/types/core/base/observer.d.ts +0 -33
  54. package/dist/types/core/nodes/actionNode.d.ts +0 -1
  55. package/dist/types/core/nodes/arrayNode.d.ts +0 -1
  56. package/dist/types/core/nodes/effectNode.d.ts +0 -1
  57. package/dist/types/core/nodes/index.d.ts +0 -8
  58. package/dist/types/core/nodes/mapNode.d.ts +0 -1
  59. package/dist/types/core/nodes/signalNode.d.ts +0 -1
  60. package/dist/types/core/nodes/valueAsyncNode.d.ts +0 -1
  61. package/dist/types/core/nodes/valueNode.d.ts +0 -1
  62. package/dist/types/core/nodes/valueSyncNode.d.ts +0 -1
  63. package/dist/types/core/schedulers/asyncResolver.d.ts +0 -1
  64. package/dist/types/core/schedulers/asyncScheduler.d.ts +0 -1
  65. package/dist/types/core/schedulers/index.d.ts +0 -4
  66. package/dist/types/core/schedulers/pendingError.d.ts +0 -1
  67. package/dist/types/core/schedulers/scheduler.d.ts +0 -1
  68. package/dist/types/core/schedulers/syncResolver.d.ts +0 -1
  69. package/dist/types/core/schedulers/syncScheduler.d.ts +0 -1
  70. package/dist/types/index.d.ts +0 -2
package/dist/shared.js ADDED
@@ -0,0 +1,1496 @@
1
+ //#region src/core/base/dependenciesRegistry.ts
2
+ /**
3
+ * Shared bookkeeping for the "set of sources this observer reads" half of the
4
+ * dependency graph (A2-ARCH-5). `Observer` and `Node` both hold one instead of
5
+ * each carrying their own `Set<IObservable<unknown>>` and reimplementing
6
+ * register/unregister/clear.
7
+ * @internal
8
+ */
9
+ var DependenciesRegistry = class {
10
+ _dependencies = /* @__PURE__ */ new Set();
11
+ register(dependency, dependent) {
12
+ this._dependencies.add(dependency);
13
+ dependency.registerDependent(dependent);
14
+ }
15
+ unregister(dependency, dependent) {
16
+ this._dependencies.delete(dependency);
17
+ dependency.unregisterDependent(dependent);
18
+ }
19
+ clear(dependent) {
20
+ this._dependencies.forEach((dependency) => {
21
+ dependency.unregisterDependent(dependent);
22
+ });
23
+ this._dependencies.clear();
24
+ }
25
+ forEach(fn) {
26
+ Array.from(this._dependencies).forEach(fn);
27
+ }
28
+ };
29
+
30
+ //#endregion
31
+ //#region src/core/base/dependentsRegistry.ts
32
+ /**
33
+ * Shared bookkeeping for the "set of watchers" half of the dependency graph
34
+ * (A2-ARCH-5). `Observable` and `Node` both hold one instead of each carrying
35
+ * their own `Set<IObserver>` and reimplementing register/unregister/notify.
36
+ * @internal
37
+ */
38
+ var DependentsRegistry = class {
39
+ _dependents = /* @__PURE__ */ new Set();
40
+ register(dependent) {
41
+ this._dependents.add(dependent);
42
+ }
43
+ unregister(dependent) {
44
+ this._dependents.delete(dependent);
45
+ }
46
+ notifyAll() {
47
+ this._dependents.forEach((dependent) => {
48
+ dependent.notify();
49
+ });
50
+ }
51
+ forEach(fn) {
52
+ Array.from(this._dependents).forEach(fn);
53
+ }
54
+ };
55
+
56
+ //#endregion
57
+ //#region src/core/base/errors.ts
58
+ /**
59
+ * Base class for picoflow's typed errors, so a consumer can `catch` one with a single
60
+ * `instanceof PicoFlowError` check instead of matching on message text. Every throw the library
61
+ * raises is one of its subclasses, and every message carries the `[PicoFlow]` prefix.
62
+ *
63
+ * It is never thrown directly: catch it to handle any picoflow failure, and narrow to a subclass to
64
+ * tell one kind from another.
65
+ * @public
66
+ */
67
+ var PicoFlowError = class extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "PicoFlowError";
71
+ }
72
+ };
73
+ /**
74
+ * Thrown when a disposed primitive (or its underlying scheduler) is read, written, or otherwise
75
+ * used after `dispose()`.
76
+ * @public
77
+ */
78
+ var PicoFlowDisposedError = class extends PicoFlowError {
79
+ constructor(message) {
80
+ super(message);
81
+ this.name = "PicoFlowDisposedError";
82
+ }
83
+ };
84
+ /**
85
+ * Thrown when the reactive flush detects a feedback loop that exceeds the maximum step guard.
86
+ * @public
87
+ */
88
+ var PicoFlowCycleError = class extends PicoFlowError {
89
+ constructor(message) {
90
+ super(message);
91
+ this.name = "PicoFlowCycleError";
92
+ }
93
+ };
94
+ /**
95
+ * Thrown when a node reaches a state its own implementation considers unreachable. Reaching this
96
+ * indicates a picoflow bug rather than a misuse of the public API.
97
+ * @public
98
+ */
99
+ var PicoFlowInternalError = class extends PicoFlowError {
100
+ constructor(message) {
101
+ super(message);
102
+ this.name = "PicoFlowInternalError";
103
+ }
104
+ };
105
+ /**
106
+ * Thrown when a documented precondition of a public method is violated by the caller: a collection
107
+ * operation naming an entry that does not exist (or, for `add`, one that already does), and
108
+ * `from()`'s check on the value it was handed. Reaching this indicates a misuse of the API rather
109
+ * than a picoflow bug, and the call had no effect (#123).
110
+ * @public
111
+ */
112
+ var PicoFlowPreconditionError = class extends PicoFlowError {
113
+ constructor(message) {
114
+ super(message);
115
+ this.name = "PicoFlowPreconditionError";
116
+ }
117
+ };
118
+ /**
119
+ * Normalises a thrown value to an `Error`, so that every read path — `pick()`, `get()`, `value`,
120
+ * and `subscribe`'s `onError` — surfaces the same type whatever the computation threw (A2-SEC-3,
121
+ * #67). A genuine `Error` passes through by identity; anything else is wrapped, with its string
122
+ * form as the message and the original value as `cause`.
123
+ * @internal
124
+ */
125
+ function toError(thrown) {
126
+ return thrown instanceof Error ? thrown : new Error(String(thrown), { cause: thrown });
127
+ }
128
+
129
+ //#endregion
130
+ //#region src/core/base/liveNodeRegistry.ts
131
+ /**
132
+ * Census of every graph participant ever created, held weakly (#50, A2-DX-2).
133
+ *
134
+ * It is what an inspection dump reads. The graph has no roots of its own, and the nodes a census
135
+ * exists to reveal — the ones leaked because nothing disposed them — are precisely the ones no
136
+ * handle points at any more, so a traversal from caller-supplied roots could never reach them.
137
+ *
138
+ * Tracking is unconditional: an opt-in census misses everything built before it was switched on,
139
+ * which is the confusing half-graph an introspection tool must never show. That puts its cost on
140
+ * every node's construction, a path picoflow is already slow on, so it is pared to one
141
+ * {@link WeakRef} and one array push. Two things it deliberately does not do: register with a
142
+ * `FinalizationRegistry` (whose `register` measured four times the cost of the reference it was
143
+ * there to clean up), and keep a `Set` (whose hashing measured half again the cost of the push).
144
+ *
145
+ * Entries are **not** removed on disposal: a disposed node something still references is forensic
146
+ * evidence, and a dump filters it out by status instead. What removes an entry is the garbage
147
+ * collector taking its target, after which the compaction below drops the empty reference — on
148
+ * every read, and while writing whenever the census has doubled since it was last compacted.
149
+ * @internal
150
+ */
151
+ var LiveNodeRegistry = class LiveNodeRegistry {
152
+ static _refs = [];
153
+ /** Length the census has to reach before compacting is worth the walk. */
154
+ static _compactAt = 64;
155
+ /** Records a node. Called from the {@link Disposable} constructor, so nothing escapes the census. */
156
+ static track(node) {
157
+ if (LiveNodeRegistry._refs.length >= LiveNodeRegistry._compactAt) LiveNodeRegistry._compact();
158
+ LiveNodeRegistry._refs.push(new WeakRef(node));
159
+ }
160
+ /** Visits the nodes still alive, in creation order. Reads no status and touches no registry. */
161
+ static forEach(fn) {
162
+ LiveNodeRegistry._compact();
163
+ for (const ref of [...LiveNodeRegistry._refs]) {
164
+ const node = ref.deref();
165
+ if (node !== void 0) fn(node);
166
+ }
167
+ }
168
+ /**
169
+ * Empties the census.
170
+ *
171
+ * Test support, deliberately not part of the public `reset()`: that one returns the *scheduler*
172
+ * to its just-started state and documents that primitives created before it stay usable, so
173
+ * dropping them from the census there would hide live nodes. A suite asserting on the census
174
+ * needs the clean slate all the same, the census being process-global like the scheduler.
175
+ */
176
+ static clear() {
177
+ LiveNodeRegistry._refs = [];
178
+ LiveNodeRegistry._compactAt = 64;
179
+ }
180
+ /**
181
+ * Drops the references whose target has been collected and sets the next compaction a doubling
182
+ * away, so the walk is amortised to a constant per node. In place, keeping creation order: a
183
+ * fresh array would allocate one slot per live node every time.
184
+ */
185
+ static _compact() {
186
+ const refs = LiveNodeRegistry._refs;
187
+ let kept = 0;
188
+ for (let i = 0; i < refs.length; i++) {
189
+ const ref = refs[i];
190
+ if (ref.deref() !== void 0) refs[kept++] = ref;
191
+ }
192
+ refs.length = kept;
193
+ LiveNodeRegistry._compactAt = Math.max(64, kept * 2);
194
+ }
195
+ };
196
+
197
+ //#endregion
198
+ //#region src/core/base/disposable.ts
199
+ /**
200
+ * Base implementation of the disposable pattern for reactive primitives.
201
+ * Concrete subclasses implement `dispose()`, which is idempotent: a second call is a no-op.
202
+ */
203
+ var Disposable = class Disposable {
204
+ static _nextId = 0;
205
+ _disposed = false;
206
+ _name;
207
+ /**
208
+ * Identity for introspection (#50): monotonic per process, assigned at construction and never
209
+ * reused, so the same node is recognisable across successive dumps — and so an unnamed one can
210
+ * still be labelled `kind#id` rather than being indistinguishable from its siblings.
211
+ * @internal
212
+ */
213
+ _id = ++Disposable._nextId;
214
+ /**
215
+ * Which primitive this node belongs to, for introspection (#50).
216
+ *
217
+ * A plain field written once at construction rather than something an inspector works out
218
+ * afterwards: deriving it would mean keeping a map from every engine back to the facade that
219
+ * names it, and a `WeakMap.set` per node costs about as much as building the node did.
220
+ * @internal
221
+ */
222
+ _kind = "unknown";
223
+ constructor(name) {
224
+ this._name = name;
225
+ LiveNodeRegistry.track(this);
226
+ }
227
+ /**
228
+ * The `name` given at creation, for introspection. A getter rather than making `_name` itself
229
+ * public: the field stays protected, as the error builders below are its only other readers.
230
+ * @internal
231
+ */
232
+ get _inspectName() {
233
+ return this._name;
234
+ }
235
+ /** Builds the disposed-primitive error, naming the primitive when a `name` was given at creation. */
236
+ _disposedError() {
237
+ return new PicoFlowDisposedError(this._name ? `[PicoFlow] Primitive "${this._name}" is disposed` : "[PicoFlow] Primitive is disposed");
238
+ }
239
+ /**
240
+ * Releases the primitive when it leaves a `using` block, by delegating to {@link dispose}.
241
+ *
242
+ * Declared once here rather than per subclass: the standard `Disposable` protocol keys off this
243
+ * symbol, and every primitive reaches it through this base (A2-ARCH-2).
244
+ */
245
+ [Symbol.dispose]() {
246
+ this.dispose();
247
+ }
248
+ };
249
+
250
+ //#endregion
251
+ //#region src/core/base/traceHub.ts
252
+ /**
253
+ * Where the engine reports what it is doing, for a trace to pick up (#50, A2-DX-2).
254
+ *
255
+ * The engine knows nothing about traces: it calls {@link emit} at the handful of points where
256
+ * something happens, and the first thing `emit` does is return unless someone is listening. That
257
+ * one boolean is the entire cost of tracing being available, which is what lets the hooks sit on
258
+ * the flush path at all.
259
+ *
260
+ * What a listener does with an event — turning a node into a readable reference, keeping a bounded
261
+ * history of them — belongs to the inspection layer, so that this stays free of any notion of how
262
+ * a node is described.
263
+ * @internal
264
+ */
265
+ var TraceHub = class TraceHub {
266
+ /** Read before anything else on every hot path; kept in step with `_listeners` below. */
267
+ static enabled = false;
268
+ static _listeners = /* @__PURE__ */ new Set();
269
+ static emit(type, node) {
270
+ if (!TraceHub.enabled) return;
271
+ for (const listener of TraceHub._listeners) listener(type, node);
272
+ }
273
+ static listen(listener) {
274
+ TraceHub._listeners.add(listener);
275
+ TraceHub.enabled = true;
276
+ }
277
+ static unlisten(listener) {
278
+ TraceHub._listeners.delete(listener);
279
+ TraceHub.enabled = TraceHub._listeners.size > 0;
280
+ }
281
+ };
282
+
283
+ //#endregion
284
+ //#region src/core/base/executionStack.ts
285
+ /**
286
+ * Global scheduler that batches and executes reactive computations asynchronously.
287
+ *
288
+ * Flush order: pending value nodes first, then effects, both in the next task.
289
+ * That task is posted on a MessageChannel port, not setTimeout(0): the HTML spec clamps nested
290
+ * timers to >= 4 ms (~5-6 ms per reactive round measured on Chromium/Firefox, A2-PERF-1), while
291
+ * message tasks are never clamped — and fake timers cannot freeze them either (A2-RT-2).
292
+ * Queues are index-drained so items appended mid-flush are not lost (see _drain).
293
+ */
294
+ var ExecutionStack = class ExecutionStack {
295
+ static _MAX_FLUSH_STEPS = 1e4;
296
+ static _pendingQueue = [];
297
+ static _effectQueue = [];
298
+ static _executionScheduled;
299
+ /** Resolver of _executionScheduled, invoked by the flush task once the queues have drained. */
300
+ static _executionResolve;
301
+ /**
302
+ * Bumped by reset(). A posted flush carries the generation it was scheduled under; a message
303
+ * arriving with a stale generation is a flush reset() cancelled, and is dropped on delivery —
304
+ * MessageChannel has no clearTimeout equivalent.
305
+ */
306
+ static _flushGeneration = 0;
307
+ /** Lazily created channel carrying flush tasks; post schedules, receive drains. */
308
+ static _flushChannel;
309
+ /** Handle of the fallback timer backing _executionScheduled where MessageChannel is missing. */
310
+ static _executionTimer;
311
+ static _coalesceResetScheduled = false;
312
+ /** Per-effect resets accumulated for the microtask scheduled below, so notifying more than one
313
+ * effect in the same sync turn doesn't discard every reset after the first (A2-SEM-5). */
314
+ static _coalesceResets = /* @__PURE__ */ new Set();
315
+ /** Routes any error escaping the flush loop; defaults to logging rather than crashing (A2-SEM-2, A2-SEC-1). */
316
+ static _onFlushError = (error) => console.error(error);
317
+ /**
318
+ * True once the current synchronous call stack has finished and microtasks have started.
319
+ * EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
320
+ * from coalesced sync-batch notifications.
321
+ */
322
+ static _pastSyncTurn = false;
323
+ /** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
324
+ static get pastSyncTurn() {
325
+ return ExecutionStack._pastSyncTurn;
326
+ }
327
+ /**
328
+ * @internal Runs after the current sync turn ends (queueMicrotask).
329
+ * Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
330
+ * and resets per-effect coalesce flags via onReset.
331
+ */
332
+ static scheduleCoalesceReset(onReset) {
333
+ ExecutionStack._coalesceResets.add(onReset);
334
+ if (ExecutionStack._coalesceResetScheduled) return;
335
+ ExecutionStack._coalesceResetScheduled = true;
336
+ queueMicrotask(() => {
337
+ ExecutionStack._pastSyncTurn = true;
338
+ ExecutionStack._coalesceResetScheduled = false;
339
+ const resets = ExecutionStack._coalesceResets;
340
+ ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
341
+ resets.forEach((reset) => {
342
+ reset();
343
+ });
344
+ });
345
+ }
346
+ /** @internal Wired to the public `onFlushError()` API in `src/api/base/flowConfig.ts`. */
347
+ static setFlushErrorHandler(handler) {
348
+ ExecutionStack._onFlushError = handler;
349
+ }
350
+ /**
351
+ * @internal Wired to the public `reset()` API in `src/core/api/base/flowConfig.ts`.
352
+ *
353
+ * Discards every queue and flag, returning the scheduler to its just-started state (A2-ARCH-1).
354
+ * Queued work is dropped, not run: nodes left in the queues stay `dirty` and only recompute if
355
+ * something notifies them again.
356
+ */
357
+ static reset() {
358
+ ExecutionStack._pendingQueue.length = 0;
359
+ ExecutionStack._effectQueue.length = 0;
360
+ ExecutionStack._flushGeneration++;
361
+ ExecutionStack._flushChannel?.receive.unref?.();
362
+ clearTimeout(ExecutionStack._executionTimer);
363
+ ExecutionStack._executionTimer = void 0;
364
+ ExecutionStack._executionScheduled = void 0;
365
+ ExecutionStack._executionResolve = void 0;
366
+ ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
367
+ ExecutionStack._coalesceResetScheduled = false;
368
+ ExecutionStack._pastSyncTurn = false;
369
+ ExecutionStack._onFlushError = (error) => console.error(error);
370
+ }
371
+ static pushPending(node) {
372
+ TraceHub.emit("queue-pending", node);
373
+ ExecutionStack._beginSyncTurn();
374
+ ExecutionStack._scheduleExecution();
375
+ ExecutionStack._pendingQueue.push(node);
376
+ }
377
+ static pushEffect(effect) {
378
+ TraceHub.emit("queue-effect", effect);
379
+ ExecutionStack._beginSyncTurn();
380
+ ExecutionStack._scheduleExecution();
381
+ ExecutionStack._effectQueue.push(effect);
382
+ }
383
+ static _beginSyncTurn() {
384
+ ExecutionStack._pastSyncTurn = false;
385
+ }
386
+ /** Lazily builds the channel carrying flush tasks and installs the drain handler. */
387
+ static _channel() {
388
+ if (!ExecutionStack._flushChannel) {
389
+ const { port1, port2 } = new MessageChannel();
390
+ const receive = port1;
391
+ receive.onmessage = (event) => {
392
+ if (event.data !== ExecutionStack._flushGeneration) return;
393
+ const resolve = ExecutionStack._executionResolve;
394
+ ExecutionStack._executionScheduled = void 0;
395
+ ExecutionStack._executionResolve = void 0;
396
+ receive.unref?.();
397
+ ExecutionStack._execute();
398
+ resolve?.();
399
+ };
400
+ receive.unref?.();
401
+ ExecutionStack._flushChannel = {
402
+ post: port2,
403
+ receive
404
+ };
405
+ }
406
+ return ExecutionStack._flushChannel;
407
+ }
408
+ static _scheduleExecution() {
409
+ if (ExecutionStack._executionScheduled) return;
410
+ if (typeof MessageChannel === "undefined") {
411
+ ExecutionStack._executionScheduled = new Promise((resolve) => {
412
+ ExecutionStack._executionTimer = setTimeout(() => {
413
+ ExecutionStack._executionScheduled = void 0;
414
+ ExecutionStack._executionTimer = void 0;
415
+ ExecutionStack._execute();
416
+ resolve();
417
+ }, 0);
418
+ });
419
+ return;
420
+ }
421
+ const channel = ExecutionStack._channel();
422
+ ExecutionStack._executionScheduled = new Promise((resolve) => {
423
+ ExecutionStack._executionResolve = resolve;
424
+ });
425
+ channel.receive.ref?.();
426
+ channel.post.postMessage(ExecutionStack._flushGeneration);
427
+ }
428
+ static _drain(queue) {
429
+ for (let i = 0; i < queue.length; i++) {
430
+ if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
431
+ const looping = queue[i];
432
+ if (looping) TraceHub.emit("cycle-error", looping);
433
+ ExecutionStack._pendingQueue.length = 0;
434
+ ExecutionStack._effectQueue.length = 0;
435
+ ExecutionStack._reportFlushError(new PicoFlowCycleError("[PicoFlow] Reactive update cycle exceeded maximum depth"));
436
+ return;
437
+ }
438
+ const node = queue[i];
439
+ try {
440
+ node?.execute();
441
+ } catch (error) {
442
+ ExecutionStack._reportFlushError(error);
443
+ }
444
+ }
445
+ queue.length = 0;
446
+ }
447
+ /** Invokes the flush-error hook under a guard of its own (A4-CORR-1). The hook exists so an
448
+ * error escaping the flush cannot become an uncaught exception; a hook that throws must not
449
+ * reintroduce one, nor abandon the effects queued behind it. Reported to console.error — the
450
+ * documented default, and the only reporter that cannot itself be the broken one. */
451
+ static _reportFlushError(error) {
452
+ try {
453
+ ExecutionStack._onFlushError(error);
454
+ } catch (handlerError) {
455
+ console.error("[PicoFlow] the onFlushError handler threw", handlerError);
456
+ console.error(error);
457
+ }
458
+ }
459
+ static _execute() {
460
+ ExecutionStack._drain(ExecutionStack._pendingQueue);
461
+ ExecutionStack._drain(ExecutionStack._effectQueue);
462
+ }
463
+ };
464
+
465
+ //#endregion
466
+ //#region src/core/base/observer.ts
467
+ /**
468
+ * Narrows the public tracker type to the internal observer it always is.
469
+ *
470
+ * The one place this conversion happens, and the only cast left in the graph-wiring path. It is safe
471
+ * by construction rather than by inspection: {@link FlowTracker} is branded (A2-TYPE-3), the brand is
472
+ * declared only on {@link Observer} and {@link Node}, and both implement {@link IObserver} — so no
473
+ * value can reach here typed as a tracker without being one. A duck-typed object can still reach it
474
+ * at runtime through a double cast on the caller's side, which is exactly the A2-RT-3 behaviour left
475
+ * deliberately untouched.
476
+ */
477
+ function asObserver(t) {
478
+ return t;
479
+ }
480
+ /**
481
+ * Base implementation managing dependency tracking for reactive observers.
482
+ */
483
+ var Observer = class extends Disposable {
484
+ _dependenciesRegistry = new DependenciesRegistry();
485
+ /** Set when a dependency disposed while this observer was queued; consumed by the next execute(). */
486
+ _dependencyDisposed = false;
487
+ /** Set while this observer awaits a flush; cleared at the start of execute(). */
488
+ _queued = false;
489
+ /**
490
+ * Reports this observer's lifecycle. Never throws, so a debugger or a `console.log` can inspect
491
+ * a released effect — the same reasoning as the observable `status` getter (A2-ARCH-2).
492
+ */
493
+ get status() {
494
+ if (this._disposed) return "disposed";
495
+ return this._queued ? "queued" : "idle";
496
+ }
497
+ /** Visits the sources this observer currently reads, for introspection — see {@link Observable._forEachDependent}. @internal */
498
+ _forEachDependency(fn) {
499
+ this._dependenciesRegistry.forEach(fn);
500
+ }
501
+ markDependencyDisposed() {
502
+ this._dependencyDisposed = true;
503
+ }
504
+ registerDependency(dependency) {
505
+ if (this._disposed) throw this._disposedError();
506
+ this._dependenciesRegistry.register(dependency, this);
507
+ }
508
+ unregisterDependency(dependency) {
509
+ if (this._disposed) throw this._disposedError();
510
+ this._dependenciesRegistry.unregister(dependency, this);
511
+ }
512
+ clearDependencies() {
513
+ if (this._disposed) throw this._disposedError();
514
+ this._dependenciesRegistry.clear(this);
515
+ }
516
+ dispose() {
517
+ if (this._disposed) return;
518
+ this._dependenciesRegistry.clear(this);
519
+ this._disposed = true;
520
+ }
521
+ };
522
+
523
+ //#endregion
524
+ //#region src/core/base/observable.ts
525
+ /**
526
+ * Base implementation managing the dependency graph for reactive observables.
527
+ */
528
+ var Observable = class extends Disposable {
529
+ _dependentsRegistry = new DependentsRegistry();
530
+ _status = "resolved";
531
+ get status() {
532
+ if (this._disposed) return "disposed";
533
+ return this._status;
534
+ }
535
+ set status(status) {
536
+ if (this._disposed) throw this._disposedError();
537
+ this._status = status;
538
+ }
539
+ registerDependent(dependent) {
540
+ if (this._disposed) throw this._disposedError();
541
+ this._dependentsRegistry.register(dependent);
542
+ }
543
+ unregisterDependent(dependent) {
544
+ if (this._disposed) throw this._disposedError();
545
+ this._dependentsRegistry.unregister(dependent);
546
+ }
547
+ notifyDependents() {
548
+ if (this._disposed) return;
549
+ this._dependentsRegistry.notifyAll();
550
+ }
551
+ /**
552
+ * Visits the observers currently registered on this node, for introspection (#50).
553
+ *
554
+ * No disposal guard, unlike every mutating method here: an inspector must be able to describe a
555
+ * released node, and disposal has already emptied the registry, so a disposed node simply
556
+ * reports no dependent. Reading the edges registers nothing and computes nothing.
557
+ * @internal
558
+ */
559
+ _forEachDependent(fn) {
560
+ this._dependentsRegistry.forEach(fn);
561
+ }
562
+ watch(tracker) {
563
+ if (this._disposed) throw this._disposedError();
564
+ asObserver(tracker).registerDependency(this);
565
+ }
566
+ trigger() {
567
+ if (this._disposed) throw this._disposedError();
568
+ this.notifyDependents();
569
+ }
570
+ /**
571
+ * Hook for subclasses that also hold dependencies ({@link Node}): runs inside {@link dispose}
572
+ * after the dependents are detached and before the disposal flag is set, so the override can
573
+ * still call the guarded unregister methods (A2-ARCH-5, #111).
574
+ */
575
+ _disposeDependencies() {}
576
+ dispose() {
577
+ if (this._disposed) return;
578
+ this._dependentsRegistry.forEach((dependant) => {
579
+ dependant.markDependencyDisposed();
580
+ dependant.unregisterDependency(this);
581
+ this.unregisterDependent(dependant);
582
+ });
583
+ this._disposeDependencies();
584
+ this._disposed = true;
585
+ }
586
+ };
587
+
588
+ //#endregion
589
+ //#region src/core/base/node.ts
590
+ /**
591
+ * Base implementation for reactive nodes that propagate changes through the dependency graph.
592
+ *
593
+ * A node is both observed and observing. The observed half — dependents registry, `status`,
594
+ * `watch`, `trigger`, `notifyDependents`, `dispose` — is inherited from {@link Observable} rather
595
+ * than copied (A2-ARCH-5, #34, #111); this class adds only the observing half, as guarded
596
+ * delegations to {@link DependenciesRegistry}, the same shape {@link Observer} has.
597
+ */
598
+ var Node = class extends Observable {
599
+ _dependenciesRegistry = new DependenciesRegistry();
600
+ /** Set when a dependency disposed while this node was dirty; consumed by the next execute(). */
601
+ _dependencyDisposed = false;
602
+ registerDependency(dependency) {
603
+ if (this._disposed) throw this._disposedError();
604
+ this._dependenciesRegistry.register(dependency, this);
605
+ }
606
+ unregisterDependency(dependency) {
607
+ if (this._disposed) throw this._disposedError();
608
+ this._dependenciesRegistry.unregister(dependency, this);
609
+ }
610
+ clearDependencies() {
611
+ if (this._disposed) throw this._disposedError();
612
+ this._dependenciesRegistry.clear(this);
613
+ }
614
+ /** Visits the sources this node currently reads, for introspection — see {@link Observable._forEachDependent}. @internal */
615
+ _forEachDependency(fn) {
616
+ this._dependenciesRegistry.forEach(fn);
617
+ }
618
+ markDependencyDisposed() {
619
+ this._dependencyDisposed = true;
620
+ }
621
+ /** Runs inside Observable.dispose(), before the disposal flag is set. */
622
+ _disposeDependencies() {
623
+ this._dependenciesRegistry.forEach((dependency) => {
624
+ this.unregisterDependency(dependency);
625
+ });
626
+ }
627
+ };
628
+
629
+ //#endregion
630
+ //#region src/core/schedulers/pendingError.ts
631
+ /**
632
+ * Error thrown when an async computation is pending, encapsulating its promise for cascading dependencies.
633
+ * @internal
634
+ */
635
+ var PendingError = class extends Error {
636
+ pendingPromise;
637
+ constructor(promise) {
638
+ super("[PicoFlow] Computation is pending");
639
+ this.name = "PendingError";
640
+ this.pendingPromise = promise;
641
+ }
642
+ };
643
+
644
+ //#endregion
645
+ //#region src/core/schedulers/asyncResolver.ts
646
+ /**
647
+ * Manages asynchronous computation lifecycle with iteration tracking to prevent race conditions.
648
+ * @internal
649
+ */
650
+ var AsyncResolver = class {
651
+ _compute;
652
+ _computed = Promise.withResolvers();
653
+ _iteration = 0;
654
+ _aborted = false;
655
+ _finished = false;
656
+ /** Set when the computation rejected with `PendingError`: it waits on a dependency (#113). */
657
+ _blocked = false;
658
+ constructor(compute) {
659
+ this._compute = compute;
660
+ this._computed.promise.catch(() => {});
661
+ }
662
+ get computed() {
663
+ return this._computed.promise;
664
+ }
665
+ get aborted() {
666
+ return this._aborted;
667
+ }
668
+ get finished() {
669
+ return this._finished;
670
+ }
671
+ get blocked() {
672
+ return this._blocked;
673
+ }
674
+ compute() {
675
+ if (this._finished) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't restart a settled resolver");
676
+ if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't restart an aborted resolver");
677
+ this._iteration++;
678
+ this._blocked = false;
679
+ const currentIteration = this._iteration;
680
+ this._compute().then((value) => {
681
+ if (this._iteration === currentIteration && !this._aborted) this._resolve(value);
682
+ }).catch((error) => {
683
+ if (this._iteration === currentIteration && !this._aborted) {
684
+ if (error instanceof PendingError) this._blocked = true;
685
+ else this._reject(error);
686
+ }
687
+ });
688
+ }
689
+ overwrite(promise) {
690
+ if (this._finished) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't overwrite a settled resolver");
691
+ if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] AsyncResolver: Can't overwrite an aborted resolver");
692
+ this._iteration++;
693
+ this._blocked = false;
694
+ const currentIteration = this._iteration;
695
+ promise.then((value) => {
696
+ if (this._iteration === currentIteration && !this._aborted) this._resolve(value);
697
+ }).catch((error) => {
698
+ if (this._iteration === currentIteration && !this._aborted) this._reject(error);
699
+ });
700
+ }
701
+ _resolve(value) {
702
+ this._finished = true;
703
+ this._computed.resolve(value);
704
+ }
705
+ _reject(error) {
706
+ this._finished = true;
707
+ this._computed.reject(error);
708
+ }
709
+ abort() {
710
+ this._aborted = true;
711
+ }
712
+ };
713
+
714
+ //#endregion
715
+ //#region src/core/schedulers/asyncScheduler.ts
716
+ /**
717
+ * Scheduler that manages asynchronous computation lifecycle with resolver instances.
718
+ * @internal
719
+ */
720
+ var AsyncScheduler = class {
721
+ inline = false;
722
+ _compute;
723
+ _resolver;
724
+ _onResolve;
725
+ _onReject;
726
+ _settled = Promise.withResolvers();
727
+ _disposed = false;
728
+ constructor(compute, onResolve, onReject) {
729
+ this._compute = compute;
730
+ this._onResolve = onResolve;
731
+ this._onReject = onReject;
732
+ this._resolver = new AsyncResolver(compute);
733
+ this._attachHandlers(this._resolver);
734
+ }
735
+ get settled() {
736
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
737
+ return this._settled.promise;
738
+ }
739
+ get disposed() {
740
+ return this._disposed;
741
+ }
742
+ get blocked() {
743
+ return this._resolver.blocked;
744
+ }
745
+ wrap(value) {
746
+ return Promise.resolve(value);
747
+ }
748
+ chain(input, fn) {
749
+ return input.then((value) => fn(value));
750
+ }
751
+ overwrite(promise) {
752
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
753
+ if (this._resolver.finished) {
754
+ this._resolver = new AsyncResolver(this._compute);
755
+ this._settled = Promise.withResolvers();
756
+ this._attachHandlers(this._resolver);
757
+ }
758
+ this._resolver.overwrite(promise);
759
+ }
760
+ compute() {
761
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
762
+ if (this._resolver.finished) {
763
+ this._resolver = new AsyncResolver(this._compute);
764
+ this._settled = Promise.withResolvers();
765
+ this._attachHandlers(this._resolver);
766
+ }
767
+ this._resolver.compute();
768
+ }
769
+ _attachHandlers(resolver) {
770
+ const settled = this._settled;
771
+ resolver.computed.then((value) => {
772
+ if (this._disposed) return;
773
+ this._onResolve(value);
774
+ }).catch((error) => {
775
+ if (this._disposed) return;
776
+ this._onReject(error);
777
+ }).finally(() => settled.resolve());
778
+ }
779
+ dispose() {
780
+ this._resolver.abort();
781
+ this._disposed = true;
782
+ this._settled.resolve();
783
+ }
784
+ };
785
+
786
+ //#endregion
787
+ //#region src/core/schedulers/syncResolver.ts
788
+ /**
789
+ * Manages synchronous computation lifecycle with abort and settled-state guards.
790
+ * @internal
791
+ */
792
+ var SyncResolver = class {
793
+ _compute;
794
+ _aborted = false;
795
+ _finished = false;
796
+ /** Set when the computation threw `PendingError`: it waits on a dependency (#113). */
797
+ _blocked = false;
798
+ _onValue;
799
+ _onError;
800
+ constructor(compute, onValue, onError) {
801
+ this._compute = compute;
802
+ this._onValue = onValue;
803
+ this._onError = onError;
804
+ }
805
+ get aborted() {
806
+ return this._aborted;
807
+ }
808
+ get finished() {
809
+ return this._finished;
810
+ }
811
+ get blocked() {
812
+ return this._blocked;
813
+ }
814
+ compute() {
815
+ if (this._finished) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't restart a settled resolver");
816
+ if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't restart an aborted resolver");
817
+ this._blocked = false;
818
+ try {
819
+ const value = this._compute();
820
+ if (!this._finished && !this._aborted) {
821
+ this._finished = true;
822
+ this._onValue(value);
823
+ }
824
+ } catch (error) {
825
+ if (!this._finished && !this._aborted) {
826
+ if (error instanceof PendingError) this._blocked = true;
827
+ else {
828
+ this._finished = true;
829
+ this._onError(error);
830
+ }
831
+ }
832
+ }
833
+ }
834
+ overwrite(value) {
835
+ if (this._finished) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't overwrite a settled resolver");
836
+ if (this._aborted) throw new PicoFlowInternalError("[PicoFlow] SyncResolver: Can't overwrite an aborted resolver");
837
+ this._blocked = false;
838
+ this._finished = true;
839
+ this._onValue(value);
840
+ }
841
+ abort() {
842
+ this._aborted = true;
843
+ }
844
+ };
845
+
846
+ //#endregion
847
+ //#region src/core/schedulers/syncScheduler.ts
848
+ /**
849
+ * Scheduler that manages synchronous computation lifecycle with resolver instances.
850
+ * @internal
851
+ */
852
+ var SyncScheduler = class {
853
+ inline = true;
854
+ _compute;
855
+ _resolver;
856
+ _onResolve;
857
+ _onReject;
858
+ _settled = Promise.withResolvers();
859
+ _disposed = false;
860
+ constructor(compute, onResolve, onReject) {
861
+ this._compute = compute;
862
+ this._onResolve = onResolve;
863
+ this._onReject = onReject;
864
+ this._resolver = this._newResolver();
865
+ }
866
+ get settled() {
867
+ return this._settled.promise;
868
+ }
869
+ get blocked() {
870
+ return this._resolver.blocked;
871
+ }
872
+ wrap(value) {
873
+ return value;
874
+ }
875
+ chain(input, fn) {
876
+ return fn(input);
877
+ }
878
+ /**
879
+ * Each resolver settles the deferred that was current when it was created — captured, not read
880
+ * at settlement time: the node's resolution handler may re-enter `overwrite()` with a queued
881
+ * write (#43, #113), which swaps `_settled` for a fresh deferred; the `pick()` awaiting the old
882
+ * one must still be released. Same shape as AsyncScheduler._attachHandlers.
883
+ */
884
+ _newResolver() {
885
+ const settled = this._settled;
886
+ return new SyncResolver(this._compute, (value) => {
887
+ this._onResolve(value);
888
+ settled.resolve();
889
+ }, (error) => {
890
+ this._onReject(error);
891
+ settled.resolve();
892
+ });
893
+ }
894
+ overwrite(value) {
895
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
896
+ if (this._resolver.finished) {
897
+ this._settled = Promise.withResolvers();
898
+ this._resolver = this._newResolver();
899
+ }
900
+ this._resolver.overwrite(value);
901
+ }
902
+ compute() {
903
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
904
+ if (this._resolver.finished) {
905
+ this._settled = Promise.withResolvers();
906
+ this._resolver = this._newResolver();
907
+ }
908
+ this._resolver.compute();
909
+ }
910
+ dispose() {
911
+ this._resolver.abort();
912
+ this._disposed = true;
913
+ this._settled.resolve();
914
+ }
915
+ };
916
+
917
+ //#endregion
918
+ //#region src/core/nodes/effectNode.ts
919
+ /**
920
+ * Reactive effect that automatically re-executes when its dependencies change.
921
+ *
922
+ * Scheduling uses epoch-based coalescing:
923
+ * - Multiple dependency changes in the same synchronous turn produce one queue entry
924
+ * but may still require multiple runs when notifications arrive in later microtasks
925
+ * (e.g. async pending → resolved).
926
+ * - Re-notifications during execute() (cascading effects) enqueue a fresh run because
927
+ * _queued is cleared at the start of execute().
928
+ *
929
+ * @internal
930
+ */
931
+ var EffectNode = class extends Observer {
932
+ _kind = "effect";
933
+ _data;
934
+ _onData;
935
+ _onError;
936
+ _onPending;
937
+ /** True while this effect is already waiting in the effect queue for the next flush. */
938
+ /** Guards the single coalesced bump of _notifyEpoch while _queued during one sync turn. */
939
+ _coalescedWhileQueued = false;
940
+ /** Monotonic count of notifications that must eventually be executed. */
941
+ _notifyEpoch = 0;
942
+ /** How many notification epochs have been executed so far. */
943
+ _executedEpoch = 0;
944
+ /** Callbacks registered via {@link onDispose}, run once after dispose() tears down the graph edges. */
945
+ _disposeListeners = [];
946
+ constructor(data, onData, onError, onPending, name) {
947
+ super(name);
948
+ this._data = data;
949
+ this._onData = onData;
950
+ this._onError = onError;
951
+ this._onPending = onPending;
952
+ this._notifyEpoch = 1;
953
+ this.execute();
954
+ }
955
+ /**
956
+ * Registers a callback to run once this effect is disposed, after the graph edges are torn
957
+ * down. Lets a caller that wraps an effect (e.g. {@link ActionNode.subscribe}) attach its own
958
+ * teardown without patching `dispose` on the returned instance (A2-ARCH-8).
959
+ * @internal
960
+ */
961
+ onDispose(fn) {
962
+ this._disposeListeners.push(fn);
963
+ }
964
+ dispose() {
965
+ if (this._disposed) return;
966
+ super.dispose();
967
+ this._disposeListeners.forEach((fn) => {
968
+ fn();
969
+ });
970
+ }
971
+ notify() {
972
+ if (this._disposed) throw this._disposedError();
973
+ if (this._queued) {
974
+ if (!this._coalescedWhileQueued) {
975
+ this._coalescedWhileQueued = true;
976
+ if (ExecutionStack.pastSyncTurn) this._notifyEpoch++;
977
+ }
978
+ return;
979
+ }
980
+ this._notifyEpoch++;
981
+ this._queued = true;
982
+ ExecutionStack.scheduleCoalesceReset(() => {
983
+ this._coalescedWhileQueued = false;
984
+ });
985
+ ExecutionStack.pushEffect(this);
986
+ }
987
+ execute() {
988
+ this._queued = false;
989
+ if (this._disposed) {
990
+ this._executedEpoch = this._notifyEpoch;
991
+ return;
992
+ }
993
+ if (this._dependencyDisposed) {
994
+ this._dependencyDisposed = false;
995
+ this._executedEpoch = this._notifyEpoch;
996
+ return;
997
+ }
998
+ const targetEpoch = this._notifyEpoch;
999
+ while (this._executedEpoch < targetEpoch) {
1000
+ this._executedEpoch++;
1001
+ if (this._disposed) {
1002
+ this._executedEpoch = this._notifyEpoch;
1003
+ return;
1004
+ }
1005
+ if (this._dependencyDisposed) {
1006
+ this._dependencyDisposed = false;
1007
+ this._executedEpoch = this._notifyEpoch;
1008
+ return;
1009
+ }
1010
+ TraceHub.emit("effect-run", this);
1011
+ try {
1012
+ this.clearDependencies();
1013
+ const data = this._data(this);
1014
+ this._onData(data);
1015
+ } catch (error) {
1016
+ if (error instanceof PendingError) this._onPending?.();
1017
+ else if (this._onError) this._onError(toError(error));
1018
+ else throw error;
1019
+ }
1020
+ }
1021
+ }
1022
+ };
1023
+
1024
+ //#endregion
1025
+ //#region src/core/nodes/readableNode.ts
1026
+ /**
1027
+ * Global-registry symbol branding a {@link ReadableNode} instance, so `from()` can recognize a node
1028
+ * built by a different copy of the library — where its class objects, and thus `instanceof`, differ.
1029
+ * @internal
1030
+ */
1031
+ const PICOFLOW_VALUE_NODE = Symbol.for("picoflow.node");
1032
+ /**
1033
+ * Public face of a reactive value: owns a {@link ValueCore} and implements `FlowValue` by delegating
1034
+ * to it. It has no graph membership of its own — the core is what other nodes depend on, what the
1035
+ * execution stack queues, and the tracker compute functions receive (A2-GOV-5, #71, #112 — step 2
1036
+ * of #110).
1037
+ *
1038
+ * This class carries the readers only. Each public primitive has a subclass that adds exactly the
1039
+ * mutators its interface declares — {@link ConstantNode}, {@link DerivationNode}, {@link StateNode},
1040
+ * {@link WritableDerivationNode}, {@link ArrayNode}, {@link MapNode} — so a method absent from a
1041
+ * primitive's type is absent from its prototype chain too (#114, step 4 of #110; closes #26, #32,
1042
+ * #108). `trigger()` is among those mutators: it stayed here one round longer, which let a constant
1043
+ * force its whole subtree to re-run, and moved down with the others in #132 (A3-ARCH-1). Sync and async are not classes: they differ by the core a facade is given, built with
1044
+ * {@link syncCore} or {@link asyncCore}.
1045
+ *
1046
+ * The core is a `#private` field, not an underscore-prefixed property: an ordinary property is
1047
+ * enumerable, so `JSON.stringify`, `Object.keys` or a spread of a primitive walked the whole engine —
1048
+ * current value included — into logs and persisted state (A2-SEC-5, #69). Subclasses reach it
1049
+ * through the protected `_core` accessor, a prototype getter nothing structural walks.
1050
+ * @internal
1051
+ */
1052
+ var ReadableNode = class {
1053
+ [PICOFLOW_VALUE_NODE] = true;
1054
+ #core;
1055
+ constructor(core, kind = "unknown") {
1056
+ this.#core = core;
1057
+ core._kind = kind;
1058
+ }
1059
+ /** The engine this facade delegates to. Subclasses narrow the input type by overriding it. */
1060
+ get _core() {
1061
+ return this.#core;
1062
+ }
1063
+ get status() {
1064
+ return this._core.status;
1065
+ }
1066
+ get(tracker) {
1067
+ return this._core.read(tracker, () => this._core.current, () => {
1068
+ this._core.markPendingRead();
1069
+ throw new PendingError(this._core.settled);
1070
+ });
1071
+ }
1072
+ watch(tracker) {
1073
+ this._core.read(tracker, () => void 0, () => {
1074
+ this._core.markPendingRead();
1075
+ throw new PendingError(this._core.settled);
1076
+ });
1077
+ }
1078
+ async pick() {
1079
+ return await this._core.read(void 0, () => this._core.current, () => this._core.settle());
1080
+ }
1081
+ /** Passive snapshot (A2-ARCH-10, #80) — see {@link ValueCore.snapshot}. */
1082
+ get value() {
1083
+ return this._core.snapshot();
1084
+ }
1085
+ /** Passive read of the cached value (A2-ARCH-10, #80); it survives disposal, so this does too. */
1086
+ get latest() {
1087
+ return this._core.current;
1088
+ }
1089
+ subscribe(onValue, onError, onPending, options) {
1090
+ this._core.assertAlive();
1091
+ return new EffectNode((t) => this.get(t), onValue, onError, onPending, options?.name);
1092
+ }
1093
+ dispose() {
1094
+ this._core.dispose();
1095
+ }
1096
+ /** Releases the primitive when it leaves a `using` block, by delegating to {@link dispose}. */
1097
+ [Symbol.dispose]() {
1098
+ this.dispose();
1099
+ }
1100
+ };
1101
+
1102
+ //#endregion
1103
+ //#region src/core/nodes/valueCore.ts
1104
+ /**
1105
+ * Engine behind every reactive value: the state (`status`, value, error), the scheduler, the graph
1106
+ * membership inherited from {@link Node}, the write queue, and the one status machine every read
1107
+ * goes through.
1108
+ *
1109
+ * A core has no public face of its own — {@link ReadableNode} owns one and implements `FlowValue` by
1110
+ * delegation (A2-GOV-5, #71, #112 — step 2 of #110). The core is what the registries hold, what the
1111
+ * `ExecutionStack` queues, and the `FlowTracker` handed to compute functions.
1112
+ *
1113
+ * One class serves sync and async values (#113 — step 3 of #110): `I` is the scheduler's input
1114
+ * type (the value itself, or a promise of it), and everything that used to tell the two apart is
1115
+ * asked of the {@link Scheduler} — `inline`, `blocked`, `wrap`, `chain`.
1116
+ * @internal
1117
+ */
1118
+ var ValueCore = class extends Node {
1119
+ _scheduler;
1120
+ _value;
1121
+ _error;
1122
+ /** Set by {@link markPendingRead} when a dependent registers while this node is mid-flight. */
1123
+ _hasPendingReader = false;
1124
+ /**
1125
+ * True only while the in-flight computation was started by a write, which already ran its own
1126
+ * eager `notifyDependents()` cascade before this resolution. `refresh()` and a recompute pulled
1127
+ * lazily after an upstream `notify()` have no such pre-cascade — for them, `_onResolve`'s own
1128
+ * `notifyDependents()` call is the only notification, so it must never be skipped (A2-PERF-4).
1129
+ */
1130
+ _pendingFromSet = false;
1131
+ /** True once a value has resolved at least once, so `_value` is a real value and not the initial gap. */
1132
+ _hasResolved = false;
1133
+ /**
1134
+ * Writes that arrived while the node was mid-flight, applied in call order on the settled value
1135
+ * (A2-TYPE-1, #43). A `set` discards everything queued before it, so the queue is at most one
1136
+ * `set` followed by `update`s.
1137
+ */
1138
+ _ops = [];
1139
+ /**
1140
+ * Change detection for this node. Defaults to `===` rather than `Object.is`, which is what
1141
+ * semantics guarantees 3 and 4 describe; a consumer opts out per primitive via the `equals`
1142
+ * option (A2-SEM-4, A2-ARCH-6).
1143
+ */
1144
+ _equals;
1145
+ /**
1146
+ * Built from a compute function that tracks dependencies, as opposed to a value or a lazy
1147
+ * initializer. Decides whether a replacing write on a never-computed node must run the
1148
+ * computation first so the dependency edges exist — see {@link overwrite} (#109).
1149
+ */
1150
+ _derivation;
1151
+ /**
1152
+ * Recompute on dirty instead of on read (#118): `notify()` enqueues this node in the flush, so
1153
+ * it computes without waiting for a reader. Only meaningful on a dependency-tracking compute
1154
+ * function, so the option is gated on `_derivation` — `state`, `constant`, the collections and
1155
+ * `signal` ignore it, as documented on {@link FlowPrimitiveOptions.eager}.
1156
+ */
1157
+ _eager;
1158
+ constructor(input, options, makeScheduler, derivation = false) {
1159
+ super(options?.name);
1160
+ this._derivation = derivation;
1161
+ this._eager = derivation && (options?.eager ?? false);
1162
+ this._equals = options?.equals ?? ((previous, next) => previous === next);
1163
+ const builtFromValue = typeof input !== "function";
1164
+ const compute = builtFromValue ? () => input : () => input(this, this._value);
1165
+ this.status = "dirty";
1166
+ this._scheduler = makeScheduler(compute, (value) => this._onResolve(value), (error) => this._onReject(error));
1167
+ if (builtFromValue && this._scheduler.inline) this._scheduler.compute();
1168
+ if (this._eager) ExecutionStack.pushPending(this);
1169
+ }
1170
+ /** Throws the disposed-primitive error when the core has been released; the guard every facade method starts with. */
1171
+ assertAlive() {
1172
+ if (this._disposed) throw this._disposedError();
1173
+ }
1174
+ /** Builds the "unreachable state" error, naming the primitive when a `name` was given at creation. */
1175
+ internalError() {
1176
+ return new PicoFlowInternalError(this._name ? `[PicoFlow] Internal error (${this._name})` : "[PicoFlow] Internal error");
1177
+ }
1178
+ /**
1179
+ * Called just before a tracked read throws `PendingError`, i.e. a dependent registered while
1180
+ * this node is mid-flight and has no other way to learn the computation later settled — so the
1181
+ * post-resolve `notifyDependents()` cannot be skipped even if the value is unchanged (A2-PERF-4).
1182
+ */
1183
+ markPendingRead() {
1184
+ this._hasPendingReader = true;
1185
+ }
1186
+ /** Whether the in-flight computation waits on a pending dependency — see {@link Scheduler.blocked}. */
1187
+ get blocked() {
1188
+ return this._scheduler.blocked;
1189
+ }
1190
+ /** Whether this core recomputes on dirty rather than on read (#118), for introspection (#50). @internal */
1191
+ get _isEager() {
1192
+ return this._eager;
1193
+ }
1194
+ /** The promise the scheduler settles on; what a pending tracked read hands to `PendingError`. */
1195
+ get settled() {
1196
+ return this._scheduler.settled;
1197
+ }
1198
+ /**
1199
+ * The one status machine behind `get()`, `watch()` and `pick()` (A2-GOV-5). `track` registers the
1200
+ * caller as a dependent first; a `dirty` core computes, then the settled status is dispatched
1201
+ * once: `resolved` → `onResolved`, `pending` → `onPending`, `error` → the error is thrown, and
1202
+ * `disposed` — reachable only when the computation just run disposed its own node — throws the
1203
+ * disposed error whichever way the node was being read (A2-ARCH-2).
1204
+ */
1205
+ read(track, onResolved, onPending) {
1206
+ this.assertAlive();
1207
+ if (track) this.watch(track);
1208
+ if (this.status === "dirty") this.execute();
1209
+ switch (this.status) {
1210
+ case "resolved": return onResolved();
1211
+ case "error": throw this._error;
1212
+ case "pending": return onPending();
1213
+ case "disposed": throw this._disposedError();
1214
+ /* v8 ignore next 2 */
1215
+ case "dirty": throw this.internalError();
1216
+ }
1217
+ }
1218
+ /** The raw cached value — what collections mutate in place, and what `latest` reports. */
1219
+ get current() {
1220
+ return this._value;
1221
+ }
1222
+ set current(value) {
1223
+ this._value = value;
1224
+ }
1225
+ /**
1226
+ * Passive snapshot (A2-ARCH-10, #80). No disposal guard on purpose: `status` already reports
1227
+ * `"disposed"` instead of throwing, and an inspector must be able to read a released node.
1228
+ */
1229
+ snapshot() {
1230
+ const status = this.status;
1231
+ switch (status) {
1232
+ case "resolved": return {
1233
+ status,
1234
+ data: this._value
1235
+ };
1236
+ case "error": return {
1237
+ status,
1238
+ error: this._error
1239
+ };
1240
+ default: return { status };
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Waits until the node leaves "pending". One settlement may not be enough: writes queued while
1245
+ * the node was mid-flight are applied on the settled value by re-overwriting the scheduler,
1246
+ * which hands out a fresh `settled` promise (A2-TYPE-1, #43).
1247
+ */
1248
+ async settle() {
1249
+ while (this.status === "pending") {
1250
+ const settled = this._scheduler.settled;
1251
+ await settled;
1252
+ if (this.status === "disposed") throw this._disposedError();
1253
+ /* v8 ignore next 3 */
1254
+ if (this.status === "pending" && this._scheduler.settled === settled) throw this.internalError();
1255
+ }
1256
+ if (this.status === "resolved") return this._value;
1257
+ if (this.status === "error") throw this._error;
1258
+ throw this.internalError();
1259
+ }
1260
+ notify() {
1261
+ if (this._disposed) throw this._disposedError();
1262
+ if (this.status === "dirty") return;
1263
+ TraceHub.emit("notify", this);
1264
+ if (this.status === "pending" || this._eager) ExecutionStack.pushPending(this);
1265
+ this.status = "dirty";
1266
+ this.notifyDependents();
1267
+ }
1268
+ execute() {
1269
+ if (this._disposed) return;
1270
+ if (this.status !== "dirty") return;
1271
+ if (this._dependencyDisposed && this._hasResolved && this._error === void 0) {
1272
+ this._dependencyDisposed = false;
1273
+ this.status = "resolved";
1274
+ return;
1275
+ }
1276
+ this.run();
1277
+ }
1278
+ /** Unconditional computation: clears the dependencies and runs the scheduler. */
1279
+ run() {
1280
+ TraceHub.emit("execute", this);
1281
+ this.clearDependencies();
1282
+ this.status = "pending";
1283
+ this._scheduler.compute();
1284
+ }
1285
+ dispose() {
1286
+ this._ops = [];
1287
+ super.dispose();
1288
+ this._scheduler.dispose();
1289
+ }
1290
+ /**
1291
+ * The `equals` comparator, guarded. `undefined` means it threw, in which case the node has
1292
+ * already been failed with that error and the caller must abandon what it was doing: the write
1293
+ * is not applied, the settlement is dropped (A3-SEM-2-b, #127).
1294
+ *
1295
+ * The two routes differ only in who notifies. At settlement `_onReject` is the node's own
1296
+ * failure handler and its notification rules already apply; a write call has no settlement to
1297
+ * notify for it, which is what `fail()` exists to cover (A3-SEM-2-a). `from` is a string rather
1298
+ * than a callback so a comparison allocates nothing — it runs on every settlement.
1299
+ */
1300
+ _compare(previous, next, from) {
1301
+ try {
1302
+ return this._equals(previous, next);
1303
+ } catch (error) {
1304
+ if (from === "settlement") this._onReject(error);
1305
+ else this.fail(error);
1306
+ return;
1307
+ }
1308
+ }
1309
+ _onResolve(value) {
1310
+ if (this._disposed) return;
1311
+ TraceHub.emit("resolve", this);
1312
+ if (this._ops.length > 0) {
1313
+ this._applyOps(this._scheduler.wrap(value));
1314
+ return;
1315
+ }
1316
+ let changed = true;
1317
+ if (this._hasResolved) {
1318
+ const same = this._compare(this._value, value, "settlement");
1319
+ if (same === void 0) return;
1320
+ changed = !same;
1321
+ }
1322
+ const hadPendingReader = this._hasPendingReader;
1323
+ const pendingFromSet = this._pendingFromSet;
1324
+ this._hasPendingReader = false;
1325
+ this._pendingFromSet = false;
1326
+ this.status = "resolved";
1327
+ this._value = value;
1328
+ this._hasResolved = true;
1329
+ this._error = void 0;
1330
+ if (this._scheduler.inline) return;
1331
+ if (changed || hadPendingReader || !pendingFromSet) this.notifyDependents();
1332
+ }
1333
+ _onReject(error) {
1334
+ if (this._disposed) return;
1335
+ TraceHub.emit("reject", this);
1336
+ const first = this._ops[0];
1337
+ if (first?.kind === "set") {
1338
+ this._applyOps(first.input, 1);
1339
+ return;
1340
+ }
1341
+ this._ops = [];
1342
+ this.status = "error";
1343
+ this._error = toError(error);
1344
+ if (this._scheduler.inline) return;
1345
+ this.notifyDependents();
1346
+ }
1347
+ /**
1348
+ * Fails the node from a write-path callback that threw, the way a failing computation fails it:
1349
+ * the error is recorded and normalised through `toError`, `_value` is left on the last resolved
1350
+ * value, and a `set()` recovers the node (A3-SEM-2-a, #126).
1351
+ *
1352
+ * The notification is the one thing that does not simply delegate. Inline, `_onReject` returns
1353
+ * before notifying because an inline settlement always happens inside the call that will notify
1354
+ * — `_overwrite`, `refresh()`. A callback that threw before any of those ran has no such caller,
1355
+ * so the notification is issued here. A deferred scheduler is the opposite case: `_onReject`
1356
+ * notifies for itself, and a second call here would notify twice.
1357
+ */
1358
+ fail(error) {
1359
+ this._onReject(error);
1360
+ if (this._scheduler.inline) this.notifyDependents();
1361
+ }
1362
+ /**
1363
+ * Chains the queued writes from `from` on `base` and hands the chain to the scheduler as a new
1364
+ * input: the node stays pending and dependents never observe the intermediate value. Called
1365
+ * from the settlement handlers, where the resolver is already marked finished, so `overwrite`
1366
+ * starts a fresh resolver with its own `settled`. Inline, the chain is folded on the spot and a
1367
+ * throwing updater fails the node like a computation would.
1368
+ */
1369
+ _applyOps(base, from = 0) {
1370
+ const ops = this._ops;
1371
+ this._ops = [];
1372
+ let chain = base;
1373
+ try {
1374
+ for (let i = from; i < ops.length; i++) {
1375
+ const op = ops[i];
1376
+ chain = op.kind === "set" ? op.input : this._scheduler.chain(chain, op.updater);
1377
+ }
1378
+ } catch (error) {
1379
+ this._onReject(error);
1380
+ return;
1381
+ }
1382
+ this.status = "pending";
1383
+ this._pendingFromSet = true;
1384
+ this._scheduler.overwrite(chain);
1385
+ }
1386
+ /**
1387
+ * The replacing write behind `set()`. Returns whether the write is going to land: `false` only
1388
+ * when the comparator dropped it as unchanged, `true` when it applied now or was queued to apply
1389
+ * at settlement. Collections read that answer to keep `$lastAction` in step with the value view —
1390
+ * a dropped write must not emit a `set` action (A3-SEC-1, #129).
1391
+ */
1392
+ overwrite(input) {
1393
+ this.assertAlive();
1394
+ if (this.status === "dirty" && this._derivation) this.execute();
1395
+ if (this.status === "pending" && this._scheduler.blocked) {
1396
+ this._ops = [{
1397
+ kind: "set",
1398
+ input
1399
+ }];
1400
+ return true;
1401
+ }
1402
+ if (this.status === "resolved" && this._scheduler.inline) {
1403
+ const same = this._compare(this._value, input, "write");
1404
+ if (same === void 0 || same) return false;
1405
+ }
1406
+ this._ops = [];
1407
+ this._overwrite(input);
1408
+ return true;
1409
+ }
1410
+ /** The deriving write behind `update()`. */
1411
+ update(updater) {
1412
+ this.assertAlive();
1413
+ if (this.status === "dirty") this.execute();
1414
+ switch (this.status) {
1415
+ case "resolved": {
1416
+ let next;
1417
+ try {
1418
+ next = updater(this._value);
1419
+ } catch (error) {
1420
+ this.fail(error);
1421
+ return;
1422
+ }
1423
+ if (this._scheduler.inline) {
1424
+ const same = this._compare(this._value, next, "write");
1425
+ if (same === void 0 || same) return;
1426
+ }
1427
+ this._overwrite(next);
1428
+ return;
1429
+ }
1430
+ case "pending":
1431
+ this._ops.push({
1432
+ kind: "update",
1433
+ updater
1434
+ });
1435
+ return;
1436
+ case "error": return;
1437
+ /* v8 ignore next 2 */
1438
+ case "disposed": throw this._disposedError();
1439
+ /* v8 ignore next 2 */
1440
+ case "dirty": throw this.internalError();
1441
+ }
1442
+ }
1443
+ _overwrite(input) {
1444
+ this._ops = [];
1445
+ this.status = "pending";
1446
+ this._pendingFromSet = true;
1447
+ this._scheduler.overwrite(input);
1448
+ this.notifyDependents();
1449
+ }
1450
+ refresh() {
1451
+ this.assertAlive();
1452
+ const currentValue = this._value;
1453
+ const currentStatus = this.status;
1454
+ this.run();
1455
+ if (this.status === "pending" && !this._scheduler.blocked) return;
1456
+ switch (this.status) {
1457
+ case "resolved": {
1458
+ if (currentStatus !== "resolved") {
1459
+ this.notifyDependents();
1460
+ return;
1461
+ }
1462
+ const same = this._compare(currentValue, this._value, "write");
1463
+ if (same === void 0 || same) return;
1464
+ this.notifyDependents();
1465
+ return;
1466
+ }
1467
+ case "error":
1468
+ case "pending":
1469
+ case "dirty":
1470
+ this.notifyDependents();
1471
+ return;
1472
+ case "disposed": return;
1473
+ }
1474
+ }
1475
+ };
1476
+ /**
1477
+ * Builds the engine of a synchronous primitive: the input is the value itself and settlement is
1478
+ * inline. The facades take a built core, so which scheduler drives a primitive is decided here and
1479
+ * in {@link asyncCore} alone (#114, step 4 of #110). `derivation` marks a core built from a
1480
+ * dependency-tracking compute function rather than a lazy initializer (#109).
1481
+ * @internal
1482
+ */
1483
+ function syncCore(input, options, derivation = false) {
1484
+ return new ValueCore(input, options, (compute, onResolve, onReject) => new SyncScheduler(compute, onResolve, onReject), derivation);
1485
+ }
1486
+ /**
1487
+ * Builds the engine of an asynchronous primitive: the input is a promise of the value and settlement
1488
+ * is deferred — see {@link syncCore}.
1489
+ * @internal
1490
+ */
1491
+ function asyncCore(input, options, derivation = false) {
1492
+ return new ValueCore(input, options, (compute, onResolve, onReject) => new AsyncScheduler(compute, onResolve, onReject), derivation);
1493
+ }
1494
+
1495
+ //#endregion
1496
+ export { PicoFlowPreconditionError as _, ReadableNode as a, Observable as c, TraceHub as d, LiveNodeRegistry as f, PicoFlowInternalError as g, PicoFlowError as h, PICOFLOW_VALUE_NODE as i, Observer as l, PicoFlowDisposedError as m, asyncCore as n, EffectNode as o, PicoFlowCycleError as p, syncCore as r, Node as s, ValueCore as t, ExecutionStack as u };