@ersbeth/picoflow 2.2.0 → 2.3.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 (109) hide show
  1. package/README.md +22 -0
  2. package/SKILL.md +5 -0
  3. package/dist/picoflow.js +252 -131
  4. package/dist/types/api/base/flowConfig.d.ts +0 -1
  5. package/dist/types/api/base/flowDisposable.d.ts +0 -1
  6. package/dist/types/api/base/flowErrors.d.ts +1 -0
  7. package/dist/types/api/base/flowObservable.d.ts +0 -1
  8. package/dist/types/api/base/flowPrimitiveOptions.d.ts +14 -0
  9. package/dist/types/api/base/flowSubscribable.d.ts +0 -1
  10. package/dist/types/api/base/flowTracker.d.ts +0 -1
  11. package/dist/types/api/base/index.d.ts +2 -1
  12. package/dist/types/api/index.d.ts +0 -1
  13. package/dist/types/api/nodes/async/flowConstantAsync.d.ts +4 -3
  14. package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +3 -3
  15. package/dist/types/api/nodes/async/flowStateAsync.d.ts +4 -3
  16. package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +3 -2
  17. package/dist/types/api/nodes/async/index.d.ts +0 -1
  18. package/dist/types/api/nodes/collections/flowArray.d.ts +3 -2
  19. package/dist/types/api/nodes/collections/flowMap.d.ts +3 -2
  20. package/dist/types/api/nodes/collections/index.d.ts +0 -1
  21. package/dist/types/api/nodes/flowEffect.d.ts +0 -1
  22. package/dist/types/api/nodes/flowSignal.d.ts +3 -2
  23. package/dist/types/api/nodes/flowValue.d.ts +0 -1
  24. package/dist/types/api/nodes/index.d.ts +0 -1
  25. package/dist/types/api/nodes/sync/flowConstant.d.ts +3 -2
  26. package/dist/types/api/nodes/sync/flowDerivation.d.ts +3 -2
  27. package/dist/types/api/nodes/sync/flowState.d.ts +4 -3
  28. package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +3 -2
  29. package/dist/types/api/nodes/sync/index.d.ts +0 -1
  30. package/dist/types/api/nodes/utils.d.ts +0 -1
  31. package/dist/types/base/dependenciesRegistry.d.ts +1 -0
  32. package/dist/types/base/dependentsRegistry.d.ts +1 -0
  33. package/dist/types/base/disposable.d.ts +5 -1
  34. package/dist/types/base/errors.d.ts +31 -0
  35. package/dist/types/base/executionStack.d.ts +3 -11
  36. package/dist/types/base/index.d.ts +3 -1
  37. package/dist/types/base/node.d.ts +2 -3
  38. package/dist/types/base/observable.d.ts +1 -2
  39. package/dist/types/base/observer.d.ts +1 -2
  40. package/dist/types/converters/index.d.ts +0 -1
  41. package/dist/types/converters/solid.d.ts +0 -1
  42. package/dist/types/index.d.ts +0 -1
  43. package/dist/types/nodes/actionNode.d.ts +1 -23
  44. package/dist/types/nodes/arrayNode.d.ts +1 -26
  45. package/dist/types/nodes/effectNode.d.ts +1 -32
  46. package/dist/types/nodes/index.d.ts +0 -1
  47. package/dist/types/nodes/mapNode.d.ts +1 -20
  48. package/dist/types/nodes/signalNode.d.ts +1 -10
  49. package/dist/types/nodes/valueAsyncNode.d.ts +1 -23
  50. package/dist/types/nodes/valueNode.d.ts +1 -27
  51. package/dist/types/nodes/valueSyncNode.d.ts +1 -23
  52. package/dist/types/schedulers/asyncResolver.d.ts +1 -19
  53. package/dist/types/schedulers/asyncScheduler.d.ts +1 -21
  54. package/dist/types/schedulers/index.d.ts +0 -1
  55. package/dist/types/schedulers/pendingError.d.ts +1 -9
  56. package/dist/types/schedulers/scheduler.d.ts +1 -10
  57. package/dist/types/schedulers/syncResolver.d.ts +1 -18
  58. package/dist/types/schedulers/syncScheduler.d.ts +1 -19
  59. package/package.json +10 -8
  60. package/dist/types/api/base/flowConfig.d.ts.map +0 -1
  61. package/dist/types/api/base/flowDisposable.d.ts.map +0 -1
  62. package/dist/types/api/base/flowObservable.d.ts.map +0 -1
  63. package/dist/types/api/base/flowSubscribable.d.ts.map +0 -1
  64. package/dist/types/api/base/flowTracker.d.ts.map +0 -1
  65. package/dist/types/api/base/index.d.ts.map +0 -1
  66. package/dist/types/api/index.d.ts.map +0 -1
  67. package/dist/types/api/nodes/async/flowConstantAsync.d.ts.map +0 -1
  68. package/dist/types/api/nodes/async/flowDerivationAsync.d.ts.map +0 -1
  69. package/dist/types/api/nodes/async/flowStateAsync.d.ts.map +0 -1
  70. package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts.map +0 -1
  71. package/dist/types/api/nodes/async/index.d.ts.map +0 -1
  72. package/dist/types/api/nodes/collections/flowArray.d.ts.map +0 -1
  73. package/dist/types/api/nodes/collections/flowMap.d.ts.map +0 -1
  74. package/dist/types/api/nodes/collections/index.d.ts.map +0 -1
  75. package/dist/types/api/nodes/flowEffect.d.ts.map +0 -1
  76. package/dist/types/api/nodes/flowSignal.d.ts.map +0 -1
  77. package/dist/types/api/nodes/flowValue.d.ts.map +0 -1
  78. package/dist/types/api/nodes/index.d.ts.map +0 -1
  79. package/dist/types/api/nodes/sync/flowConstant.d.ts.map +0 -1
  80. package/dist/types/api/nodes/sync/flowDerivation.d.ts.map +0 -1
  81. package/dist/types/api/nodes/sync/flowState.d.ts.map +0 -1
  82. package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts.map +0 -1
  83. package/dist/types/api/nodes/sync/index.d.ts.map +0 -1
  84. package/dist/types/api/nodes/utils.d.ts.map +0 -1
  85. package/dist/types/base/disposable.d.ts.map +0 -1
  86. package/dist/types/base/executionStack.d.ts.map +0 -1
  87. package/dist/types/base/index.d.ts.map +0 -1
  88. package/dist/types/base/node.d.ts.map +0 -1
  89. package/dist/types/base/observable.d.ts.map +0 -1
  90. package/dist/types/base/observer.d.ts.map +0 -1
  91. package/dist/types/converters/index.d.ts.map +0 -1
  92. package/dist/types/converters/solid.d.ts.map +0 -1
  93. package/dist/types/index.d.ts.map +0 -1
  94. package/dist/types/nodes/actionNode.d.ts.map +0 -1
  95. package/dist/types/nodes/arrayNode.d.ts.map +0 -1
  96. package/dist/types/nodes/effectNode.d.ts.map +0 -1
  97. package/dist/types/nodes/index.d.ts.map +0 -1
  98. package/dist/types/nodes/mapNode.d.ts.map +0 -1
  99. package/dist/types/nodes/signalNode.d.ts.map +0 -1
  100. package/dist/types/nodes/valueAsyncNode.d.ts.map +0 -1
  101. package/dist/types/nodes/valueNode.d.ts.map +0 -1
  102. package/dist/types/nodes/valueSyncNode.d.ts.map +0 -1
  103. package/dist/types/schedulers/asyncResolver.d.ts.map +0 -1
  104. package/dist/types/schedulers/asyncScheduler.d.ts.map +0 -1
  105. package/dist/types/schedulers/index.d.ts.map +0 -1
  106. package/dist/types/schedulers/pendingError.d.ts.map +0 -1
  107. package/dist/types/schedulers/scheduler.d.ts.map +0 -1
  108. package/dist/types/schedulers/syncResolver.d.ts.map +0 -1
  109. package/dist/types/schedulers/syncScheduler.d.ts.map +0 -1
