@ersbeth/picoflow 2.0.3 → 2.2.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/SKILL.md +6 -1
  4. package/dist/picoflow.js +414 -241
  5. package/dist/types/api/base/flowConfig.d.ts +18 -0
  6. package/dist/types/api/base/flowConfig.d.ts.map +1 -0
  7. package/dist/types/api/base/flowObservable.d.ts +3 -3
  8. package/dist/types/api/base/flowObservable.d.ts.map +1 -1
  9. package/dist/types/api/base/flowSubscribable.d.ts +2 -2
  10. package/dist/types/api/base/flowSubscribable.d.ts.map +1 -1
  11. package/dist/types/api/base/flowTracker.d.ts +1 -1
  12. package/dist/types/api/base/flowTracker.d.ts.map +1 -1
  13. package/dist/types/api/base/index.d.ts +5 -4
  14. package/dist/types/api/base/index.d.ts.map +1 -1
  15. package/dist/types/api/index.d.ts +2 -2
  16. package/dist/types/api/index.d.ts.map +1 -1
  17. package/dist/types/api/nodes/async/flowConstantAsync.d.ts +1 -1
  18. package/dist/types/api/nodes/async/flowConstantAsync.d.ts.map +1 -1
  19. package/dist/types/api/nodes/async/flowDerivationAsync.d.ts +3 -3
  20. package/dist/types/api/nodes/async/flowDerivationAsync.d.ts.map +1 -1
  21. package/dist/types/api/nodes/async/flowStateAsync.d.ts +3 -3
  22. package/dist/types/api/nodes/async/flowStateAsync.d.ts.map +1 -1
  23. package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts +2 -2
  24. package/dist/types/api/nodes/async/flowWritableDerivationAsync.d.ts.map +1 -1
  25. package/dist/types/api/nodes/async/index.d.ts +4 -4
  26. package/dist/types/api/nodes/async/index.d.ts.map +1 -1
  27. package/dist/types/api/nodes/collections/flowArray.d.ts +7 -4
  28. package/dist/types/api/nodes/collections/flowArray.d.ts.map +1 -1
  29. package/dist/types/api/nodes/collections/flowMap.d.ts +3 -2
  30. package/dist/types/api/nodes/collections/flowMap.d.ts.map +1 -1
  31. package/dist/types/api/nodes/collections/index.d.ts +2 -2
  32. package/dist/types/api/nodes/collections/index.d.ts.map +1 -1
  33. package/dist/types/api/nodes/flowEffect.d.ts +2 -2
  34. package/dist/types/api/nodes/flowEffect.d.ts.map +1 -1
  35. package/dist/types/api/nodes/flowSignal.d.ts +1 -1
  36. package/dist/types/api/nodes/flowSignal.d.ts.map +1 -1
  37. package/dist/types/api/nodes/flowValue.d.ts +2 -2
  38. package/dist/types/api/nodes/flowValue.d.ts.map +1 -1
  39. package/dist/types/api/nodes/index.d.ts +7 -7
  40. package/dist/types/api/nodes/index.d.ts.map +1 -1
  41. package/dist/types/api/nodes/sync/flowConstant.d.ts +2 -2
  42. package/dist/types/api/nodes/sync/flowConstant.d.ts.map +1 -1
  43. package/dist/types/api/nodes/sync/flowDerivation.d.ts +3 -3
  44. package/dist/types/api/nodes/sync/flowDerivation.d.ts.map +1 -1
  45. package/dist/types/api/nodes/sync/flowState.d.ts +3 -3
  46. package/dist/types/api/nodes/sync/flowState.d.ts.map +1 -1
  47. package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts +2 -2
  48. package/dist/types/api/nodes/sync/flowWritableDerivation.d.ts.map +1 -1
  49. package/dist/types/api/nodes/sync/index.d.ts +4 -4
  50. package/dist/types/api/nodes/sync/index.d.ts.map +1 -1
  51. package/dist/types/api/nodes/utils.d.ts +1 -1
  52. package/dist/types/api/nodes/utils.d.ts.map +1 -1
  53. package/dist/types/base/disposable.d.ts +4 -4
  54. package/dist/types/base/disposable.d.ts.map +1 -1
  55. package/dist/types/base/executionStack.d.ts +26 -1
  56. package/dist/types/base/executionStack.d.ts.map +1 -1
  57. package/dist/types/base/index.d.ts +5 -5
  58. package/dist/types/base/index.d.ts.map +1 -1
  59. package/dist/types/base/node.d.ts +6 -5
  60. package/dist/types/base/node.d.ts.map +1 -1
  61. package/dist/types/base/observable.d.ts +3 -3
  62. package/dist/types/base/observable.d.ts.map +1 -1
  63. package/dist/types/base/observer.d.ts +11 -2
  64. package/dist/types/base/observer.d.ts.map +1 -1
  65. package/dist/types/converters/index.d.ts +1 -1
  66. package/dist/types/converters/index.d.ts.map +1 -1
  67. package/dist/types/converters/solid.d.ts +12 -8
  68. package/dist/types/converters/solid.d.ts.map +1 -1
  69. package/dist/types/index.d.ts +2 -2
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/nodes/actionNode.d.ts +23 -0
  72. package/dist/types/nodes/actionNode.d.ts.map +1 -0
  73. package/dist/types/nodes/arrayNode.d.ts +4 -3
  74. package/dist/types/nodes/arrayNode.d.ts.map +1 -1
  75. package/dist/types/nodes/effectNode.d.ts +18 -2
  76. package/dist/types/nodes/effectNode.d.ts.map +1 -1
  77. package/dist/types/nodes/index.d.ts +8 -7
  78. package/dist/types/nodes/index.d.ts.map +1 -1
  79. package/dist/types/nodes/mapNode.d.ts +4 -3
  80. package/dist/types/nodes/mapNode.d.ts.map +1 -1
  81. package/dist/types/nodes/signalNode.d.ts +2 -2
  82. package/dist/types/nodes/signalNode.d.ts.map +1 -1
  83. package/dist/types/nodes/valueAsyncNode.d.ts +3 -3
  84. package/dist/types/nodes/valueAsyncNode.d.ts.map +1 -1
  85. package/dist/types/nodes/valueNode.d.ts +10 -3
  86. package/dist/types/nodes/valueNode.d.ts.map +1 -1
  87. package/dist/types/nodes/valueSyncNode.d.ts +3 -3
  88. package/dist/types/nodes/valueSyncNode.d.ts.map +1 -1
  89. package/dist/types/schedulers/asyncScheduler.d.ts +2 -1
  90. package/dist/types/schedulers/asyncScheduler.d.ts.map +1 -1
  91. package/dist/types/schedulers/index.d.ts +4 -4
  92. package/dist/types/schedulers/index.d.ts.map +1 -1
  93. package/dist/types/schedulers/syncResolver.d.ts +1 -2
  94. package/dist/types/schedulers/syncResolver.d.ts.map +1 -1
  95. package/dist/types/schedulers/syncScheduler.d.ts +1 -1
  96. package/dist/types/schedulers/syncScheduler.d.ts.map +1 -1
  97. package/package.json +22 -2
