@ersbeth/picoflow 0.2.4 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.cursor/plans/update-js-e795d61b.plan.md +567 -0
  2. package/.gitlab-ci.yml +24 -0
  3. package/.vscode/settings.json +3 -3
  4. package/CHANGELOG.md +51 -0
  5. package/IMPLEMENTATION_GUIDE.md +1578 -0
  6. package/README.md +9 -134
  7. package/biome.json +32 -32
  8. package/dist/picoflow.js +610 -436
  9. package/dist/types/advanced/array.d.ts +0 -6
  10. package/dist/types/advanced/array.d.ts.map +1 -1
  11. package/dist/types/advanced/index.d.ts +5 -5
  12. package/dist/types/advanced/index.d.ts.map +1 -1
  13. package/dist/types/advanced/map.d.ts +114 -23
  14. package/dist/types/advanced/map.d.ts.map +1 -1
  15. package/dist/types/advanced/resource.d.ts +51 -12
  16. package/dist/types/advanced/resource.d.ts.map +1 -1
  17. package/dist/types/advanced/resourceAsync.d.ts +28 -13
  18. package/dist/types/advanced/resourceAsync.d.ts.map +1 -1
  19. package/dist/types/advanced/stream.d.ts +74 -16
  20. package/dist/types/advanced/stream.d.ts.map +1 -1
  21. package/dist/types/advanced/streamAsync.d.ts +69 -15
  22. package/dist/types/advanced/streamAsync.d.ts.map +1 -1
  23. package/dist/types/basic/constant.d.ts +44 -16
  24. package/dist/types/basic/constant.d.ts.map +1 -1
  25. package/dist/types/basic/derivation.d.ts +73 -24
  26. package/dist/types/basic/derivation.d.ts.map +1 -1
  27. package/dist/types/basic/disposable.d.ts +65 -6
  28. package/dist/types/basic/disposable.d.ts.map +1 -1
  29. package/dist/types/basic/effect.d.ts +27 -16
  30. package/dist/types/basic/effect.d.ts.map +1 -1
  31. package/dist/types/basic/index.d.ts +7 -8
  32. package/dist/types/basic/index.d.ts.map +1 -1
  33. package/dist/types/basic/observable.d.ts +62 -13
  34. package/dist/types/basic/observable.d.ts.map +1 -1
  35. package/dist/types/basic/signal.d.ts +35 -6
  36. package/dist/types/basic/signal.d.ts.map +1 -1
  37. package/dist/types/basic/state.d.ts +25 -4
  38. package/dist/types/basic/state.d.ts.map +1 -1
  39. package/dist/types/basic/trackingContext.d.ts +33 -0
  40. package/dist/types/basic/trackingContext.d.ts.map +1 -0
  41. package/dist/types/creators.d.ts +271 -26
  42. package/dist/types/creators.d.ts.map +1 -1
  43. package/dist/types/index.d.ts +60 -7
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/solid/converters.d.ts +5 -5
  46. package/dist/types/solid/converters.d.ts.map +1 -1
  47. package/dist/types/solid/index.d.ts +2 -2
  48. package/dist/types/solid/index.d.ts.map +1 -1
  49. package/dist/types/solid/primitives.d.ts +96 -4
  50. package/dist/types/solid/primitives.d.ts.map +1 -1
  51. package/docs/.vitepress/config.mts +110 -0
  52. package/docs/api/classes/FlowArray.md +489 -0
  53. package/docs/api/classes/FlowConstant.md +350 -0
  54. package/docs/api/classes/FlowDerivation.md +334 -0
  55. package/docs/api/classes/FlowEffect.md +100 -0
  56. package/docs/api/classes/FlowMap.md +512 -0
  57. package/docs/api/classes/FlowObservable.md +306 -0
  58. package/docs/api/classes/FlowResource.md +380 -0
  59. package/docs/api/classes/FlowResourceAsync.md +362 -0
  60. package/docs/api/classes/FlowSignal.md +160 -0
  61. package/docs/api/classes/FlowState.md +368 -0
  62. package/docs/api/classes/FlowStream.md +367 -0
  63. package/docs/api/classes/FlowStreamAsync.md +364 -0
  64. package/docs/api/classes/SolidDerivation.md +75 -0
  65. package/docs/api/classes/SolidResource.md +91 -0
  66. package/docs/api/classes/SolidState.md +71 -0
  67. package/docs/api/classes/TrackingContext.md +33 -0
  68. package/docs/api/functions/array.md +58 -0
  69. package/docs/api/functions/constant.md +45 -0
  70. package/docs/api/functions/derivation.md +53 -0
  71. package/docs/api/functions/effect.md +49 -0
  72. package/docs/api/functions/from.md +220 -0
  73. package/docs/api/functions/isDisposable.md +49 -0
  74. package/docs/api/functions/map.md +57 -0
  75. package/docs/api/functions/resource.md +52 -0
  76. package/docs/api/functions/resourceAsync.md +50 -0
  77. package/docs/api/functions/signal.md +36 -0
  78. package/docs/api/functions/state.md +47 -0
  79. package/docs/api/functions/stream.md +53 -0
  80. package/docs/api/functions/streamAsync.md +50 -0
  81. package/docs/api/index.md +118 -0
  82. package/docs/api/interfaces/FlowDisposable.md +65 -0
  83. package/docs/api/interfaces/SolidObservable.md +19 -0
  84. package/docs/api/type-aliases/FlowArrayAction.md +49 -0
  85. package/docs/api/type-aliases/FlowStreamDisposer.md +15 -0
  86. package/docs/api/type-aliases/FlowStreamSetter.md +27 -0
  87. package/docs/api/type-aliases/FlowStreamUpdater.md +32 -0
  88. package/docs/api/type-aliases/NotPromise.md +18 -0
  89. package/docs/api/type-aliases/SolidGetter.md +17 -0
  90. package/docs/api/typedoc-sidebar.json +1 -0
  91. package/docs/examples/examples.md +2313 -0
  92. package/docs/examples/patterns.md +649 -0
  93. package/docs/guide/advanced/disposal.md +426 -0
  94. package/docs/guide/advanced/solidjs.md +221 -0
  95. package/docs/guide/advanced/upgrading.md +464 -0
  96. package/docs/guide/introduction/concepts.md +56 -0
  97. package/docs/guide/introduction/conventions.md +61 -0
  98. package/docs/guide/introduction/getting-started.md +134 -0
  99. package/docs/guide/introduction/lifecycle.md +371 -0
  100. package/docs/guide/primitives/array.md +400 -0
  101. package/docs/guide/primitives/constant.md +380 -0
  102. package/docs/guide/primitives/derivations.md +348 -0
  103. package/docs/guide/primitives/effects.md +458 -0
  104. package/docs/guide/primitives/map.md +387 -0
  105. package/docs/guide/primitives/overview.md +175 -0
  106. package/docs/guide/primitives/resources.md +858 -0
  107. package/docs/guide/primitives/signal.md +259 -0
  108. package/docs/guide/primitives/state.md +368 -0
  109. package/docs/guide/primitives/streams.md +931 -0
  110. package/docs/index.md +47 -0
  111. package/docs/public/logo.svg +1 -0
  112. package/package.json +57 -41
  113. package/src/advanced/array.ts +208 -210
  114. package/src/advanced/index.ts +7 -7
  115. package/src/advanced/map.ts +178 -68
  116. package/src/advanced/resource.ts +87 -43
  117. package/src/advanced/resourceAsync.ts +62 -42
  118. package/src/advanced/stream.ts +113 -50
  119. package/src/advanced/streamAsync.ts +120 -61
  120. package/src/basic/constant.ts +82 -49
  121. package/src/basic/derivation.ts +128 -84
  122. package/src/basic/disposable.ts +74 -15
  123. package/src/basic/effect.ts +85 -77
  124. package/src/basic/index.ts +7 -8
  125. package/src/basic/observable.ts +94 -36
  126. package/src/basic/signal.ts +133 -105
  127. package/src/basic/state.ts +46 -25
  128. package/src/basic/trackingContext.ts +45 -0
  129. package/src/creators.ts +297 -54
  130. package/src/index.ts +96 -43
  131. package/src/solid/converters.ts +186 -67
  132. package/src/solid/index.ts +8 -2
  133. package/src/solid/primitives.ts +167 -65
  134. package/test/array.test.ts +592 -612
  135. package/test/constant.test.ts +31 -33
  136. package/test/derivation.test.ts +531 -536
  137. package/test/effect.test.ts +21 -21
  138. package/test/map.test.ts +233 -137
  139. package/test/resource.test.ts +119 -121
  140. package/test/resourceAsync.test.ts +98 -100
  141. package/test/signal.test.ts +51 -55
  142. package/test/state.test.ts +186 -168
  143. package/test/stream.test.ts +189 -189
  144. package/test/streamAsync.test.ts +186 -186
  145. package/tsconfig.json +19 -18
  146. package/typedoc.json +37 -0
  147. package/vite.config.ts +23 -23
  148. package/vitest.config.ts +7 -7
  149. package/api/doc/index.md +0 -31
  150. package/api/doc/picoflow.array.md +0 -55
  151. package/api/doc/picoflow.constant.md +0 -55
  152. package/api/doc/picoflow.derivation.md +0 -55
  153. package/api/doc/picoflow.effect.md +0 -55
  154. package/api/doc/picoflow.flowarray._constructor_.md +0 -49
  155. package/api/doc/picoflow.flowarray._lastaction.md +0 -13
  156. package/api/doc/picoflow.flowarray.clear.md +0 -17
  157. package/api/doc/picoflow.flowarray.dispose.md +0 -55
  158. package/api/doc/picoflow.flowarray.get.md +0 -19
  159. package/api/doc/picoflow.flowarray.length.md +0 -13
  160. package/api/doc/picoflow.flowarray.md +0 -273
  161. package/api/doc/picoflow.flowarray.pop.md +0 -17
  162. package/api/doc/picoflow.flowarray.push.md +0 -53
  163. package/api/doc/picoflow.flowarray.set.md +0 -53
  164. package/api/doc/picoflow.flowarray.setitem.md +0 -69
  165. package/api/doc/picoflow.flowarray.shift.md +0 -17
  166. package/api/doc/picoflow.flowarray.splice.md +0 -85
  167. package/api/doc/picoflow.flowarray.unshift.md +0 -53
  168. package/api/doc/picoflow.flowarrayaction.md +0 -37
  169. package/api/doc/picoflow.flowconstant._constructor_.md +0 -49
  170. package/api/doc/picoflow.flowconstant.get.md +0 -25
  171. package/api/doc/picoflow.flowconstant.md +0 -88
  172. package/api/doc/picoflow.flowderivation._constructor_.md +0 -49
  173. package/api/doc/picoflow.flowderivation.get.md +0 -23
  174. package/api/doc/picoflow.flowderivation.md +0 -86
  175. package/api/doc/picoflow.flowdisposable.dispose.md +0 -55
  176. package/api/doc/picoflow.flowdisposable.md +0 -43
  177. package/api/doc/picoflow.floweffect._constructor_.md +0 -54
  178. package/api/doc/picoflow.floweffect.dispose.md +0 -21
  179. package/api/doc/picoflow.floweffect.disposed.md +0 -13
  180. package/api/doc/picoflow.floweffect.md +0 -131
  181. package/api/doc/picoflow.flowgetter.md +0 -15
  182. package/api/doc/picoflow.flowmap._lastdeleted.md +0 -21
  183. package/api/doc/picoflow.flowmap._lastset.md +0 -21
  184. package/api/doc/picoflow.flowmap.delete.md +0 -61
  185. package/api/doc/picoflow.flowmap.md +0 -133
  186. package/api/doc/picoflow.flowmap.setat.md +0 -77
  187. package/api/doc/picoflow.flowobservable.get.md +0 -19
  188. package/api/doc/picoflow.flowobservable.md +0 -68
  189. package/api/doc/picoflow.flowobservable.subscribe.md +0 -55
  190. package/api/doc/picoflow.flowresource._constructor_.md +0 -49
  191. package/api/doc/picoflow.flowresource.fetch.md +0 -27
  192. package/api/doc/picoflow.flowresource.get.md +0 -23
  193. package/api/doc/picoflow.flowresource.md +0 -100
  194. package/api/doc/picoflow.flowresourceasync._constructor_.md +0 -49
  195. package/api/doc/picoflow.flowresourceasync.fetch.md +0 -27
  196. package/api/doc/picoflow.flowresourceasync.get.md +0 -23
  197. package/api/doc/picoflow.flowresourceasync.md +0 -100
  198. package/api/doc/picoflow.flowsignal.dispose.md +0 -59
  199. package/api/doc/picoflow.flowsignal.disposed.md +0 -18
  200. package/api/doc/picoflow.flowsignal.md +0 -112
  201. package/api/doc/picoflow.flowsignal.trigger.md +0 -21
  202. package/api/doc/picoflow.flowstate.md +0 -52
  203. package/api/doc/picoflow.flowstate.set.md +0 -61
  204. package/api/doc/picoflow.flowstream._constructor_.md +0 -49
  205. package/api/doc/picoflow.flowstream.dispose.md +0 -21
  206. package/api/doc/picoflow.flowstream.get.md +0 -23
  207. package/api/doc/picoflow.flowstream.md +0 -100
  208. package/api/doc/picoflow.flowstreamasync._constructor_.md +0 -54
  209. package/api/doc/picoflow.flowstreamasync.dispose.md +0 -21
  210. package/api/doc/picoflow.flowstreamasync.get.md +0 -23
  211. package/api/doc/picoflow.flowstreamasync.md +0 -100
  212. package/api/doc/picoflow.flowstreamdisposer.md +0 -13
  213. package/api/doc/picoflow.flowstreamsetter.md +0 -13
  214. package/api/doc/picoflow.flowstreamupdater.md +0 -19
  215. package/api/doc/picoflow.flowwatcher.md +0 -15
  216. package/api/doc/picoflow.from.md +0 -55
  217. package/api/doc/picoflow.from_1.md +0 -55
  218. package/api/doc/picoflow.from_2.md +0 -55
  219. package/api/doc/picoflow.from_3.md +0 -55
  220. package/api/doc/picoflow.from_4.md +0 -55
  221. package/api/doc/picoflow.from_5.md +0 -55
  222. package/api/doc/picoflow.isdisposable.md +0 -55
  223. package/api/doc/picoflow.map.md +0 -59
  224. package/api/doc/picoflow.md +0 -544
  225. package/api/doc/picoflow.resource.md +0 -55
  226. package/api/doc/picoflow.resourceasync.md +0 -55
  227. package/api/doc/picoflow.signal.md +0 -19
  228. package/api/doc/picoflow.solidderivation._constructor_.md +0 -49
  229. package/api/doc/picoflow.solidderivation.get.md +0 -13
  230. package/api/doc/picoflow.solidderivation.md +0 -94
  231. package/api/doc/picoflow.solidgetter.md +0 -13
  232. package/api/doc/picoflow.solidobservable.get.md +0 -13
  233. package/api/doc/picoflow.solidobservable.md +0 -57
  234. package/api/doc/picoflow.solidresource._constructor_.md +0 -49
  235. package/api/doc/picoflow.solidresource.get.md +0 -13
  236. package/api/doc/picoflow.solidresource.latest.md +0 -13
  237. package/api/doc/picoflow.solidresource.md +0 -157
  238. package/api/doc/picoflow.solidresource.refetch.md +0 -13
  239. package/api/doc/picoflow.solidresource.state.md +0 -13
  240. package/api/doc/picoflow.solidstate._constructor_.md +0 -49
  241. package/api/doc/picoflow.solidstate.get.md +0 -13
  242. package/api/doc/picoflow.solidstate.md +0 -115
  243. package/api/doc/picoflow.solidstate.set.md +0 -13
  244. package/api/doc/picoflow.state.md +0 -55
  245. package/api/doc/picoflow.stream.md +0 -55
  246. package/api/doc/picoflow.streamasync.md +0 -55
  247. package/api/picoflow.public.api.md +0 -244
  248. package/api-extractor.json +0 -61