package/dist/picoflow.js CHANGED
@@ -1,10 +1,84 @@
1
1
  import { createResource, onMount, createEffect, resetErrorBoundaries, onCleanup } from 'solid-js';
2
2
 
3
+ class DependenciesRegistry {
4
+ _dependencies = /* @__PURE__ */ new Set();
5
+ register(dependency, dependent) {
6
+ this._dependencies.add(dependency);
7
+ dependency.registerDependent(dependent);
8
+ }
9
+ unregister(dependency, dependent) {
10
+ this._dependencies.delete(dependency);
11
+ dependency.unregisterDependent(dependent);
12
+ }
13
+ clear(dependent) {
14
+ this._dependencies.forEach((dependency) => {
15
+ dependency.unregisterDependent(dependent);
16
+ });
17
+ this._dependencies.clear();
18
+ }
19
+ forEach(fn) {
20
+ Array.from(this._dependencies).forEach(fn);
21
+ }
22
+ }
23
+
24
+ class DependentsRegistry {
25
+ _dependents = /* @__PURE__ */ new Set();
26
+ register(dependent) {
27
+ this._dependents.add(dependent);
28
+ }
29
+ unregister(dependent) {
30
+ this._dependents.delete(dependent);
31
+ }
32
+ notifyAll() {
33
+ this._dependents.forEach((dependent) => {
34
+ dependent.notify();
35
+ });
36
+ }
37
+ forEach(fn) {
38
+ Array.from(this._dependents).forEach(fn);
39
+ }
40
+ }
41
+
42
+ class PicoFlowError extends Error {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "PicoFlowError";
46
+ }
47
+ }
48
+ class PicoFlowDisposedError extends PicoFlowError {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "PicoFlowDisposedError";
52
+ }
53
+ }
54
+ class PicoFlowCycleError extends PicoFlowError {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = "PicoFlowCycleError";
58
+ }
59
+ }
60
+ class PicoFlowInternalError extends PicoFlowError {
61
+ constructor(message) {
62
+ super(message);
63
+ this.name = "PicoFlowInternalError";
64
+ }
65
+ }
66
+
3
67
  class Disposable {
4
68
  _disposed = false;
69
+ _name;
70
+ constructor(name) {
71
+ this._name = name;
72
+ }
5
73
  get disposed() {
6
74
  return this._disposed;
7
75
  }
76
+ /** Builds the disposed-primitive error, naming the primitive when a `name` was given at creation. */
77
+ _disposedError() {
78
+ return new PicoFlowDisposedError(
79
+ this._name ? `[PicoFlow] Primitive "${this._name}" is disposed` : "[PicoFlow] Primitive is disposed"
80
+ );
81
+ }
8
82
  }
9
83
 
