@ersbeth/picoflow 1.0.0 → 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 +25 -171
  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
@@ -0,0 +1,508 @@
1
+ import {
2
+ FlowEffect,
3
+ FlowGraph,
4
+ FlowSignal,
5
+ type FlowTracker,
6
+ } from "../../base";
7
+
8
+ /**
9
+ * A versatile reactive node that can operate as mutable state or computed derivation, with asynchronous values.
10
+ *
11
+ * @typeParam T - The type of the value held by this node (not the Promise itself).
12
+ *
13
+ * @remarks
14
+ * FlowNodeAwait is a flexible reactive primitive that adapts its behavior based on how it's initialized.
15
+ * It extends {@link FlowSignal} and provides reactive value access with automatic dependency tracking.
16
+ * Unlike its synchronous counterpart {@link FlowNode}, FlowNodeAwait works with `Promise<T>` values internally,
17
+ * but provides a synchronous-looking API by automatically awaiting Promises and caching the resolved values.
18
+ *
19
+ * **Key Difference from FlowNodeAsync:**
20
+ * Unlike {@link FlowNodeAsync}, which returns Promises from `get()`, FlowNodeAwait returns the resolved value
21
+ * directly. This makes the API more convenient for reactive code that doesn't want to deal with Promises
22
+ * in every access. The Promise is awaited internally and the resolved value is cached.
23
+ *
24
+ * **Operating Modes:**
25
+ *
26
+ * - **Mutable State Mode**: When constructed with a constant Promise, FlowNodeAwait acts as a mutable
27
+ * reactive state. You can update its value using `set()`, and all dependents are automatically
28
+ * notified when the value changes. The Promise is resolved internally and the value is cached.
29
+ *
30
+ * - **Computed Derivation Mode**: When constructed with a compute function, FlowNodeAwait acts as a
31
+ * computed value that automatically tracks its dependencies. The compute function runs lazily
32
+ * (only when the value is first accessed) and recomputes when any tracked dependency changes.
33
+ * The compute function must return a `Promise<T>`. The computed value can be temporarily overridden
34
+ * using `set()`, but the override is cleared on the next recomputation (triggered by dependency changes).
35
+ *
36
+ * **Asynchronous Nature:**
37
+ * FlowNodeAwait works with Promises internally, but provides a synchronous-looking API:
38
+ * - `get(t)` returns the resolved value synchronously (`T | undefined`). The value may be `undefined`
39
+ * if the Promise hasn't resolved yet on first access.
40
+ * - `pick()` returns a `Promise<T>` that must be awaited. Use this when you need to ensure the value
41
+ * is fully resolved before proceeding.
42
+ * - The Promise is awaited internally in `_computeValue()` and the resolved value is cached in `_value`.
43
+ *
44
+ * **Lazy Computation:**
45
+ * When using a compute function, the computation doesn't run immediately upon construction. It
46
+ * executes only when:
47
+ * - The value is first read via `get()` or `pick()`
48
+ * - The node is watched via `watch()`
49
+ *
50
+ * **Dirty Tracking:**
51
+ * When a tracked dependency changes, the node is marked as "dirty" but doesn't recompute
52
+ * immediately. Recomputation happens lazily on the next value access, preventing unnecessary
53
+ * computations when multiple dependencies change in quick succession. The resolved value is cached
54
+ * until dependencies change, ensuring efficient access patterns.
55
+ *
56
+ * **Dynamic Dependencies:**
57
+ * Dependencies are tracked dynamically during each computation. If the compute function
58
+ * conditionally tracks different observables, the dependency graph updates automatically.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Mutable state mode
63
+ * const $count = new FlowNodeAwait(Promise.resolve(0));
64
+ * await $count.set(Promise.resolve(1)); // Updates the value
65
+ *
66
+ * // Computed derivation mode
67
+ * const $firstName = new FlowNodeAwait(Promise.resolve('John'));
68
+ * const $lastName = new FlowNodeAwait(Promise.resolve('Doe'));
69
+ * const $fullName = new FlowNodeAwait(async (t) => {
70
+ * const first = $firstName.get(t); // Synchronous - returns resolved value
71
+ * const last = $lastName.get(t); // Synchronous - returns resolved value
72
+ * return `${first} ${last}`;
73
+ * });
74
+ *
75
+ * // Compute function hasn't run yet (lazy)
76
+ * const name = await $fullName.pick(); // Now it computes: "John Doe"
77
+ *
78
+ * // Temporarily override computed value
79
+ * await $fullName.set(Promise.resolve('Override'));
80
+ * console.log(await $fullName.pick()); // "Override"
81
+ *
82
+ * // Trigger recomputation (clears override)
83
+ * await $firstName.set(Promise.resolve('Jane'));
84
+ * console.log(await $fullName.pick()); // "Jane Doe" (override cleared)
85
+ * ```
86
+ *
87
+ * @public
88
+ */
89
+ export class FlowNodeAwait<T> extends FlowSignal {
90
+ // protected _promise?: Promise<T>;
91
+ private _dirty = true;
92
+ private _value?: T;
93
+ private _compute?: (t: FlowTracker) => Promise<T>;
94
+
95
+ /**
96
+ * Creates a new FlowNodeAwait.
97
+ *
98
+ * @param compute - Either a constant Promise or a compute function that derives the value.
99
+ *
100
+ * @remarks
101
+ * The constructor accepts two different initialization modes:
102
+ *
103
+ * - **Constant Promise**: Creates a mutable reactive node that can be updated via `set()`.
104
+ * The Promise is stored immediately and resolved internally. The resolved value is cached
105
+ * and can be accessed synchronously via `get()`. The Promise can be changed at any time via `set()`.
106
+ *
107
+ * - **Compute function**: Creates a computed reactive node that automatically tracks dependencies
108
+ * and recomputes when they change. The compute function receives a tracking context (`t`)
109
+ * that should be used to access dependencies via `.get(t)`. The function must return a
110
+ * `Promise<T>`. The function is not executed immediately; it runs lazily on first access.
111
+ * The Promise is awaited internally and the resolved value is cached. The computed value can
112
+ * be temporarily overridden using `set()`, but the override is cleared on the next recomputation
113
+ * (triggered by dependency changes).
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * // Mutable state with constant Promise
118
+ * const $count = new FlowNodeAwait(Promise.resolve(0));
119
+ * await $count.set(Promise.resolve(5));
120
+ *
121
+ * // Computed derivation with async function
122
+ * const $a = new FlowNodeAwait(Promise.resolve(10));
123
+ * const $b = new FlowNodeAwait(Promise.resolve(20));
124
+ * const $sum = new FlowNodeAwait(async (t) => {
125
+ * const a = $a.get(t); // Synchronous - returns resolved value
126
+ * const b = $b.get(t); // Synchronous - returns resolved value
127
+ * return a + b;
128
+ * });
129
+ *
130
+ * // Lazy evaluation - compute function hasn't run yet
131
+ * console.log(await $sum.pick()); // Now it computes: 30
132
+ * ```
133
+ *
134
+ * @public
135
+ */
136
+ constructor(compute: Promise<T> | ((t: FlowTracker) => Promise<T>)) {
137
+ super();
138
+ if (typeof compute === "function") {
139
+ this._compute = compute as (t: FlowTracker) => Promise<T>;
140
+ } else {
141
+ this.set(compute);
142
+ }
143
+ }
144
+
145
+ override watch(tracker: FlowTracker): void {
146
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
147
+ this._computeValue();
148
+ super.watch(tracker);
149
+ }
150
+
151
+ /**
152
+ * Gets the current value with dependency tracking.
153
+ *
154
+ * @param tracker - The tracking context for reactive tracking. This observable is registered
155
+ * as a dependency when accessed through this method.
156
+ *
157
+ * @returns The current resolved value of type T, or `undefined` if the Promise hasn't resolved yet.
158
+ *
159
+ * @remarks
160
+ * Use `get(t)` within effects and derivations to create reactive dependencies. The tracker
161
+ * parameter must be provided from the reactive context (typically the `t` parameter in effect
162
+ * or derivation callbacks).
163
+ *
164
+ * **Important:** This method returns the resolved value synchronously (`T | undefined`), not a Promise.
165
+ * The Promise is awaited internally and the resolved value is cached. This provides a convenient
166
+ * synchronous-looking API while working with asynchronous data sources.
167
+ *
168
+ * **Undefined Value:**
169
+ * On the first access before the Promise has resolved, this method may return `undefined`. Once
170
+ * the Promise resolves, subsequent calls will return the cached value. If you need to ensure the
171
+ * value is fully resolved, use `pick()` instead, which returns a Promise that you can await.
172
+ *
173
+ * To read a value without creating a dependency, use `pick()` instead.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * effect((t) => {
178
+ * const tracked = $state.get(t); // Synchronous - returns resolved value, creates dependency
179
+ * const untracked = await $other.pick(); // Async - no dependency
180
+ * });
181
+ * ```
182
+ *
183
+ * @throws Error if the node has been disposed.
184
+ *
185
+ * @public
186
+ */
187
+ get(tracker: FlowTracker): T | undefined {
188
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
189
+ this.watch(tracker);
190
+ return this._value;
191
+ }
192
+
193
+ /**
194
+ * Updates the node with a new value.
195
+ *
196
+ * @param value - A new Promise or a callback function that computes a new Promise based on the current value.
197
+ *
198
+ * @returns A promise that resolves after the update is processed and all dependent effects have been notified.
199
+ *
200
+ * @remarks
201
+ * This method can be used in two ways:
202
+ *
203
+ * **For mutable state nodes** (constructed with a constant Promise):
204
+ * - Updates the stored Promise directly
205
+ * - All dependents are notified of the change
206
+ *
207
+ * **For computed nodes** (constructed with a compute function):
208
+ * - Temporarily overrides the computed value
209
+ * - The override persists until the next recomputation, which occurs when:
210
+ * - A tracked dependency changes, or
211
+ * - The node is refreshed
212
+ * - This allows temporarily overriding computed values for testing or manual control
213
+ *
214
+ * **Value Comparison:**
215
+ * The Promises are resolved and their values are compared. If the resolved new value is strictly
216
+ * equal (`===`) to the current resolved value, no update occurs and subscribers are not notified.
217
+ * This prevents unnecessary re-renders and effect executions.
218
+ *
219
+ * **Asynchronous Processing:**
220
+ * The update is processed asynchronously through the reactive graph, ensuring proper
221
+ * ordering of updates and effect execution. The method returns a Promise that resolves
222
+ * after the update is complete.
223
+ *
224
+ * @throws Error if the node has been disposed.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // Mutable state usage
229
+ * const $count = new FlowNodeAwait(Promise.resolve(0));
230
+ * await $count.set(Promise.resolve(5));
231
+ * await $count.set(async (current) => Promise.resolve(current + 1)); // 6
232
+ *
233
+ * // Temporary override of computed value
234
+ * const $source = new FlowNodeAwait(Promise.resolve(10));
235
+ * const $doubled = new FlowNodeAwait(async (t) => {
236
+ * const val = $source.get(t); // Synchronous - returns resolved value
237
+ * return val * 2;
238
+ * });
239
+ *
240
+ * console.log(await $doubled.pick()); // 20
241
+ *
242
+ * // Temporarily override
243
+ * await $doubled.set(Promise.resolve(50));
244
+ * console.log(await $doubled.pick()); // 50 (override active)
245
+ *
246
+ * // Dependency change clears override
247
+ * await $source.set(Promise.resolve(15));
248
+ * console.log(await $doubled.pick()); // 30 (recomputed, override cleared)
249
+ * ```
250
+ *
251
+ * @public
252
+ */
253
+ async set(
254
+ value: T | Promise<T> | ((current: T) => Promise<T>),
255
+ ): Promise<void> {
256
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
257
+
258
+ const update = async () => {
259
+ this._dirty = false;
260
+
261
+ // compute current value
262
+ const currentValue = this._value as T;
263
+
264
+ // compute new value
265
+ let nextValue: T;
266
+ switch (true) {
267
+ case value instanceof Promise: {
268
+ nextValue = await value;
269
+ break;
270
+ }
271
+ case typeof value === "function": {
272
+ nextValue = await (value as (current: T) => Promise<T>)(currentValue);
273
+ break;
274
+ }
275
+ default: {
276
+ nextValue = value as T;
277
+ break;
278
+ }
279
+ }
280
+
281
+ // assign new value
282
+ this._value = nextValue;
283
+
284
+ // return value changed check
285
+ return nextValue !== currentValue;
286
+ };
287
+
288
+ const notify = () => {
289
+ // call super method to avoid setting dirty flag back to true
290
+ super._notify();
291
+ };
292
+
293
+ return FlowGraph.requestWrite(notify, update);
294
+ }
295
+
296
+ /**
297
+ * Forces recomputation of the value, even if it's not marked as dirty.
298
+ *
299
+ * @returns A promise that resolves after the recomputation is complete and all
300
+ * dependent effects have been notified.
301
+ *
302
+ * @remarks
303
+ * This method is useful when you need to force a recomputation of a computed value,
304
+ * for example when the computation depends on external data that has changed outside
305
+ * the reactive system.
306
+ *
307
+ * **Behavior:**
308
+ * - For nodes with a compute function: Forces the compute function to run again,
309
+ * even if no dependencies have changed. This effectively clears any temporary
310
+ * override that was set using `set()`.
311
+ * - For nodes without a compute function (mutable state): Recomputes the current
312
+ * value (which is just the stored value), useful for consistency but typically
313
+ * not necessary.
314
+ *
315
+ * The recomputation happens asynchronously through the reactive graph, ensuring
316
+ * proper ordering of updates and effect execution.
317
+ *
318
+ * @throws Error if the node has been disposed.
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const $externalData = new FlowNodeAwait(async () => fetchExternalData());
323
+ *
324
+ * // Some external event occurs that changes the data source
325
+ * externalDataChanged();
326
+ *
327
+ * // Force recomputation to get the new value
328
+ * await $externalData.refresh();
329
+ *
330
+ * // For computed nodes with temporary overrides
331
+ * const $source = new FlowNodeAwait(Promise.resolve(10));
332
+ * const $computed = new FlowNodeAwait(async (t) => {
333
+ * const val = $source.get(t); // Synchronous - returns resolved value
334
+ * return val * 2;
335
+ * });
336
+ * await $computed.set(Promise.resolve(100)); // Temporary override
337
+ * await $computed.refresh(); // Clears override, recomputes from source
338
+ * ```
339
+ *
340
+ * @public
341
+ */
342
+ async refresh(): Promise<void> {
343
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
344
+
345
+ const update = async () => {
346
+ // compute current value
347
+ await this._computeValue();
348
+ const currentValue = this._value as T;
349
+
350
+ // compute new value
351
+ await this._computeValue({ force: true });
352
+ const nextValue = this._value as T;
353
+
354
+ // return value changed check
355
+ return nextValue !== currentValue;
356
+ };
357
+
358
+ const notify = () => {
359
+ // call super method to avoid setting dirty flag back to true
360
+ super._notify();
361
+ };
362
+
363
+ return await FlowGraph.requestWrite(notify, update);
364
+ }
365
+
366
+ /**
367
+ * Gets the current value without any dependency tracking.
368
+ *
369
+ * @returns A promise that resolves with the current value of type T.
370
+ *
371
+ * @remarks
372
+ * This method reads the value asynchronously through the reactive graph, ensuring proper ordering of
373
+ * read operations. Unlike `get(t)`, this method does not create a reactive dependency.
374
+ *
375
+ * **Important:** This is an async method that returns `Promise<T>`. Always use `await` when calling it.
376
+ * This ensures the Promise is fully resolved before returning the value. This is different from `get()`,
377
+ * which returns the cached resolved value synchronously (or `undefined` if not yet resolved).
378
+ *
379
+ * Use `pick()` when you want to read a snapshot of the current value without creating a reactive
380
+ * dependency. This is useful for:
381
+ * - Reading initial values outside reactive contexts
382
+ * - Accessing configuration that shouldn't trigger updates
383
+ * - Ensuring the value is fully resolved before using it
384
+ * - Mixing tracked and untracked reads in the same effect
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * // Read a snapshot outside reactive context
389
+ * const currentValue = await $state.pick();
390
+ *
391
+ * // Mix tracked and untracked reads
392
+ * effect(async (t) => {
393
+ * const tracked = $reactive.get(t); // Synchronous - triggers re-runs
394
+ * const snapshot = await $config.pick(); // Async - doesn't trigger re-runs
395
+ * processData(tracked, snapshot);
396
+ * });
397
+ * ```
398
+ *
399
+ * @throws Error if the node has been disposed.
400
+ *
401
+ * @public
402
+ */
403
+ async pick(): Promise<T> {
404
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
405
+ await FlowGraph.requestRead(() => this._computeValue());
406
+ return this._value as T;
407
+ }
408
+
409
+ /**
410
+ * Subscribes a listener function to changes of this node.
411
+ *
412
+ * @param listener - A callback function that receives the new resolved value whenever it changes.
413
+ *
414
+ * @returns A disposer function that cancels the subscription when called.
415
+ *
416
+ * @remarks
417
+ * This method creates a reactive subscription that automatically tracks this node as a dependency.
418
+ * The listener is executed:
419
+ * - Immediately with the current resolved value when the subscription is created
420
+ * - Automatically whenever the value changes
421
+ *
422
+ * **Important:** The listener receives the resolved value directly (`T | undefined`), not a Promise.
423
+ * The value may be `undefined` if the Promise hasn't resolved yet on the first call. The Promise
424
+ * is awaited internally and the resolved value is cached, so the listener receives the same value
425
+ * that would be returned by `get()`.
426
+ *
427
+ * The subscription uses a {@link FlowEffect} internally to manage the reactive tracking and
428
+ * automatic re-execution. When the value changes, the listener is called with the new resolved
429
+ * value after the update is processed through the reactive graph.
430
+ *
431
+ * **Cleanup:**
432
+ * Always call the returned disposer function when you no longer need the subscription to
433
+ * prevent memory leaks and unnecessary computations.
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const $count = new FlowNodeAwait(Promise.resolve(0));
438
+ *
439
+ * // Subscribe to changes
440
+ * const unsubscribe = $count.subscribe((value) => {
441
+ * console.log(`Count is now: ${value}`); // value is T | undefined
442
+ * });
443
+ * // Logs immediately: "Count is now: 0"
444
+ *
445
+ * await $count.set(Promise.resolve(5));
446
+ * // Logs: "Count is now: 5"
447
+ *
448
+ * // Clean up when done
449
+ * unsubscribe();
450
+ * ```
451
+ *
452
+ * @throws Error if the node has been disposed.
453
+ *
454
+ * @public
455
+ */
456
+ subscribe(listener: (value: T | undefined) => void): () => void {
457
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
458
+ const effect = new FlowEffect((t) => {
459
+ listener(this.get(t));
460
+ });
461
+ return () => effect.dispose();
462
+ }
463
+
464
+ /* INTERNAL --------------------------------------------------------- */
465
+
466
+ /* @internal */ override _notify(): void {
467
+ if (this._dirty) {
468
+ return;
469
+ }
470
+ this._dirty = true;
471
+ super._notify();
472
+ }
473
+
474
+ private async _computeValue(options?: { force?: boolean }): Promise<void> {
475
+ if (!this._dirty && !options?.force) return;
476
+
477
+ if (!this._compute) {
478
+ this._dirty = false;
479
+ return;
480
+ }
481
+
482
+ this._dirty = false;
483
+
484
+ // Store current dependencies
485
+ const dependencies = [...this._dependencies];
486
+
487
+ // Clear current dependencies, compute and retrack dependencies
488
+ this._dependencies.clear();
489
+
490
+ // Compute the value and wait until all dependencies are computed
491
+ const previousValue = this._value;
492
+ this._value = await this._compute(this);
493
+
494
+ // Unsubscribe from dependencies that are no longer needed
495
+ const dependenciesToRemove = dependencies.filter(
496
+ (dependency) => !this._dependencies.has(dependency),
497
+ );
498
+ dependenciesToRemove.forEach((dependency) => {
499
+ dependency._unregisterDependency(this);
500
+ });
501
+
502
+ // Notify dependencies
503
+ // use super method to avoid setting dirty flag back to true
504
+ if (!options?.force && previousValue !== this._value) {
505
+ FlowGraph.requestTrigger(() => super._notify());
506
+ }
507
+ }
508
+ }
@@ -0,0 +1,89 @@
1
+ import type { FlowNodeAwait } from "./flowNodeAwait";
2
+
3
+ /**
4
+ * A generic utility type for creating read-only views of reactive nodes, with asynchronous values.
5
+ *
6
+ * @typeParam T - The type of the value held by the reactive node (not the Promise itself).
7
+ *
8
+ * @remarks
9
+ * `FlowReadonlyAwait` is a type alias based on {@link FlowNodeAwait} with the `set()` and `refresh()`
10
+ * methods removed, making it a read-only view. It provides all the reactive capabilities of `FlowNodeAwait`
11
+ * (dependency tracking, lazy computation, caching, reading values) but prevents mutation.
12
+ *
13
+ * **Key Difference from FlowReadonlyAsync:**
14
+ * Unlike {@link FlowReadonlyAsync}, which returns Promises from `get()`, FlowReadonlyAwait returns
15
+ * the resolved value directly. This makes the API more convenient for reactive code that doesn't want
16
+ * to deal with Promises in every access. The Promise is awaited internally and the resolved value is cached.
17
+ *
18
+ * **Asynchronous Nature:**
19
+ * FlowReadonlyAwait works with Promises internally, but provides a synchronous-looking API:
20
+ * - `get(t)` returns the resolved value synchronously (`T | undefined`). The value may be `undefined`
21
+ * if the Promise hasn't resolved yet on first access.
22
+ * - `pick()` returns a `Promise<T>` that must be awaited. Use this when you need to ensure the value
23
+ * is fully resolved before proceeding.
24
+ * - The Promise is awaited internally and the resolved value is cached until dependencies change.
25
+ *
26
+ * **Difference from FlowConstantAwait and FlowDerivationAwait:**
27
+ * - {@link FlowConstantAwait}: Specialized type for immutable constants that compute once and never change
28
+ * - {@link FlowDerivationAwait}: Specialized type for computed values that automatically recompute when dependencies change
29
+ * - `FlowReadonlyAwait`: Generic utility type that can be used with any `FlowNodeAwait` to create a read-only view
30
+ *
31
+ * **Use Cases:**
32
+ * - **Type Safety**: Ensure functions don't accidentally mutate state by accepting `FlowReadonlyAwait` instead of `FlowStateAwait`
33
+ * - **API Design**: Expose read-only views of internal state to prevent external mutation
34
+ * - **Function Parameters**: Accept any reactive node (state, derivation, constant) but prevent mutation
35
+ * - **Composition**: Create read-only wrappers around mutable states for safer sharing
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Type safety: Function that only reads state
40
+ * function displayCount(count: FlowReadonlyAwait<number>) {
41
+ * // count.set(Promise.resolve(100)); // TypeScript error: Property 'set' does not exist
42
+ * effect((t) => {
43
+ * const value = count.get(t); // Synchronous - returns resolved value
44
+ * console.log(value);
45
+ * });
46
+ * }
47
+ *
48
+ * const $count = stateAwait(Promise.resolve(0));
49
+ * displayCount($count); // OK: FlowStateAwait is assignable to FlowReadonlyAwait
50
+ *
51
+ * // API design: Expose read-only view
52
+ * class Counter {
53
+ * private _count = stateAwait(Promise.resolve(0));
54
+ *
55
+ * get count(): FlowReadonlyAwait<number> {
56
+ * return this._count; // FlowStateAwait is assignable to FlowReadonlyAwait
57
+ * }
58
+ *
59
+ * async increment() {
60
+ * await this._count.set(async (current) => Promise.resolve(current + 1)); // Internal mutation allowed
61
+ * }
62
+ * }
63
+ *
64
+ * const counter = new Counter();
65
+ * // counter.count.set(Promise.resolve(100)); // TypeScript error: Cannot mutate read-only view
66
+ * const current = await counter.count.pick(); // Async - returns Promise
67
+ *
68
+ * // Works with derivations too
69
+ * const $firstName = stateAwait(Promise.resolve('John'));
70
+ * const $lastName = stateAwait(Promise.resolve('Doe'));
71
+ * const $fullName = derivationAwait(async (t) => {
72
+ * const first = $firstName.get(t); // Synchronous - returns resolved value
73
+ * const last = $lastName.get(t); // Synchronous - returns resolved value
74
+ * return `${first} ${last}`;
75
+ * });
76
+ *
77
+ * function displayName(name: FlowReadonlyAwait<string>) {
78
+ * effect((t) => {
79
+ * const fullName = name.get(t); // Synchronous - returns resolved value
80
+ * console.log(fullName);
81
+ * });
82
+ * }
83
+ *
84
+ * displayName($fullName); // OK: FlowDerivationAwait is assignable to FlowReadonlyAwait
85
+ * ```
86
+ *
87
+ * @public
88
+ */
89
+ export type FlowReadonlyAwait<T> = Omit<FlowNodeAwait<T>, "set" | "refresh">;