package/dist/picoflow.js CHANGED
@@ -1,5 +1,242 @@
1
1
  import { createResource, onMount, createEffect, resetErrorBoundaries, onCleanup } from 'solid-js';
2
2
 
3
+ class Disposable {
4
+ _disposed = false;
5
+ get disposed() {
6
+ return this._disposed;
7
+ }
8
+ }
9
+
10
+ class ExecutionStack {
11
+ static _MAX_FLUSH_STEPS = 1e4;
12
+ static _pendingQueue = [];
13
+ static _effectQueue = [];
14
+ static _executionScheduled;
15
+ static _coalesceResetScheduled = false;
16
+ /** Routes any error escaping the flush loop; defaults to logging rather than crashing (A2-SEM-2, A2-SEC-1). */
17
+ static _onFlushError = (error) => console.error(error);
18
+ /**
19
+ * True once the current synchronous call stack has finished and microtasks have started.
20
+ * EffectNode uses this to treat microtask notifications (e.g. async resolve) differently
21
+ * from coalesced sync-batch notifications.
22
+ */
23
+ static _pastSyncTurn = false;
24
+ /** @internal Used by EffectNode to distinguish sync-batch from microtask notifications. */
25
+ static get pastSyncTurn() {
26
+ return ExecutionStack._pastSyncTurn;
27
+ }
28
+ /**
29
+ * @internal Runs after the current sync turn ends (queueMicrotask).
30
+ * Marks pastSyncTurn so the next coalesced effect notify can bump its epoch,
31
+ * and resets per-effect coalesce flags via onReset.
32
+ */
33
+ static scheduleCoalesceReset(onReset) {
34
+ if (ExecutionStack._coalesceResetScheduled) return;
35
+ ExecutionStack._coalesceResetScheduled = true;
36
+ queueMicrotask(() => {
37
+ ExecutionStack._pastSyncTurn = true;
38
+ ExecutionStack._coalesceResetScheduled = false;
39
+ onReset();
40
+ });
41
+ }
42
+ /** @internal Wired to the public `onFlushError()` API in `src/api/base/flowConfig.ts`. */
43
+ static setFlushErrorHandler(handler) {
44
+ ExecutionStack._onFlushError = handler;
45
+ }
46
+ static pushPending(node) {
47
+ ExecutionStack._beginSyncTurn();
48
+ ExecutionStack._scheduleExecution();
49
+ ExecutionStack._pendingQueue.push(node);
50
+ }
51
+ static pushEffect(effect) {
52
+ ExecutionStack._beginSyncTurn();
53
+ ExecutionStack._scheduleExecution();
54
+ ExecutionStack._effectQueue.push(effect);
55
+ }
56
+ static _beginSyncTurn() {
57
+ ExecutionStack._pastSyncTurn = false;
58
+ }
59
+ static _scheduleExecution() {
60
+ if (ExecutionStack._executionScheduled) return;
61
+ ExecutionStack._executionScheduled = new Promise((resolve) => {
62
+ setTimeout(() => {
63
+ ExecutionStack._executionScheduled = void 0;
64
+ ExecutionStack._execute();
65
+ resolve();
66
+ }, 0);
67
+ });
68
+ }
69
+ static _drain(queue) {
70
+ for (let i = 0; i < queue.length; i++) {
71
+ if (i >= ExecutionStack._MAX_FLUSH_STEPS) {
72
+ ExecutionStack._pendingQueue.length = 0;
73
+ ExecutionStack._effectQueue.length = 0;
74
+ ExecutionStack._onFlushError(new Error("[PicoFlow] Reactive update cycle exceeded maximum depth"));
75
+ return;
76
+ }
77
+ const node = queue[i];
78
+ try {
79
+ node?.execute();
80
+ } catch (error) {
81
+ ExecutionStack._onFlushError(error);
82
+ }
83
+ }
84
+ queue.length = 0;
85
+ }
86
+ static _execute() {
87
+ ExecutionStack._drain(ExecutionStack._pendingQueue);
88
+ ExecutionStack._drain(ExecutionStack._effectQueue);
89
+ }
90
+ }
91
+
92
+ class Node extends Disposable {
93
+ _dependencies = /* @__PURE__ */ new Set();
94
+ _dependents = /* @__PURE__ */ new Set();
95
+ _status = "resolved";
96
+ get status() {
97
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
98
+ return this._status;
99
+ }
100
+ set status(status) {
101
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
102
+ this._status = status;
103
+ }
104
+ registerDependency(dependency) {
105
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
106
+ this._dependencies.add(dependency);
107
+ dependency.registerDependent(this);
108
+ }
109
+ unregisterDependency(dependency) {
110
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
111
+ this._dependencies.delete(dependency);
112
+ dependency.unregisterDependent(this);
113
+ }
114
+ 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();
120
+ }
121
+ registerDependent(dependent) {
122
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
123
+ this._dependents.add(dependent);
124
+ }
125
+ unregisterDependent(dependent) {
126
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
127
+ this._dependents.delete(dependent);
128
+ }
129
+ markDependencyDisposed() {
130
+ }
131
+ notifyDependents() {
132
+ if (this.disposed) return;
133
+ this._dependents.forEach((dependent) => {
134
+ dependent.notify();
135
+ });
136
+ }
137
+ watch(tracker) {
138
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
139
+ tracker.registerDependency(this);
140
+ }
141
+ trigger() {
142
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
143
+ this.notifyDependents();
144
+ }
145
+ dispose() {
146
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
147
+ Array.from(this._dependents).forEach((dependant) => {
148
+ dependant.markDependencyDisposed();
149
+ dependant.unregisterDependency(this);
150
+ this.unregisterDependent(dependant);
151
+ });
152
+ Array.from(this._dependencies).forEach((dependency) => {
153
+ this.unregisterDependency(dependency);
154
+ });
155
+ this._disposed = true;
156
+ }
157
+ }
158
+
159
+ class Observable extends Disposable {
160
+ _dependents = /* @__PURE__ */ new Set();
161
+ _status = "resolved";
162
+ get status() {
163
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
164
+ return this._status;
165
+ }
166
+ set status(status) {
167
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
168
+ this._status = status;
169
+ }
170
+ registerDependent(dependent) {
171
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
172
+ this._dependents.add(dependent);
173
+ }
174
+ unregisterDependent(dependent) {
175
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
176
+ this._dependents.delete(dependent);
177
+ }
178
+ notifyDependents() {
179
+ if (this.disposed) return;
180
+ this._dependents.forEach((dependent) => {
181
+ dependent.notify();
182
+ });
183
+ }
184
+ watch(tracker) {
185
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
186
+ tracker.registerDependency(this);
187
+ }
188
+ trigger() {
189
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
190
+ this.notifyDependents();
191
+ }
192
+ dispose() {
193
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
194
+ Array.from(this._dependents).forEach((dependant) => {
195
+ dependant.markDependencyDisposed();
196
+ dependant.unregisterDependency(this);
197
+ this.unregisterDependent(dependant);
198
+ });
199
+ this._disposed = true;
200
+ }
201
+ }
202
+
203
+ class Observer extends Disposable {
204
+ _dependencies = /* @__PURE__ */ new Set();
205
+ /** Set when a dependency disposed while this observer was queued; consumed by the next execute(). */
206
+ _dependencyDisposed = false;
207
+ markDependencyDisposed() {
208
+ this._dependencyDisposed = true;
209
+ }
210
+ registerDependency(dependency) {
211
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
212
+ this._dependencies.add(dependency);
213
+ dependency.registerDependent(this);
214
+ }
215
+ unregisterDependency(dependency) {
216
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
217
+ this._dependencies.delete(dependency);
218
+ dependency.unregisterDependent(this);
219
+ }
220
+ clearDependencies() {
221
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
222
+ this._dependencies.forEach((dependency) => {
223
+ dependency.unregisterDependent(this);
224
+ });
225
+ this._dependencies.clear();
226
+ }
227
+ dispose() {
228
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
229
+ Array.from(this._dependencies).forEach((dependency) => {
230
+ this.unregisterDependency(dependency);
231
+ });
232
+ this._disposed = true;
233
+ }
234
+ }
235
+
236
+ function onFlushError(handler) {
237
+ ExecutionStack.setFlushErrorHandler(handler);
238
+ }
239
+
3
240
  function isDisposable(obj) {
4
241
  return obj !== null && obj !== void 0 && typeof obj.dispose === "function";
5
242
  }
