@ersbeth/picoflow 0.2.3 → 1.0.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 (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 +62 -25
  7. package/biome.json +32 -32
  8. package/dist/picoflow.js +557 -1099
  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 -20
  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
@@ -0,0 +1,858 @@
1
+ # Resources
2
+
3
+ Resources are specialized reactive primitives for managing data that needs to be fetched, loaded, or retrieved. They're perfect for API calls, file loading, and any data that requires a fetch operation.
4
+
5
+ ## Understanding Resources
6
+
7
+ A **resource** is like a state that knows how to fetch its own data. Instead of manually fetching and setting state, you provide a fetch function and let the resource manage the lifecycle.
8
+
9
+ Think of it like a **self-updating contact card** - instead of manually calling someone to get their latest info, the card knows how to call them and update itself.
10
+
11
+ ### Resources vs State
12
+
13
+ | Feature | State | Resource |
14
+ |---------|-------|----------|
15
+ | Initial value | Required | Optional (can be undefined) |
16
+ | How to update | `.set()` manually | `.fetch()` to refetch |
17
+ | Use case | User input, toggles | API data, file loading |
18
+ | Data source | Set by your code | Fetched by provided function |
19
+
20
+ ## When to Use Resources
21
+
22
+ Use resources when:
23
+ - ✅ Data comes from an API or external source
24
+ - ✅ You need to refetch data based on triggers
25
+ - ✅ You want built-in loading/undefined states
26
+ - ✅ The fetch logic should be encapsulated
27
+
28
+ Use regular state when:
29
+ - ✅ You control the data directly (form inputs, UI state)
30
+ - ✅ Data doesn't need to be fetched
31
+ - ✅ Updates come from user actions
32
+
33
+ ## Synchronous Resources
34
+
35
+ Synchronous resources use a regular function (not async) to fetch data:
36
+
37
+ ```typescript
38
+ import { resource } from '@ersbeth/picoflow'
39
+
40
+ const $config = resource(
41
+ () => {
42
+ // Fetch function (synchronous)
43
+ return loadConfigFromFile()
44
+ },
45
+ null // Initial value (optional)
46
+ )
47
+ ```
48
+
49
+ ### Creating a Resource
50
+
51
+ ```typescript
52
+ import { resource, effect } from '@ersbeth/picoflow'
53
+
54
+ // Resource with initial value
55
+ const $userData = resource(
56
+ () => fetchUserFromCache(),
57
+ { id: 0, name: 'Guest' } // Initial value
58
+ )
59
+
60
+ // Resource without initial value (starts as undefined)
61
+ const $data = resource(
62
+ () => loadData(),
63
+ undefined
64
+ )
65
+
66
+ // Use in effect
67
+ effect((t) => {
68
+ const data = $data.get(t)
69
+ if (data) {
70
+ console.log('Data loaded:', data)
71
+ } else {
72
+ console.log('No data yet')
73
+ }
74
+ })
75
+ ```
76
+
77
+ ### Fetching and Refetching
78
+
79
+ Call `.fetch()` to trigger a data fetch:
80
+
81
+ ```typescript
82
+ const $posts = resource(
83
+ () => getPosts(),
84
+ []
85
+ )
86
+
87
+ // Initial fetch
88
+ $posts.fetch()
89
+
90
+ // Later, refetch when needed
91
+ function refreshPosts() {
92
+ $posts.fetch() // Fetches and updates
93
+ }
94
+
95
+ // Automatic refetch in effect
96
+ const $userId = state(1)
97
+
98
+ effect((t) => {
99
+ const userId = $userId.get(t)
100
+ $posts.fetch() // Refetch when userId changes
101
+ })
102
+ ```
103
+
104
+ ### Resource Lifecycle
105
+
106
+ ```mermaid
107
+ stateDiagram-v2
108
+ [*] --> Idle: Create resource
109
+ Idle --> Fetching: .fetch() called
110
+ Fetching --> Ready: Fetch complete
111
+ Ready --> Fetching: .fetch() called again
112
+ Ready --> Disposed: .dispose()
113
+ Fetching --> Disposed: .dispose()
114
+ Idle --> Disposed: .dispose()
115
+ Disposed --> [*]
116
+
117
+ note right of Idle: Value is initial value<br/>or undefined
118
+ note right of Ready: Value is fetched data
119
+ ```
120
+
121
+ ## Asynchronous Resources
122
+
123
+ Asynchronous resources use async functions and return Promises:
124
+
125
+ ```typescript
126
+ import { resourceAsync } from '@ersbeth/picoflow'
127
+
128
+ const $user = resourceAsync(async () => {
129
+ const response = await fetch('/api/user')
130
+ return response.json()
131
+ })
132
+ ```
133
+
134
+ ### How Async Resources Work
135
+
136
+ ```mermaid
137
+ flowchart TD
138
+ A[Create resourceAsync] --> B[.fetch called or reactive dependency triggers]
139
+ B --> C[Execute async fetch function]
140
+ C --> D[Return Promise]
141
+ D --> E{Use .get t in effect}
142
+ E --> F[await Promise]
143
+ F --> G[Effect has resolved value]
144
+
145
+ H[Dependency changes] --> B
146
+ ```
147
+
148
+ The key difference: `.get(t)` returns a **Promise**, not the value directly.
149
+
150
+ ### Using Async Resources
151
+
152
+ ```typescript
153
+ import { resourceAsync, effect } from '@ersbeth/picoflow'
154
+
155
+ const $posts = resourceAsync(async () => {
156
+ const response = await fetch('https://api.example.com/posts')
157
+ return response.json()
158
+ })
159
+
160
+ // Use with async effect
161
+ effect(async (t) => {
162
+ const posts = await $posts.get(t) // Wait for Promise
163
+ console.log('Posts:', posts)
164
+ })
165
+
166
+ // Trigger fetch
167
+ $posts.fetch()
168
+ ```
169
+
170
+ ### Reactive Fetching
171
+
172
+ Combine with state for reactive data loading:
173
+
174
+ ```typescript
175
+ const $userId = state(1)
176
+
177
+ const $user = resourceAsync(async () => {
178
+ const id = $userId.pick() // Read current user ID
179
+ const response = await fetch(`/api/users/${id}`)
180
+ return response.json()
181
+ })
182
+
183
+ // Refetch when user ID changes
184
+ effect((t) => {
185
+ const userId = $userId.get(t)
186
+ $user.fetch() // Fetch new user data
187
+ })
188
+
189
+ // Display user data
190
+ effect(async (t) => {
191
+ const user = await $user.get(t)
192
+ displayUser(user)
193
+ })
194
+ ```
195
+
196
+ ## Handling Undefined State
197
+
198
+ Resources can be undefined initially:
199
+
200
+ ```typescript
201
+ const $data = resource(
202
+ () => loadData(),
203
+ undefined // Starts as undefined
204
+ )
205
+
206
+ effect((t) => {
207
+ const data = $data.get(t)
208
+
209
+ if (data === undefined) {
210
+ showLoading()
211
+ } else {
212
+ showData(data)
213
+ }
214
+ })
215
+
216
+ // Trigger fetch
217
+ $data.fetch()
218
+ ```
219
+
220
+ For better type safety, use a discriminated union:
221
+
222
+ ```typescript
223
+ type DataState<T> =
224
+ | { status: 'loading' }
225
+ | { status: 'loaded'; data: T }
226
+ | { status: 'error'; error: string }
227
+
228
+ const $userState = state<DataState<User>>({ status: 'loading' })
229
+
230
+ const $user = resourceAsync(async () => {
231
+ try {
232
+ $userState.set({ status: 'loading' })
233
+ const data = await fetchUser()
234
+ $userState.set({ status: 'loaded', data })
235
+ return data
236
+ } catch (error) {
237
+ $userState.set({ status: 'error', error: error.message })
238
+ throw error
239
+ }
240
+ })
241
+ ```
242
+
243
+ ## Practical Examples
244
+
245
+ ### Example 1: API Data Fetching
246
+
247
+ ```typescript
248
+ import { resourceAsync, state, effect } from '@ersbeth/picoflow'
249
+
250
+ interface Post {
251
+ id: number
252
+ title: string
253
+ body: string
254
+ }
255
+
256
+ const $posts = resourceAsync(async () => {
257
+ const response = await fetch('https://jsonplaceholder.typicode.com/posts')
258
+ if (!response.ok) throw new Error('Failed to fetch')
259
+ return response.json() as Promise<Post[]>
260
+ })
261
+
262
+ // Loading state
263
+ const $loading = state(false)
264
+
265
+ // Fetch with loading state
266
+ async function fetchPosts() {
267
+ $loading.set(true)
268
+ try {
269
+ await $posts.fetch()
270
+ } finally {
271
+ $loading.set(false)
272
+ }
273
+ }
274
+
275
+ // Display posts
276
+ effect(async (t) => {
277
+ const posts = await $posts.get(t)
278
+ const loading = $loading.get(t)
279
+
280
+ if (loading) {
281
+ showSpinner()
282
+ } else {
283
+ displayPosts(posts)
284
+ }
285
+ })
286
+
287
+ // Initial fetch
288
+ fetchPosts()
289
+ ```
290
+
291
+ ### Example 2: User Profile with Dependencies
292
+
293
+ ```typescript
294
+ const $userId = state<number | null>(null)
295
+
296
+ const $userProfile = resourceAsync(async () => {
297
+ const userId = $userId.pick()
298
+ if (!userId) throw new Error('No user ID')
299
+
300
+ const response = await fetch(`/api/users/${userId}`)
301
+ return response.json()
302
+ })
303
+
304
+ // Refetch when user ID changes
305
+ effect((t) => {
306
+ const userId = $userId.get(t)
307
+ if (userId) {
308
+ $userProfile.fetch()
309
+ }
310
+ })
311
+
312
+ // Display profile
313
+ effect(async (t) => {
314
+ try {
315
+ const profile = await $userProfile.get(t)
316
+ displayProfile(profile)
317
+ } catch (error) {
318
+ showError('Failed to load user')
319
+ }
320
+ })
321
+
322
+ // Change user
323
+ $userId.set(42) // Automatically triggers refetch
324
+ ```
325
+
326
+ ### Example 3: Configuration Loading
327
+
328
+ ```typescript
329
+ interface AppConfig {
330
+ apiUrl: string
331
+ features: Record<string, boolean>
332
+ theme: string
333
+ }
334
+
335
+ const $config = resource(
336
+ () => {
337
+ // Load from localStorage or defaults
338
+ const stored = localStorage.getItem('config')
339
+ return stored
340
+ ? JSON.parse(stored)
341
+ : { apiUrl: '/api', features: {}, theme: 'light' }
342
+ },
343
+ null // No initial value, will fetch on first access
344
+ )
345
+
346
+ // Load config on app start
347
+ $config.fetch()
348
+
349
+ // Use config throughout app
350
+ effect((t) => {
351
+ const config = $config.get(t)
352
+ if (config) {
353
+ applyTheme(config.theme)
354
+ }
355
+ })
356
+ ```
357
+
358
+ ### Example 4: File Loading
359
+
360
+ ```typescript
361
+ const $selectedFile = state<File | null>(null)
362
+
363
+ const $fileContent = resourceAsync(async () => {
364
+ const file = $selectedFile.pick()
365
+ if (!file) throw new Error('No file selected')
366
+
367
+ return file.text()
368
+ })
369
+
370
+ // Handle file selection
371
+ function handleFileSelect(file: File) {
372
+ $selectedFile.set(file)
373
+ $fileContent.fetch()
374
+ }
375
+
376
+ // Display content
377
+ effect(async (t) => {
378
+ try {
379
+ const content = await $fileContent.get(t)
380
+ displayContent(content)
381
+ } catch (error) {
382
+ showError('Failed to read file')
383
+ }
384
+ })
385
+ ```
386
+
387
+ ## Error Handling
388
+
389
+ ### Basic Error Handling
390
+
391
+ ```typescript
392
+ const $data = resourceAsync(async () => {
393
+ const response = await fetch('/api/data')
394
+ if (!response.ok) {
395
+ throw new Error(`HTTP ${response.status}`)
396
+ }
397
+ return response.json()
398
+ })
399
+
400
+ effect(async (t) => {
401
+ try {
402
+ const data = await $data.get(t)
403
+ displayData(data)
404
+ } catch (error) {
405
+ console.error('Failed to load data:', error)
406
+ showError(error.message)
407
+ }
408
+ })
409
+ ```
410
+
411
+ ### With Error State
412
+
413
+ ```typescript
414
+ const $data = resourceAsync(async () => {
415
+ const response = await fetch('/api/data')
416
+ if (!response.ok) throw new Error('Failed to fetch')
417
+ return response.json()
418
+ })
419
+
420
+ const $error = state<string | null>(null)
421
+ const $loading = state(false)
422
+
423
+ async function loadData() {
424
+ $loading.set(true)
425
+ $error.set(null)
426
+
427
+ try {
428
+ await $data.fetch()
429
+ } catch (error) {
430
+ $error.set(error.message)
431
+ } finally {
432
+ $loading.set(false)
433
+ }
434
+ }
435
+
436
+ effect(async (t) => {
437
+ const loading = $loading.get(t)
438
+ const error = $error.get(t)
439
+
440
+ if (loading) return showLoading()
441
+ if (error) return showError(error)
442
+
443
+ const data = await $data.get(t)
444
+ showData(data)
445
+ })
446
+ ```
447
+
448
+ ### Retry Logic
449
+
450
+ ```typescript
451
+ async function fetchWithRetry(
452
+ resourceFn: () => Promise<void>,
453
+ maxRetries = 3
454
+ ) {
455
+ for (let i = 0; i < maxRetries; i++) {
456
+ try {
457
+ await resourceFn()
458
+ return // Success!
459
+ } catch (error) {
460
+ if (i === maxRetries - 1) throw error // Last attempt failed
461
+ await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
462
+ }
463
+ }
464
+ }
465
+
466
+ // Usage
467
+ const $data = resourceAsync(async () => {
468
+ const response = await fetch('/api/data')
469
+ if (!response.ok) throw new Error('Failed to fetch')
470
+ return response.json()
471
+ })
472
+
473
+ fetchWithRetry(() => $data.fetch())
474
+ ```
475
+
476
+ ## Resource State Transitions
477
+
478
+ Understanding the states a resource goes through:
479
+
480
+ ```mermaid
481
+ stateDiagram-v2
482
+ [*] --> Initial: Create resource
483
+ Initial --> Fetching: fetch() called
484
+ Fetching --> Loaded: Success
485
+ Fetching --> Error: Fetch failed
486
+ Loaded --> Fetching: fetch() called
487
+ Error --> Fetching: fetch() called (retry)
488
+ Loaded --> [*]: dispose()
489
+ Error --> [*]: dispose()
490
+
491
+ note right of Initial: value = initial value<br/>or undefined
492
+ note right of Fetching: Promise pending
493
+ note right of Loaded: value = fetched data
494
+ note right of Error: Exception thrown
495
+ ```
496
+
497
+ ## Best Practices
498
+
499
+ ### 1. Provide Initial Values When Possible
500
+
501
+ ```typescript
502
+ // ✅ Good - immediate data available
503
+ const $user = resource(
504
+ () => loadUserFromCache(),
505
+ { id: 0, name: 'Guest' }
506
+ )
507
+
508
+ // ⚠️ Okay but requires undefined check
509
+ const $user = resource(
510
+ () => loadUserFromCache(),
511
+ undefined
512
+ )
513
+ ```
514
+
515
+ ### 2. Handle Errors Gracefully
516
+
517
+ ```typescript
518
+ // ✅ Always handle errors in async resources
519
+ effect(async (t) => {
520
+ try {
521
+ const data = await $resource.get(t)
522
+ showData(data)
523
+ } catch (error) {
524
+ showError(error)
525
+ }
526
+ })
527
+ ```
528
+
529
+ ### 3. Use Loading States
530
+
531
+ ```typescript
532
+ const $loading = state(false)
533
+
534
+ async function loadResource() {
535
+ $loading.set(true)
536
+ try {
537
+ await $resource.fetch()
538
+ } finally {
539
+ $loading.set(false)
540
+ }
541
+ }
542
+ ```
543
+
544
+ ### 4. Cleanup on Disposal
545
+
546
+ ```typescript
547
+ const $resource = resourceAsync(async () => {
548
+ const controller = new AbortController()
549
+
550
+ try {
551
+ const response = await fetch('/api/data', {
552
+ signal: controller.signal
553
+ })
554
+ return response.json()
555
+ } catch (error) {
556
+ if (error.name === 'AbortError') {
557
+ return null
558
+ }
559
+ throw error
560
+ }
561
+ })
562
+
563
+ // Later, dispose to cancel pending requests
564
+ $resource.dispose()
565
+ ```
566
+
567
+ ### 5. Cache Results When Appropriate
568
+
569
+ ```typescript
570
+ // Simple cache wrapper
571
+ function cachedResource<T>(
572
+ fetchFn: () => T,
573
+ cacheTime: number
574
+ ) {
575
+ let lastFetch = 0
576
+ let cached: T | undefined
577
+
578
+ return resource(
579
+ () => {
580
+ const now = Date.now()
581
+ if (cached && now - lastFetch < cacheTime) {
582
+ return cached // Return cached value
583
+ }
584
+ cached = fetchFn()
585
+ lastFetch = now
586
+ return cached
587
+ },
588
+ undefined
589
+ )
590
+ }
591
+
592
+ const $data = cachedResource(
593
+ () => expensiveFetchOperation(),
594
+ 60000 // Cache for 1 minute
595
+ )
596
+ ```
597
+
598
+ ## Comparing: Resource vs ResourceAsync
599
+
600
+ ```mermaid
601
+ flowchart TD
602
+ A{Is fetch function async?} -->|Yes| B[Use resourceAsync]
603
+ A -->|No| C[Use resource]
604
+ B --> D[Returns Promise]
605
+ C --> E[Returns value directly]
606
+ D --> F[Use await in effects]
607
+ E --> G[Use value directly]
608
+
609
+ style B fill:#FFE6E6
610
+ style C fill:#E6FFE6
611
+ ```
612
+
613
+ ## Complete Example: User Dashboard
614
+
615
+ ```typescript
616
+ import { state, resourceAsync, derivation, effect } from '@ersbeth/picoflow'
617
+
618
+ interface User {
619
+ id: number
620
+ name: string
621
+ email: string
622
+ role: string
623
+ }
624
+
625
+ interface Stats {
626
+ loginCount: number
627
+ lastLogin: string
628
+ }
629
+
630
+ // Current user ID
631
+ const $currentUserId = state<number>(1)
632
+
633
+ // User resource
634
+ const $user = resourceAsync(async () => {
635
+ const userId = $currentUserId.pick()
636
+ const response = await fetch(`/api/users/${userId}`)
637
+ if (!response.ok) throw new Error('User not found')
638
+ return response.json() as Promise<User>
639
+ })
640
+
641
+ // User stats resource
642
+ const $stats = resourceAsync(async () => {
643
+ const userId = $currentUserId.pick()
644
+ const response = await fetch(`/api/users/${userId}/stats`)
645
+ return response.json() as Promise<Stats>
646
+ })
647
+
648
+ // Loading states
649
+ const $loading = state(false)
650
+ const $error = state<string | null>(null)
651
+
652
+ // Derived: is admin
653
+ const $isAdmin = derivation(async (t) => {
654
+ const user = await $user.get(t)
655
+ return user.role === 'admin'
656
+ })
657
+
658
+ // Fetch user data when ID changes
659
+ effect((t) => {
660
+ const userId = $currentUserId.get(t)
661
+
662
+ async function loadUser() {
663
+ $loading.set(true)
664
+ $error.set(null)
665
+
666
+ try {
667
+ await Promise.all([
668
+ $user.fetch(),
669
+ $stats.fetch()
670
+ ])
671
+ } catch (error) {
672
+ $error.set(error.message)
673
+ } finally {
674
+ $loading.set(false)
675
+ }
676
+ }
677
+
678
+ loadUser()
679
+ })
680
+
681
+ // Display user info
682
+ effect(async (t) => {
683
+ const loading = $loading.get(t)
684
+ if (loading) return showLoadingSpinner()
685
+
686
+ const error = $error.get(t)
687
+ if (error) return showError(error)
688
+
689
+ try {
690
+ const user = await $user.get(t)
691
+ const stats = await $stats.get(t)
692
+
693
+ displayUserInfo(user, stats)
694
+ } catch (error) {
695
+ showError('Failed to load user data')
696
+ }
697
+ })
698
+
699
+ // Switch user
700
+ function switchUser(newUserId: number) {
701
+ $currentUserId.set(newUserId) // Automatically triggers refetch
702
+ }
703
+ ```
704
+
705
+ ## Common Patterns
706
+
707
+ ### Pattern 1: Dependent Resources
708
+
709
+ ```typescript
710
+ // First resource
711
+ const $userId = state(1)
712
+ const $user = resourceAsync(async () => {
713
+ const response = await fetch(`/api/users/${$userId.pick()}`)
714
+ return response.json()
715
+ })
716
+
717
+ // Second resource depends on first
718
+ const $userPosts = resourceAsync(async () => {
719
+ const user = await $user.get(null) // Read without tracking
720
+ const response = await fetch(`/api/users/${user.id}/posts`)
721
+ return response.json()
722
+ })
723
+
724
+ // Fetch both when user changes
725
+ effect((t) => {
726
+ $userId.get(t)
727
+
728
+ async function loadAll() {
729
+ await $user.fetch()
730
+ await $userPosts.fetch() // Fetch posts after user loads
731
+ }
732
+
733
+ loadAll()
734
+ })
735
+ ```
736
+
737
+ ### Pattern 2: Polling
738
+
739
+ ```typescript
740
+ const $liveData = resourceAsync(async () => {
741
+ const response = await fetch('/api/live-data')
742
+ return response.json()
743
+ })
744
+
745
+ // Poll every 5 seconds
746
+ const pollInterval = setInterval(() => {
747
+ $liveData.fetch()
748
+ }, 5000)
749
+
750
+ // Don't forget cleanup!
751
+ function stopPolling() {
752
+ clearInterval(pollInterval)
753
+ }
754
+ ```
755
+
756
+ ### Pattern 3: Optimistic Updates
757
+
758
+ ```typescript
759
+ const $items = state([{ id: 1, name: 'Item 1' }])
760
+
761
+ async function addItem(name: string) {
762
+ const newItem = { id: Date.now(), name }
763
+
764
+ // Optimistic update
765
+ $items.set(items => [...items, newItem])
766
+
767
+ try {
768
+ await fetch('/api/items', {
769
+ method: 'POST',
770
+ body: JSON.stringify(newItem)
771
+ })
772
+ // Success - keep optimistic update
773
+ } catch (error) {
774
+ // Rollback on error
775
+ $items.set(items => items.filter(i => i.id !== newItem.id))
776
+ showError('Failed to add item')
777
+ }
778
+ }
779
+ ```
780
+
781
+ ## Best Practices Summary
782
+
783
+ ### ✅ Do
784
+
785
+ - Handle errors in async resources
786
+ - Provide initial values when possible
787
+ - Use loading states for better UX
788
+ - Cancel requests on disposal
789
+ - Cache when appropriate
790
+
791
+ ### ❌ Don't
792
+
793
+ - Forget error handling
794
+ - Fetch unnecessarily (check if data is still fresh)
795
+ - Create resources inside effects
796
+ - Ignore loading states
797
+ - Leave pending requests on cleanup
798
+
799
+ ## Common Pitfalls
800
+
801
+ ### Pitfall 1: Not Handling Async Errors
802
+
803
+ ```typescript
804
+ // ❌ Unhandled error crashes effect
805
+ effect(async (t) => {
806
+ const data = await $resource.get(t) // Might throw!
807
+ display(data)
808
+ })
809
+
810
+ // ✅ Proper error handling
811
+ effect(async (t) => {
812
+ try {
813
+ const data = await $resource.get(t)
814
+ display(data)
815
+ } catch (error) {
816
+ handleError(error)
817
+ }
818
+ })
819
+ ```
820
+
821
+ ### Pitfall 2: Forgetting to Call `.fetch()`
822
+
823
+ ```typescript
824
+ const $data = resource(() => loadData(), undefined)
825
+
826
+ // ❌ Resource never fetches!
827
+ effect((t) => {
828
+ const data = $data.get(t) // Always undefined
829
+ })
830
+
831
+ // ✅ Trigger fetch
832
+ $data.fetch()
833
+ ```
834
+
835
+ ### Pitfall 3: Race Conditions
836
+
837
+ ```typescript
838
+ const $query = state('initial')
839
+
840
+ const $results = resourceAsync(async () => {
841
+ const query = $query.pick()
842
+ const response = await fetch(`/api/search?q=${query}`)
843
+ return response.json()
844
+ })
845
+
846
+ effect((t) => {
847
+ $query.get(t)
848
+ $results.fetch()
849
+ })
850
+
851
+ // ⚠️ Race condition if user types fast:
852
+ $query.set('a') // Fetch starts for 'a'
853
+ $query.set('ab') // Fetch starts for 'ab'
854
+ $query.set('abc') // Fetch starts for 'abc'
855
+ // Results might arrive out of order!
856
+
857
+ // ✅ Better - use AbortController or check query hasn't changed
858
+ ```