@@ -1,103 +1,147 @@
1
- import { type FlowGetter, FlowObservable } from "./observable";
2
- import type { FlowWatcher } from "./signal";
1
+ import { FlowObservable } from "./observable";
2
+ import { TrackingContext } from "./trackingContext";
3
3
 
4
4
  /**
5
5
  * Represents a reactive derivation whose value is computed based on other reactive signals.
6
+ *
6
7
  * @remarks
7
- * It tracks dependencies automatically and recomputes its value when any dependency changes.
8
- * Use FlowDerivation to create derived values in a reactive manner. It lazily initializes the computed value,
9
- * ensuring that computations only occur when necessary.
10
- * @typeparam T - The type of the computed value.
8
+ * FlowDerivation creates a computed value that automatically tracks its dependencies and
9
+ * recomputes when any dependency changes. The computation is lazy: it doesn't execute until
10
+ * the value is first accessed (via `get()` or `pick()`), and subsequent accesses return the
11
+ * cached value until a dependency changes.
12
+ *
13
+ * **Lazy Initialization:**
14
+ * The compute function doesn't run immediately upon creation. It runs only when:
15
+ * - The value is first read via `get()` or `pick()`
16
+ * - The derivation is watched via `watch()`
17
+ *
18
+ * **Dirty Checking and Recomputation:**
19
+ * When a tracked dependency changes, the derivation is marked as "dirty" but doesn't recompute
20
+ * immediately. Recomputation happens lazily on the next value access. This prevents unnecessary
21
+ * computations when multiple dependencies change in quick succession.
22
+ *
23
+ * **Dynamic Dependencies:**
24
+ * Dependencies are tracked dynamically during each computation. If the compute function
25
+ * conditionally tracks different observables, the dependency graph updates automatically.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const $firstName = state('John');
30
+ * const $lastName = state('Doe');
31
+ *
32
+ * const $fullName = derivation((t) => {
33
+ * return `${$firstName.get(t)} ${$lastName.get(t)}`;
34
+ * });
35
+ *
36
+ * // Compute function hasn't run yet (lazy)
37
+ * const name = $fullName.pick(); // Now it computes
38
+ * ```
39
+ *
40
+ * @typeParam T - The type of the computed value.
11
41
  * @public
12
42
  */
13
43
  export class FlowDerivation<T> extends FlowObservable<T> {
14
- /**
15
- * Creates a new FlowDerivation.
16
- * @param compute - A function that computes the derived value. It is provided with two parameters:
17
- * a getter and a watcher that respect dependency tracking.
18
- * @public
19
- */
20
- constructor(compute: (get: FlowGetter, watch: FlowWatcher) => T) {
21
- super();
22
- this._initEager(compute);
23
- }
44
+ /**
45
+ * Creates a new FlowDerivation.
46
+ *
47
+ * @param compute - A function that computes the derived value using a tracking context.
48
+ * The function receives a TrackingContext and should use it to access dependencies via
49
+ * `.get(t)`. The function is not executed immediately; it runs lazily on first access.
50
+ *
51
+ * @public
52
+ */
53
+ constructor(compute: (t: TrackingContext) => T) {
54
+ super();
55
+ this._compute = compute;
56
+ this._trackedContext = new TrackingContext(this);
57
+ }
24
58
 
25
- /**
26
- * Gets the current derived value.
27
- * @returns The current computed value.
28
- * @remarks
29
- * This method lazily initializes and updates the derivation if it is marked as dirty. It throws an error
30
- * if the derivation has been disposed.
31
- * @public
32
- */
33
- public get(): T {
34
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
35
- this._initLazy();
36
- this._compute();
37
- return this._value;
38
- }
59
+ /**
60
+ * Internal method to get the raw value.
61
+ * @internal
62
+ */
63
+ protected _getRaw(): T {
64
+ if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
65
+ this._initLazy();
66
+ this._update();
67
+ return this._value;
68
+ }
39
69
 
40
- /* INTERNAL --------------------------------------------------------- */
70
+ /* INTERNAL --------------------------------------------------------- */
41
71
 
42
- private _initialized = false;
43
- private _dirty = false;
72
+ private _initialized = false;
73
+ private _dirty = false;
74
+ private _compute: (t: TrackingContext) => T;
75
+ private _trackedContext: TrackingContext;
44
76
 
45
- private _trackedGet: FlowGetter = (observable) => observable._getFrom(this);
46
- private _trackedWatch: FlowWatcher = (signal) => signal._watchFrom(this);
47
- private _untrackedGet: FlowGetter = (observable) => observable.get();
48
- private _untrackedWatch: FlowWatcher = (signal) => signal._watch();
49
- private _trackedCompute!: () => T;
50
- private _untrackedCompute!: () => T;
77
+ private _initLazy(): void {
78
+ if (!this._initialized) {
79
+ this._value = this._compute(this._trackedContext);
80
+ this._initialized = true;
81
+ }
82
+ }
51
83
 
52
- private _initEager(
53
- compute: (get: FlowGetter, watch: FlowWatcher) => T,
54
- ): void {
55
- this._trackedCompute = () =>
56
- compute(this._trackedGet, this._trackedWatch);
57
- this._untrackedCompute = () =>
58
- compute(this._untrackedGet, this._untrackedWatch);
59
- }
84
+ /* @internal */ private _update(): void {
85
+ if (this._dirty) {
86
+ // Store current dependencies
87
+ const dependencies = [...this._dependencies];
60
88
 
61
- private _initLazy(): void {
62
- if (!this._initialized) {
63
- this._value = this._trackedCompute();
64
- this._initialized = true;
65
- }
66
- }
89
+ // Clear current dependencies, compute and retrack dependencies
90
+ this._dependencies.clear();
91
+ this._value = this._compute(this._trackedContext);
67
92
 
68
- /* @internal */ _compute(): void {
69
- if (this._dirty) {
70
- // we cant use untrackedCompute because it will not track dependencies
71
- // that changed dynamically
93
+ // Unsubscribe from dependencies that are no longer needed
94
+ const dependenciesToRemove = dependencies.filter(
95
+ (dependency) => !this._dependencies.has(dependency),
96
+ );
97
+ dependenciesToRemove.forEach((dependency) => {
98
+ dependency._unregisterDependency(this);
99
+ });
72
100
 
73
- // store current dependencies
74
- const dependencies = [...this._dependencies];
101
+ this._dirty = false;
102
+ }
103
+ }
75
104
 
76
- // clear current dependencies, compute and retrack dependencies
77
- this._dependencies.clear();
78
- this._value = this._trackedCompute();
105
+ /* @internal */ override _notify(): void {
106
+ this._dirty = true;
107
+ super._notify();
108
+ }
79
109
 
80
- // unsubscribe from dependencies that are no longer needed
81
- const dependenciesToRemove = dependencies.filter(
82
- (dependency) => !this._dependencies.has(dependency),
83
- );
84
- dependenciesToRemove.forEach((dependency) =>
85
- dependency._unregisterDependency(this),
86
- );
110
+ /**
111
+ * Watches the derivation, registering it as a dependency in the given context.
112
+ *
113
+ * @param context - The tracking context in which to register this derivation.
114
+ *
115
+ * @remarks
116
+ * This method overrides the base `watch()` to handle a special case: when a derivation
117
+ * is watched without having its value read (e.g., `$derivation.watch(t)` instead of
118
+ * `$derivation.get(t)`), the derivation still needs to compute its value to establish
119
+ * its own dependencies. Otherwise, the derivation would be tracked but wouldn't track
120
+ * its own dependencies, breaking the reactive chain.
121
+ *
122
+ * This override ensures that:
123
+ * 1. The derivation is registered as a dependency in the provided context
124
+ * 2. The derivation computes its value (if not already computed)
125
+ * 3. The derivation tracks its own dependencies during computation
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const $derived = derivation((t) => $state.get(t) * 2);
130
+ *
131
+ * effect((t) => {
132
+ * $derived.watch(t); // Derivation computes even without reading value
133
+ * doSomething(); // Effect re-runs when $derived's dependencies change
134
+ * });
135
+ * ```
136
+ *
137
+ * @public
138
+ */
139
+ public override watch(context: TrackingContext): void {
140
+ super.watch(context); // Register this derivation as a dependency
87
141
 
88
- this._dirty = false;
89
- }
90
- }
91
-
92
- /* @internal */ override _notify(): void {
93
- this._dirty = true;
94
- super._notify();
95
- }
96
-
97
- /* @internal */ override _watch(): void {
98
- /* v8 ignore next 1 */
99
- if (this._disposed) throw new Error("[PicoFlow] Primitive is disposed");
100
- this._initLazy();
101
- this._compute();
102
- }
142
+ // Ensure the derivation is initialized and up-to-date
143
+ // This is needed when watching a derivation without reading its value
144
+ this._initLazy();
145
+ this._update();
146
+ }
103
147
  }
@@ -1,27 +1,86 @@
1
1
  /**
2
- * Represents an object with a disposable lifecycle.
2
+ * Represents an object with a disposable lifecycle that manages resources requiring cleanup.
3
+ *
3
4
  * @remarks
4
- * Objects implementing this interface require explicit resource disposal.
5
+ * FlowDisposable is the interface for PicoFlow primitives that hold resources needing
6
+ * explicit cleanup. Implementing this interface ensures that objects can properly release
7
+ * resources such as subscriptions, event listeners, and dependent effects.
8
+ *
9
+ * **Disposal Behavior:**
10
+ * All PicoFlow reactive primitives (signals, states, effects, derivations, etc.) implement
11
+ * this interface. When disposed:
12
+ * - The primitive becomes unusable and throws errors on further access
13
+ * - All subscriptions and dependencies are cleaned up
14
+ * - Memory is freed for garbage collection
15
+ *
16
+ * **Options:**
17
+ * The optional `options` parameter controls disposal behavior:
18
+ * - `{ self: true }`: Dispose only this object, leaving dependent effects/listeners active
19
+ * - `{ self: false }` or omitted: Dispose this object AND all dependent effects/listeners (cascade)
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const $state = state(0);
24
+ * const fx = effect((t) => console.log($state.get(t)));
25
+ *
26
+ * // Cascade disposal - disposes $state and all its effects
27
+ * $state.dispose();
28
+ *
29
+ * // Self-only disposal - disposes $state but leaves effects intact
30
+ * $state.dispose({ self: true });
31
+ * ```
32
+ *
5
33
  * @public
6
34
  */
7
35
  export interface FlowDisposable {
8
- /**
9
- * Disposes resources held by this object.
10
- * @param options - Options to specify disposal behavior.
11
- */
12
- dispose(options?: { self: boolean }): void;
36
+ /**
37
+ * Disposes resources held by this object.
38
+ *
39
+ * @param options - Optional configuration for disposal behavior.
40
+ * @param options.self - When true, disposes only this object without cascading to dependents.
41
+ * When false or omitted, disposes this object and all its dependent effects and listeners.
42
+ *
43
+ * @throws Error if the object has already been disposed (behavior may vary by implementation).
44
+ */
45
+ dispose(options?: { self: boolean }): void;
13
46
  }
14
47
 
15
48
  /**
16
- * Checks whether an object implements the FlowDisposable interface.
17
- * @param obj - The object to test.
18
- * @returns True if the object has a dispose method, otherwise false.
49
+ * Type guard that checks whether an object implements the FlowDisposable interface.
50
+ *
51
+ * @param obj - The object to test for disposability.
52
+ * @returns True if the object has a `dispose` method and is therefore disposable, false otherwise.
53
+ *
54
+ * @remarks
55
+ * This utility function is useful for safely checking if an object needs disposal before
56
+ * attempting cleanup operations. It performs a runtime check for the presence of a `dispose`
57
+ * method, making it safe to use with unknown types.
58
+ *
59
+ * **Common Use Cases:**
60
+ * - Conditionally disposing items in collections (arrays, maps)
61
+ * - Generic cleanup functions that handle both disposable and non-disposable objects
62
+ * - Defensive programming when working with mixed types
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * function cleanupArray<T>(items: T[]) {
67
+ * items.forEach(item => {
68
+ * if (isDisposable(item)) {
69
+ * item.dispose();
70
+ * }
71
+ * });
72
+ * }
73
+ *
74
+ * const mixed = [state(1), "string", signal(), 42];
75
+ * cleanupArray(mixed); // Only disposes the state and signal
76
+ * ```
77
+ *
19
78
  * @public
20
79
  */
