@ersbeth/picoflow 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +23 -1
  2. package/SKILL.md +5 -0
  3. package/dist/picoflow.js +252 -132
  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 +11 -23
  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
  }
@@ -301,7 +369,6 @@ class AsyncResolver {
301
369
  });
302
370
  }
303
371
  abort() {
304
- this._iteration++;
305
372
  this._aborted = true;
306
373
  }
307
374
  }
@@ -321,14 +388,14 @@ class AsyncScheduler {
321
388
  this._attachHandlers(this._resolver);
322
389
  }
323
390
  get settled() {
324
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
391
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
325
392
  return this._settled.promise;
326
393
  }
327
394
  get disposed() {
328
395
  return this._disposed;
329
396
  }
330
397
  overwrite(promise) {
331
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
398
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
332
399
  if (this._resolver.finished) {
333
400
  this._resolver = new AsyncResolver(this._compute);
334
401
  this._settled = Promise.withResolvers();
@@ -337,7 +404,7 @@ class AsyncScheduler {
337
404
  this._resolver.overwrite(promise);
338
405
  }
339
406
  compute() {
340
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
407
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
341
408
  if (this._resolver.finished) {
342
409
  this._resolver = new AsyncResolver(this._compute);
343
410
  this._settled = Promise.withResolvers();
@@ -429,7 +496,7 @@ class SyncScheduler {
429
496
  return this._settled.promise;
430
497
  }
431
498
  overwrite(value) {
432
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
499
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
433
500
  if (this._resolver.finished) {
434
501
  this._settled = Promise.withResolvers();
435
502
  this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
@@ -437,7 +504,7 @@ class SyncScheduler {
437
504
  this._resolver.overwrite(value);
438
505
  }
439
506
  compute() {
440
- if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
507
+ if (this._disposed) throw new PicoFlowDisposedError("[PicoFlow] ComputationScheduler is disposed");
441
508
  if (this._resolver.finished) {
442
509
  this._settled = Promise.withResolvers();
443
510
  this._resolver = new SyncResolver(this._compute, this._onResolve, this._onReject);
@@ -463,6 +530,8 @@ class EffectNode extends Observer {
463
530
  _notifyEpoch = 0;
464
531
  /** How many notification epochs have been executed so far. */
465
532
  _executedEpoch = 0;
533
+ /** Callbacks registered via {@link onDispose}, run once after dispose() tears down the graph edges. */
534
+ _disposeListeners = [];
466
535
  constructor(data, onData, onError, onPending) {
467
536
  super();
468
537
  this._data = data;
@@ -472,8 +541,23 @@ class EffectNode extends Observer {
472
541
  this._notifyEpoch = 1;
473
542
  this.execute();
474
543
  }
544
+ /**
545
+ * Registers a callback to run once this effect is disposed, after the graph edges are torn
546
+ * down. Lets a caller that wraps an effect (e.g. {@link ActionNode.subscribe}) attach its own
547
+ * teardown without patching `dispose` on the returned instance (A2-ARCH-8).
548
+ * @internal
549
+ */
550
+ onDispose(fn) {
551
+ this._disposeListeners.push(fn);
552
+ }
553
+ dispose() {
554
+ super.dispose();
555
+ this._disposeListeners.forEach((fn) => {
556
+ fn();
557
+ });
558
+ }
475
559
  notify() {
476
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
560
+ if (this._disposed) throw this._disposedError();
477
561
  if (this._queued) {
478
562
  if (!this._coalescedWhileQueued) {
479
563
  this._coalescedWhileQueued = true;
@@ -537,8 +621,22 @@ class ValueNode extends Node {
537
621
  [PICOFLOW_VALUE_NODE] = true;
538
622
  _value;
539
623
  _error;
624
+ /** Builds the "unreachable state" error, naming the primitive when a `name` was given at creation. */
625
+ _internalError() {
626
+ return new PicoFlowInternalError(
627
+ this._name ? `[PicoFlow] Internal error (${this._name})` : "[PicoFlow] Internal error"
628
+ );
629
+ }
630
+ /**
631
+ * Hook called just before a tracked read throws `PendingError`, i.e. a dependent registered while
632
+ * this node is mid-flight and has no other way to learn the computation later settled (A2-PERF-4).
633
+ * No-op by default; overridden by {@link ValueAsyncNode} to know when its own post-resolve
634
+ * `notifyDependents()` call cannot be skipped even if the resolved value is unchanged.
635
+ */
636
+ _markPendingRead() {
637
+ }
540
638
  watch(tracker) {
541
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
639
+ if (this.disposed) throw this._disposedError();
542
640
  super.watch(tracker);
543
641
  switch (this.status) {
544
642
  case "resolved":
@@ -546,26 +644,28 @@ class ValueNode extends Node {
546
644
  case "error":
547
645
  throw this._error;
548
646
  case "pending": {
647
+ this._markPendingRead();
549
648
  throw new PendingError(this._scheduler.settled);
550
649
  }
551
650
  case "dirty": {
552
651
  this.execute();
553
652
  switch (this.status) {
554
653
  case "pending":
654
+ this._markPendingRead();
555
655
  throw new PendingError(this._scheduler.settled);
556
656
  case "resolved":
557
657
  return;
558
658
  case "error":
559
659
  throw this._error;
560
660
  case "dirty": {
561
- throw new Error("[PicoFlow] Internal error");
661
+ throw this._internalError();
562
662
  }
563
663
  }
564
664
  }
565
665
  }
566
666
  }
567
667
  notify() {
568
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
668
+ if (this.disposed) throw this._disposedError();
569
669
  if (this.status === "dirty") return;
570
670
  if (this.status === "pending") {
571
671
  ExecutionStack.pushPending(this);
@@ -578,7 +678,7 @@ class ValueNode extends Node {
578
678
  this._scheduler.dispose();
579
679
  }
580
680
  get(tracker) {
581
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
681
+ if (this.disposed) throw this._disposedError();
582
682
  super.watch(tracker);
583
683
  switch (this.status) {
584
684
  case "resolved":
@@ -586,6 +686,7 @@ class ValueNode extends Node {
586
686
  case "error":
587
687
  throw this._error;
588
688
  case "pending": {
689
+ this._markPendingRead();
589
690
  throw new PendingError(this._scheduler.settled);
590
691
  }
591
692
  case "dirty": {
@@ -596,17 +697,18 @@ class ValueNode extends Node {
596
697
  case "error":
597
698
  throw this._error;
598
699
  case "pending": {
700
+ this._markPendingRead();
599
701
  throw new PendingError(this._scheduler.settled);
600
702
  }
601
703
  case "dirty": {
602
- throw new Error("[PicoFlow] Internal error");
704
+ throw this._internalError();
603
705
  }
604
706
  }
605
707
  }
606
708
  }
607
709
  }
608
710
  async pick() {
609
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
711
+ if (this.disposed) throw this._disposedError();
610
712
  switch (this.status) {
611
713
  case "resolved":
612
714
  return this._value;
@@ -614,7 +716,7 @@ class ValueNode extends Node {
614
716
  await this._scheduler.settled;
615
717
  if (this.status === "resolved") return this._value;
616
718
  if (this.status === "error") throw this._error;
617
- throw new Error("[PicoFlow] Internal error");
719
+ throw this._internalError();
618
720
  }
619
721
  case "error":
620
722
  throw this._error;
@@ -627,12 +729,12 @@ class ValueNode extends Node {
627
729
  await this._scheduler.settled;
628
730
  if (this.status === "resolved") return this._value;
629
731
  if (this.status === "error") throw this._error;
630
- throw new Error("[PicoFlow] Internal error");
732
+ throw this._internalError();
631
733
  }
632
734
  case "error":
633
735
  throw this._error;
634
736
  case "dirty": {
635
- throw new Error("[PicoFlow] Internal error");
737
+ throw this._internalError();
636
738
  }
637
739
  }
638
740
  }
@@ -645,7 +747,7 @@ class ValueNode extends Node {
645
747
  this._scheduler.compute();
646
748
  }
647
749
  subscribe(onValue, onError, onPending) {
648
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
750
+ if (this.disposed) throw this._disposedError();
649
751
  const effect = new EffectNode((t) => this.get(t), onValue, onError, onPending);
650
752
  return effect;
651
753
  }
@@ -654,8 +756,17 @@ class ValueNode extends Node {
654
756
  class ValueAsyncNode extends ValueNode {
655
757
  _scheduler;
656
758
  _compute;
657
- constructor(promiseOrCompute) {
658
- super();
759
+ /** Set by {@link ValueNode._markPendingRead} when a dependent registers while this node is mid-flight. */
760
+ _hasPendingReader = false;
761
+ /**
762
+ * True only while the in-flight computation was started by `set()`, which already ran its own
763
+ * eager `notifyDependents()` cascade before this resolution. `refresh()` and a recompute pulled
764
+ * lazily after an upstream `notify()` have no such pre-cascade — for them, `_onResolve`'s own
765
+ * `notifyDependents()` call is the only notification, so it must never be skipped (A2-PERF-4).
766
+ */
767
+ _pendingFromSet = false;
768
+ constructor(promiseOrCompute, name) {
769
+ super(name);
659
770
  if (typeof promiseOrCompute === "function") {
660
771
  this._compute = () => promiseOrCompute(this, this._value);
661
772
  } else {
@@ -670,10 +781,18 @@ class ValueAsyncNode extends ValueNode {
670
781
  }
671
782
  _onResolve(value) {
672
783
  if (this.disposed) return;
784
+ const changed = value !== this._value;
785
+ const hadPendingReader = this._hasPendingReader;
786
+ const pendingFromSet = this._pendingFromSet;
787
+ this._hasPendingReader = false;
788
+ this._pendingFromSet = false;
673
789
  this.status = "resolved";
674
790
  this._value = value;
675
791
  this._error = void 0;
676
- this.notifyDependents();
792
+ if (changed || hadPendingReader || !pendingFromSet) this.notifyDependents();
793
+ }
794
+ _markPendingRead() {
795
+ this._hasPendingReader = true;
677
796
  }
678
797
  _onReject(error) {
679
798
  if (this.disposed) return;
@@ -682,12 +801,13 @@ class ValueAsyncNode extends ValueNode {
682
801
  this.notifyDependents();
683
802
  }
684
803
  set(promiseOrUpdater) {
685
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
804
+ if (this.disposed) throw this._disposedError();
686
805
  if (typeof promiseOrUpdater === "function") {
687
806
  const updater = promiseOrUpdater;
688
807
  switch (this.status) {
689
808
  case "resolved": {
690
809
  this.status = "pending";
810
+ this._pendingFromSet = true;
691
811
  this._scheduler.overwrite(updater(this._value));
692
812
  this.notifyDependents();
693
813
  return;
@@ -696,6 +816,7 @@ class ValueAsyncNode extends ValueNode {
696
816
  case "error":
697
817
  case "dirty": {
698
818
  this.status = "pending";
819
+ this._pendingFromSet = true;
699
820
  this._scheduler.overwrite(updater(this._value));
700
821
  this.notifyDependents();
701
822
  return;
@@ -703,38 +824,39 @@ class ValueAsyncNode extends ValueNode {
703
824
  }
704
825
  } else {
705
826
  this.status = "pending";
827
+ this._pendingFromSet = true;
706
828
  const promise = promiseOrUpdater;
707
829
  this._scheduler.overwrite(promise);
708
830
  this.notifyDependents();
709
831
  }
710
832
  }
711
833
  refresh() {
712
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
834
+ if (this.disposed) throw this._disposedError();
713
835
  this.execute();
714
836
  }
715
837
  }
716
838
 
717
- function constantAsync(valueOrInitializer) {
718
- return new ValueAsyncNode(valueOrInitializer);
839
+ function constantAsync(valueOrInitializer, options) {
840
+ return new ValueAsyncNode(valueOrInitializer, options?.name);
719
841
  }
720
842
 
721
- function derivationAsync(compute) {
722
- return new ValueAsyncNode(compute);
843
+ function derivationAsync(compute, options) {
844
+ return new ValueAsyncNode(compute, options?.name);
723
845
  }
724
846
 
725
- function stateAsync(valueOrInitializer) {
726
- return new ValueAsyncNode(valueOrInitializer);
847
+ function stateAsync(valueOrInitializer, options) {
848
+ return new ValueAsyncNode(valueOrInitializer, options?.name);
727
849
  }
728
850
 
729
- function writableDerivationAsync(compute) {
730
- return new ValueAsyncNode(compute);
851
+ function writableDerivationAsync(compute, options) {
852
+ return new ValueAsyncNode(compute, options?.name);
731
853
  }
732
854
 
733
855
  class ValueSyncNode extends ValueNode {
734
856
  _scheduler;
735
857
  _compute;
736
- constructor(valueOrCompute) {
737
- super();
858
+ constructor(valueOrCompute, name) {
859
+ super(name);
738
860
  if (typeof valueOrCompute === "function") {
739
861
  this._compute = () => valueOrCompute(this, this._value);
740
862
  } else {
@@ -760,7 +882,7 @@ class ValueSyncNode extends ValueNode {
760
882
  this._error = error;
761
883
  }
762
884
  set(valueOrUpdater) {
763
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
885
+ if (this.disposed) throw this._disposedError();
764
886
  if (typeof valueOrUpdater === "function") {
765
887
  const updater = valueOrUpdater;
766
888
  switch (this.status) {
@@ -804,7 +926,7 @@ class ValueSyncNode extends ValueNode {
804
926
  }
805
927
  }
806
928
  refresh() {
807
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
929
+ if (this.disposed) throw this._disposedError();
808
930
  const currentValue = this._value;
809
931
  const currentStatus = this.status;
810
932
  this.execute();
@@ -833,7 +955,7 @@ class ActionNode extends ValueSyncNode {
833
955
  }
834
956
  }
835
957
  set(valueOrUpdater) {
836
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
958
+ if (this.disposed) throw this._disposedError();
837
959
  if (typeof valueOrUpdater === "function") {
838
960
  const updater = valueOrUpdater;
839
961
  const nextValue2 = updater(this._value);
@@ -848,7 +970,7 @@ class ActionNode extends ValueSyncNode {
848
970
  super.set(nextValue);
849
971
  }
850
972
  subscribe(onValue, onError, onPending) {
851
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
973
+ if (this.disposed) throw this._disposedError();
852
974
  const slot = { queue: [] };
853
975
  this._slots.add(slot);
854
976
  const effect = new EffectNode(
@@ -867,11 +989,9 @@ class ActionNode extends ValueSyncNode {
867
989
  onError,
868
990
  onPending
869
991
  );
870
- const originalDispose = effect.dispose.bind(effect);
871
- effect.dispose = () => {
992
+ effect.onDispose(() => {
872
993
  this._slots.delete(slot);
873
- originalDispose();
874
- };
994
+ });
875
995
  return effect;
876
996
  }
877
997
  dispose() {
@@ -882,8 +1002,8 @@ class ActionNode extends ValueSyncNode {
882
1002
 
883
1003
  class ArrayNode extends ValueSyncNode {
884
1004
  $lastAction;
885
- constructor(value = []) {
886
- super(value);
1005
+ constructor(value = [], name) {
1006
+ super(value, name);
887
1007
  this.$lastAction = new ActionNode({
888
1008
  type: "set",
889
1009
  setItems: value,
@@ -899,11 +1019,11 @@ class ArrayNode extends ValueSyncNode {
899
1019
  return [...values];
900
1020
  }
901
1021
  get length() {
902
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1022
+ if (this.disposed) throw this._disposedError();
903
1023
  return this._value.length;
904
1024
  }
905
1025
  set(itemsOrUpdater) {
906
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1026
+ if (this.disposed) throw this._disposedError();
907
1027
  const previousValue = this._value;
908
1028
  super.set(itemsOrUpdater);
909
1029
  this.$lastAction.set({
@@ -914,7 +1034,7 @@ class ArrayNode extends ValueSyncNode {
914
1034
  return previousValue;
915
1035
  }
916
1036
  update(index, item) {
917
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1037
+ if (this.disposed) throw this._disposedError();
918
1038
  if (index < 0 || index >= this._value.length || !Number.isInteger(index)) {
919
1039
  throw new Error("[PicoFlow] Index out of bounds");
920
1040
  }
@@ -930,13 +1050,13 @@ class ArrayNode extends ValueSyncNode {
930
1050
  return previousValue;
931
1051
  }
932
1052
  push(item) {
933
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1053
+ if (this.disposed) throw this._disposedError();
934
1054
  this._value.push(item);
935
1055
  this.notifyDependents();
936
1056
  this.$lastAction.set({ type: "push", addedItem: item });
937
1057
  }
938
1058
  pop() {
939
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1059
+ if (this.disposed) throw this._disposedError();
940
1060
  if (this._value.length === 0) return void 0;
941
1061
  const item = this._value.pop();
942
1062
  this.notifyDependents();
@@ -944,13 +1064,13 @@ class ArrayNode extends ValueSyncNode {
944
1064
  return item;
945
1065
  }
946
1066
  unshift(item) {
947
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1067
+ if (this.disposed) throw this._disposedError();
948
1068
  this._value.unshift(item);
949
1069
  this.notifyDependents();
950
1070
  this.$lastAction.set({ type: "unshift", addedItem: item });
951
1071
  }
952
1072
  shift() {
953
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1073
+ if (this.disposed) throw this._disposedError();
954
1074
  if (this._value.length === 0) return void 0;
955
1075
  const item = this._value.shift();
956
1076
  this.notifyDependents();
@@ -958,7 +1078,7 @@ class ArrayNode extends ValueSyncNode {
958
1078
  return item;
959
1079
  }
960
1080
  splice(start, deleteCount, ...newItems) {
961
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1081
+ if (this.disposed) throw this._disposedError();
962
1082
  const items = this._value.splice(start, deleteCount, ...newItems);
963
1083
  this.notifyDependents();
964
1084
  this.$lastAction.set({
@@ -971,7 +1091,7 @@ class ArrayNode extends ValueSyncNode {
971
1091
  return items;
972
1092
  }
973
1093
  clear() {
974
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1094
+ if (this.disposed) throw this._disposedError();
975
1095
  const previousValue = [...this._value];
976
1096
  this._value = [];
977
1097
  this.notifyDependents();
@@ -987,14 +1107,14 @@ class ArrayNode extends ValueSyncNode {
987
1107
  }
988
1108
  }
989
1109
 
990
- function array(initial) {
991
- return new ArrayNode(initial);
1110
+ function array(initial, options) {
1111
+ return new ArrayNode(initial, options?.name);
992
1112
  }
993
1113
 
994
1114
  class MapNode extends ValueSyncNode {
995
1115
  $lastAction;
996
- constructor(value = /* @__PURE__ */ new Map()) {
997
- super(value);
1116
+ constructor(value = /* @__PURE__ */ new Map(), name) {
1117
+ super(value, name);
998
1118
  this.$lastAction = new ActionNode({
999
1119
  type: "set",
1000
1120
  setMap: value,
@@ -1002,7 +1122,7 @@ class MapNode extends ValueSyncNode {
1002
1122
  });
1003
1123
  }
1004
1124
  add(key, value) {
1005
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1125
+ if (this.disposed) throw this._disposedError();
1006
1126
  if (this._value.has(key)) {
1007
1127
  throw new Error("[PicoFlow] Key already exists");
1008
1128
  }
@@ -1011,7 +1131,7 @@ class MapNode extends ValueSyncNode {
1011
1131
  this.$lastAction.set({ type: "add", key, addedValue: value });
1012
1132
  }
1013
1133
  update(key, value) {
1014
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1134
+ if (this.disposed) throw this._disposedError();
1015
1135
  if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
1016
1136
  const previousValue = this._value.get(key);
1017
1137
  this._value.set(key, value);
@@ -1025,7 +1145,7 @@ class MapNode extends ValueSyncNode {
1025
1145
  return previousValue;
1026
1146
  }
1027
1147
  delete(key) {
1028
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1148
+ if (this.disposed) throw this._disposedError();
1029
1149
  if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
1030
1150
  const value = this._value.get(key);
1031
1151
  this._value.delete(key);
@@ -1034,7 +1154,7 @@ class MapNode extends ValueSyncNode {
1034
1154
  return value;
1035
1155
  }
1036
1156
  set(mapOrUpdater) {
1037
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1157
+ if (this.disposed) throw this._disposedError();
1038
1158
  const previousValue = this._value;
1039
1159
  super.set(mapOrUpdater);
1040
1160
  this.$lastAction.set({
@@ -1045,7 +1165,7 @@ class MapNode extends ValueSyncNode {
1045
1165
  return previousValue;
1046
1166
  }
1047
1167
  clear() {
1048
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1168
+ if (this.disposed) throw this._disposedError();
1049
1169
  const previousValue = this._value;
1050
1170
  this._value = /* @__PURE__ */ new Map();
1051
1171
  this.notifyDependents();
@@ -1061,11 +1181,11 @@ class MapNode extends ValueSyncNode {
1061
1181
  }
1062
1182
  }
1063
1183
 
1064
- function map(initial) {
1184
+ function map(initial, options) {
1065
1185
  if (initial instanceof Map) {
1066
- return new MapNode(initial);
1186
+ return new MapNode(initial, options?.name);
1067
1187
  }
1068
- return new MapNode(new Map(initial ? Object.entries(initial) : []));
1188
+ return new MapNode(new Map(initial ? Object.entries(initial) : []), options?.name);
1069
1189
  }
1070
1190
 
1071
1191
  function subscribe(data, onData, onError, onPending) {
@@ -1074,30 +1194,30 @@ function subscribe(data, onData, onError, onPending) {
1074
1194
 
1075
1195
  class SignalNode extends Observable {
1076
1196
  subscribe(onTrigger, onError, onPending) {
1077
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1197
+ if (this.disposed) throw this._disposedError();
1078
1198
  const effect = new EffectNode((t) => this.watch(t), onTrigger, onError, onPending);
1079
1199
  return effect;
1080
1200
  }
1081
1201
  }
1082
1202
 
1083
- function signal() {
1084
- return new SignalNode();
1203
+ function signal(options) {
1204
+ return new SignalNode(options?.name);
1085
1205
  }
1086
1206
 
1087
- function constant(initializer) {
1088
- return new ValueSyncNode(initializer);
1207
+ function constant(initializer, options) {
1208
+ return new ValueSyncNode(initializer, options?.name);
1089
1209
  }
1090
1210
 
1091
- function derivation(compute) {
1092
- return new ValueSyncNode(compute);
1211
+ function derivation(compute, options) {
1212
+ return new ValueSyncNode(compute, options?.name);
1093
1213
  }
1094
1214
 
1095
- function state(valueOrInitializer) {
1096
- return new ValueSyncNode(valueOrInitializer);
1215
+ function state(valueOrInitializer, options) {
1216
+ return new ValueSyncNode(valueOrInitializer, options?.name);
1097
1217
  }
1098
1218
 
1099
- function writableDerivation(compute) {
1100
- return new ValueSyncNode(compute);
1219
+ function writableDerivation(compute, options) {
1220
+ return new ValueSyncNode(compute, options?.name);
1101
1221
  }
1102
1222
 
1103
1223
  function describeFlowInput(value) {
@@ -1160,4 +1280,4 @@ function from(flow) {
1160
1280
  throw new Error(`[PicoFlow] from(): expected a FlowValue or getter function, received ${describeFlowInput(flow)}`);
1161
1281
  }
1162
1282
 
1163
- export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
1283
+ export { PicoFlowCycleError, PicoFlowDisposedError, PicoFlowError, PicoFlowInternalError, array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };