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