21
80
  export function isDisposable(obj: unknown): obj is FlowDisposable {
22
- return (
23
- obj !== null &&
24
- obj !== undefined &&
25
- typeof (obj as FlowDisposable).dispose === "function"
26
- );
81
+ return (
82
+ obj !== null &&
83
+ obj !== undefined &&
84
+ typeof (obj as FlowDisposable).dispose === "function"
85
+ );
27
86
  }
@@ -1,96 +1,104 @@
1
- import type { FlowGetter } from "./observable";
2
- import type { FlowSignal, FlowWatcher } from "./signal";
1
+ import type { FlowSignal } from "./signal";
2
+ import { TrackingContext } from "./trackingContext";
3
3
 
4
4
  /**
5
5
  * Represents a reactive effect that executes side-effect functions based
6
6
  * on its tracked dependencies.
7
7
  *
8
8
  * @remarks
9
- * The FlowEffect executes an apply function that performs side effects,
10
- * running initially in a tracked mode to register dependencies and then in
11
- * an untracked mode to re-execute the effect on updates.
9
+ * FlowEffect executes an apply function that performs side effects. The effect always runs
10
+ * with a tracking context, allowing you to explicitly control which observables and signals
11
+ * become dependencies using `.get(t)` for tracked reads or `.pick()` for untracked reads.
12
+ *
13
+ * When any tracked dependency changes, the effect automatically re-executes. The effect
14
+ * runs immediately upon creation and whenever its dependencies trigger updates.
15
+ *
16
+ * Unlike the old API, effects in the new TrackingContext-based system always execute with
17
+ * a tracking context available. You control reactivity by choosing which observables to track
18
+ * within the effect body.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const fx = effect((t) => {
23
+ * const reactive = $stateA.get(t); // Tracked - effect re-runs when $stateA changes
24
+ * const snapshot = $stateB.pick(); // Not tracked - changes don't trigger re-runs
25
+ * console.log(reactive, snapshot);
26
+ * });
27
+ * ```
12
28
  *
13
29
  * @public
14
30
  */