10
84
  class ExecutionStack {
@@ -13,6 +87,9 @@ class ExecutionStack {
13
87
  static _effectQueue = [];
14
88
  static _executionScheduled;
15
89
  static _coalesceResetScheduled = false;
90
+ /** Per-effect resets accumulated for the microtask scheduled below, so notifying more than one
91
+ * effect in the same sync turn doesn't discard every reset after the first (A2-SEM-5). */
92
+ static _coalesceResets = /* @__PURE__ */ new Set();
16
93
  /** Routes any error escaping the flush loop; defaults to logging rather than crashing (A2-SEM-2, A2-SEC-1). */
17
94
  static _onFlushError = (error) => console.error(error);
18
95
  /**
@@ -31,12 +108,17 @@ class ExecutionStack {
31
108
  * and resets per-effect coalesce flags via onReset.
32
109
  */
33
110
  static scheduleCoalesceReset(onReset) {
111
+ ExecutionStack._coalesceResets.add(onReset);
34
112
  if (ExecutionStack._coalesceResetScheduled) return;
35
113
  ExecutionStack._coalesceResetScheduled = true;
36
114
  queueMicrotask(() => {
37
115
  ExecutionStack._pastSyncTurn = true;
38
116
  ExecutionStack._coalesceResetScheduled = false;
39
- onReset();
117
+ const resets = ExecutionStack._coalesceResets;
118
+ ExecutionStack._coalesceResets = /* @__PURE__ */ new Set();
119
+ resets.forEach((reset) => {
120
+ reset();
121
+ });
40
122
  });
41
123
  }
42
124
  /** @internal Wired to the public `onFlushError()` API in `src/api/base/flowConfig.ts`. */
@@ -71,7 +153,9 @@ class ExecutionStack {
71
153
  if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
72
154
  ExecutionStack._pendingQueue.length = 0;
73
155
  ExecutionStack._effectQueue.length = 0;
74
- ExecutionStack._onFlushError(new Error("[PicoFlow] Reactive update cycle exceeded maximum depth"));
156
+ ExecutionStack._onFlushError(
157
+ new PicoFlowCycleError("[PicoFlow] Reactive update cycle exceeded maximum depth")
158
+ );
75
159
  return;
76
160
  }
77
161
  const node = queue[i];
@@ -90,66 +174,59 @@ class ExecutionStack {
90
174
  }
91
175
 
92
176
  class Node extends Disposable {
93
- _dependencies = /* @__PURE__ */ new Set();
94
- _dependents = /* @__PURE__ */ new Set();
177
+ _dependenciesRegistry = new DependenciesRegistry();
178
+ _dependentsRegistry = new DependentsRegistry();
95
179
  _status = "resolved";
96
180
  get status() {
97
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
181
+ if (this.disposed) throw this._disposedError();
98
182
  return this._status;
99
183
  }
100
184
  set status(status) {
101
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
185
+ if (this.disposed) throw this._disposedError();
102
186
  this._status = status;
103
187
  }
104
188
  registerDependency(dependency) {
105
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
106
- this._dependencies.add(dependency);
107
- dependency.registerDependent(this);
189
+ if (this._disposed) throw this._disposedError();
190
+ this._dependenciesRegistry.register(dependency, this);
108
191
  }
109
192
  unregisterDependency(dependency) {
110
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
111
- this._dependencies.delete(dependency);
112
- dependency.unregisterDependent(this);
193
+ if (this._disposed) throw this._disposedError();
194
+ this._dependenciesRegistry.unregister(dependency, this);
113
195
  }
114
196
  clearDependencies() {
115
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
116
- this._dependencies.forEach((dependency) => {
117
- dependency.unregisterDependent(this);
118
- });
119
- this._dependencies.clear();
197
+ if (this.disposed) throw this._disposedError();
198
+ this._dependenciesRegistry.clear(this);
120
199
  }
121
200
  registerDependent(dependent) {
122
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
123
- this._dependents.add(dependent);
201
+ if (this.disposed) throw this._disposedError();
202
+ this._dependentsRegistry.register(dependent);
124
203
  }
125
204
  unregisterDependent(dependent) {
126
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
127
- this._dependents.delete(dependent);
205
+ if (this.disposed) throw this._disposedError();
206
+ this._dependentsRegistry.unregister(dependent);
128
207
  }
129
208
  markDependencyDisposed() {
130
209
  }
131
210
  notifyDependents() {
132
211
  if (this.disposed) return;
133
- this._dependents.forEach((dependent) => {
134
- dependent.notify();
135
- });
212
+ this._dependentsRegistry.notifyAll();
136
213
  }
137
214
  watch(tracker) {
138
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
215
+ if (this.disposed) throw this._disposedError();
139
216
  tracker.registerDependency(this);
140
217
  }
141
218
  trigger() {
142
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
219
+ if (this.disposed) throw this._disposedError();
143
220
  this.notifyDependents();
144
221
  }
145
222
  dispose() {
146
223
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
147
- Array.from(this._dependents).forEach((dependant) => {
224
+ this._dependentsRegistry.forEach((dependant) => {
148
225
  dependant.markDependencyDisposed();
149
226
  dependant.unregisterDependency(this);
150
227
  this.unregisterDependent(dependant);
151
228
  });
152
- Array.from(this._dependencies).forEach((dependency) => {
229
+ this._dependenciesRegistry.forEach((dependency) => {
153
230
  this.unregisterDependency(dependency);
154
231
  });
155
232
  this._disposed = true;
@@ -157,41 +234,39 @@ class Node extends Disposable {
157
234
  }
158
235
 
159
236
  class Observable extends Disposable {
160
- _dependents = /* @__PURE__ */ new Set();
237
+ _dependentsRegistry = new DependentsRegistry();
161
238
  _status = "resolved";
162
239
  get status() {
163
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
240
+ if (this.disposed) throw this._disposedError();
164
241
  return this._status;
165
242
  }
166
243
  set status(status) {
167
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
244
+ if (this.disposed) throw this._disposedError();
168
245
  this._status = status;
169
246
  }
170
247
  registerDependent(dependent) {
171
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
172
- this._dependents.add(dependent);
248
+ if (this.disposed) throw this._disposedError();
249
+ this._dependentsRegistry.register(dependent);
173
250
  }
174
251
  unregisterDependent(dependent) {
175
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
176
- this._dependents.delete(dependent);
252
+ if (this.disposed) throw this._disposedError();
253
+ this._dependentsRegistry.unregister(dependent);
177
254
  }
178
255
  notifyDependents() {
179
256
  if (this.disposed) return;
180
- this._dependents.forEach((dependent) => {
181
- dependent.notify();
182
- });
257
+ this._dependentsRegistry.notifyAll();
183
258
  }
184
259
  watch(tracker) {
185
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
260
+ if (this.disposed) throw this._disposedError();
186
261
  tracker.registerDependency(this);
187
262
  }
188
263
  trigger() {
189
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
264
+ if (this.disposed) throw this._disposedError();
190
265
  this.notifyDependents();
191
266
  }
192
267
  dispose() {
193
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
194
- Array.from(this._dependents).forEach((dependant) => {
268
+ if (this._disposed) throw this._disposedError();
269
+ this._dependentsRegistry.forEach((dependant) => {
195
270
  dependant.markDependencyDisposed();
196
271
  dependant.unregisterDependency(this);
197
272
  this.unregisterDependent(dependant);
@@ -201,34 +276,27 @@ class Observable extends Disposable {
201
276
  }
202
277
 
203
278
  class Observer extends Disposable {
204
- _dependencies = /* @__PURE__ */ new Set();
279
+ _dependenciesRegistry = new DependenciesRegistry();
205
280
  /** Set when a dependency disposed while this observer was queued; consumed by the next execute(). */
206
281
  _dependencyDisposed = false;
207
282
  markDependencyDisposed() {
208
283
  this._dependencyDisposed = true;
209
284
  }
210
285
  registerDependency(dependency) {
211
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
212
- this._dependencies.add(dependency);
213
- dependency.registerDependent(this);
286
+ if (this._disposed) throw this._disposedError();
287
+ this._dependenciesRegistry.register(dependency, this);
214
288
  }
215
289
  unregisterDependency(dependency) {
216
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
217
- this._dependencies.delete(dependency);
218
- dependency.unregisterDependent(this);
290
+ if (this._disposed) throw this._disposedError();
291
+ this._dependenciesRegistry.unregister(dependency, this);
219
292
  }
220
293
  clearDependencies() {
221
294
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
222
- this._dependencies.forEach((dependency) => {
223
- dependency.unregisterDependent(this);
224
- });
225
- this._dependencies.clear();
295
+ this._dependenciesRegistry.clear(this);
226
296
  }
227
297
  dispose() {
228
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
229
- Array.from(this._dependencies).forEach((dependency) => {
230
- this.unregisterDependency(dependency);
231
- });
298
+ if (this._disposed) throw this._disposedError();
299
+ this._dependenciesRegistry.clear(this);
232
300
  this._disposed = true;
233
301
  }
234
302
  }
@@ -321,14 +389,14 @@ class AsyncScheduler {
321
389
  this._attachHandlers(this._resolver);
322
390
  }
323
391
  get settled() {
324
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
392
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
325
393
  return this._settled.promise;
326
394
  }
327
395
  get disposed() {
328
396
  return this._disposed;
329
397
  }
330
398
  overwrite(promise) {
331
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
399
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
332
400
  if (this._resolver.finished) {
333
401
  this._resolver = new AsyncResolver(this._compute);
334
402
  this._settled = Promise.withResolvers();
@@ -337,7 +405,7 @@ class AsyncScheduler {
337
405
  this._resolver.overwrite(promise);
338
406
  }
339
407
  compute() {
340
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
408
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
341
409
  if (this._resolver.finished) {
342
410
  this._resolver = new AsyncResolver(this._compute);
343
411
  this._settled = Promise.withResolvers();
@@ -429,7 +497,7 @@ class SyncScheduler {
429
497
  return this._settled.promise;
430
498
  }
431
499
  overwrite(value) {
432
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
500
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
433
501
  if (this._resolver.finished) {
434
502
  this._settled = Promise.withResolvers();
435
503
  this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
@@ -437,7 +505,7 @@ class SyncScheduler {
437
505
  this._resolver.overwrite(value);
438
506
  }
439
507
  compute() {
440
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
508
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
441
509
  if (this._resolver.finished) {
442
510
  this._settled = Promise.withResolvers();
443
511
  this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
@@ -463,6 +531,8 @@ class EffectNode extends Observer {
463
531
  _notifyEpoch = 0;
464
532
  /** How many notification epochs have been executed so far. */
465
533
  _executedEpoch = 0;
534
+ /** Callbacks registered via {@link onDispose}, run once after dispose() tears down the graph edges. */
535
+ _disposeListeners = [];
466
536
  constructor(data, onData, onError, onPending) {
467
537
  super();
468
538
  this._data = data;
@@ -472,8 +542,23 @@ class EffectNode extends Observer {
472
542
  this._notifyEpoch = 1;
473
543
  this.execute();
474
544
  }
545
+ /**
546
+ * Registers a callback to run once this effect is disposed, after the graph edges are torn
547
+ * down. Lets a caller that wraps an effect (e.g. {@link ActionNode.subscribe}) attach its own
548
+ * teardown without patching `dispose` on the returned instance (A2-ARCH-8).
549
+ * @internal
550
+ */
551
+ onDispose(fn) {
552
+ this._disposeListeners.push(fn);
553
+ }
554
+ dispose() {
555
+ super.dispose();
556
+ this._disposeListeners.forEach((fn) => {
557
+ fn();
558
+ });
559
+ }
475
560
  notify() {
476
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
561
+ if (this._disposed) throw this._disposedError();
477
562
  if (this._queued) {
478
563
  if (!this._coalescedWhileQueued) {
479
564
  this._coalescedWhileQueued = true;
@@ -537,8 +622,22 @@ class ValueNode extends Node {
537
622
  [PICOFLOW_VALUE_NODE] = true;
538
623
  _value;
539
624
  _error;
625
+ /** Builds the "unreachable state" error, naming the primitive when a `name` was given at creation. */
626
+ _internalError() {
627
+ return new PicoFlowInternalError(
628
+ this._name ? `[PicoFlow] Internal error (${this._name})` : "[PicoFlow] Internal error"
629
+ );
630
+ }
631
+ /**
632
+ * Hook called just before a tracked read throws `PendingError`, i.e. a dependent registered while
633
+ * this node is mid-flight and has no other way to learn the computation later settled (A2-PERF-4).
634
+ * No-op by default; overridden by {@link ValueAsyncNode} to know when its own post-resolve
635
+ * `notifyDependents()` call cannot be skipped even if the resolved value is unchanged.
636
+ */
637
+ _markPendingRead() {
638
+ }
540
639
  watch(tracker) {
541
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
640
+ if (this.disposed) throw this._disposedError();
542
641
  super.watch(tracker);
543
642
  switch (this.status) {
544
643
  case "resolved":
@@ -546,26 +645,28 @@ class ValueNode extends Node {
546
645
  case "error":
547
646
  throw this._error;
548
647
  case "pending": {
648
+ this._markPendingRead();
549
649
  throw new PendingError(this._scheduler.settled);
550
650
  }
551
651
  case "dirty": {
552
652
  this.execute();
553
653
  switch (this.status) {
554
654
  case "pending":
655
+ this._markPendingRead();
555
656
  throw new PendingError(this._scheduler.settled);
556
657
  case "resolved":
557
658
  return;
558
659
  case "error":
559
660
  throw this._error;
560
661
  case "dirty": {
561
- throw new Error("[PicoFlow] Internal error");
662
+ throw this._internalError();
562
663
  }
563
664
  }
564
665
  }
565
666
  }
566
667
  }
567
668
  notify() {
568
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
669
+ if (this.disposed) throw this._disposedError();
569
670
  if (this.status === "dirty") return;
570
671
  if (this.status === "pending") {
571
672
  ExecutionStack.pushPending(this);
@@ -578,7 +679,7 @@ class ValueNode extends Node {
578
679
  this._scheduler.dispose();
579
680
  }
580
681
  get(tracker) {
581
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
682
+ if (this.disposed) throw this._disposedError();
582
683
  super.watch(tracker);
583
684
  switch (this.status) {
584
685
  case "resolved":
@@ -586,6 +687,7 @@ class ValueNode extends Node {
586
687
  case "error":
587
688
  throw this._error;
588
689
  case "pending": {
690
+ this._markPendingRead();
589
691
  throw new PendingError(this._scheduler.settled);
590
692
  }
591
693
  case "dirty": {
@@ -596,17 +698,18 @@ class ValueNode extends Node {
596
698
  case "error":
597
699
  throw this._error;
598
700
  case "pending": {
701
+ this._markPendingRead();
599
702
  throw new PendingError(this._scheduler.settled);
600
703
  }
601
704
  case "dirty": {
602
- throw new Error("[PicoFlow] Internal error");
705
+ throw this._internalError();
603
706
  }
604
707
  }
605
708
  }
606
709
  }
607
710
  }
608
711
  async pick() {
609
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
712
+ if (this.disposed) throw this._disposedError();
610
713
  switch (this.status) {
611
714
  case "resolved":
612
715
  return this._value;
@@ -614,7 +717,7 @@ class ValueNode extends Node {
614
717
  await this._scheduler.settled;
615
718
  if (this.status === "resolved") return this._value;
616
719
  if (this.status === "error") throw this._error;
617
- throw new Error("[PicoFlow] Internal error");
720
+ throw this._internalError();
618
721
  }
619
722
  case "error":
620
723
  throw this._error;
@@ -627,12 +730,12 @@ class ValueNode extends Node {
627
730
  await this._scheduler.settled;
628
731
  if (this.status === "resolved") return this._value;
629
732
  if (this.status === "error") throw this._error;
630
- throw new Error("[PicoFlow] Internal error");
733
+ throw this._internalError();
631
734
  }
632
735
  case "error":
633
736
  throw this._error;
634
737
  case "dirty": {
635
- throw new Error("[PicoFlow] Internal error");
738
+ throw this._internalError();
636
739
  }
637
740
  }
638
741
  }
@@ -645,7 +748,7 @@ class ValueNode extends Node {
645
748
  this._scheduler.compute();
646
749
  }
647
750
  subscribe(onValue, onError, onPending) {
648
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
751
+ if (this.disposed) throw this._disposedError();
649
752
  const effect = new EffectNode((t) => this.get(t), onValue, onError, onPending);
650
753
  return effect;
651
754
  }
@@ -654,8 +757,17 @@ class ValueNode extends Node {
654
757
  class ValueAsyncNode extends ValueNode {
655
758
  _scheduler;
656
759
  _compute;
657
- constructor(promiseOrCompute) {
658
- super();
760
+ /** Set by {@link ValueNode._markPendingRead} when a dependent registers while this node is mid-flight. */
761
+ _hasPendingReader = false;
762
+ /**
763
+ * True only while the in-flight computation was started by `set()`, which already ran its own
764
+ * eager `notifyDependents()` cascade before this resolution. `refresh()` and a recompute pulled
765
+ * lazily after an upstream `notify()` have no such pre-cascade — for them, `_onResolve`'s own
766
+ * `notifyDependents()` call is the only notification, so it must never be skipped (A2-PERF-4).
767
+ */
768
+ _pendingFromSet = false;
769
+ constructor(promiseOrCompute, name) {
770
+ super(name);
659
771
  if (typeof promiseOrCompute === "function") {
660
772
  this._compute = () => promiseOrCompute(this, this._value);
661
773
  } else {
@@ -670,10 +782,18 @@ class ValueAsyncNode extends ValueNode {
670
782
  }
671
783
  _onResolve(value) {
672
784
  if (this.disposed) return;
785
+ const changed = value !== this._value;
786
+ const hadPendingReader = this._hasPendingReader;
787
+ const pendingFromSet = this._pendingFromSet;
788
+ this._hasPendingReader = false;
789
+ this._pendingFromSet = false;
673
790
  this.status = "resolved";
674
791
  this._value = value;
675
792
  this._error = void 0;
676
- this.notifyDependents();
793
+ if (changed || hadPendingReader || !pendingFromSet) this.notifyDependents();
794
+ }
795
+ _markPendingRead() {
796
+ this._hasPendingReader = true;
677
797
  }
678
798
  _onReject(error) {
679
799
  if (this.disposed) return;
@@ -682,12 +802,13 @@ class ValueAsyncNode extends ValueNode {
682
802
  this.notifyDependents();
683
803
  }
684
804
  set(promiseOrUpdater) {
685
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
805
+ if (this.disposed) throw this._disposedError();
686
806
  if (typeof promiseOrUpdater === "function") {
687
807
  const updater = promiseOrUpdater;
688
808
  switch (this.status) {
689
809
  case "resolved": {
690
810
  this.status = "pending";
811
+ this._pendingFromSet = true;
691
812
  this._scheduler.overwrite(updater(this._value));
692
813
  this.notifyDependents();
693
814
  return;
@@ -696,6 +817,7 @@ class ValueAsyncNode extends ValueNode {
696
817
  case "error":
697
818
  case "dirty": {
698
819
  this.status = "pending";
820
+ this._pendingFromSet = true;
699
821
  this._scheduler.overwrite(updater(this._value));
700
822
  this.notifyDependents();
701
823
  return;
@@ -703,38 +825,39 @@ class ValueAsyncNode extends ValueNode {
703
825
  }
704
826
  } else {
705
827
  this.status = "pending";
828
+ this._pendingFromSet = true;
706
829
  const promise = promiseOrUpdater;
707
830
  this._scheduler.overwrite(promise);
708
831
  this.notifyDependents();
709
832
  }
710
833
  }
711
834
  refresh() {
712
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
835
+ if (this.disposed) throw this._disposedError();
713
836
  this.execute();
714
837
  }
715
838
  }
716
839
 
717
- function constantAsync(valueOrInitializer) {
718
- return new ValueAsyncNode(valueOrInitializer);
840
+ function constantAsync(valueOrInitializer, options) {
841
+ return new ValueAsyncNode(valueOrInitializer, options?.name);
719
842
  }
720
843
 
721
- function derivationAsync(compute) {
722
- return new ValueAsyncNode(compute);
844
+ function derivationAsync(compute, options) {
845
+ return new ValueAsyncNode(compute, options?.name);
723
846
  }
724
847
 
725
- function stateAsync(valueOrInitializer) {
726
- return new ValueAsyncNode(valueOrInitializer);
848
+ function stateAsync(valueOrInitializer, options) {
849
+ return new ValueAsyncNode(valueOrInitializer, options?.name);
727
850
  }
728
851
 
729
- function writableDerivationAsync(compute) {
730
- return new ValueAsyncNode(compute);
852
+ function writableDerivationAsync(compute, options) {
853
+ return new ValueAsyncNode(compute, options?.name);
731
854
  }
732
855
 
733
856
  class ValueSyncNode extends ValueNode {
734
857
  _scheduler;
735
858
  _compute;
736
- constructor(valueOrCompute) {
737
- super();
859
+ constructor(valueOrCompute, name) {
860
+ super(name);
738
861
  if (typeof valueOrCompute === "function") {
739
862
  this._compute = () => valueOrCompute(this, this._value);
740
863
  } else {
@@ -760,7 +883,7 @@ class ValueSyncNode extends ValueNode {
760
883
  this._error = error;
761
884
  }
762
885
  set(valueOrUpdater) {
763
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
886
+ if (this.disposed) throw this._disposedError();
764
887
  if (typeof valueOrUpdater === "function") {
765
888
  const updater = valueOrUpdater;
766
889
  switch (this.status) {
@@ -804,7 +927,7 @@ class ValueSyncNode extends ValueNode {
804
927
  }
805
928
  }
806
929
  refresh() {
807
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
930
+ if (this.disposed) throw this._disposedError();
808
931
  const currentValue = this._value;
809
932
  const currentStatus = this.status;
810
933
  this.execute();
@@ -833,7 +956,7 @@ class ActionNode extends ValueSyncNode {
833
956
  }
834
957
  }
835
958
  set(valueOrUpdater) {
836
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
959
+ if (this.disposed) throw this._disposedError();
837
960
  if (typeof valueOrUpdater === "function") {
838
961
  const updater = valueOrUpdater;
839
962
  const nextValue2 = updater(this._value);
@@ -848,7 +971,7 @@ class ActionNode extends ValueSyncNode {
848
971
  super.set(nextValue);
849
972
  }
850
973
  subscribe(onValue, onError, onPending) {
851
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
974
+ if (this.disposed) throw this._disposedError();
852
975
  const slot = { queue: [] };
853
976
  this._slots.add(slot);
854
977
  const effect = new EffectNode(
@@ -867,11 +990,9 @@ class ActionNode extends ValueSyncNode {
867
990
  onError,
868
991
  onPending
869
992
  );
870
- const originalDispose = effect.dispose.bind(effect);
871
- effect.dispose = () => {
993
+ effect.onDispose(() => {
872
994
  this._slots.delete(slot);
873
- originalDispose();
874
- };
995
+ });
875
996
  return effect;
876
997
  }
877
998
  dispose() {
@@ -882,8 +1003,8 @@ class ActionNode extends ValueSyncNode {
882
1003
 
883
1004
  class ArrayNode extends ValueSyncNode {
884
1005
  $lastAction;
885
- constructor(value = []) {
886
- super(value);
1006
+ constructor(value = [], name) {
1007
+ super(value, name);
887
1008
  this.$lastAction = new ActionNode({
888
1009
  type: "set",
889
1010
  setItems: value,
@@ -899,11 +1020,11 @@ class ArrayNode extends ValueSyncNode {
899
1020
  return [...values];
900
1021
  }
901
1022
  get length() {
902
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1023
+ if (this.disposed) throw this._disposedError();
903
1024
  return this._value.length;
904
1025
  }
905
1026
  set(itemsOrUpdater) {
906
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1027
+ if (this.disposed) throw this._disposedError();
907
1028
  const previousValue = this._value;
908
1029
  super.set(itemsOrUpdater);
909
1030
  this.$lastAction.set({
@@ -914,7 +1035,7 @@ class ArrayNode extends ValueSyncNode {
914
1035
  return previousValue;
915
1036
  }
916
1037
  update(index, item) {
917
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1038
+ if (this.disposed) throw this._disposedError();
918
1039
  if (index < 0 || index >= this._value.length || !Number.isInteger(index)) {
919
1040
  throw new Error("[PicoFlow] Index out of bounds");
920
1041
  }
@@ -930,13 +1051,13 @@ class ArrayNode extends ValueSyncNode {
930
1051
  return previousValue;
931
1052
  }
932
1053
  push(item) {
933
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1054
+ if (this.disposed) throw this._disposedError();
934
1055
  this._value.push(item);
935
1056
  this.notifyDependents();
936
1057
  this.$lastAction.set({ type: "push", addedItem: item });
937
1058
  }
938
1059
  pop() {
939
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1060
+ if (this.disposed) throw this._disposedError();
940
1061
  if (this._value.length === 0) return void 0;
941
1062
  const item = this._value.pop();
942
1063
  this.notifyDependents();
@@ -944,13 +1065,13 @@ class ArrayNode extends ValueSyncNode {
944
1065
  return item;
945
1066
  }
946
1067
  unshift(item) {
947
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1068
+ if (this.disposed) throw this._disposedError();
948
1069
  this._value.unshift(item);
949
1070
  this.notifyDependents();
950
1071
  this.$lastAction.set({ type: "unshift", addedItem: item });
951
1072
  }
952
1073
  shift() {
953
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1074
+ if (this.disposed) throw this._disposedError();
954
1075
  if (this._value.length === 0) return void 0;
955
1076
  const item = this._value.shift();
956
1077
  this.notifyDependents();
@@ -958,7 +1079,7 @@ class ArrayNode extends ValueSyncNode {
958
1079
  return item;
959
1080
  }
960
1081
  splice(start, deleteCount, ...newItems) {
961
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1082
+ if (this.disposed) throw this._disposedError();
962
1083
  const items = this._value.splice(start, deleteCount, ...newItems);
963
1084
  this.notifyDependents();
964
1085
  this.$lastAction.set({
@@ -971,7 +1092,7 @@ class ArrayNode extends ValueSyncNode {
971
1092
  return items;
972
1093
  }
973
1094
  clear() {
974
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1095
+ if (this.disposed) throw this._disposedError();
975
1096
  const previousValue = [...this._value];
976
1097
  this._value = [];
977
1098
  this.notifyDependents();
@@ -987,14 +1108,14 @@ class ArrayNode extends ValueSyncNode {
987
1108
  }
988
1109
  }
989
1110
 
990
- function array(initial) {
991
- return new ArrayNode(initial);
1111
+ function array(initial, options) {
1112
+ return new ArrayNode(initial, options?.name);
992
1113
  }
993
1114
 
994
1115
  class MapNode extends ValueSyncNode {
995
1116
  $lastAction;
996
- constructor(value = /* @__PURE__ */ new Map()) {
997
- super(value);
1117
+ constructor(value = /* @__PURE__ */ new Map(), name) {
1118
+ super(value, name);
998
1119
  this.$lastAction = new ActionNode({
999
1120
  type: "set",
1000
1121
  setMap: value,
@@ -1002,7 +1123,7 @@ class MapNode extends ValueSyncNode {
1002
1123
  });
1003
1124
  }
1004
1125
  add(key, value) {
1005
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1126
+ if (this.disposed) throw this._disposedError();
1006
1127
  if (this._value.has(key)) {
1007
1128
  throw new Error("[PicoFlow] Key already exists");
1008
1129
  }
@@ -1011,7 +1132,7 @@ class MapNode extends ValueSyncNode {
1011
1132
  this.$lastAction.set({ type: "add", key, addedValue: value });
1012
1133
  }
1013
1134
  update(key, value) {
1014
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1135
+ if (this.disposed) throw this._disposedError();
1015
1136
  if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
1016
1137
  const previousValue = this._value.get(key);
1017
1138
  this._value.set(key, value);
@@ -1025,7 +1146,7 @@ class MapNode extends ValueSyncNode {
1025
1146
  return previousValue;
1026
1147
  }
1027
1148
  delete(key) {
1028
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1149
+ if (this.disposed) throw this._disposedError();
1029
1150
  if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
1030
1151
  const value = this._value.get(key);
1031
1152
  this._value.delete(key);
@@ -1034,7 +1155,7 @@ class MapNode extends ValueSyncNode {
1034
1155
  return value;
1035
1156
  }
1036
1157
  set(mapOrUpdater) {
1037
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1158
+ if (this.disposed) throw this._disposedError();
1038
1159
  const previousValue = this._value;
1039
1160
  super.set(mapOrUpdater);
1040
1161
  this.$lastAction.set({
@@ -1045,7 +1166,7 @@ class MapNode extends ValueSyncNode {
1045
1166
  return previousValue;
1046
1167
  }
1047
1168
  clear() {
1048
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1169
+ if (this.disposed) throw this._disposedError();
1049
1170
  const previousValue = this._value;
1050
1171
  this._value = /* @__PURE__ */ new Map();
1051
1172
  this.notifyDependents();
@@ -1061,11 +1182,11 @@ class MapNode extends ValueSyncNode {
1061
1182
  }
1062
1183
  }
1063
1184
 
1064
- function map(initial) {
1185
+ function map(initial, options) {
1065
1186
  if (initial instanceof Map) {
1066
- return new MapNode(initial);
1187
+ return new MapNode(initial, options?.name);
1067
1188
  }
1068
- return new MapNode(new Map(initial ? Object.entries(initial) : []));
1189
+ return new MapNode(new Map(initial ? Object.entries(initial) : []), options?.name);
1069
1190
  }
1070
1191
 
1071
1192
  function subscribe(data, onData, onError, onPending) {
@@ -1074,30 +1195,30 @@ function subscribe(data, onData, onError, onPending) {
1074
1195
 
1075
1196
  class SignalNode extends Observable {
1076
1197
  subscribe(onTrigger, onError, onPending) {
1077
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1198
+ if (this.disposed) throw this._disposedError();
1078
1199
  const effect = new EffectNode((t) => this.watch(t), onTrigger, onError, onPending);
1079
1200
  return effect;
1080
1201
  }
1081
1202
  }
1082
1203
 
1083
- function signal() {
1084
- return new SignalNode();
1204
+ function signal(options) {
1205
+ return new SignalNode(options?.name);
1085
1206
  }
1086
1207
 
1087
- function constant(initializer) {
1088
- return new ValueSyncNode(initializer);
1208
+ function constant(initializer, options) {
1209
+ return new ValueSyncNode(initializer, options?.name);
1089
1210
  }
1090
1211
 
1091
- function derivation(compute) {
1092
- return new ValueSyncNode(compute);
1212
+ function derivation(compute, options) {
1213
+ return new ValueSyncNode(compute, options?.name);
1093
1214
  }
1094
1215
 
1095
- function state(valueOrInitializer) {
1096
- return new ValueSyncNode(valueOrInitializer);
1216
+ function state(valueOrInitializer, options) {
1217
+ return new ValueSyncNode(valueOrInitializer, options?.name);
1097
1218
  }
1098
1219
 
1099
- function writableDerivation(compute) {
1100
- return new ValueSyncNode(compute);
1220
+ function writableDerivation(compute, options) {
1221
+ return new ValueSyncNode(compute, options?.name);
1101
1222
  }
1102
1223
 
1103
1224
  function describeFlowInput(value) {
@@ -1160,4 +1281,4 @@ function from(flow) {
1160
1281
  throw new Error(`[PicoFlow] from(): expected a FlowValue or getter function, received ${describeFlowInput(flow)}`);
1161
1282
  }
1162
1283
 
1163
- export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
1284
+ export { PicoFlowCycleError, PicoFlowDisposedError, PicoFlowError, PicoFlowInternalError, array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };