atomirx 0.0.7 → 0.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 (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -30
  33. package/dist/react/index.js +206 -791
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. package/vite.config.ts +0 -39
package/v2.md DELETED
@@ -1,725 +0,0 @@
1
- # atomirx v2 - Design Specification
2
-
3
- ## Philosophy
4
-
5
- ```
6
- Atom = Base interface (value + subscribe)
7
- MutableAtom = Atom + set/reset (raw storage)
8
- DerivedAtom = Atom<Promise<T>> + refresh + state() (computed, always async)
9
- = DerivedAtom<T, false> without fallback (staleValue: T | undefined)
10
- = DerivedAtom<T, true> with fallback (staleValue: T, guaranteed)
11
- ```
12
-
13
- ---
14
-
15
- ## Type Hierarchy
16
-
17
- ```
18
- Atom<T>
19
-
20
- ├─► MutableAtom<T> = Atom<T> & { set(), reset() }
21
- │ Created by: atom(value)
22
-
23
- └─► DerivedAtom<T, F> = Atom<Promise<T>> & { refresh(), state(), staleValue }
24
-
25
- ├─► DerivedAtom<T, false> (staleValue: T | undefined)
26
- │ Created by: derived(compute)
27
-
28
- └─► DerivedAtom<T, true> (staleValue: T, guaranteed)
29
- Created by: derived(compute, { fallback })
30
- ```
31
-
32
- ---
33
-
34
- ## Base Interface
35
-
36
- ```typescript
37
- interface AtomMeta {
38
- key: string | undefined;
39
- }
40
-
41
- interface Atom<T> {
42
- readonly value: T;
43
- readonly meta?: AtomMeta;
44
- on(listener: () => void): () => void;
45
- }
46
- ```
47
-
48
- ---
49
-
50
- ## MutableAtom
51
-
52
- Simple, raw value container. Can store anything including Promises.
53
-
54
- ### API
55
-
56
- ```typescript
57
- function atom<T>(initialValue: T, options?: AtomOptions): MutableAtom<T>;
58
-
59
- interface AtomOptions<T> {
60
- meta?: AtomMeta;
61
- equals?: EqualsFn<T>;
62
- }
63
-
64
- interface MutableAtom<T> extends Atom<T> {
65
- set(value: T | ((prev: T) => T)): void; // throws sync errors
66
- reset(): void; // restore initial value
67
- }
68
- ```
69
-
70
- ### Behavior
71
-
72
- | Operation | Behavior |
73
- | --------------- | ----------------------------------------- |
74
- | `.value` | Returns raw value (T, including Promise) |
75
- | `.set(value)` | Stores value, notifies |
76
- | `.set(reducer)` | Applies reducer, throws if reducer throws |
77
- | `.reset()` | Restores initial value |
78
-
79
- ### Examples
80
-
81
- ```typescript
82
- // Sync
83
- const count$ = atom(0);
84
- count$.value; // 0
85
- count$.set(1); // stores 1
86
- count$.set((n) => n + 1); // stores 2
87
-
88
- // Async (stores raw Promise)
89
- const posts$ = atom(fetchPosts());
90
- posts$.value; // Promise<Post[]>
91
-
92
- // Refetch - must call set() with new Promise
93
- posts$.set(fetchPosts()); // stores new Promise
94
-
95
- // Reset - restores initial Promise (does NOT refetch)
96
- posts$.reset(); // restores original Promise object
97
- ```
98
-
99
- ---
100
-
101
- ## DerivedAtom
102
-
103
- Computed value. Always returns `Promise<T>` for `.value`.
104
-
105
- ### API
106
-
107
- ```typescript
108
- // Without fallback - staleValue is T | undefined
109
- function derived<T>(
110
- compute: (ctx: SelectContext) => T,
111
- options?: DerivedOptions
112
- ): DerivedAtom<T, false>;
113
-
114
- // With fallback - staleValue is guaranteed T
115
- function derived<T>(
116
- compute: (ctx: SelectContext) => T,
117
- options: DerivedOptions & { fallback: T }
118
- ): DerivedAtom<T, true>;
119
-
120
- interface DerivedOptions<T> {
121
- meta?: AtomMeta;
122
- equals?: EqualsFn<T>;
123
- fallback?: T; // When provided, staleValue is guaranteed T
124
- }
125
-
126
- interface DerivedAtom<T, F extends boolean = false> extends Atom<Promise<T>> {
127
- refresh(): void; // re-run computation
128
- state(): AtomState<T>; // get current state (ready/error/loading)
129
- readonly staleValue: F extends true ? T : T | undefined; // fallback or last resolved value
130
- }
131
- ```
132
-
133
- ### SelectContext
134
-
135
- ```typescript
136
- type AnyAtom<T> = MutableAtom<T> | DerivedAtom<T>;
137
- type AtomValue<A> = A extends AnyAtom<infer V> ? Awaited<V> : never;
138
-
139
- type SettledResult<T> =
140
- | { status: "ready"; value: T }
141
- | { status: "error"; error: unknown };
142
-
143
- interface SelectContext {
144
- // Single atom
145
- get<V>(atom: AnyAtom<V>): Awaited<V>;
146
-
147
- // Multiple atoms - parallel
148
- all<A extends AnyAtom<unknown>[]>(
149
- ...atoms: A
150
- ): { [K in keyof A]: AtomValue<A[K]> };
151
- race<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
152
- any<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
153
- settled<A extends AnyAtom<unknown>[]>(
154
- ...atoms: A
155
- ): { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
156
- }
157
- ```
158
-
159
- ### getAtomState()
160
-
161
- Returns the current state of an atom as a discriminated union.
162
-
163
- ```ts
164
- type AtomState<T> =
165
- | { status: "ready"; value: T }
166
- | { status: "error"; error: unknown }
167
- | { status: "loading"; promise: Promise<T> };
168
-
169
- function getAtomState<T>(atom: Atom<T>): AtomState<T> {
170
- // For derived atoms, use their own state method
171
- if (isDerived(atom)) {
172
- return atom.state();
173
- }
174
-
175
- const value = atom.value;
176
-
177
- // 1. Sync value - ready
178
- if (!isPromiseLike(value)) {
179
- return {
180
- status: "ready",
181
- value: value,
182
- };
183
- }
184
-
185
- // 2. Promise value - check state via promiseCache
186
- const state = trackPromise(value);
187
-
188
- switch (state.status) {
189
- case "fulfilled":
190
- return {
191
- status: "ready",
192
- value: state.value,
193
- };
194
-
195
- case "rejected":
196
- return {
197
- status: "error",
198
- error: state.error,
199
- };
200
-
201
- case "pending":
202
- return {
203
- status: "loading",
204
- promise: state.promise,
205
- };
206
- }
207
- }
208
- ```
209
-
210
- **Usage:**
211
-
212
- ```ts
213
- const state = getAtomState(myAtom$);
214
-
215
- switch (state.status) {
216
- case "ready":
217
- console.log(state.value); // T
218
- break;
219
- case "error":
220
- console.log(state.error); // unknown
221
- break;
222
- case "loading":
223
- console.log(state.promise); // Promise<T>
224
- break;
225
- }
226
-
227
- // For derived atoms with fallback, use staleValue during loading:
228
- if (state.status === "loading") {
229
- console.log("Using stale/fallback:", derived$.staleValue);
230
- }
231
- ```
232
-
233
- ### get() Behavior
234
-
235
- The `get()` function resolves atom values using `getAtomState()`:
236
-
237
- ```typescript
238
- function get<T>(atom: AnyAtom<T>): Awaited<T> {
239
- const state = getAtomState(atom);
240
-
241
- switch (state.status) {
242
- case "ready":
243
- return state.value;
244
- case "error":
245
- throw state.error;
246
- case "loading":
247
- throw state.promise; // Suspense pattern
248
- }
249
- }
250
- ```
251
-
252
- **Summary:**
253
-
254
- | State | Result |
255
- | ------- | ------------------------ |
256
- | ready | return value |
257
- | error | throw error |
258
- | loading | throw Promise (Suspense) |
259
-
260
- > **Note:** For derived atoms with fallback, use `staleValue` directly to access
261
- > the fallback/cached value during loading without triggering Suspense.
262
-
263
- ### all() Behavior
264
-
265
- Like `Promise.all` - waits for all atoms, returns array of values.
266
-
267
- ```typescript
268
- function all<A extends AnyAtom<unknown>[]>(...atoms: A): AwaitedAll<A> {
269
- const results: unknown[] = [];
270
- let loadingPromise: Promise<unknown> | null = null;
271
-
272
- for (const atom of atoms) {
273
- const state = getAtomState(atom);
274
-
275
- switch (state.status) {
276
- case "ready":
277
- results.push(state.value);
278
- break;
279
-
280
- case "error":
281
- // Any error → throw immediately
282
- throw state.error;
283
-
284
- case "loading":
285
- // First loading → will throw
286
- if (!loadingPromise) {
287
- loadingPromise = state.promise;
288
- }
289
- break;
290
- }
291
- }
292
-
293
- // If any loading → throw Promise
294
- if (loadingPromise) {
295
- throw loadingPromise;
296
- }
297
-
298
- return results as AwaitedAll<A>;
299
- }
300
- ```
301
-
302
- | Scenario | Result |
303
- | ----------- | ------------------------------ |
304
- | All ready | return `[value1, value2, ...]` |
305
- | Any error | throw first error |
306
- | Any loading | throw Promise |
307
-
308
- ### race() Behavior
309
-
310
- Like `Promise.race` - returns first settled value (ready or error).
311
-
312
- ```typescript
313
- function race<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]> {
314
- let firstLoadingPromise: Promise<unknown> | null = null;
315
-
316
- for (const atom of atoms) {
317
- const state = getAtomState(atom);
318
-
319
- switch (state.status) {
320
- case "ready":
321
- return state.value as AtomValue<A[number]>;
322
-
323
- case "error":
324
- throw state.error;
325
-
326
- case "loading":
327
- if (!firstLoadingPromise) {
328
- firstLoadingPromise = state.promise;
329
- }
330
- break;
331
- }
332
- }
333
-
334
- // All loading → throw first Promise
335
- if (firstLoadingPromise) {
336
- throw firstLoadingPromise;
337
- }
338
-
339
- throw new Error("race() called with no atoms");
340
- }
341
- ```
342
-
343
- | Scenario | Result |
344
- | -------------- | ----------------------- |
345
- | Any sync value | return first sync value |
346
- | Any ready | return first value |
347
- | Any error | throw first error |
348
- | All loading | throw first Promise |
349
-
350
- > **Note:** `race()` does NOT use fallback - it's meant to return the first "real" settled value.
351
-
352
- ### any() Behavior
353
-
354
- Like `Promise.any` - returns first ready, ignores errors unless all error.
355
-
356
- ```typescript
357
- function any<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]> {
358
- const errors: unknown[] = [];
359
- let firstLoadingPromise: Promise<unknown> | null = null;
360
-
361
- for (const atom of atoms) {
362
- const state = getAtomState(atom);
363
-
364
- switch (state.status) {
365
- case "ready":
366
- return state.value as AtomValue<A[number]>;
367
-
368
- case "error":
369
- errors.push(state.error);
370
- break;
371
-
372
- case "loading":
373
- if (!firstLoadingPromise) {
374
- firstLoadingPromise = state.promise;
375
- }
376
- break;
377
- }
378
- }
379
-
380
- // If any loading → throw Promise (might still succeed)
381
- if (firstLoadingPromise) {
382
- throw firstLoadingPromise;
383
- }
384
-
385
- // All errored → throw AggregateError
386
- throw new AggregateError(errors, "All atoms rejected");
387
- }
388
- ```
389
-
390
- | Scenario | Result |
391
- | -------------- | ---------------------------------- |
392
- | Any sync value | return first sync value |
393
- | Any ready | return first value |
394
- | Any loading | throw Promise (wait for potential) |
395
- | All errored | throw `AggregateError` |
396
-
397
- > **Note:** `any()` does NOT use fallback - it waits for a real ready value.
398
-
399
- ### settled() Behavior
400
-
401
- Like `Promise.allSettled` - returns status of all atoms.
402
-
403
- ```typescript
404
- function settled<A extends AnyAtom<unknown>[]>(
405
- ...atoms: A
406
- ): { [K in keyof A]: SettledResult<AtomValue<A[K]>> } {
407
- const results: SettledResult<unknown>[] = [];
408
- let loadingPromise: Promise<unknown> | null = null;
409
-
410
- for (const atom of atoms) {
411
- const state = getAtomState(atom);
412
-
413
- switch (state.status) {
414
- case "ready":
415
- results.push({ status: "ready", value: state.value });
416
- break;
417
-
418
- case "error":
419
- results.push({ status: "error", error: state.error });
420
- break;
421
-
422
- case "loading":
423
- // Loading → will throw
424
- if (!loadingPromise) {
425
- loadingPromise = state.promise;
426
- }
427
- break;
428
- }
429
- }
430
-
431
- // If any loading → throw Promise
432
- if (loadingPromise) {
433
- throw loadingPromise;
434
- }
435
-
436
- return results as { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
437
- }
438
- ```
439
-
440
- | Scenario | Result |
441
- | ----------- | --------------------------------------- |
442
- | All settled | return `[{ status, value/error }, ...]` |
443
- | Any loading | throw Promise |
444
-
445
- ### SelectContext Summary
446
-
447
- All methods use `getAtomState()` internally.
448
-
449
- | Method | Returns | Throws on Error? |
450
- | ------------------- | --------------- | ----------------- |
451
- | `get(atom)` | Single value | Yes |
452
- | `all(...atoms)` | Array of values | Yes (first error) |
453
- | `race(...atoms)` | First settled | Yes (if first) |
454
- | `any(...atoms)` | First ready | Only if all error |
455
- | `settled(...atoms)` | Array of status | No |
456
-
457
- > **Note:** `race()` and `any()` are typically used with `MutableAtom<Promise<T>>` for racing data sources.
458
-
459
- ### Behavior
460
-
461
- **Without fallback (`DerivedAtom<T, false>`):**
462
-
463
- | State | `.value` | `.staleValue` | `.state().status` |
464
- | -------- | -------------------- | ------------------------ | ----------------- |
465
- | Loading | `Promise` (pending) | `undefined` | `"loading"` |
466
- | Resolved | `Promise` (resolved) | resolved value | `"ready"` |
467
- | Error | `Promise` (rejected) | last value / `undefined` | `"error"` |
468
-
469
- **With fallback (`DerivedAtom<T, true>`):**
470
-
471
- | State | `.value` | `.staleValue` | `.state().status` |
472
- | -------- | -------------------- | ------------------- | ----------------- |
473
- | Loading | `Promise` (pending) | fallback | `"loading"` |
474
- | Resolved | `Promise` (resolved) | resolved value | `"ready"` |
475
- | Error | `Promise` (rejected) | last value/fallback | `"error"` |
476
-
477
- > **Note:** Use `state().status === "loading"` or `isPending(derived$.value)` to check loading state.
478
-
479
- ### Examples
480
-
481
- ```typescript
482
- // Basic derived (no fallback) - DerivedAtom<number, false>
483
- const count$ = atom(0);
484
- const double$ = derived(({ get }) => get(count$) * 2);
485
- await double$.value; // 0
486
- double$.staleValue; // undefined (during loading) → 0 (after)
487
- double$.state(); // { status: "ready", value: 0 }
488
-
489
- // Async derived (no fallback) - DerivedAtom<number, false>
490
- const posts$ = atom(fetchPosts());
491
- const postCount$ = derived(({ get }) => get(posts$).length);
492
- await postCount$.value; // 42 (after loading)
493
- postCount$.staleValue; // undefined (during loading) → 42 (after)
494
- postCount$.state(); // { status: "loading", promise } → { status: "ready", value: 42 }
495
-
496
- // With fallback - DerivedAtom<number, true>
497
- const postCountWithFallback$ = derived(({ get }) => get(posts$).length, {
498
- fallback: 0,
499
- });
500
- await postCountWithFallback$.value; // 42
501
- postCountWithFallback$.staleValue; // 0 (during loading, guaranteed) → 42 (after)
502
- postCountWithFallback$.state(); // { status: "loading", promise } → { status: "ready", value: 42 }
503
-
504
- // Check loading state
505
- postCount$.state().status === "loading"; // true while loading
506
- isPending(postCount$.value); // alternative: true while loading
507
-
508
- // Multiple deps
509
- const combined$ = derived(({ get, all }) => {
510
- const [posts, user] = all(posts$, user$);
511
- return posts.filter((p) => p.authorId === user.id);
512
- });
513
-
514
- // Refresh - re-run computation
515
- postCount$.refresh();
516
-
517
- // Error handling
518
- try {
519
- const count = await postCount$.value;
520
- } catch (e) {
521
- console.error("Failed:", e);
522
- }
523
- // Or check state
524
- const state = postCount$.state();
525
- if (state.status === "error") {
526
- console.error("Failed:", state.error);
527
- }
528
- ```
529
-
530
- ---
531
-
532
- ## Effect
533
-
534
- Side effect runner with same get() semantics.
535
-
536
- ### API
537
-
538
- ```typescript
539
- interface EffectContext extends SelectContext {
540
- onCleanup: (cleanup: VoidFunction) => void;
541
- onError: (handler: (error: unknown) => void) => void;
542
- }
543
-
544
- function effect(
545
- fn: (ctx: EffectContext) => void,
546
- options?: EffectOptions
547
- ): () => void;
548
-
549
- interface EffectOptions {
550
- key?: string;
551
- onError?: (error: Error) => void; // For unhandled errors
552
- }
553
- ```
554
-
555
- ### Behavior
556
-
557
- - Runs when dependencies change
558
- - `get()` works same as in derived (throws Promise/Error)
559
- - If throws Promise → re-runs when Promise resolves
560
- - If throws Error → calls registered `onError` handlers, or `options.onError` if none registered
561
- - Use `onCleanup()` to register cleanup functions
562
-
563
- ### Example
564
-
565
- ```typescript
566
- // With cleanup
567
- const dispose = effect(({ get, onCleanup }) => {
568
- const interval = get(intervalAtom);
569
- const id = setInterval(() => console.log("tick"), interval);
570
- onCleanup(() => clearInterval(id));
571
- });
572
-
573
- // With error handling (callback-based)
574
- const dispose = effect(({ get, onError }) => {
575
- onError((e) => console.error("Effect failed:", e));
576
- const posts = get(posts$);
577
- console.log("Posts loaded:", posts.length);
578
- });
579
-
580
- // With error handling (option-based for unhandled errors)
581
- const dispose = effect(
582
- ({ get }) => {
583
- const posts = get(posts$);
584
- riskyOperation(posts);
585
- },
586
- {
587
- onError: (e) => console.error("Unhandled error:", e),
588
- }
589
- );
590
- ```
591
-
592
- ---
593
-
594
- ## React Hooks
595
-
596
- ### useSelector
597
-
598
- ```typescript
599
- function useSelector<T>(atom: Atom<T>): Awaited<T>;
600
- function useSelector<T>(
601
- selector: (ctx: SelectContext) => T,
602
- equals?: Equality<Awaited<T>>
603
- ): Awaited<T>;
604
- ```
605
-
606
- ### Example
607
-
608
- ```typescript
609
- // With DerivedAtom (Suspense handles loading)
610
- function PostCount() {
611
- const count = useSelector(postCount$); // number (awaited)
612
- return <div>Count: {count}</div>;
613
- }
614
-
615
- // Usage with Suspense + ErrorBoundary
616
- <ErrorBoundary fallback={<Error />}>
617
- <Suspense fallback={<Loading />}>
618
- <PostCount />
619
- </Suspense>
620
- </ErrorBoundary>
621
-
622
- // With fallback - staleValue is guaranteed T
623
- function PostCountWithFallback() {
624
- const count = postCountWithFallback$.staleValue; // always number
625
- const isLoading = isPending(postCountWithFallback$.value);
626
-
627
- return (
628
- <div>
629
- {isLoading && <Spinner />}
630
- Count: {count}
631
- </div>
632
- );
633
- }
634
-
635
- // Without fallback - staleValue is T | undefined
636
- function PostCountNoFallback() {
637
- const count = postCount$.staleValue; // number | undefined
638
- const isLoading = isPending(postCount$.value);
639
-
640
- return (
641
- <div>
642
- {isLoading && <Spinner />}
643
- Count: {count ?? "Loading..."}
644
- </div>
645
- );
646
- }
647
- ```
648
-
649
- ---
650
-
651
- ## Promise State Cache
652
-
653
- Internal mechanism for tracking Promise states.
654
-
655
- ```typescript
656
- // Internal - not exposed
657
- type PromiseState<T> =
658
- | { status: "pending"; promise: Promise<T> }
659
- | { status: "fulfilled"; value: T }
660
- | { status: "rejected"; error: unknown };
661
-
662
- const promiseCache = new WeakMap<Promise<any>, PromiseState<any>>();
663
- ```
664
-
665
- ---
666
-
667
- ## Type Utilities
668
-
669
- ```typescript
670
- type Awaited<T> = T extends Promise<infer U> ? U : T;
671
-
672
- type AwaitedAll<T extends unknown[]> = {
673
- [K in keyof T]: Awaited<T[K]>;
674
- };
675
-
676
- type EqualsFn<T> = (a: T, b: T) => boolean;
677
- ```
678
-
679
- ---
680
-
681
- ## Complete API Surface
682
-
683
- ```typescript
684
- // Core
685
- atom<T>(initial, options?): MutableAtom<T>
686
- derived<T>(compute, options?): DerivedAtom<T, false>
687
- derived<T>(compute, { fallback }): DerivedAtom<T, true>
688
- effect(fn, options?): () => void
689
-
690
- // React
691
- useSelector(source): T | Awaited<T>
692
-
693
- // Types
694
- Atom<T>
695
- MutableAtom<T>
696
- DerivedAtom<T, F> // F = false (no fallback) | true (with fallback)
697
- SelectContext
698
- ```
699
-
700
- ---
701
-
702
- ## Summary Table
703
-
704
- | | MutableAtom | DerivedAtom | DerivedAtom with fallback |
705
- | ------------- | ----------- | ------------------- | ------------------------- |
706
- | Purpose | Raw storage | Computed (async) | Computed + sync access |
707
- | `.value` | `T` (raw) | `Promise<T>` | `Promise<T>` |
708
- | `.staleValue` | ❌ | ✅ `T \| undefined` | ✅ `T` |
709
- | `.state()` | ❌ | ✅ `AtomState<T>` | ✅ `AtomState<T>` |
710
- | `.set()` | ✅ | ❌ | ❌ |
711
- | `.reset()` | ✅ | ❌ | ❌ |
712
- | `.refresh()` | ❌ | ✅ | ✅ |
713
- | `.on()` | ✅ | ✅ | ✅ |
714
-
715
- ---
716
-
717
- ## Migration from v1
718
-
719
- | v1 | v2 |
720
- | --------------------------------- | --------------------------------------------- |
721
- | `atom(promise, { fallback })` | `atom(promise)` (no fallback on atom) |
722
- | `atom.loading` | `isPending(derived$.value)` from promiseCache |
723
- | `atom.error` | `await derived.value` throws |
724
- | `atom.stale()` | `isPending(derived$.value)` from promiseCache |
725
- | `useSelector(atom)` with Suspense | `useSelector(derived)` with Suspense |