atomirx 0.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 (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
package/v2.md ADDED
@@ -0,0 +1,725 @@
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
+ ### useValue
597
+
598
+ ```typescript
599
+ function useValue<T>(atom: Atom<T>): Awaited<T>;
600
+ function useValue<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 = useValue(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
+ useValue(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
+ | `useValue(atom)` with Suspense | `useValue(derived)` with Suspense |