@@ -7,7 +244,7 @@ function isDisposable(obj) {
7
244
  class PendingError extends Error {
8
245
  pendingPromise;
9
246
  constructor(promise) {
10
- super("[PicoFlow] PendingResolver pending");
247
+ super("[PicoFlow] Computation is pending");
11
248
  this.name = "PendingError";
12
249
  this.pendingPromise = promise;
13
250
  }
@@ -36,8 +273,8 @@ class AsyncResolver {
36
273
  return this._finished;
37
274
  }
38
275
  compute() {
39
- if (this._finished) throw new Error("[Picoflow] PendingResolver: Can't restart a settled resolver");
40
- if (this._aborted) throw new Error("[Picoflow] PendingResolver: Can't restart an aborted resolver");
276
+ if (this._finished) throw new Error("[PicoFlow] AsyncResolver: Can't restart a settled resolver");
277
+ if (this._aborted) throw new Error("[PicoFlow] AsyncResolver: Can't restart an aborted resolver");
41
278
  this._iteration++;
42
279
  const currentIteration = this._iteration;
43
280
  this._compute().then((value) => {
@@ -51,8 +288,8 @@ class AsyncResolver {
51
288
  });
52
289
  }
53
290
  overwrite(promise) {
54
- if (this._finished) throw new Error("[Picoflow] PendingResolver: Can't overwrite a settled resolver");
55
- if (this._aborted) throw new Error("[Picoflow] PendingResolver: Can't overwrite an aborted resolver");
291
+ if (this._finished) throw new Error("[PicoFlow] AsyncResolver: Can't overwrite a settled resolver");
292
+ if (this._aborted) throw new Error("[PicoFlow] AsyncResolver: Can't overwrite an aborted resolver");
56
293
  this._iteration++;
57
294
  const currentIteration = this._iteration;
58
295
  promise.then((value) => {
@@ -81,7 +318,7 @@ class AsyncScheduler {
81
318
  this._onResolve = onResolve;
82
319
  this._onReject = onReject;
83
320
  this._resolver = new AsyncResolver(compute);
84
- this._resolver.computed.then(this._onResolve).catch(this._onReject).finally(() => this._settled.resolve());
321
+ this._attachHandlers(this._resolver);
85
322
  }
86
323
  get settled() {
87
324
  if (this._disposed) throw new Error("[PicoFlow] ComputationScheduler is disposed");
@@ -95,7 +332,7 @@ class AsyncScheduler {
95
332
  if (this._resolver.finished) {
96
333
  this._resolver = new AsyncResolver(this._compute);
97
334
  this._settled = Promise.withResolvers();
98
- this._resolver.computed.then(this._onResolve).catch(this._onReject).finally(() => this._settled.resolve());
335
+ this._attachHandlers(this._resolver);
99
336
  }
100
337
  this._resolver.overwrite(promise);
101
338
  }
@@ -104,10 +341,19 @@ class AsyncScheduler {
104
341
  if (this._resolver.finished) {
105
342
  this._resolver = new AsyncResolver(this._compute);
106
343
  this._settled = Promise.withResolvers();
107
- this._resolver.computed.then(this._onResolve).catch(this._onReject).finally(() => this._settled.resolve());
344
+ this._attachHandlers(this._resolver);
108
345
  }
109
346
  this._resolver.compute();
110
347
  }
348
+ _attachHandlers(resolver) {
349
+ resolver.computed.then((value) => {
350
+ if (this._disposed) return;
351
+ this._onResolve(value);
352
+ }).catch((error) => {
353
+ if (this._disposed) return;
354
+ this._onReject(error);
355
+ }).finally(() => this._settled.resolve());
356
+ }
111
357
  dispose() {
112
358
  this._resolver.abort();
113
359
  this._disposed = true;
@@ -116,7 +362,6 @@ class AsyncScheduler {
116
362
 
117
363
  class SyncResolver {
118
364
  _compute;
119
- _iteration = 0;
120
365
  _aborted = false;
121
366
  _finished = false;
122
367
  _onValue;
@@ -133,18 +378,16 @@ class SyncResolver {
133
378
  return this._finished;
134
379
  }
135
380
  compute() {
136
- if (this._finished) throw new Error("[Picoflow] PendingResolver: Can't restart a settled resolver");
137
- if (this._aborted) throw new Error("[Picoflow] PendingResolver: Can't restart an aborted resolver");
138
- this._iteration++;
139
- const currentIteration = this._iteration;
381
+ if (this._finished) throw new Error("[PicoFlow] SyncResolver: Can't restart a settled resolver");
382
+ if (this._aborted) throw new Error("[PicoFlow] SyncResolver: Can't restart an aborted resolver");
140
383
  try {
141
384
  const value = this._compute();
142
- if (this._iteration === currentIteration && !this._aborted) {
385
+ if (!this._finished && !this._aborted) {
143
386
  this._finished = true;
144
387
  this._onValue(value);
145
388
  }
146
389
  } catch (error) {
147
- if (this._iteration === currentIteration && !this._aborted) {
390
+ if (!this._finished && !this._aborted) {
148
391
  if (!(error instanceof PendingError)) {
149
392
  this._finished = true;
150
393
  this._onError(error);
@@ -153,17 +396,12 @@ class SyncResolver {
153
396
  }
154
397
  }
155
398
  overwrite(value) {
156
- if (this._finished) throw new Error("[Picoflow] PendingResolver: Can't overwrite a settled resolver");
157
- if (this._aborted) throw new Error("[Picoflow] PendingResolver: Can't overwrite an aborted resolver");
158
- this._iteration++;
159
- const currentIteration = this._iteration;
160
- if (this._iteration === currentIteration && !this._aborted) {
161
- this._finished = true;
162
- this._onValue(value);
163
- }
399
+ if (this._finished) throw new Error("[PicoFlow] SyncResolver: Can't overwrite a settled resolver");
400
+ if (this._aborted) throw new Error("[PicoFlow] SyncResolver: Can't overwrite an aborted resolver");
401
+ this._finished = true;
402
+ this._onValue(value);
164
403
  }
165
404
  abort() {
166
- this._iteration++;
167
405
  this._aborted = true;
168
406
  }
169
407
  }
@@ -212,230 +450,91 @@ class SyncScheduler {
212
450
  }
213
451
  }
214
452
 
215
- class Disposable {
216
- _disposed = false;
217
- get disposed() {
218
- return this._disposed;
219
- }
220
- dispose() {
221
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
222
- this._disposed = true;
223
- }
224
- }
225
-
226
- class ExecutionStack {
227
- static _pendingQueue = [];
228
- static _effectQueue = [];
229
- static _executionScheduled;
230
- static pushPending(node) {
231
- ExecutionStack._scheduleExecution();
232
- ExecutionStack._pendingQueue.push(node);
233
- }
234
- static pushEffect(effect) {
235
- ExecutionStack._scheduleExecution();
236
- ExecutionStack._effectQueue.push(effect);
237
- }
238
- static _scheduleExecution() {
239
- if (ExecutionStack._executionScheduled) return;
240
- ExecutionStack._executionScheduled = new Promise((resolve) => {
241
- setTimeout(() => {
242
- ExecutionStack._executionScheduled = void 0;
243
- ExecutionStack._execute();
244
- resolve();
245
- }, 0);
246
- });
247
- }
248
- static _execute() {
249
- ExecutionStack._pendingQueue.forEach((node) => {
250
- node.execute();
251
- });
252
- ExecutionStack._pendingQueue.length = 0;
253
- ExecutionStack._effectQueue.forEach((effect) => {
254
- effect.execute();
255
- });
256
- ExecutionStack._effectQueue.length = 0;
257
- }
258
- }
259
-
260
- class Node extends Disposable {
261
- _dependencies = /* @__PURE__ */ new Set();
262
- _dependents = /* @__PURE__ */ new Set();
263
- _status = "resolved";
264
- get status() {
265
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
266
- return this._status;
267
- }
268
- set status(status) {
269
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
270
- this._status = status;
271
- }
272
- registerDependency(dependency) {
273
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
274
- this._dependencies.add(dependency);
275
- dependency.registerDependent(this);
276
- }
277
- unregisterDependency(dependency) {
278
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
279
- this._dependencies.delete(dependency);
280
- dependency.unregisterDependent(this);
281
- }
282
- clearDependencies() {
283
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
284
- this._dependencies.forEach((dependency) => {
285
- dependency.unregisterDependent(this);
286
- });
287
- this._dependencies.clear();
288
- }
289
- registerDependent(dependent) {
290
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
291
- this._dependents.add(dependent);
292
- }
293
- unregisterDependent(dependent) {
294
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
295
- this._dependents.delete(dependent);
296
- }
297
- notifyDependents() {
298
- if (this.disposed) return;
299
- this._dependents.forEach((dependent) => {
300
- dependent.notify();
301
- });
302
- }
303
- watch(tracker) {
304
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
305
- tracker.registerDependency(this);
306
- }
307
- trigger() {
308
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
309
- this.notifyDependents();
310
- }
311
- notify() {
312
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
313
- if (this.status === "dirty") return;
314
- this.status = "dirty";
315
- this.notifyDependents();
316
- }
317
- dispose() {
318
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
319
- Array.from(this._dependents).forEach((dependant) => {
320
- dependant.unregisterDependency(this);
321
- this.unregisterDependent(dependant);
322
- });
323
- Array.from(this._dependencies).forEach((dependency) => {
324
- this.unregisterDependency(dependency);
325
- });
326
- this._disposed = true;
327
- }
328
- }
329
-
330
- class Observable extends Disposable {
331
- _dependents = /* @__PURE__ */ new Set();
332
- _status = "resolved";
333
- get status() {
334
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
335
- return this._status;
336
- }
337
- set status(status) {
338
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
339
- this._status = status;
340
- }
341
- registerDependent(dependent) {
342
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
343
- this._dependents.add(dependent);
344
- }
345
- unregisterDependent(dependent) {
346
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
347
- this._dependents.delete(dependent);
348
- }
349
- notifyDependents() {
350
- if (this.disposed) return;
351
- this._dependents.forEach((dependent) => {
352
- dependent.notify();
353
- });
354
- }
355
- watch(tracker) {
356
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
357
- tracker.registerDependency(this);
358
- }
359
- trigger() {
360
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
361
- this.notifyDependents();
362
- }
363
- dispose() {
364
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
365
- Array.from(this._dependents).forEach((dependant) => {
366
- dependant.unregisterDependency(this);
367
- this.unregisterDependent(dependant);
368
- });
369
- this._disposed = true;
370
- }
371
- }
372
-
373
- class Observer extends Disposable {
374
- _dependencies = /* @__PURE__ */ new Set();
375
- registerDependency(dependency) {
376
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
377
- this._dependencies.add(dependency);
378
- dependency.registerDependent(this);
379
- }
380
- unregisterDependency(dependency) {
381
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
382
- this._dependencies.delete(dependency);
383
- dependency.unregisterDependent(this);
384
- }
385
- clearDependencies() {
386
- if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
387
- this._dependencies.forEach((dependency) => {
388
- dependency.unregisterDependent(this);
389
- });
390
- this._dependencies.clear();
391
- }
392
- dispose() {
393
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
394
- Array.from(this._dependencies).forEach((dependency) => {
395
- this.unregisterDependency(dependency);
396
- });
397
- this._disposed = true;
398
- }
399
- }
400
-
401
453
  class EffectNode extends Observer {
402
454
  _data;
403
455
  _onData;
404
456
  _onError;
405
457
  _onPending;
458
+ /** True while this effect is already waiting in the effect queue for the next flush. */
459
+ _queued = false;
460
+ /** Guards the single coalesced bump of _notifyEpoch while _queued during one sync turn. */
461
+ _coalescedWhileQueued = false;
462
+ /** Monotonic count of notifications that must eventually be executed. */
463
+ _notifyEpoch = 0;
464
+ /** How many notification epochs have been executed so far. */
465
+ _executedEpoch = 0;
406
466
  constructor(data, onData, onError, onPending) {
407
467
  super();
408
468
  this._data = data;
409
469
  this._onData = onData;
410
470
  this._onError = onError;
411
471
  this._onPending = onPending;
472
+ this._notifyEpoch = 1;
412
473
  this.execute();
413
474
  }
414
475
  notify() {
415
476
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
477
+ if (this._queued) {
478
+ if (!this._coalescedWhileQueued) {
479
+ this._coalescedWhileQueued = true;
480
+ if (ExecutionStack.pastSyncTurn) {
481
+ this._notifyEpoch++;
482
+ }
483
+ }
484
+ return;
485
+ }
486
+ this._notifyEpoch++;
487
+ this._queued = true;
488
+ ExecutionStack.scheduleCoalesceReset(() => {
489
+ this._coalescedWhileQueued = false;
490
+ });
416
491
  ExecutionStack.pushEffect(this);
417
492
  }
418
493
  execute() {
419
- try {
420
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
421
- this.clearDependencies();
422
- const data = this._data(this);
423
- this._onData(data);
424
- } catch (error) {
425
- if (error instanceof PendingError) {
426
- this._onPending?.();
427
- } else {
428
- if (this._onError) {
429
- this._onError(error instanceof Error ? error : new Error(String(error)));
494
+ this._queued = false;
495
+ if (this._disposed) {
496
+ this._executedEpoch = this._notifyEpoch;
497
+ return;
498
+ }
499
+ if (this._dependencyDisposed) {
500
+ this._dependencyDisposed = false;
501
+ this._executedEpoch = this._notifyEpoch;
502
+ return;
503
+ }
504
+ const targetEpoch = this._notifyEpoch;
505
+ while (this._executedEpoch < targetEpoch) {
506
+ this._executedEpoch++;
507
+ if (this._disposed) {
508
+ this._executedEpoch = this._notifyEpoch;
509
+ return;
510
+ }
511
+ if (this._dependencyDisposed) {
512
+ this._dependencyDisposed = false;
513
+ this._executedEpoch = this._notifyEpoch;
514
+ return;
515
+ }
516
+ try {
517
+ this.clearDependencies();
518
+ const data = this._data(this);
519
+ this._onData(data);
520
+ } catch (error) {
521
+ if (error instanceof PendingError) {
522
+ this._onPending?.();
430
523
  } else {
431
- throw error;
524
+ if (this._onError) {
525
+ this._onError(error instanceof Error ? error : new Error(String(error)));
526
+ } else {
527
+ throw error;
528
+ }
432
529
  }
433
530
  }
434
531
  }
435
532
  }
436
533
  }
437
534
 
535
+ const PICOFLOW_VALUE_NODE = /* @__PURE__ */ Symbol.for("picoflow.node");
438
536
  class ValueNode extends Node {
537
+ [PICOFLOW_VALUE_NODE] = true;
439
538
  _value;
440
539
  _error;
441
540
  watch(tracker) {
@@ -570,12 +669,14 @@ class ValueAsyncNode extends ValueNode {
570
669
  );
571
670
  }
572
671
  _onResolve(value) {
672
+ if (this.disposed) return;
573
673
  this.status = "resolved";
574
674
  this._value = value;
575
675
  this._error = void 0;
576
676
  this.notifyDependents();
577
677
  }
578
678
  _onReject(error) {
679
+ if (this.disposed) return;
579
680
  this.status = "error";
580
681
  this._error = error;
581
682
  this.notifyDependents();
@@ -724,11 +825,66 @@ class ValueSyncNode extends ValueNode {
724
825
  }
725
826
  }
726
827
 
828
+ class ActionNode extends ValueSyncNode {
829
+ _slots = /* @__PURE__ */ new Set();
830
+ _enqueueForSubscribers(action) {
831
+ for (const slot of this._slots) {
832
+ slot.queue.push(action);
833
+ }
834
+ }
835
+ set(valueOrUpdater) {
836
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
837
+ if (typeof valueOrUpdater === "function") {
838
+ const updater = valueOrUpdater;
839
+ const nextValue2 = updater(this._value);
840
+ if (this.status === "resolved" && nextValue2 === this._value) return;
841
+ this._enqueueForSubscribers(nextValue2);
842
+ super.set(updater);
843
+ return;
844
+ }
845
+ const nextValue = valueOrUpdater;
846
+ if (this.status === "resolved" && nextValue === this._value) return;
847
+ this._enqueueForSubscribers(nextValue);
848
+ super.set(nextValue);
849
+ }
850
+ subscribe(onValue, onError, onPending) {
851
+ if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
852
+ const slot = { queue: [] };
853
+ this._slots.add(slot);
854
+ const effect = new EffectNode(
855
+ (tracker) => {
856
+ this.watch(tracker);
857
+ if (slot.queue.length === 0) {
858
+ return [this.get(tracker)];
859
+ }
860
+ return slot.queue.splice(0);
861
+ },
862
+ (actions) => {
863
+ for (const action of actions) {
864
+ onValue(action);
865
+ }
866
+ },
867
+ onError,
868
+ onPending
869
+ );
870
+ const originalDispose = effect.dispose.bind(effect);
871
+ effect.dispose = () => {
872
+ this._slots.delete(slot);
873
+ originalDispose();
874
+ };
875
+ return effect;
876
+ }
877
+ dispose() {
878
+ this._slots.clear();
879
+ super.dispose();
880
+ }
881
+ }
882
+
727
883
  class ArrayNode extends ValueSyncNode {
728
884
  $lastAction;
729
885
  constructor(value = []) {
730
886
  super(value);
731
- this.$lastAction = new ValueSyncNode({
887
+ this.$lastAction = new ActionNode({
732
888
  type: "set",
733
889
  setItems: value,
734
890
  clearedItems: []
@@ -759,7 +915,7 @@ class ArrayNode extends ValueSyncNode {
759
915
  }
760
916
  update(index, item) {
761
917
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
762
- if (index < 0 || index >= this._value.length) {
918
+ if (index < 0 || index >= this._value.length || !Number.isInteger(index)) {
763
919
  throw new Error("[PicoFlow] Index out of bounds");
764
920
  }
765
921
  const previousValue = this._value[index];
@@ -781,6 +937,7 @@ class ArrayNode extends ValueSyncNode {
781
937
  }
782
938
  pop() {
783
939
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
940
+ if (this._value.length === 0) return void 0;
784
941
  const item = this._value.pop();
785
942
  this.notifyDependents();
786
943
  this.$lastAction.set({ type: "pop", removedItem: item });
@@ -794,6 +951,7 @@ class ArrayNode extends ValueSyncNode {
794
951
  }
795
952
  shift() {
796
953
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
954
+ if (this._value.length === 0) return void 0;
797
955
  const item = this._value.shift();
798
956
  this.notifyDependents();
799
957
  this.$lastAction.set({ type: "shift", removedItem: item });
@@ -822,9 +980,9 @@ class ArrayNode extends ValueSyncNode {
822
980
  }
823
981
  dispose() {
824
982
  super.dispose();
825
- this._value.forEach((item) => {
826
- if (item instanceof Disposable) item.dispose();
827
- });
983
+ if (!this.$lastAction.disposed) {
984
+ this.$lastAction.dispose();
985
+ }
828
986
  this._value = [];
829
987
  }
830
988
  }
@@ -837,7 +995,7 @@ class MapNode extends ValueSyncNode {
837
995
  $lastAction;
838
996
  constructor(value = /* @__PURE__ */ new Map()) {
839
997
  super(value);
840
- this.$lastAction = new ValueSyncNode({
998
+ this.$lastAction = new ActionNode({
841
999
  type: "set",
842
1000
  setMap: value,
843
1001
  clearedMap: /* @__PURE__ */ new Map()
@@ -845,8 +1003,7 @@ class MapNode extends ValueSyncNode {
845
1003
  }
846
1004
  add(key, value) {
847
1005
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
848
- const previousValue = this._value.get(key);
849
- if (previousValue) {
1006
+ if (this._value.has(key)) {
850
1007
  throw new Error("[PicoFlow] Key already exists");
851
1008
  }
852
1009
  this._value.set(key, value);
@@ -855,8 +1012,8 @@ class MapNode extends ValueSyncNode {
855
1012
  }
856
1013
  update(key, value) {
857
1014
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1015
+ if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
858
1016
  const previousValue = this._value.get(key);
859
- if (!previousValue) throw new Error("[PicoFlow] Key does not exist");
860
1017
  this._value.set(key, value);
861
1018
  this.notifyDependents();
862
1019
  this.$lastAction.set({
@@ -869,8 +1026,8 @@ class MapNode extends ValueSyncNode {
869
1026
  }
870
1027
  delete(key) {
871
1028
  if (this.disposed) throw new Error("[PicoFlow] Primitive is disposed");
1029
+ if (!this._value.has(key)) throw new Error("[PicoFlow] Key does not exist");
872
1030
  const value = this._value.get(key);
873
- if (value === void 0) throw new Error("[PicoFlow] Key does not exist");
874
1031
  this._value.delete(key);
875
1032
  this.notifyDependents();
876
1033
  this.$lastAction.set({ type: "delete", key, removedValue: value });
@@ -897,9 +1054,9 @@ class MapNode extends ValueSyncNode {
897
1054
  }
898
1055
  dispose() {
899
1056
  super.dispose();
900
- this._value.forEach((item) => {
901
- if (item instanceof Disposable) item.dispose();
902
- });
1057
+ if (!this.$lastAction.disposed) {
1058
+ this.$lastAction.dispose();
1059
+ }
903
1060
  this._value.clear();
904
1061
  }
905
1062
  }
@@ -943,7 +1100,18 @@ function writableDerivation(compute) {
943
1100
  return new ValueSyncNode(compute);
944
1101
  }
945
1102
 
946
- function fromNode(node) {
1103
+ function describeFlowInput(value) {
1104
+ if (value === null) return "null";
1105
+ if (typeof value === "object") {
1106
+ const name = value.constructor?.name;
1107
+ return name ? `object (${name})` : "object";
1108
+ }
1109
+ return typeof value;
1110
+ }
1111
+ function isValueNode(flow) {
1112
+ return typeof flow === "object" && flow !== null && PICOFLOW_VALUE_NODE in flow;
1113
+ }
1114
+ function fromNode(node, options) {
947
1115
  const [resource, { refetch }] = createResource(() => node.pick());
948
1116
  let fx;
949
1117
  onMount(() => {
@@ -968,23 +1136,28 @@ function fromNode(node) {
968
1136
  resource.error ?? resource();
969
1137
  resetErrorBoundaries();
970
1138
  });
971
- onCleanup(() => fx.dispose());
1139
+ onCleanup(() => {
1140
+ fx.dispose();
1141
+ if (options?.disposeNode && !node.disposed) {
1142
+ node.dispose();
1143
+ }
1144
+ });
972
1145
  return resource;
973
1146
  }
974
1147
  function fromGetter(getter) {
975
1148
  const derivation = new ValueSyncNode((t) => {
976
1149
  return getter(t);
977
1150
  });
978
- return fromNode(derivation);
1151
+ return fromNode(derivation, { disposeNode: true });
979
1152
  }
980
1153
  function from(flow) {
981
- if (flow instanceof ValueAsyncNode || flow instanceof ValueSyncNode) {
1154
+ if (isValueNode(flow)) {
982
1155
  return fromNode(flow);
983
1156
  }
984
1157
  if (typeof flow === "function") {
985
1158
  return fromGetter(flow);
986
1159
  }
987
- throw new Error("Invalid flow type");
1160
+ throw new Error(`[PicoFlow] from(): expected a FlowValue or getter function, received ${describeFlowInput(flow)}`);
988
1161
  }
989
1162
 
990
- export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };
1163
+ export { array, constant, constantAsync, derivation, derivationAsync, from, isDisposable, map, onFlushError, signal, state, stateAsync, subscribe, writableDerivation, writableDerivationAsync };