15
31
  export class FlowEffect {
16
- /**
17
- * Creates a new FlowEffect.
18
- *
19
- * @param apply - A side-effect function that receives a getter and a watcher to
20
- * access and register dependencies on reactive observables and signals.
21
- *
22
- * @remarks
23
- * The provided function is executed immediately in a tracked mode to collect dependencies.
24
- * On subsequent executions, it runs in an untracked mode.
25
- *
26
- * @public
27
- */
28
- constructor(apply: (get: FlowGetter, watch: FlowWatcher) => void) {
29
- this._trackedExec = () => apply(this._trackedGet, this._trackedWatch);
30
- this._untrackedExec = () =>
31
- apply(this._untrackedGet, this._untrackedWatch);
32
- this._exec();
33
- }
32
+ /**
33
+ * Creates a new FlowEffect.
34
+ *
35
+ * @param apply - A side-effect function that receives a tracking context to
36
+ * access and register dependencies on reactive observables and signals.
37
+ *
38
+ * @remarks
39
+ * The provided function is executed immediately upon construction with a tracking context.
40
+ * Use the context parameter to call `.get(t)` on observables you want to track, or `.pick()`
41
+ * on observables you want to read without creating dependencies.
42
+ *
43
+ * @public
44
+ */
45
+ constructor(apply: (t: TrackingContext) => void) {
46
+ this._trackedContext = new TrackingContext(this);
47
+ this._apply = apply;
48
+ this._exec();
49
+ }
34
50
 
35
- /**
36
- * Disposes the effect, unregistering all its tracked dependencies.
37
- *
38
- * @remarks
39
- * Once disposed, the effect must no longer be used. Trying to dispose an effect
40
- * that is already disposed will throw an error.
41
- *
42
- * @public
43
- */
44
- public dispose(): void {
45
- if (this._disposed) throw new Error("[PicoFlow] Effect is disposed");
46
- Array.from(this._dependencies).forEach((dependency) => {
47
- this._unregisterDependency(dependency);
48
- });
49
- this._disposed = true;
50
- }
51
+ /**
52
+ * Disposes the effect, unregistering all its tracked dependencies.
53
+ *
54
+ * @remarks
55
+ * Once disposed, the effect must no longer be used. Trying to dispose an effect
56
+ * that is already disposed will throw an error.
57
+ *
58
+ * @public
59
+ */
60
+ public dispose(): void {
61
+ if (this._disposed) throw new Error("[PicoFlow] Effect is disposed");
62
+ Array.from(this._dependencies).forEach((dependency) => {
63
+ this._unregisterDependency(dependency);
64
+ });
65
+ this._disposed = true;
66
+ }
51
67
 
52
- /**
53
- * Indicates whether this effect has been disposed.
54
- *
55
- * @returns A boolean value that is true if the effect is disposed, false otherwise.
56
- *
57
- * @public
58
- */
59
- public get disposed(): boolean {
60
- return this._disposed;
61
- }
68
+ /**
69
+ * Indicates whether this effect has been disposed.
70
+ *
71
+ * @returns A boolean value that is true if the effect is disposed, false otherwise.
72
+ *
73
+ * @public
74
+ */
75
+ public get disposed(): boolean {
76
+ return this._disposed;
77
+ }
62
78
 
63
- /* INTERNAL ------------------------------------------------------------ */
79
+ /* INTERNAL ------------------------------------------------------------ */
64
80
 
65
- private _disposed = false;
66
- private _initialized = false;
67
- private _dependencies = new Set<FlowSignal>();
81
+ private _disposed = false;
82
+ private _dependencies = new Set<FlowSignal>();
83
+ private _trackedContext: TrackingContext;
84
+ private _apply: (t: TrackingContext) => void;
68
85
 
69
- private _trackedGet: FlowGetter = (observable) => observable._getFrom(this);
70
- private _trackedWatch: FlowWatcher = (signal) => signal._watchFrom(this);
71
- private _untrackedGet: FlowGetter = (observable) => observable.get();
72
- private _untrackedWatch: FlowWatcher = (signal) => signal._watch();
73
- private _trackedExec: () => void;
74
- private _untrackedExec: () => void;
86
+ /** @internal */ _exec(): void {
87
+ if (this._disposed)
88
+ /* v8 ignore next 1 */
89
+ throw new Error("[PicoFlow] Effect is disposed");
75
90
 
76
- /*@internal*/ _exec(): void {
77
- if (this._disposed)
78
- /* v8 ignore next 1 */
79
- throw new Error("[PicoFlow] Effect is disposed");
80
- if (this._initialized) this._untrackedExec();
81
- else {
82
- this._trackedExec();
83
- this._initialized = true;
84
- }
85
- }
91
+ // Always execute with tracking context
92
+ this._apply(this._trackedContext);
93
+ }
86
94
 
87
- /*@internal*/ _registerDependency(dependency: FlowSignal): void {
88
- this._dependencies.add(dependency);
89
- dependency._registerEffect(this);
90
- }
95
+ /** @internal */ _registerDependency(dependency: FlowSignal): void {
96
+ this._dependencies.add(dependency);
97
+ dependency._registerEffect(this);
98
+ }
91
99
 
92
- /*@internal*/ _unregisterDependency(dependency: FlowSignal): void {
93
- this._dependencies.delete(dependency);
94
- dependency._unregisterEffect(this);
95
- }
100
+ /** @internal */ _unregisterDependency(dependency: FlowSignal): void {
101
+ this._dependencies.delete(dependency);
102
+ dependency._unregisterEffect(this);
103
+ }
96
104
  }
@@ -1,10 +1,9 @@
1
- export { FlowSignal } from "./signal";
2
- export { FlowState } from "./state";
3
- export { FlowObservable } from "./observable";
4
- export { FlowDerivation } from "./derivation";
5
- export { FlowEffect } from "./effect";
6
1
  export { FlowConstant } from "./constant";
7
- export { isDisposable } from "./disposable";
8
- export type { FlowGetter } from "./observable";
9
- export type { FlowWatcher } from "./signal";
2
+ export { FlowDerivation } from "./derivation";
10
3
  export type { FlowDisposable } from "./disposable";
4
+ export { isDisposable } from "./disposable";
5
+ export { FlowEffect } from "./effect";
6
+ export { FlowObservable } from "./observable";
7
+ export { FlowSignal } from "./signal";
8
+ export { FlowState } from "./state";
9
+ export { TrackingContext } from "./trackingContext";