@ersbeth/picoflow 1.0.1 → 1.1.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 (183) hide show
  1. package/.cursor/plans/unifier-flowresource-avec-flowderivation-c9506e24.plan.md +372 -0
  2. package/README.md +17 -1
  3. package/biome.json +4 -1
  4. package/dist/picoflow.js +1155 -582
  5. package/dist/types/flow/base/flowDisposable.d.ts +67 -0
  6. package/dist/types/flow/base/flowDisposable.d.ts.map +1 -0
  7. package/dist/types/flow/base/flowEffect.d.ts +127 -0
  8. package/dist/types/flow/base/flowEffect.d.ts.map +1 -0
  9. package/dist/types/flow/base/flowGraph.d.ts +97 -0
  10. package/dist/types/flow/base/flowGraph.d.ts.map +1 -0
  11. package/dist/types/flow/base/flowSignal.d.ts +134 -0
  12. package/dist/types/flow/base/flowSignal.d.ts.map +1 -0
  13. package/dist/types/flow/base/flowTracker.d.ts +15 -0
  14. package/dist/types/flow/base/flowTracker.d.ts.map +1 -0
  15. package/dist/types/flow/base/index.d.ts +7 -0
  16. package/dist/types/flow/base/index.d.ts.map +1 -0
  17. package/dist/types/flow/base/utils.d.ts +20 -0
  18. package/dist/types/flow/base/utils.d.ts.map +1 -0
  19. package/dist/types/{advanced/array.d.ts → flow/collections/flowArray.d.ts} +50 -12
  20. package/dist/types/flow/collections/flowArray.d.ts.map +1 -0
  21. package/dist/types/flow/collections/flowMap.d.ts +224 -0
  22. package/dist/types/flow/collections/flowMap.d.ts.map +1 -0
  23. package/dist/types/flow/collections/index.d.ts +3 -0
  24. package/dist/types/flow/collections/index.d.ts.map +1 -0
  25. package/dist/types/flow/index.d.ts +4 -0
  26. package/dist/types/flow/index.d.ts.map +1 -0
  27. package/dist/types/flow/nodes/async/flowConstantAsync.d.ts +137 -0
  28. package/dist/types/flow/nodes/async/flowConstantAsync.d.ts.map +1 -0
  29. package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts +137 -0
  30. package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts.map +1 -0
  31. package/dist/types/flow/nodes/async/flowNodeAsync.d.ts +343 -0
  32. package/dist/types/flow/nodes/async/flowNodeAsync.d.ts.map +1 -0
  33. package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts +81 -0
  34. package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts.map +1 -0
  35. package/dist/types/flow/nodes/async/flowStateAsync.d.ts +111 -0
  36. package/dist/types/flow/nodes/async/flowStateAsync.d.ts.map +1 -0
  37. package/dist/types/flow/nodes/async/index.d.ts +6 -0
  38. package/dist/types/flow/nodes/async/index.d.ts.map +1 -0
  39. package/dist/types/flow/nodes/index.d.ts +3 -0
  40. package/dist/types/flow/nodes/index.d.ts.map +1 -0
  41. package/dist/types/flow/nodes/sync/flowConstant.d.ts +108 -0
  42. package/dist/types/flow/nodes/sync/flowConstant.d.ts.map +1 -0
  43. package/dist/types/flow/nodes/sync/flowDerivation.d.ts +100 -0
  44. package/dist/types/flow/nodes/sync/flowDerivation.d.ts.map +1 -0
  45. package/dist/types/flow/nodes/sync/flowNode.d.ts +314 -0
  46. package/dist/types/flow/nodes/sync/flowNode.d.ts.map +1 -0
  47. package/dist/types/flow/nodes/sync/flowReadonly.d.ts +57 -0
  48. package/dist/types/flow/nodes/sync/flowReadonly.d.ts.map +1 -0
  49. package/dist/types/flow/nodes/sync/flowState.d.ts +96 -0
  50. package/dist/types/flow/nodes/sync/flowState.d.ts.map +1 -0
  51. package/dist/types/flow/nodes/sync/index.d.ts +6 -0
  52. package/dist/types/flow/nodes/sync/index.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +2 -4
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/types/solid/converters.d.ts +34 -45
  56. package/dist/types/solid/converters.d.ts.map +1 -1
  57. package/dist/types/solid/index.d.ts +2 -2
  58. package/dist/types/solid/index.d.ts.map +1 -1
  59. package/dist/types/solid/primitives.d.ts +1 -0
  60. package/dist/types/solid/primitives.d.ts.map +1 -1
  61. package/docs/.vitepress/config.mts +1 -1
  62. package/docs/api/typedoc-sidebar.json +81 -1
  63. package/package.json +60 -58
  64. package/src/flow/base/flowDisposable.ts +71 -0
  65. package/src/flow/base/flowEffect.ts +171 -0
  66. package/src/flow/base/flowGraph.ts +288 -0
  67. package/src/flow/base/flowSignal.ts +207 -0
  68. package/src/flow/base/flowTracker.ts +17 -0
  69. package/src/flow/base/index.ts +6 -0
  70. package/src/flow/base/utils.ts +19 -0
  71. package/src/flow/collections/flowArray.ts +409 -0
  72. package/src/flow/collections/flowMap.ts +398 -0
  73. package/src/flow/collections/index.ts +2 -0
  74. package/src/flow/index.ts +3 -0
  75. package/src/flow/nodes/async/flowConstantAsync.ts +142 -0
  76. package/src/flow/nodes/async/flowDerivationAsync.ts +143 -0
  77. package/src/flow/nodes/async/flowNodeAsync.ts +474 -0
  78. package/src/flow/nodes/async/flowReadonlyAsync.ts +81 -0
  79. package/src/flow/nodes/async/flowStateAsync.ts +116 -0
  80. package/src/flow/nodes/async/index.ts +5 -0
  81. package/src/flow/nodes/await/advanced/index.ts +5 -0
  82. package/src/{advanced → flow/nodes/await/advanced}/resource.ts +37 -3
  83. package/src/{advanced → flow/nodes/await/advanced}/resourceAsync.ts +35 -3
  84. package/src/{advanced → flow/nodes/await/advanced}/stream.ts +40 -2
  85. package/src/{advanced → flow/nodes/await/advanced}/streamAsync.ts +38 -3
  86. package/src/flow/nodes/await/flowConstantAwait.ts +154 -0
  87. package/src/flow/nodes/await/flowDerivationAwait.ts +154 -0
  88. package/src/flow/nodes/await/flowNodeAwait.ts +508 -0
  89. package/src/flow/nodes/await/flowReadonlyAwait.ts +89 -0
  90. package/src/flow/nodes/await/flowStateAwait.ts +130 -0
  91. package/src/flow/nodes/await/index.ts +5 -0
  92. package/src/flow/nodes/index.ts +3 -0
  93. package/src/flow/nodes/sync/flowConstant.ts +111 -0
  94. package/src/flow/nodes/sync/flowDerivation.ts +105 -0
  95. package/src/flow/nodes/sync/flowNode.ts +439 -0
  96. package/src/flow/nodes/sync/flowReadonly.ts +57 -0
  97. package/src/flow/nodes/sync/flowState.ts +101 -0
  98. package/src/flow/nodes/sync/index.ts +5 -0
  99. package/src/index.ts +2 -47
  100. package/src/solid/converters.ts +60 -199
  101. package/src/solid/index.ts +2 -8
  102. package/src/solid/primitives.ts +4 -0
  103. package/test/base/flowEffect.test.ts +108 -0
  104. package/test/base/flowGraph.test.ts +485 -0
  105. package/test/base/flowSignal.test.ts +372 -0
  106. package/test/collections/flowArray.asyncStates.test.ts +1553 -0
  107. package/test/collections/flowArray.scalars.test.ts +1129 -0
  108. package/test/collections/flowArray.states.test.ts +1365 -0
  109. package/test/collections/flowMap.asyncStates.test.ts +1105 -0
  110. package/test/collections/flowMap.scalars.test.ts +877 -0
  111. package/test/collections/flowMap.states.test.ts +1097 -0
  112. package/test/nodes/async/flowConstantAsync.test.ts +860 -0
  113. package/test/nodes/async/flowDerivationAsync.test.ts +1517 -0
  114. package/test/nodes/async/flowStateAsync.test.ts +1387 -0
  115. package/test/{resource.test.ts → nodes/await/advanced/resource.test.ts} +21 -19
  116. package/test/{resourceAsync.test.ts → nodes/await/advanced/resourceAsync.test.ts} +3 -1
  117. package/test/{stream.test.ts → nodes/await/advanced/stream.test.ts} +30 -28
  118. package/test/{streamAsync.test.ts → nodes/await/advanced/streamAsync.test.ts} +16 -14
  119. package/test/nodes/await/flowConstantAwait.test.ts +643 -0
  120. package/test/nodes/await/flowDerivationAwait.test.ts +1583 -0
  121. package/test/nodes/await/flowStateAwait.test.ts +999 -0
  122. package/test/nodes/mixed/derivation.test.ts +1527 -0
  123. package/test/nodes/sync/flowConstant.test.ts +620 -0
  124. package/test/nodes/sync/flowDerivation.test.ts +1373 -0
  125. package/test/nodes/sync/flowState.test.ts +945 -0
  126. package/test/solid/converters.test.ts +721 -0
  127. package/test/solid/primitives.test.ts +1031 -0
  128. package/tsconfig.json +2 -1
  129. package/vitest.config.ts +7 -1
  130. package/IMPLEMENTATION_GUIDE.md +0 -1578
  131. package/dist/types/advanced/array.d.ts.map +0 -1
  132. package/dist/types/advanced/index.d.ts +0 -9
  133. package/dist/types/advanced/index.d.ts.map +0 -1
  134. package/dist/types/advanced/map.d.ts +0 -166
  135. package/dist/types/advanced/map.d.ts.map +0 -1
  136. package/dist/types/advanced/resource.d.ts +0 -78
  137. package/dist/types/advanced/resource.d.ts.map +0 -1
  138. package/dist/types/advanced/resourceAsync.d.ts +0 -56
  139. package/dist/types/advanced/resourceAsync.d.ts.map +0 -1
  140. package/dist/types/advanced/stream.d.ts +0 -117
  141. package/dist/types/advanced/stream.d.ts.map +0 -1
  142. package/dist/types/advanced/streamAsync.d.ts +0 -97
  143. package/dist/types/advanced/streamAsync.d.ts.map +0 -1
  144. package/dist/types/basic/constant.d.ts +0 -60
  145. package/dist/types/basic/constant.d.ts.map +0 -1
  146. package/dist/types/basic/derivation.d.ts +0 -89
  147. package/dist/types/basic/derivation.d.ts.map +0 -1
  148. package/dist/types/basic/disposable.d.ts +0 -82
  149. package/dist/types/basic/disposable.d.ts.map +0 -1
  150. package/dist/types/basic/effect.d.ts +0 -67
  151. package/dist/types/basic/effect.d.ts.map +0 -1
  152. package/dist/types/basic/index.d.ts +0 -10
  153. package/dist/types/basic/index.d.ts.map +0 -1
  154. package/dist/types/basic/observable.d.ts +0 -83
  155. package/dist/types/basic/observable.d.ts.map +0 -1
  156. package/dist/types/basic/signal.d.ts +0 -69
  157. package/dist/types/basic/signal.d.ts.map +0 -1
  158. package/dist/types/basic/state.d.ts +0 -47
  159. package/dist/types/basic/state.d.ts.map +0 -1
  160. package/dist/types/basic/trackingContext.d.ts +0 -33
  161. package/dist/types/basic/trackingContext.d.ts.map +0 -1
  162. package/dist/types/creators.d.ts +0 -340
  163. package/dist/types/creators.d.ts.map +0 -1
  164. package/src/advanced/array.ts +0 -222
  165. package/src/advanced/index.ts +0 -12
  166. package/src/advanced/map.ts +0 -193
  167. package/src/basic/constant.ts +0 -97
  168. package/src/basic/derivation.ts +0 -147
  169. package/src/basic/disposable.ts +0 -86
  170. package/src/basic/effect.ts +0 -104
  171. package/src/basic/index.ts +0 -9
  172. package/src/basic/observable.ts +0 -109
  173. package/src/basic/signal.ts +0 -145
  174. package/src/basic/state.ts +0 -60
  175. package/src/basic/trackingContext.ts +0 -45
  176. package/src/creators.ts +0 -395
  177. package/test/array.test.ts +0 -600
  178. package/test/constant.test.ts +0 -44
  179. package/test/derivation.test.ts +0 -539
  180. package/test/effect.test.ts +0 -29
  181. package/test/map.test.ts +0 -240
  182. package/test/signal.test.ts +0 -72
  183. package/test/state.test.ts +0 -212
package/dist/picoflow.js CHANGED
@@ -1,45 +1,230 @@
1
- import { createMemo, createResource, createSignal, onMount, onCleanup } from 'solid-js';
1
+ import { createSignal, createMemo, createResource, onMount, onCleanup } from 'solid-js';
2
2
 
3
- class TrackingContext {
4
- /** @internal */
5
- constructor(_owner) {
6
- this._owner = _owner;
3
+ function isDisposable(obj) {
4
+ return obj !== null && obj !== void 0 && typeof obj.dispose === "function";
5
+ }
6
+
7
+ class FlowGraph {
8
+ static _effectsQueue = [];
9
+ static _actionQueue = [];
10
+ static _processingActionQueue = false;
11
+ /**
12
+ * Resets all internal queues and processing state.
13
+ *
14
+ * @remarks
15
+ * Use this method primarily for testing scenarios where you need to ensure
16
+ * a clean state between test runs. It clears pending effects and queued
17
+ * operations.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * beforeEach(() => {
22
+ * FlowGraph.clear();
23
+ * });
24
+ * ```
25
+ *
26
+ * @public
27
+ */
28
+ static clear() {
29
+ FlowGraph._effectsQueue = [];
30
+ FlowGraph._actionQueue = [];
31
+ FlowGraph._processingActionQueue = false;
7
32
  }
8
33
  /**
9
- * Registers a dependency on the given signal.
10
- * @internal
34
+ * Queues a trigger notification for processing.
35
+ *
36
+ * @param notify - Function to call when the trigger is processed.
37
+ * @returns A promise that resolves after the trigger is processed.
38
+ *
39
+ * @remarks
40
+ * This method is used internally by reactive primitives to coordinate
41
+ * signal triggers. The promise resolves after all associated effects
42
+ * have been executed.
43
+ *
44
+ * @public
11
45
  */
12
- /** @internal */
13
- _registerDependency(signal) {
14
- this._owner._registerDependency(signal);
46
+ static requestTrigger(notify) {
47
+ const promiseWithResolvers = Promise.withResolvers();
48
+ FlowGraph._actionQueue.push({
49
+ ...promiseWithResolvers,
50
+ type: "trigger",
51
+ notify
52
+ });
53
+ FlowGraph._processActionQueue();
54
+ return promiseWithResolvers.promise;
55
+ }
56
+ /**
57
+ * Queues a read operation for processing.
58
+ *
59
+ * @param read - Function that performs the read operation.
60
+ * @returns A promise that resolves with the read value.
61
+ *
62
+ * @remarks
63
+ * This method is used internally by reactive primitives to coordinate
64
+ * read operations. The read function is executed and its result (or promise)
65
+ * is returned.
66
+ *
67
+ * @public
68
+ */
69
+ static requestRead(read) {
70
+ const promiseWithResolvers = Promise.withResolvers();
71
+ FlowGraph._actionQueue.push({
72
+ ...promiseWithResolvers,
73
+ type: "read",
74
+ read
75
+ });
76
+ FlowGraph._processActionQueue();
77
+ return promiseWithResolvers.promise;
78
+ }
79
+ /**
80
+ * Queues a write operation with update logic for processing.
81
+ *
82
+ * @param notify - Function to call if the update succeeds.
83
+ * @param update - Function that performs the update and returns whether
84
+ * it changed the value.
85
+ * @returns A promise that resolves after the write is processed.
86
+ *
87
+ * @remarks
88
+ * This method is used internally by reactive primitives to coordinate
89
+ * write operations. The update function is executed, and if it returns true
90
+ * (or a promise resolving to true), the notify function is called to
91
+ * trigger dependent effects.
92
+ *
93
+ * @public
94
+ */
95
+ static requestWrite(notify, update) {
96
+ const promiseWithResolvers = Promise.withResolvers();
97
+ FlowGraph._actionQueue.push({
98
+ ...promiseWithResolvers,
99
+ type: "write",
100
+ notify,
101
+ update
102
+ });
103
+ FlowGraph._processActionQueue();
104
+ return promiseWithResolvers.promise;
105
+ }
106
+ /**
107
+ * Queues effects for execution after the current operation completes.
108
+ *
109
+ * @param effects - Array of effects to queue for execution.
110
+ *
111
+ * @remarks
112
+ * This method is used internally by reactive primitives to schedule effect
113
+ * execution. Effects are executed after the current read/write/trigger
114
+ * operation completes, ensuring proper ordering of reactive updates.
115
+ *
116
+ * @public
117
+ */
118
+ static pushEffects(effects) {
119
+ FlowGraph._effectsQueue.push(...effects);
15
120
  }
121
+ /** @internal */
122
+ static _processActionQueue = async () => {
123
+ if (FlowGraph._processingActionQueue) return;
124
+ FlowGraph._processingActionQueue = true;
125
+ while (FlowGraph._actionQueue.length > 0) {
126
+ const actionRequest = FlowGraph._actionQueue.shift();
127
+ if (!actionRequest) break;
128
+ if (actionRequest.type === "read") {
129
+ try {
130
+ const read = actionRequest.read();
131
+ let value;
132
+ if (read instanceof Promise) {
133
+ value = await read;
134
+ } else {
135
+ value = read;
136
+ }
137
+ actionRequest.resolve(value);
138
+ } catch (error) {
139
+ const safeError = error instanceof Error ? error : new Error(String(error));
140
+ actionRequest.reject(safeError);
141
+ }
142
+ continue;
143
+ }
144
+ if (actionRequest.type === "write") {
145
+ try {
146
+ const updateResult = actionRequest.update();
147
+ let updated = false;
148
+ if (updateResult instanceof Promise) {
149
+ updated = await updateResult;
150
+ } else {
151
+ updated = updateResult;
152
+ }
153
+ if (!updated) {
154
+ actionRequest.resolve();
155
+ continue;
156
+ }
157
+ actionRequest.notify();
158
+ } catch (error) {
159
+ const safeError = error instanceof Error ? error : new Error(String(error));
160
+ actionRequest.reject(safeError);
161
+ continue;
162
+ }
163
+ }
164
+ if (actionRequest.type === "trigger") {
165
+ actionRequest.notify();
166
+ }
167
+ let effectError = null;
168
+ for (const effect of FlowGraph._effectsQueue) {
169
+ try {
170
+ await effect._requestExec();
171
+ } catch (error) {
172
+ effectError = error instanceof Error ? error : new Error(String(error));
173
+ break;
174
+ }
175
+ }
176
+ FlowGraph._effectsQueue = [];
177
+ if (effectError) {
178
+ actionRequest.reject(effectError);
179
+ } else {
180
+ actionRequest.resolve();
181
+ }
182
+ }
183
+ FlowGraph._processingActionQueue = false;
184
+ };
16
185
  }
17
186
 
18
187
  class FlowEffect {
188
+ _disposed = false;
189
+ _dependencies = /* @__PURE__ */ new Set();
190
+ _apply;
191
+ settled;
19
192
  /**
20
- * Creates a new FlowEffect.
193
+ * Creates a new effect and runs it once immediately.
21
194
  *
22
- * @param apply - A side-effect function that receives a tracking context to
23
- * access and register dependencies on reactive observables and signals.
195
+ * @param apply - Side-effect function receiving the tracking context (`t`).
196
+ * It can be sync or async (returning `Promise<void>`).
24
197
  *
25
198
  * @remarks
26
- * The provided function is executed immediately upon construction with a tracking context.
27
- * Use the context parameter to call `.get(t)` on observables you want to track, or `.pick()`
28
- * on observables you want to read without creating dependencies.
199
+ * Use `t` to opt-in to reactive tracking:
200
+ * - `observable.get(t)` / `signal.watch(t)` to re-run when they change
201
+ * - `observable.pick()` for reads that should not re-run the effect
202
+ *
203
+ * The effect schedules its first run during construction. Each subsequent
204
+ * change to a tracked dependency queues another run.
29
205
  *
30
206
  * @public
31
207
  */
32
208
  constructor(apply) {
33
- this._trackedContext = new TrackingContext(this);
34
209
  this._apply = apply;
35
- this._exec();
210
+ this.settled = FlowGraph.requestRead(() => this._exec());
36
211
  }
37
212
  /**
38
- * Disposes the effect, unregistering all its tracked dependencies.
213
+ * Stops the effect and detaches it from all tracked dependencies.
39
214
  *
40
215
  * @remarks
41
- * Once disposed, the effect must no longer be used. Trying to dispose an effect
42
- * that is already disposed will throw an error.
216
+ * Call this when the effect's work is no longer needed (e.g., component
217
+ * unmount, teardown of a feature). After disposal:
218
+ * - The effect will not re-run.
219
+ * - All dependency links are removed.
220
+ * - Calling `dispose()` again throws an error.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const fx = effect((t) => $signal.watch(t));
225
+ * // ... later
226
+ * fx.dispose();
227
+ * ```
43
228
  *
44
229
  * @public
45
230
  */
@@ -51,25 +236,19 @@ class FlowEffect {
51
236
  this._disposed = true;
52
237
  }
53
238
  /**
54
- * Indicates whether this effect has been disposed.
239
+ * Whether the effect has been disposed.
55
240
  *
56
- * @returns A boolean value that is true if the effect is disposed, false otherwise.
241
+ * @returns `true` once disposal has run; `false` while the effect is active.
57
242
  *
58
243
  * @public
59
244
  */
60
245
  get disposed() {
61
246
  return this._disposed;
62
247
  }
63
- /* INTERNAL ------------------------------------------------------------ */
64
- _disposed = false;
65
- _dependencies = /* @__PURE__ */ new Set();
66
- _trackedContext;
67
- _apply;
68
248
  /** @internal */
69
- _exec() {
70
- if (this._disposed)
71
- throw new Error("[PicoFlow] Effect is disposed");
72
- this._apply(this._trackedContext);
249
+ async _requestExec() {
250
+ this.settled = this._exec();
251
+ return this.settled;
73
252
  }
74
253
  /** @internal */
75
254
  _registerDependency(dependency) {
@@ -81,61 +260,95 @@ class FlowEffect {
81
260
  this._dependencies.delete(dependency);
82
261
  dependency._unregisterEffect(this);
83
262
  }
263
+ async _exec() {
264
+ if (this._disposed)
265
+ throw new Error("[PicoFlow] Effect is disposed");
266
+ const result = this._apply(this);
267
+ if (result instanceof Promise) {
268
+ await result;
269
+ }
270
+ }
271
+ }
272
+ function effect(fn) {
273
+ return new FlowEffect(fn);
84
274
  }
85
275
 
86
276
  class FlowSignal {
87
277
  /**
88
- * Triggers the FlowSignal.
89
- * Notifies all registered listeners and schedules execution of associated effects.
90
- * @throws If the FlowSignal has already been disposed.
278
+ * Triggers the signal and notifies all dependents.
279
+ *
280
+ * @remarks
281
+ * Any effect/derivation that called `watch(t)` on this signal will re-run.
282
+ * Returns a promise that settles after notifications complete.
283
+ *
284
+ * @throws Error if the signal is disposed.
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * const $tick = signal();
289
+ *
290
+ * effect((t) => {
291
+ * $tick.watch(t);
292
+ * console.log("tick");
293
+ * });
294
+ *
295
+ * $tick.trigger(); // logs "tick"
296
+ * ```
297
+ *
91
298
  * @public
92
299
  */
93
- trigger() {
300
+ async trigger() {
94
301
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
95
- this._notify();
302
+ return FlowGraph.requestTrigger(() => this._notify());
96
303
  }
97
304
  /**
98
- * Watches the signal, registering it as a dependency in the tracking context.
305
+ * Registers this signal as a dependency in the current tracking context.
99
306
  *
100
- * @param context - The tracking context in which to register this signal as a dependency.
307
+ * @param context - The tracking context (`t`) provided to effects/derivations.
101
308
  *
102
309
  * @remarks
103
- * Use `watch()` when you want to track a signal without reading its value (signals don't
104
- * have values to read). This is useful for triggering effects based on signal events
105
- * without needing associated data.
106
- *
107
- * When the signal is triggered via `trigger()`, any effects or derivations that have
108
- * watched this signal will automatically re-execute.
109
- *
110
- * This method must be called within an effect or derivation context where a TrackingContext
111
- * is available. For observables (which hold values), use `.get(t)` instead, which both
112
- * reads the value and watches for changes.
310
+ * Signals have no value to read; calling `watch(t)` simply means “re-run me
311
+ * when this signal is triggered.” Call this inside an effect/derivation
312
+ * callback where a tracking context is available.
113
313
  *
114
314
  * @throws Error if the signal has been disposed.
115
315
  *
116
316
  * @example
117
317
  * ```typescript
118
- * const $signal = signal();
318
+ * const $refresh = signal();
119
319
  *
120
320
  * effect((t) => {
121
- * $signal.watch(t); // Track the signal
122
- * console.log('Signal triggered!');
321
+ * $refresh.watch(t);
322
+ * console.log("refresh triggered");
123
323
  * });
124
324
  *
125
- * $signal.trigger(); // Logs: "Signal triggered!"
325
+ * $refresh.trigger(); // logs "refresh triggered"
126
326
  * ```
127
327
  *
128
328
  * @public
129
329
  */
130
- watch(context) {
330
+ watch(caller) {
131
331
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
132
- context._registerDependency(this);
332
+ caller._registerDependency(this);
133
333
  }
134
334
  /**
135
- * Disposes the FlowSignal.
136
- * Cleans up all registered effects, listeners, and dependencies.
137
- * Once disposed, further usage of the signal will throw an error.
138
- * @throws If the FlowSignal is already disposed.
335
+ * Disposes the signal and cleans up its dependencies/listeners.
336
+ *
337
+ * @remarks
338
+ * After disposal the signal must not be used; calling `trigger` or `watch`
339
+ * will throw. If `options?.self` is true, only this signal is disposed; when
340
+ * false or omitted, dependents may also be disposed depending on the
341
+ * implementation.
342
+ *
343
+ * @throws Error if the signal is already disposed.
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * const $refresh = signal();
348
+ * // ... use it
349
+ * $refresh.dispose();
350
+ * ```
351
+ *
139
352
  * @public
140
353
  */
141
354
  dispose(options) {
@@ -161,30 +374,27 @@ class FlowSignal {
161
374
  this._disposed = true;
162
375
  }
163
376
  /**
164
- * Indicates whether the FlowSignal has been disposed.
165
- * @remarks Once disposed, the signal should not be used.
377
+ * Whether the signal has been disposed.
378
+ *
379
+ * @returns `true` if disposed; `false` while active.
380
+ *
381
+ * @remarks Use to guard operations or avoid double disposal.
382
+ *
166
383
  * @public
167
384
  */
168
385
  get disposed() {
169
386
  return this._disposed;
170
387
  }
171
388
  /* INTERNAL ------------------------------------------------------------- */
172
- /** @internal */
173
389
  _disposed = false;
174
- /** @internal */
175
390
  _dependencies = /* @__PURE__ */ new Set();
176
- /** @internal */
177
391
  _listeners = /* @__PURE__ */ new Set();
178
- /** @internal */
179
392
  _effects = /* @__PURE__ */ new Set();
180
- /** @internal */
181
393
  _notify() {
394
+ FlowGraph.pushEffects(Array.from(this._effects));
182
395
  this._listeners.forEach((listener) => {
183
396
  listener._notify();
184
397
  });
185
- this._effects.forEach((effect) => {
186
- effect._exec();
187
- });
188
398
  }
189
399
  /** @internal */
190
400
  _registerDependency(dependency) {
@@ -197,12 +407,12 @@ class FlowSignal {
197
407
  dependency._unregisterListener(this);
198
408
  }
199
409
  /** @internal */
200
- _registerListener(signal) {
201
- this._listeners.add(signal);
410
+ _registerListener(signal2) {
411
+ this._listeners.add(signal2);
202
412
  }
203
413
  /** @internal */
204
- _unregisterListener(signal) {
205
- this._listeners.delete(signal);
414
+ _unregisterListener(signal2) {
415
+ this._listeners.delete(signal2);
206
416
  }
207
417
  /** @internal */
208
418
  _registerEffect(effect) {
@@ -213,285 +423,715 @@ class FlowSignal {
213
423
  this._effects.delete(effect);
214
424
  }
215
425
  }
426
+ function signal() {
427
+ return new FlowSignal();
428
+ }
216
429
 
217
- class FlowObservable extends FlowSignal {
430
+ class FlowNodeAsync extends FlowSignal {
431
+ _promise;
432
+ _dirty = true;
433
+ _compute;
218
434
  /**
219
- * Gets the current value with optional dependency tracking.
435
+ * Creates a new FlowNodeAsync.
220
436
  *
221
- * @param context - The tracking context for reactive tracking, or null for untracked access.
222
- * When a context is provided, this observable is registered as a dependency. When null,
223
- * the value is read without any tracking.
224
- *
225
- * @returns The current value of type T.
437
+ * @param compute - Either a constant Promise or a compute function that derives the value.
226
438
  *
227
439
  * @remarks
228
- * Use `get(t)` within effects and derivations to create reactive dependencies.
229
- * Use `get(null)` when you need to read a value without tracking (though `pick()` is more idiomatic).
440
+ * The constructor accepts two different initialization modes:
441
+ *
442
+ * - **Constant Promise**: Creates a mutable reactive node that can be updated via `set()`.
443
+ * The Promise is stored immediately and can be changed at any time. The Promise resolves
444
+ * to the value of type T.
445
+ *
446
+ * - **Compute function**: Creates a computed reactive node that automatically tracks dependencies
447
+ * and recomputes when they change. The compute function receives a tracking context (`t`)
448
+ * that should be used to access dependencies via `.get(t)`. The function must return a
449
+ * `Promise<T>`. The function is not executed immediately; it runs lazily on first access.
450
+ * The computed value can be temporarily overridden using `set()`, but the override is cleared
451
+ * on the next recomputation (triggered by dependency changes).
230
452
  *
231
453
  * @example
232
454
  * ```typescript
233
- * effect((t) => {
234
- * const tracked = $state.get(t); // Dependency registered
235
- * const untracked = $other.get(null); // No dependency
455
+ * // Mutable state with constant Promise
456
+ * const $count = new FlowNodeAsync(Promise.resolve(0));
457
+ * await $count.set(Promise.resolve(5));
458
+ *
459
+ * // Computed derivation with async function
460
+ * const $a = new FlowNodeAsync(Promise.resolve(10));
461
+ * const $b = new FlowNodeAsync(Promise.resolve(20));
462
+ * const $sum = new FlowNodeAsync(async (t) => {
463
+ * const a = await $a.get(t);
464
+ * const b = await $b.get(t);
465
+ * return a + b;
236
466
  * });
467
+ *
468
+ * // Lazy evaluation - compute function hasn't run yet
469
+ * console.log(await $sum.pick()); // Now it computes: 30
237
470
  * ```
238
471
  *
239
472
  * @public
240
473
  */
241
- get(context) {
242
- if (context) {
243
- this.watch(context);
474
+ constructor(compute) {
475
+ super();
476
+ if (typeof compute === "function") {
477
+ this._compute = compute;
478
+ } else {
479
+ this._promise = compute;
480
+ this._dirty = false;
244
481
  }
245
- return this._getRaw();
482
+ }
483
+ get settled() {
484
+ return this._promise;
485
+ }
486
+ async watch(tracker) {
487
+ if (this._disposed)
488
+ return Promise.reject(new Error("[PicoFlow] Primitive is disposed"));
489
+ super.watch(tracker);
490
+ return this._computeValue();
491
+ }
492
+ /**
493
+ * Gets the current value with dependency tracking.
494
+ *
495
+ * @param tracker - The tracking context for reactive tracking. This observable is registered
496
+ * as a dependency when accessed through this method.
497
+ *
498
+ * @returns A Promise that resolves to the current value of type T.
499
+ *
500
+ * @remarks
501
+ * Use `get(t)` within effects and derivations to create reactive dependencies. The tracker
502
+ * parameter must be provided from the reactive context (typically the `t` parameter in effect
503
+ * or derivation callbacks).
504
+ *
505
+ * **Important:** This method returns a `Promise<T>`, not `T`. You must await the Promise to
506
+ * get the actual value. The Promise is cached until dependencies change, ensuring efficient
507
+ * access patterns.
508
+ *
509
+ * To read a value without creating a dependency, use `pick()` instead.
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * effect(async (t) => {
514
+ * const tracked = await $state.get(t); // Dependency registered, await the Promise
515
+ * const untracked = await $other.pick(); // No dependency
516
+ * });
517
+ * ```
518
+ *
519
+ * @throws Error if the node has been disposed.
520
+ *
521
+ * @public
522
+ */
523
+ async get(tracker) {
524
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
525
+ await this._computeValue();
526
+ if (tracker) super.watch(tracker);
527
+ return this._promise;
528
+ }
529
+ /**
530
+ * Updates the node with a new value.
531
+ *
532
+ * @param value - A new Promise or a callback function that computes a new Promise based on the current value.
533
+ *
534
+ * @returns A promise that resolves after the update is processed and all dependent effects have been notified.
535
+ *
536
+ * @remarks
537
+ * This method can be used in two ways:
538
+ *
539
+ * **For mutable state nodes** (constructed with a constant Promise):
540
+ * - Updates the stored Promise directly
541
+ * - All dependents are notified of the change
542
+ *
543
+ * **For computed nodes** (constructed with a compute function):
544
+ * - Temporarily overrides the computed value
545
+ * - The override persists until the next recomputation, which occurs when:
546
+ * - A tracked dependency changes, or
547
+ * - The node is refreshed
548
+ * - This allows temporarily overriding computed values for testing or manual control
549
+ *
550
+ * **Value Comparison:**
551
+ * The Promises are resolved and their values are compared. If the resolved new value is strictly
552
+ * equal (`===`) to the current resolved value, no update occurs and subscribers are not notified.
553
+ * This prevents unnecessary re-renders and effect executions.
554
+ *
555
+ * **Asynchronous Processing:**
556
+ * The update is processed asynchronously through the reactive graph, ensuring proper
557
+ * ordering of updates and effect execution. The method returns a Promise that resolves
558
+ * after the update is complete.
559
+ *
560
+ * @throws Error if the node has been disposed.
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * // Mutable state usage
565
+ * const $count = new FlowNodeAsync(Promise.resolve(0));
566
+ * await $count.set(Promise.resolve(5));
567
+ * await $count.set(async (current) => Promise.resolve(current + 1)); // 6
568
+ *
569
+ * // Temporary override of computed value
570
+ * const $source = new FlowNodeAsync(Promise.resolve(10));
571
+ * const $doubled = new FlowNodeAsync(async (t) => {
572
+ * const val = await $source.get(t);
573
+ * return val * 2;
574
+ * });
575
+ *
576
+ * console.log(await $doubled.pick()); // 20
577
+ *
578
+ * // Temporarily override
579
+ * await $doubled.set(Promise.resolve(50));
580
+ * console.log(await $doubled.pick()); // 50 (override active)
581
+ *
582
+ * // Dependency change clears override
583
+ * await $source.set(Promise.resolve(15));
584
+ * console.log(await $doubled.pick()); // 30 (recomputed, override cleared)
585
+ * ```
586
+ *
587
+ * @public
588
+ */
589
+ async set(value) {
590
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
591
+ const update = async () => {
592
+ await this._computeValue();
593
+ const currentValue = await this._promise;
594
+ const nextPromise = typeof value === "function" ? value(currentValue) : value;
595
+ const nextValue = await nextPromise;
596
+ this._promise = nextPromise;
597
+ return nextValue !== currentValue;
598
+ };
599
+ const notify = () => {
600
+ super._notify();
601
+ };
602
+ return FlowGraph.requestWrite(notify, update);
603
+ }
604
+ /**
605
+ * Forces recomputation of the value, even if it's not marked as dirty.
606
+ *
607
+ * @returns A promise that resolves after the recomputation is complete and all
608
+ * dependent effects have been notified.
609
+ *
610
+ * @remarks
611
+ * This method is useful when you need to force a recomputation of a computed value,
612
+ * for example when the computation depends on external data that has changed outside
613
+ * the reactive system.
614
+ *
615
+ * **Behavior:**
616
+ * - For nodes with a compute function: Forces the compute function to run again,
617
+ * even if no dependencies have changed. This effectively clears any temporary
618
+ * override that was set using `set()`.
619
+ * - For nodes without a compute function (mutable state): Recomputes the current
620
+ * value (which is just the stored value), useful for consistency but typically
621
+ * not necessary.
622
+ *
623
+ * The recomputation happens asynchronously through the reactive graph, ensuring
624
+ * proper ordering of updates and effect execution.
625
+ *
626
+ * @throws Error if the node has been disposed.
627
+ *
628
+ * @example
629
+ * ```typescript
630
+ * const $externalData = new FlowNode(() => fetchExternalData());
631
+ *
632
+ * // Some external event occurs that changes the data source
633
+ * externalDataChanged();
634
+ *
635
+ * // Force recomputation to get the new value
636
+ * await $externalData.refresh();
637
+ *
638
+ * // For computed nodes with temporary overrides
639
+ * const $computed = new FlowNode((t) => $source.get(t) * 2);
640
+ * $computed.set(100); // Temporary override
641
+ * await $computed.refresh(); // Clears override, recomputes from source
642
+ * ```
643
+ *
644
+ * @public
645
+ */
646
+ async refresh() {
647
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
648
+ const update = async () => {
649
+ await this._computeValue();
650
+ const currentValue = await this._promise;
651
+ await this._computeValue({ force: true });
652
+ const nextValue = await this._promise;
653
+ return nextValue !== currentValue;
654
+ };
655
+ const notify = () => {
656
+ super._notify();
657
+ };
658
+ return await FlowGraph.requestWrite(notify, update);
246
659
  }
247
660
  /**
248
661
  * Gets the current value without any dependency tracking.
249
662
  *
250
- * @returns The current value of type T.
663
+ * @returns A promise that resolves with the current value of type T.
251
664
  *
252
665
  * @remarks
253
- * This method is equivalent to calling `get(null)` but provides a more semantic and readable API.
666
+ * This method reads the value asynchronously through the reactive graph, ensuring proper ordering of
667
+ * read operations. Unlike `get(t)`, this method does not create a reactive dependency.
668
+ *
669
+ * **Important:** This is an async method that returns `Promise<T>`. Always use `await` when calling it.
670
+ *
254
671
  * Use `pick()` when you want to read a snapshot of the current value without creating a reactive
255
672
  * dependency. This is useful for:
256
- * - Reading initial values
673
+ * - Reading initial values outside reactive contexts
257
674
  * - Accessing configuration that shouldn't trigger updates
258
675
  * - Mixing tracked and untracked reads in the same effect
259
676
  *
260
677
  * @example
261
678
  * ```typescript
262
679
  * // Read a snapshot outside reactive context
263
- * const currentValue = $state.pick();
680
+ * const currentValue = await $state.pick();
264
681
  *
265
682
  * // Mix tracked and untracked reads
266
- * effect((t) => {
267
- * const tracked = $reactive.get(t); // Triggers re-runs
268
- * const snapshot = $config.pick(); // Doesn't trigger re-runs
683
+ * effect(async (t) => {
684
+ * const tracked = await $reactive.get(t); // Triggers re-runs
685
+ * const snapshot = await $config.pick(); // Doesn't trigger re-runs
269
686
  * processData(tracked, snapshot);
270
687
  * });
271
688
  * ```
272
689
  *
690
+ * @throws Error if the node has been disposed.
691
+ *
273
692
  * @public
274
693
  */
275
- pick() {
276
- return this._getRaw();
694
+ async pick() {
695
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
696
+ await FlowGraph.requestRead(() => this._computeValue());
697
+ return this._promise;
277
698
  }
278
- /* INTERNAL -------------------------------------------*/
279
- /** @internal */
280
- _value;
281
699
  /**
282
- * Subscribes a listener function to changes of the observable.
283
- * The listener is executed immediately with the current value and on subsequent updates.
284
- * @param listener - A callback function that receives the new value.
285
- * @returns A disposer function to cancel the subscription.
700
+ * Subscribes a listener function to changes of this node.
701
+ *
702
+ * @param listener - A callback function that receives the new value whenever it changes.
703
+ *
704
+ * @returns A disposer function that cancels the subscription when called.
705
+ *
706
+ * @remarks
707
+ * This method creates a reactive subscription that automatically tracks this node as a dependency.
708
+ * The listener is executed:
709
+ * - Immediately with the current value when the subscription is created
710
+ * - Automatically whenever the value changes
711
+ *
712
+ * **Important:** The listener receives a `Promise<T>`, not `T`. You must await the Promise
713
+ * within the listener to access the actual value. The Promise is the same one returned by `get()`,
714
+ * so it's cached until dependencies change.
715
+ *
716
+ * The subscription uses a {@link FlowEffect} internally to manage the reactive tracking and
717
+ * automatic re-execution. When the value changes, the listener is called with the new Promise
718
+ * after the update is processed through the reactive graph.
719
+ *
720
+ * **Cleanup:**
721
+ * Always call the returned disposer function when you no longer need the subscription to
722
+ * prevent memory leaks and unnecessary computations.
723
+ *
724
+ * @example
725
+ * ```typescript
726
+ * const $count = new FlowNodeAsync(Promise.resolve(0));
727
+ *
728
+ * // Subscribe to changes
729
+ * const unsubscribe = $count.subscribe(async (valuePromise) => {
730
+ * const value = await valuePromise;
731
+ * console.log(`Count is now: ${value}`);
732
+ * });
733
+ * // Logs immediately: "Count is now: 0"
734
+ *
735
+ * await $count.set(Promise.resolve(5));
736
+ * // Logs: "Count is now: 5"
737
+ *
738
+ * // Clean up when done
739
+ * unsubscribe();
740
+ * ```
741
+ *
742
+ * @throws Error if the node has been disposed.
743
+ *
744
+ * @public
286
745
  */
287
746
  subscribe(listener) {
747
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
288
748
  const effect = new FlowEffect((t) => {
289
749
  listener(this.get(t));
290
750
  });
291
751
  return () => effect.dispose();
292
752
  }
753
+ /* INTERNAL --------------------------------------------------------- */
754
+ /* @internal */
755
+ _notify() {
756
+ if (this._dirty) {
757
+ return;
758
+ }
759
+ this._dirty = true;
760
+ super._notify();
761
+ }
762
+ async _computeValue(options) {
763
+ if (!this._dirty && !options?.force) return;
764
+ if (!this._compute) {
765
+ this._dirty = false;
766
+ return;
767
+ }
768
+ this._dirty = false;
769
+ const dependencies = [...this._dependencies];
770
+ this._dependencies.clear();
771
+ this._promise = this._compute(this);
772
+ await this._promise;
773
+ const dependenciesToRemove = dependencies.filter(
774
+ (dependency) => !this._dependencies.has(dependency)
775
+ );
776
+ dependenciesToRemove.forEach((dependency) => {
777
+ dependency._unregisterDependency(this);
778
+ });
779
+ }
293
780
  }
294
781
 
295
- class FlowConstant extends FlowObservable {
782
+ function constantAsync(value) {
783
+ return new FlowNodeAsync(value);
784
+ }
785
+
786
+ function derivationAsync(fn) {
787
+ return new FlowNodeAsync(fn);
788
+ }
789
+
790
+ function stateAsync(value) {
791
+ return new FlowNodeAsync(value);
792
+ }
793
+
794
+ class FlowNode extends FlowSignal {
795
+ _value;
796
+ _dirty = true;
797
+ _compute;
296
798
  /**
297
- * Creates a new FlowConstant instance.
799
+ * Creates a new FlowNode.
800
+ *
801
+ * @param compute - Either a constant value or a compute function that derives the value.
802
+ *
803
+ * @remarks
804
+ * The constructor accepts two different initialization modes:
805
+ *
806
+ * - **Constant value**: Creates a mutable reactive node that can be updated via `set()`.
807
+ * The value is stored immediately and can be changed at any time.
808
+ *
809
+ * - **Compute function**: Creates a computed reactive node that automatically tracks dependencies
810
+ * and recomputes when they change. The compute function receives a tracking context (`t`)
811
+ * that should be used to access dependencies via `.get(t)`. The function is not executed
812
+ * immediately; it runs lazily on first access. The computed value can be temporarily
813
+ * overridden using `set()`, but the override is cleared on the next recomputation
814
+ * (triggered by dependency changes or `refresh()`).
815
+ *
816
+ * @example
817
+ * ```typescript
818
+ * // Mutable state with constant value
819
+ * const $count = new FlowNode(0);
820
+ * $count.set(5);
821
+ *
822
+ * // Computed derivation with function
823
+ * const $a = new FlowNode(10);
824
+ * const $b = new FlowNode(20);
825
+ * const $sum = new FlowNode((t) => $a.get(t) + $b.get(t));
298
826
  *
299
- * @param value - Either a direct value of type T or a function returning a value of type T.
300
- * If a function is provided, it will be invoked lazily on the first value access.
301
- * If a direct value is provided, it is stored immediately.
827
+ * // Lazy evaluation - compute function hasn't run yet
828
+ * console.log(await $sum.pick()); // Now it computes: 30
829
+ * ```
302
830
  *
303
831
  * @public
304
832
  */
305
- constructor(value) {
833
+ constructor(compute) {
306
834
  super();
307
- this._initEager(value);
835
+ if (typeof compute === "function") {
836
+ this._compute = compute;
837
+ } else {
838
+ this._value = compute;
839
+ this._dirty = false;
840
+ }
841
+ }
842
+ watch(tracker) {
843
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
844
+ this._computeValue();
845
+ super.watch(tracker);
308
846
  }
309
847
  /**
310
- * Internal method to get the raw value.
311
- * @internal
848
+ * Gets the current value with dependency tracking.
849
+ *
850
+ * @param tracker - The tracking context for reactive tracking. This observable is registered
851
+ * as a dependency when accessed through this method.
852
+ *
853
+ * @returns The current value of type T.
854
+ *
855
+ * @remarks
856
+ * Use `get(t)` within effects and derivations to create reactive dependencies. The tracker
857
+ * parameter must be provided from the reactive context (typically the `t` parameter in effect
858
+ * or derivation callbacks).
859
+ *
860
+ * To read a value without creating a dependency, use `pick()` instead.
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * effect((t) => {
865
+ * const tracked = $state.get(t); // Dependency registered
866
+ * const untracked = await $other.pick(); // No dependency
867
+ * });
868
+ * ```
869
+ *
870
+ * @throws Error if the node has been disposed.
871
+ *
872
+ * @public
312
873
  */
313
- _getRaw() {
874
+ get(tracker) {
314
875
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
315
- this._initLazy();
876
+ this._computeValue();
877
+ if (tracker) super.watch(tracker);
316
878
  return this._value;
317
879
  }
318
- /* INTERNAL --------------------------------------------------------- */
319
- /** @internal */
320
- _initialized = false;
321
- /** @internal */
322
- _init;
323
- /** @internal */
324
- _initEager(value) {
325
- if (typeof value === "function") {
326
- this._init = value;
327
- } else {
328
- this._value = value;
329
- this._initialized = true;
330
- }
331
- }
332
- /** @internal */
333
- _initLazy() {
334
- if (!this._initialized && this._init) {
335
- this._value = this._init();
336
- this._initialized = true;
337
- }
338
- if (!this._initialized)
339
- throw new Error("[PicoFlow] Primitive can't be initialized");
880
+ /**
881
+ * Updates the node with a new value.
882
+ *
883
+ * @param value - A new value or a callback function that computes a new value based on the current value.
884
+ *
885
+ * @returns A promise that resolves after the update is processed and all dependent effects have been notified.
886
+ *
887
+ * @remarks
888
+ * This method can be used in two ways:
889
+ *
890
+ * **For mutable state nodes** (constructed with a constant value):
891
+ * - Updates the stored value directly
892
+ * - All dependents are notified of the change
893
+ *
894
+ * **For computed nodes** (constructed with a compute function):
895
+ * - Temporarily overrides the computed value
896
+ * - The override persists until the next recomputation, which occurs when:
897
+ * - A tracked dependency changes, or
898
+ * - `refresh()` is called
899
+ * - This allows temporarily overriding computed values for testing or manual control
900
+ *
901
+ * **Value Comparison:**
902
+ * If the new value is strictly equal (`===`) to the current value, no update occurs
903
+ * and subscribers are not notified. This prevents unnecessary re-renders and effect
904
+ * executions.
905
+ *
906
+ * **Asynchronous Processing:**
907
+ * The update is processed asynchronously through the reactive graph, ensuring proper
908
+ * ordering of updates and effect execution.
909
+ *
910
+ * @throws Error if the node has been disposed.
911
+ *
912
+ * @example
913
+ * ```typescript
914
+ * // Mutable state usage
915
+ * const $count = new FlowNode(0);
916
+ * await $count.set(5);
917
+ * await $count.set(current => current + 1); // 6
918
+ *
919
+ * // Temporary override of computed value
920
+ * const $source = new FlowNode(10);
921
+ * const $doubled = new FlowNode((t) => $source.get(t) * 2);
922
+ *
923
+ * console.log(await $doubled.pick()); // 20
924
+ *
925
+ * // Temporarily override
926
+ * await $doubled.set(50);
927
+ * console.log(await $doubled.pick()); // 50 (override active)
928
+ *
929
+ * // Dependency change clears override
930
+ * await $source.set(15);
931
+ * console.log(await $doubled.pick()); // 30 (recomputed, override cleared)
932
+ * ```
933
+ *
934
+ * @public
935
+ */
936
+ set(value) {
937
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
938
+ const update = () => {
939
+ this._computeValue();
940
+ const currentValue = this._value;
941
+ const next = typeof value === "function" ? value(currentValue) : value;
942
+ this._value = next;
943
+ return next !== currentValue;
944
+ };
945
+ const notify = () => {
946
+ super._notify();
947
+ };
948
+ return FlowGraph.requestWrite(notify, update);
340
949
  }
341
- }
342
-
343
- class FlowDerivation extends FlowObservable {
344
950
  /**
345
- * Creates a new FlowDerivation.
951
+ * Forces recomputation of the value, even if it's not marked as dirty.
952
+ *
953
+ * @returns A promise that resolves after the recomputation is complete and all
954
+ * dependent effects have been notified.
955
+ *
956
+ * @remarks
957
+ * This method is useful when you need to force a recomputation of a computed value,
958
+ * for example when the computation depends on external data that has changed outside
959
+ * the reactive system.
960
+ *
961
+ * **Behavior:**
962
+ * - For nodes with a compute function: Forces the compute function to run again,
963
+ * even if no dependencies have changed. This effectively clears any temporary
964
+ * override that was set using `set()`.
965
+ * - For nodes without a compute function (mutable state): Recomputes the current
966
+ * value (which is just the stored value), useful for consistency but typically
967
+ * not necessary.
968
+ *
969
+ * The recomputation happens asynchronously through the reactive graph, ensuring
970
+ * proper ordering of updates and effect execution.
971
+ *
972
+ * @throws Error if the node has been disposed.
973
+ *
974
+ * @example
975
+ * ```typescript
976
+ * const $externalData = new FlowNode(() => fetchExternalData());
977
+ *
978
+ * // Some external event occurs that changes the data source
979
+ * externalDataChanged();
980
+ *
981
+ * // Force recomputation to get the new value
982
+ * await $externalData.refresh();
346
983
  *
347
- * @param compute - A function that computes the derived value using a tracking context.
348
- * The function receives a TrackingContext and should use it to access dependencies via
349
- * `.get(t)`. The function is not executed immediately; it runs lazily on first access.
984
+ * // For computed nodes with temporary overrides
985
+ * const $computed = new FlowNode((t) => $source.get(t) * 2);
986
+ * $computed.set(100); // Temporary override
987
+ * await $computed.refresh(); // Clears override, recomputes from source
988
+ * ```
350
989
  *
351
990
  * @public
352
991
  */
353
- constructor(compute) {
354
- super();
355
- this._compute = compute;
356
- this._trackedContext = new TrackingContext(this);
357
- }
358
- /**
359
- * Internal method to get the raw value.
360
- * @internal
361
- */
362
- _getRaw() {
992
+ async refresh() {
363
993
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
364
- this._initLazy();
365
- this._update();
366
- return this._value;
367
- }
368
- /* INTERNAL --------------------------------------------------------- */
369
- _initialized = false;
370
- _dirty = false;
371
- _compute;
372
- _trackedContext;
373
- _initLazy() {
374
- if (!this._initialized) {
375
- this._value = this._compute(this._trackedContext);
376
- this._initialized = true;
377
- }
378
- }
379
- /* @internal */
380
- _update() {
381
- if (this._dirty) {
382
- const dependencies = [...this._dependencies];
383
- this._dependencies.clear();
384
- this._value = this._compute(this._trackedContext);
385
- const dependenciesToRemove = dependencies.filter(
386
- (dependency) => !this._dependencies.has(dependency)
387
- );
388
- dependenciesToRemove.forEach((dependency) => {
389
- dependency._unregisterDependency(this);
390
- });
391
- this._dirty = false;
392
- }
393
- }
394
- /* @internal */
395
- _notify() {
396
- this._dirty = true;
397
- super._notify();
994
+ const update = () => {
995
+ this._computeValue();
996
+ const currentValue = this._value;
997
+ this._computeValue({ force: true });
998
+ const nextValue = this._value;
999
+ return nextValue !== currentValue;
1000
+ };
1001
+ const notify = () => {
1002
+ super._notify();
1003
+ };
1004
+ return await FlowGraph.requestWrite(notify, update);
398
1005
  }
399
1006
  /**
400
- * Watches the derivation, registering it as a dependency in the given context.
1007
+ * Gets the current value without any dependency tracking.
401
1008
  *
402
- * @param context - The tracking context in which to register this derivation.
1009
+ * @returns A promise that resolves with the current value of type T.
403
1010
  *
404
1011
  * @remarks
405
- * This method overrides the base `watch()` to handle a special case: when a derivation
406
- * is watched without having its value read (e.g., `$derivation.watch(t)` instead of
407
- * `$derivation.get(t)`), the derivation still needs to compute its value to establish
408
- * its own dependencies. Otherwise, the derivation would be tracked but wouldn't track
409
- * its own dependencies, breaking the reactive chain.
1012
+ * This method reads the value asynchronously through the reactive graph, ensuring proper ordering of
1013
+ * read operations. Unlike `get(t)`, this method does not create a reactive dependency.
1014
+ *
1015
+ * Use `pick()` when you want to read a snapshot of the current value without creating a reactive
1016
+ * dependency. This is useful for:
1017
+ * - Reading initial values outside reactive contexts
1018
+ * - Accessing configuration that shouldn't trigger updates
1019
+ * - Mixing tracked and untracked reads in the same effect
410
1020
  *
411
- * This override ensures that:
412
- * 1. The derivation is registered as a dependency in the provided context
413
- * 2. The derivation computes its value (if not already computed)
414
- * 3. The derivation tracks its own dependencies during computation
1021
+ * **Note:** This is an async method. Always use `await` when calling it.
415
1022
  *
416
1023
  * @example
417
1024
  * ```typescript
418
- * const $derived = derivation((t) => $state.get(t) * 2);
1025
+ * // Read a snapshot outside reactive context
1026
+ * const currentValue = await $state.pick();
419
1027
  *
420
- * effect((t) => {
421
- * $derived.watch(t); // Derivation computes even without reading value
422
- * doSomething(); // Effect re-runs when $derived's dependencies change
1028
+ * // Mix tracked and untracked reads
1029
+ * effect(async (t) => {
1030
+ * const tracked = $reactive.get(t); // Triggers re-runs
1031
+ * const snapshot = await $config.pick(); // Doesn't trigger re-runs
1032
+ * processData(tracked, snapshot);
423
1033
  * });
424
1034
  * ```
425
1035
  *
1036
+ * @throws Error if the node has been disposed.
1037
+ *
426
1038
  * @public
427
1039
  */
428
- watch(context) {
429
- super.watch(context);
430
- this._initLazy();
431
- this._update();
1040
+ async pick() {
1041
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
1042
+ await FlowGraph.requestRead(() => this._computeValue());
1043
+ return this._value;
432
1044
  }
433
- }
434
-
435
- function isDisposable(obj) {
436
- return obj !== null && obj !== void 0 && typeof obj.dispose === "function";
437
- }
438
-
439
- class FlowState extends FlowConstant {
440
1045
  /**
441
- * Updates the state with a new value.
442
- * @param value - A new value or a callback function that computes a new value based on the current state.
1046
+ * Subscribes a listener function to changes of this node.
1047
+ *
1048
+ * @param listener - A callback function that receives the new value whenever it changes.
1049
+ *
1050
+ * @returns A disposer function that cancels the subscription when called.
1051
+ *
443
1052
  * @remarks
444
- * If the computed new value is strictly equal to the current state value, no change is made and subscribers
445
- * will not be notified. Otherwise, the state is updated and all subscribers are informed of the change.
446
- * @throws Error if the state has been disposed.
1053
+ * This method creates a reactive subscription that automatically tracks this node as a dependency.
1054
+ * The listener is executed:
1055
+ * - Immediately with the current value when the subscription is created
1056
+ * - Automatically whenever the value changes
1057
+ *
1058
+ * The subscription uses a {@link FlowEffect} internally to manage the reactive tracking and
1059
+ * automatic re-execution. When the value changes, the listener is called with the new value
1060
+ * after the update is processed through the reactive graph.
1061
+ *
1062
+ * **Cleanup:**
1063
+ * Always call the returned disposer function when you no longer need the subscription to
1064
+ * prevent memory leaks and unnecessary computations.
1065
+ *
1066
+ * @example
1067
+ * ```typescript
1068
+ * const $count = new FlowNode(0);
1069
+ *
1070
+ * // Subscribe to changes
1071
+ * const unsubscribe = $count.subscribe((value) => {
1072
+ * console.log(`Count is now: ${value}`);
1073
+ * });
1074
+ * // Logs immediately: "Count is now: 0"
1075
+ *
1076
+ * await $count.set(5);
1077
+ * // Logs: "Count is now: 5"
1078
+ *
1079
+ * // Clean up when done
1080
+ * unsubscribe();
1081
+ * ```
1082
+ *
1083
+ * @throws Error if the node has been disposed.
1084
+ *
447
1085
  * @public
448
1086
  */
449
- set(value) {
1087
+ subscribe(listener) {
450
1088
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
451
- const next = typeof value === "function" ? value(this._value) : value;
452
- if (next === this._value) return;
453
- this._value = next;
454
- this._notify();
1089
+ const effect = new FlowEffect((t) => {
1090
+ listener(this.get(t));
1091
+ });
1092
+ return () => effect.dispose();
1093
+ }
1094
+ /* INTERNAL --------------------------------------------------------- */
1095
+ /* @internal */
1096
+ _notify() {
1097
+ if (this._dirty) {
1098
+ return;
1099
+ }
1100
+ this._dirty = true;
1101
+ super._notify();
1102
+ }
1103
+ _computeValue(options) {
1104
+ if (!this._dirty && !options?.force) return;
1105
+ if (!this._compute) {
1106
+ this._dirty = false;
1107
+ return;
1108
+ }
1109
+ this._dirty = false;
1110
+ const dependencies = [...this._dependencies];
1111
+ this._dependencies.clear();
1112
+ this._value = this._compute(this);
1113
+ const dependenciesToRemove = dependencies.filter(
1114
+ (dependency) => !this._dependencies.has(dependency)
1115
+ );
1116
+ dependenciesToRemove.forEach((dependency) => {
1117
+ dependency._unregisterDependency(this);
1118
+ });
455
1119
  }
456
1120
  }
457
1121
 
458
- function signal() {
459
- return new FlowSignal();
460
- }
461
1122
  function constant(value) {
462
- return new FlowConstant(value);
463
- }
464
- function state(value) {
465
- return new FlowState(value);
466
- }
467
- function resource(fn) {
468
- return new FlowResource(fn);
469
- }
470
- function resourceAsync(fn) {
471
- return new FlowResourceAsync(fn);
472
- }
473
- function stream(updater) {
474
- return new FlowStream(updater);
475
- }
476
- function streamAsync(updater) {
477
- return new FlowStreamAsync(updater);
1123
+ return new FlowNode(value);
478
1124
  }
1125
+
479
1126
  function derivation(fn) {
480
- return new FlowDerivation(fn);
481
- }
482
- function effect(fn) {
483
- return new FlowEffect(fn);
484
- }
485
- function map(initial) {
486
- return new FlowMap(
487
- new Map(initial ? Object.entries(initial) : [])
488
- );
1127
+ return new FlowNode(fn);
489
1128
  }
490
- function array(initial) {
491
- return new FlowArray(initial);
1129
+
1130
+ function state(value) {
1131
+ return new FlowNode(value);
492
1132
  }
493
1133
 
494
- class FlowArray extends FlowObservable {
1134
+ class FlowArray extends FlowNode {
495
1135
  /**
496
1136
  * Last action performed on the FlowArray.
497
1137
  * @public
@@ -503,8 +1143,7 @@ class FlowArray extends FlowObservable {
503
1143
  * @public
504
1144
  */
505
1145
  constructor(value = []) {
506
- super();
507
- this._value = value;
1146
+ super(value);
508
1147
  this.$lastAction = state({
509
1148
  type: "set",
510
1149
  items: value
@@ -532,14 +1171,23 @@ class FlowArray extends FlowObservable {
532
1171
  * @param items - The new array of items.
533
1172
  * @public
534
1173
  */
535
- set(items) {
1174
+ async set(items) {
536
1175
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
537
- this._value.forEach((item) => {
538
- if (isDisposable(item)) item.dispose({ self: true });
539
- });
540
- this._value = items;
541
- this._notify();
542
- this.$lastAction.set({ type: "set", items });
1176
+ const update = () => {
1177
+ const currentValue = this._value;
1178
+ if (currentValue !== items) {
1179
+ currentValue.forEach((item) => {
1180
+ if (isDisposable(item)) item.dispose({ self: true });
1181
+ });
1182
+ this._value = items;
1183
+ this.$lastAction.set({ type: "set", items });
1184
+ }
1185
+ return currentValue !== items;
1186
+ };
1187
+ const notify = () => {
1188
+ super._notify();
1189
+ };
1190
+ return FlowGraph.requestWrite(notify, update);
543
1191
  }
544
1192
  /**
545
1193
  * Replaces an item at a specific index.
@@ -547,62 +1195,104 @@ class FlowArray extends FlowObservable {
547
1195
  * @param item - The new item.
548
1196
  * @public
549
1197
  */
550
- setItem(index, item) {
1198
+ async update(index, item) {
551
1199
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
552
- if (index < 0 || index >= this._value.length) {
553
- throw new Error("[PicoFlow] Index out of bounds");
554
- }
555
- this._value[index] = item;
556
- this._notify();
557
- this.$lastAction.set({ type: "setItem", index, item });
1200
+ const update = () => {
1201
+ if (index < 0 || index >= this._value.length) {
1202
+ throw new Error("[PicoFlow] Index out of bounds");
1203
+ }
1204
+ const currentValue = this._value[index];
1205
+ if (currentValue !== item) {
1206
+ if (isDisposable(currentValue)) {
1207
+ currentValue.dispose({ self: true });
1208
+ }
1209
+ this._value[index] = item;
1210
+ this.$lastAction.set({ type: "update", index, item });
1211
+ }
1212
+ return currentValue !== item;
1213
+ };
1214
+ const notify = () => {
1215
+ super._notify();
1216
+ };
1217
+ return FlowGraph.requestWrite(notify, update);
558
1218
  }
559
1219
  /**
560
1220
  * Appends an item to the end of the array.
561
1221
  * @param item - The item to append.
562
1222
  * @public
563
1223
  */
564
- push(item) {
1224
+ async push(item) {
565
1225
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
566
- this._value.push(item);
567
- this._notify();
568
- this.$lastAction.set({ type: "push", item });
1226
+ const update = () => {
1227
+ this._value.push(item);
1228
+ this.$lastAction.set({ type: "push", item });
1229
+ return true;
1230
+ };
1231
+ const notify = () => {
1232
+ super._notify();
1233
+ };
1234
+ return FlowGraph.requestWrite(notify, update);
569
1235
  }
570
1236
  /**
571
1237
  * Removes the last item from the array.
572
1238
  * @public
573
1239
  */
574
- pop() {
1240
+ async pop() {
575
1241
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
576
- const item = this._value.pop();
577
- if (isDisposable(item)) {
578
- item.dispose({ self: true });
579
- }
580
- this._notify();
581
- this.$lastAction.set({ type: "pop" });
1242
+ const update = () => {
1243
+ const item = this._value.pop();
1244
+ if (item !== void 0) {
1245
+ if (isDisposable(item)) {
1246
+ item.dispose({ self: true });
1247
+ }
1248
+ this.$lastAction.set({ type: "pop" });
1249
+ return true;
1250
+ }
1251
+ return false;
1252
+ };
1253
+ const notify = () => {
1254
+ super._notify();
1255
+ };
1256
+ return FlowGraph.requestWrite(notify, update);
582
1257
  }
583
1258
  /**
584
1259
  * Inserts an item at the beginning of the array.
585
1260
  * @param item - The item to insert.
586
1261
  * @public
587
1262
  */
588
- unshift(item) {
1263
+ async unshift(item) {
589
1264
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
590
- this._value.unshift(item);
591
- this._notify();
592
- this.$lastAction.set({ type: "unshift", item });
1265
+ const update = () => {
1266
+ this._value.unshift(item);
1267
+ this.$lastAction.set({ type: "unshift", item });
1268
+ return true;
1269
+ };
1270
+ const notify = () => {
1271
+ super._notify();
1272
+ };
1273
+ return FlowGraph.requestWrite(notify, update);
593
1274
  }
594
1275
  /**
595
1276
  * Removes the first item from the array.
596
1277
  * @public
597
1278
  */
598
- shift() {
1279
+ async shift() {
599
1280
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
600
- const item = this._value.shift();
601
- if (isDisposable(item)) {
602
- item.dispose({ self: true });
603
- }
604
- this._notify();
605
- this.$lastAction.set({ type: "shift" });
1281
+ const update = () => {
1282
+ const item = this._value.shift();
1283
+ if (item !== void 0) {
1284
+ if (isDisposable(item)) {
1285
+ item.dispose({ self: true });
1286
+ }
1287
+ this.$lastAction.set({ type: "shift" });
1288
+ return true;
1289
+ }
1290
+ return false;
1291
+ };
1292
+ const notify = () => {
1293
+ super._notify();
1294
+ };
1295
+ return FlowGraph.requestWrite(notify, update);
606
1296
  }
607
1297
  /**
608
1298
  * Changes the content of the array.
@@ -611,33 +1301,45 @@ class FlowArray extends FlowObservable {
611
1301
  * @param newItems - New items to add.
612
1302
  * @public
613
1303
  */
614
- splice(start, deleteCount, ...newItems) {
1304
+ async splice(start, deleteCount, ...newItems) {
615
1305
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
616
- const items = this._value.splice(start, deleteCount, ...newItems);
617
- items.forEach((item) => {
618
- if (isDisposable(item)) item.dispose({ self: true });
619
- });
620
- this._notify();
621
- this.$lastAction.set({
622
- type: "splice",
623
- start,
624
- deleteCount,
625
- items: newItems
626
- });
1306
+ const update = () => {
1307
+ const items = this._value.splice(start, deleteCount, ...newItems);
1308
+ items.forEach((item) => {
1309
+ if (isDisposable(item)) item.dispose({ self: true });
1310
+ });
1311
+ this.$lastAction.set({
1312
+ type: "splice",
1313
+ start,
1314
+ deleteCount,
1315
+ items: newItems
1316
+ });
1317
+ return true;
1318
+ };
1319
+ const notify = () => {
1320
+ super._notify();
1321
+ };
1322
+ return FlowGraph.requestWrite(notify, update);
627
1323
  }
628
1324
  /**
629
1325
  * Clears all items from the array.
630
1326
  * @public
631
1327
  */
632
- clear() {
1328
+ async clear() {
633
1329
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
634
- const items = [...this._value];
635
- items.forEach((item) => {
636
- if (isDisposable(item)) item.dispose({ self: true });
637
- });
638
- this._value = [];
639
- this._notify();
640
- this.$lastAction.set({ type: "clear" });
1330
+ const update = () => {
1331
+ const items = [...this._value];
1332
+ items.forEach((item) => {
1333
+ if (isDisposable(item)) item.dispose({ self: true });
1334
+ });
1335
+ this._value = [];
1336
+ this.$lastAction.set({ type: "clear" });
1337
+ return true;
1338
+ };
1339
+ const notify = () => {
1340
+ super._notify();
1341
+ };
1342
+ return FlowGraph.requestWrite(notify, update);
641
1343
  }
642
1344
  /**
643
1345
  * Disposes the FlowArray and its items.
@@ -652,41 +1354,29 @@ class FlowArray extends FlowObservable {
652
1354
  this._value = [];
653
1355
  }
654
1356
  /* INTERNAL */
655
- /** @internal */
656
- _value = [];
1357
+ }
1358
+ function array(initial) {
1359
+ return new FlowArray(initial);
657
1360
  }
658
1361
 
659
- class FlowMap extends FlowState {
660
- /**
661
- * A reactive state that holds the most recent key and value that were added.
662
- *
663
- * @remarks
664
- * When a key is added via {@link FlowMap.add}, this state is updated with
665
- * the corresponding key and value.
666
- *
667
- * @public
668
- */
669
- $lastAdded = new FlowState(null);
1362
+ class FlowMap extends FlowNode {
670
1363
  /**
671
- * A reactive state that holds the most recent key and value that were updated.
672
- *
673
- * @remarks
674
- * When a key is updated via {@link FlowMap.update}, this state is updated with
675
- * the corresponding key and value.
676
- *
1364
+ * Last action performed on the FlowMap.
677
1365
  * @public
678
1366
  */
679
- $lastUpdated = new FlowState(null);
1367
+ $lastAction;
680
1368
  /**
681
- * A reactive state that holds the most recent key and value that were deleted.
682
- *
683
- * @remarks
684
- * When a key is deleted via {@link FlowMap.delete}, this state is updated with
685
- * the corresponding key and its last known value.
686
- *
1369
+ * Creates an instance of FlowMap.
1370
+ * @param value - Initial map value.
687
1371
  * @public
688
1372
  */
689
- $lastDeleted = new FlowState(null);
1373
+ constructor(value = /* @__PURE__ */ new Map()) {
1374
+ super(value);
1375
+ this.$lastAction = state({
1376
+ type: "set",
1377
+ map: value
1378
+ });
1379
+ }
690
1380
  /**
691
1381
  * Adds a new key-value pair to the map.
692
1382
  *
@@ -696,19 +1386,26 @@ class FlowMap extends FlowState {
696
1386
  * @throws If the key already exists in the map.
697
1387
  *
698
1388
  * @remarks
699
- * Adds a new entry to the internal map, emits the key-value pair via {@link FlowMap.$lastAdded},
1389
+ * Adds a new entry to the internal map, emits the key-value pair via {@link FlowMap.$lastAction},
700
1390
  * and notifies all subscribers of the change.
701
1391
  *
702
1392
  * @public
703
1393
  */
704
- add(key, value) {
1394
+ async add(key, value) {
705
1395
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
706
- if (this._value.has(key)) {
707
- throw new Error("[PicoFlow] Key already exists");
708
- }
709
- this._value.set(key, value);
710
- this.$lastAdded.set({ key, value });
711
- this._notify();
1396
+ const update = () => {
1397
+ const currentValue = this._value.get(key);
1398
+ if (currentValue) {
1399
+ throw new Error("[PicoFlow] Key already exists");
1400
+ }
1401
+ this._value.set(key, value);
1402
+ this.$lastAction.set({ type: "add", key, value });
1403
+ return true;
1404
+ };
1405
+ const notify = () => {
1406
+ super._notify();
1407
+ };
1408
+ return FlowGraph.requestWrite(notify, update);
712
1409
  }
713
1410
  /**
714
1411
  * Updates an existing key-value pair in the map.
@@ -719,19 +1416,28 @@ class FlowMap extends FlowState {
719
1416
  * @throws If the key does not exist in the map.
720
1417
  *
721
1418
  * @remarks
722
- * Updates an existing entry in the internal map, emits the key-value pair via {@link FlowMap.$lastUpdated},
1419
+ * Updates an existing entry in the internal map, emits the key-value pair via {@link FlowMap.$lastAction},
723
1420
  * and notifies all subscribers of the change.
724
1421
  *
725
1422
  * @public
726
1423
  */
727
- update(key, value) {
1424
+ async update(key, value) {
728
1425
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
729
- if (!this._value.has(key)) {
730
- throw new Error("[PicoFlow] Key does not exist");
731
- }
732
- this._value.set(key, value);
733
- this.$lastUpdated.set({ key, value });
734
- this._notify();
1426
+ const update = () => {
1427
+ const currentValue = this._value.get(key);
1428
+ if (!currentValue) throw new Error("[PicoFlow] Key does not exist");
1429
+ if (currentValue !== value) {
1430
+ if (isDisposable(currentValue)) currentValue.dispose({ self: true });
1431
+ this._value.set(key, value);
1432
+ this.$lastAction.set({ type: "update", key, value });
1433
+ return true;
1434
+ }
1435
+ return false;
1436
+ };
1437
+ const notify = () => {
1438
+ super._notify();
1439
+ };
1440
+ return FlowGraph.requestWrite(notify, update);
735
1441
  }
736
1442
  /**
737
1443
  * Deletes the value at the specified key from the underlying map.
@@ -740,225 +1446,105 @@ class FlowMap extends FlowState {
740
1446
  * @throws If the FlowMap instance is disposed.
741
1447
  *
742
1448
  * @remarks
743
- * Removes the key from the internal map, emits the deleted key and its value via {@link FlowMap.$lastDeleted},
1449
+ * Removes the key from the internal map, emits the deleted key and its value via {@link FlowMap.$lastAction},
744
1450
  * and notifies all subscribers of the change.
745
1451
  *
746
1452
  * @public
747
1453
  */
748
- delete(key) {
1454
+ async delete(key) {
749
1455
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
750
- const value = this._value.get(key);
751
- if (!value) throw new Error("[PicoFlow] Key does not exist");
752
- this._value.delete(key);
753
- this.$lastDeleted.set({ key, value });
754
- this._notify();
755
- }
756
- /**
757
- * Disposes the FlowMap and its values.
758
- * @param options - Disposal options.
759
- * @public
760
- */
761
- dispose(options) {
762
- super.dispose(options);
763
- this._value.forEach((item) => {
764
- if (isDisposable(item)) item.dispose(options);
765
- });
766
- this._value.clear();
767
- this.$lastAdded.dispose(options);
768
- this.$lastUpdated.dispose(options);
769
- this.$lastDeleted.dispose(options);
1456
+ const update = () => {
1457
+ const value = this._value.get(key);
1458
+ if (value === void 0) throw new Error("[PicoFlow] Key does not exist");
1459
+ if (isDisposable(value)) value.dispose({ self: true });
1460
+ this._value.delete(key);
1461
+ this.$lastAction.set({ type: "delete", key, value });
1462
+ return true;
1463
+ };
1464
+ const notify = () => {
1465
+ super._notify();
1466
+ };
1467
+ return FlowGraph.requestWrite(notify, update);
770
1468
  }
771
- }
772
-
773
- class FlowResource extends FlowObservable {
774
1469
  /**
775
- * Creates a new FlowResource.
1470
+ * Replaces the entire map with new entries.
776
1471
  *
777
- * @param fetch - An asynchronous function that retrieves the resource's value.
778
- * This function is not invoked on construction; you must call the `fetch()` method
779
- * to execute it.
1472
+ * @param map - The new map of entries.
1473
+ * @throws If the FlowMap instance is disposed.
780
1474
  *
781
- * @public
782
- */
783
- constructor(fetch) {
784
- super();
785
- this._fetch = fetch;
786
- }
787
- /**
788
- * Internal method to get the raw value.
789
- * @internal
790
- */
791
- _getRaw() {
792
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
793
- return this._value;
794
- }
795
- /**
796
- * Asynchronously fetches a new value for the resource.
797
- * @remarks
798
- * Executes the internal fetch function. If the fetched value differs from the current one,
799
- * updates the resource's value and notifies subscribers.
800
- * @returns A Promise that resolves when the fetch operation is complete.
801
- * @throws Error if the resource is disposed.
802
- * @public
803
- */
804
- async fetch() {
805
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
806
- const value = await this._fetch();
807
- if (value === this._value) return;
808
- this._value = value;
809
- this._notify();
810
- }
811
- /* INTERNAL ------------------------------------------------ */
812
- _fetch;
813
- }
814
-
815
- class FlowResourceAsync extends FlowObservable {
816
- /**
817
- * Creates a new FlowResource.
818
- * @param fetch - An asynchronous function that retrieves the resource's value.
819
- * @public
820
- */
821
- constructor(fetch) {
822
- super();
823
- this._fetch = fetch;
824
- }
825
- /**
826
- * Internal method to get the raw value.
827
- * @internal
828
- */
829
- _getRaw() {
830
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
831
- if (!this._value) this._value = this._fetch();
832
- return this._value;
833
- }
834
- /**
835
- * Asynchronously fetches a new value for the resource.
836
1475
  * @remarks
837
- * Executes the internal fetch function. If the fetched value differs from the current one,
838
- * updates the resource's value and notifies subscribers.
839
- * @returns A Promise that resolves when the fetch operation is complete.
840
- * @throws Error if the resource is disposed.
1476
+ * Replaces all entries in the internal map, disposes the old values (if they are disposable),
1477
+ * emits the new map via {@link FlowMap.$lastAction}, and notifies all subscribers of the change.
1478
+ *
841
1479
  * @public
842
1480
  */
843
- async fetch() {
1481
+ async set(map2) {
844
1482
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
845
- this._value = this._fetch();
846
- this._notify();
1483
+ const update = () => {
1484
+ const currentValue = this._value;
1485
+ if (currentValue !== map2) {
1486
+ currentValue.forEach((item) => {
1487
+ if (isDisposable(item)) item.dispose({ self: true });
1488
+ });
1489
+ }
1490
+ this._value = map2;
1491
+ if (currentValue !== map2) {
1492
+ this.$lastAction.set({ type: "set", map: map2 });
1493
+ }
1494
+ return currentValue !== map2;
1495
+ };
1496
+ const notify = () => {
1497
+ super._notify();
1498
+ };
1499
+ return FlowGraph.requestWrite(notify, update);
847
1500
  }
848
- /* INTERNAL ------------------------------------------------ */
849
- _fetch;
850
- }
851
-
852
- class FlowStream extends FlowObservable {
853
1501
  /**
854
- * Creates a new FlowStream.
1502
+ * Clears all entries from the map.
855
1503
  *
856
- * @param updater - A function that receives a setter callback and returns a disposer.
857
- * The setter should be called whenever new data is available. The disposer will be
858
- * invoked when the stream is disposed to clean up resources.
1504
+ * @throws If the FlowMap instance is disposed.
859
1505
  *
860
1506
  * @remarks
861
- * The updater is invoked immediately during construction. Make sure to return a proper
862
- * cleanup function to avoid resource leaks.
1507
+ * Removes all entries from the internal map, disposes the removed values (if they are disposable),
1508
+ * emits a clear action via {@link FlowMap.$lastAction}, and notifies all subscribers of the change.
863
1509
  *
864
1510
  * @public
865
1511
  */
866
- constructor(updater) {
867
- super();
868
- this._disposer = updater((value) => {
869
- this._set(value);
870
- });
871
- }
872
- /**
873
- * Internal method to get the raw value.
874
- * @internal
875
- */
876
- _getRaw() {
877
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
878
- return this._value;
879
- }
880
- /**
881
- * Disposes the stream, releasing all resources.
882
- * @remarks
883
- * In addition to disposing the underlying observable, this method calls the disposer
884
- * returned by the updater.
885
- * @public
886
- */
887
- dispose() {
888
- super.dispose();
889
- this._disposer();
890
- }
891
- /* INTERNAL ------------------------------------------------------ */
892
- _disposer;
893
- _set(value) {
1512
+ async clear() {
894
1513
  if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
895
- if (value === this._value) return;
896
- this._value = value;
897
- this._notify();
1514
+ const update = () => {
1515
+ const items = [...this._value.values()];
1516
+ items.forEach((item) => {
1517
+ if (isDisposable(item)) item.dispose({ self: true });
1518
+ });
1519
+ this._value.clear();
1520
+ this.$lastAction.set({ type: "clear" });
1521
+ return true;
1522
+ };
1523
+ const notify = () => {
1524
+ super._notify();
1525
+ };
1526
+ return FlowGraph.requestWrite(notify, update);
898
1527
  }
899
- }
900
-
901
- class FlowStreamAsync extends FlowObservable {
902
1528
  /**
903
- * Creates a new asynchronous FlowStream.
904
- *
905
- * @param updater - A function that receives a setter callback and returns a disposer.
906
- * The setter should be called whenever new data is available (can be called asynchronously).
907
- * The disposer will be invoked when the stream is disposed to clean up resources.
908
- *
909
- * @remarks
910
- * The updater is invoked immediately during construction. An initial Promise is created
911
- * that will resolve when the setter is first called. Make sure to return a proper cleanup
912
- * function to avoid resource leaks.
913
- *
1529
+ * Disposes the FlowMap and its values.
1530
+ * @param options - Disposal options.
914
1531
  * @public
915
1532
  */
916
- constructor(updater) {
917
- super();
918
- this._disposer = updater((value) => {
919
- this._set(value);
920
- });
921
- this._value = new Promise((resolve) => {
922
- this._resolve = resolve;
1533
+ dispose(options) {
1534
+ super.dispose(options);
1535
+ this._value.forEach((item) => {
1536
+ if (isDisposable(item)) item.dispose(options);
923
1537
  });
1538
+ this._value.clear();
924
1539
  }
925
- /**
926
- * Internal method to get the raw value.
927
- * @internal
928
- */
929
- _getRaw() {
930
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
931
- return this._value;
932
- }
933
- /**
934
- * Disposes the stream, releasing all resources.
935
- * @remarks In addition to disposing the underlying observable, this method calls the disposer
936
- * returned by the updater.
937
- * @public
938
- */
939
- dispose() {
940
- super.dispose();
941
- this._disposer();
942
- }
943
- /* INTERNAL ------------------------------------------------------ */
944
- _initialized = false;
945
- _awaitedValue;
946
- _resolve;
947
- _disposer;
948
- _set(value) {
949
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
950
- if (!this._initialized) {
951
- this._resolve(value);
952
- this._initialized = true;
953
- this._awaitedValue = value;
954
- this._notify();
955
- return;
956
- }
957
- if (value === this._awaitedValue) return;
958
- this._value = Promise.resolve(value);
959
- this._awaitedValue = value;
960
- this._notify();
1540
+ }
1541
+ function map(initial) {
1542
+ if (initial instanceof Map) {
1543
+ return new FlowMap(initial);
961
1544
  }
1545
+ return new FlowMap(
1546
+ new Map(initial ? Object.entries(initial) : [])
1547
+ );
962
1548
  }
963
1549
 
964
1550
  class SolidState {
@@ -1011,6 +1597,7 @@ class SolidResource {
1011
1597
  * Triggers a refetch of the resource.
1012
1598
  */
1013
1599
  refetch;
1600
+ set;
1014
1601
  /**
1015
1602
  * Creates a new SolidResource from a fetcher function.
1016
1603
  * @param fetcher - The async fetcher function.
@@ -1021,60 +1608,46 @@ class SolidResource {
1021
1608
  this.state = () => get.state;
1022
1609
  this.latest = () => get.latest;
1023
1610
  this.refetch = () => set.refetch();
1611
+ this.set = (value) => set.mutate(value);
1024
1612
  }
1025
1613
  }
1026
1614
 
1027
- function fromSync(state) {
1028
- const solidState = new SolidState(state.pick());
1029
- let fx;
1030
- onMount(() => {
1031
- fx = new FlowEffect((t) => {
1032
- const value = state.get(t);
1033
- solidState.set(() => value);
1034
- });
1035
- });
1036
- onCleanup(() => fx.dispose());
1037
- return solidState;
1038
- }
1039
- function fromAsync(derivation) {
1615
+ function fromNode(node) {
1616
+ let initialized = false;
1040
1617
  const solidResource = new SolidResource(async () => {
1041
- const value = await derivation.pick();
1618
+ let value;
1619
+ if (initialized) {
1620
+ value = await node.get(null);
1621
+ } else {
1622
+ value = await node.pick();
1623
+ initialized = true;
1624
+ }
1042
1625
  return value;
1043
1626
  });
1044
1627
  let fx;
1045
1628
  onMount(() => {
1046
1629
  fx = new FlowEffect(async (t) => {
1047
- await derivation.get(t);
1630
+ node.watch(t);
1048
1631
  solidResource.refetch();
1049
1632
  });
1050
1633
  });
1051
1634
  onCleanup(() => fx.dispose());
1052
1635
  return solidResource;
1053
1636
  }
1054
- function shallowFrom(flow) {
1055
- const initialValue = flow.pick();
1056
- const isAsync = initialValue instanceof Promise;
1057
- if (isAsync) {
1058
- return fromAsync(flow);
1059
- }
1060
- return fromSync(flow);
1061
- }
1062
- function deepFrom(getter) {
1063
- const derivation = new FlowDerivation((t) => {
1637
+ function fromGetter(getter) {
1638
+ const derivation = new FlowNodeAsync(async (t) => {
1064
1639
  return getter(t);
1065
1640
  });
1066
- const initialValue = derivation.pick();
1067
- const isAsync = initialValue instanceof Promise;
1068
- if (isAsync) {
1069
- return fromAsync(derivation);
1070
- }
1071
- return fromSync(derivation);
1641
+ return fromNode(derivation);
1072
1642
  }
1073
1643
  function from(flow) {
1074
- if (flow instanceof FlowObservable) {
1075
- return shallowFrom(flow);
1644
+ if (flow instanceof FlowNodeAsync || flow instanceof FlowNode) {
1645
+ return fromNode(flow);
1646
+ }
1647
+ if (typeof flow === "function") {
1648
+ return fromGetter(flow);
1076
1649
  }
1077
- return deepFrom(flow);
1650
+ throw new Error("Invalid flow type");
1078
1651
  }
1079
1652
 
1080
- export { FlowArray, FlowConstant, FlowDerivation, FlowEffect, FlowMap, FlowObservable, FlowResource, FlowResourceAsync, FlowSignal, FlowState, FlowStream, FlowStreamAsync, SolidDerivation, SolidResource, SolidState, TrackingContext, array, constant, derivation, effect, from, isDisposable, map, resource, resourceAsync, signal, state, stream, streamAsync };
1653
+ export { FlowArray, FlowEffect, FlowGraph, FlowMap, FlowNode, FlowNodeAsync, FlowSignal, SolidDerivation, SolidResource, SolidState, array, constant, constantAsync, derivation, derivationAsync, effect, from, isDisposable, map, signal, state, stateAsync };