atomirx 0.0.2 → 0.0.5

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 (65) hide show
  1. package/README.md +868 -161
  2. package/coverage/src/core/onCreateHook.ts.html +72 -70
  3. package/dist/core/atom.d.ts +83 -6
  4. package/dist/core/batch.d.ts +3 -3
  5. package/dist/core/derived.d.ts +69 -22
  6. package/dist/core/effect.d.ts +52 -52
  7. package/dist/core/getAtomState.d.ts +29 -0
  8. package/dist/core/hook.d.ts +1 -1
  9. package/dist/core/onCreateHook.d.ts +37 -23
  10. package/dist/core/onErrorHook.d.ts +49 -0
  11. package/dist/core/promiseCache.d.ts +23 -32
  12. package/dist/core/select.d.ts +208 -29
  13. package/dist/core/types.d.ts +107 -22
  14. package/dist/core/withReady.d.ts +115 -0
  15. package/dist/core/withReady.test.d.ts +1 -0
  16. package/dist/index-CBVj1kSj.js +1350 -0
  17. package/dist/index-Cxk9v0um.cjs +1 -0
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +12 -8
  20. package/dist/index.js +18 -15
  21. package/dist/react/index.cjs +10 -10
  22. package/dist/react/index.d.ts +2 -1
  23. package/dist/react/index.js +422 -377
  24. package/dist/react/rx.d.ts +114 -25
  25. package/dist/react/useAction.d.ts +5 -4
  26. package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
  27. package/dist/react/useSelector.test.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/src/core/atom.test.ts +307 -43
  30. package/src/core/atom.ts +144 -22
  31. package/src/core/batch.test.ts +10 -10
  32. package/src/core/batch.ts +3 -3
  33. package/src/core/define.test.ts +12 -11
  34. package/src/core/define.ts +1 -1
  35. package/src/core/derived.test.ts +906 -72
  36. package/src/core/derived.ts +192 -81
  37. package/src/core/effect.test.ts +651 -45
  38. package/src/core/effect.ts +102 -98
  39. package/src/core/getAtomState.ts +69 -0
  40. package/src/core/hook.test.ts +5 -5
  41. package/src/core/hook.ts +1 -1
  42. package/src/core/onCreateHook.ts +38 -23
  43. package/src/core/onErrorHook.test.ts +350 -0
  44. package/src/core/onErrorHook.ts +52 -0
  45. package/src/core/promiseCache.test.ts +5 -3
  46. package/src/core/promiseCache.ts +76 -71
  47. package/src/core/select.ts +405 -130
  48. package/src/core/selector.test.ts +574 -32
  49. package/src/core/types.ts +107 -29
  50. package/src/core/withReady.test.ts +534 -0
  51. package/src/core/withReady.ts +191 -0
  52. package/src/core/withUse.ts +1 -1
  53. package/src/index.test.ts +4 -4
  54. package/src/index.ts +21 -7
  55. package/src/react/index.ts +2 -1
  56. package/src/react/rx.test.tsx +173 -18
  57. package/src/react/rx.tsx +274 -43
  58. package/src/react/useAction.test.ts +12 -14
  59. package/src/react/useAction.ts +11 -9
  60. package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
  61. package/src/react/{useValue.ts → useSelector.ts} +64 -33
  62. package/v2.md +44 -44
  63. package/dist/index-2ok7ilik.js +0 -1217
  64. package/dist/index-B_5SFzfl.cjs +0 -1
  65. /package/dist/{react/useValue.test.d.ts → core/onErrorHook.test.d.ts} +0 -0
@@ -1,68 +1,64 @@
1
-
2
1
  <!doctype html>
3
2
  <html lang="en">
4
-
5
- <head>
3
+ <head>
6
4
  <title>Code coverage report for src/core/onCreateHook.ts</title>
7
5
  <meta charset="utf-8" />
8
6
  <link rel="stylesheet" href="../../prettify.css" />
9
7
  <link rel="stylesheet" href="../../base.css" />
10
8
  <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
11
9
  <meta name="viewport" content="width=device-width, initial-scale=1" />
12
- <style type='text/css'>
13
- .coverage-summary .sorter {
14
- background-image: url(../../sort-arrow-sprite.png);
15
- }
10
+ <style type="text/css">
11
+ .coverage-summary .sorter {
12
+ background-image: url(../../sort-arrow-sprite.png);
13
+ }
16
14
  </style>
17
- </head>
18
-
19
- <body>
20
- <div class='wrapper'>
21
- <div class='pad1'>
22
- <h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> onCreateHook.ts</h1>
23
- <div class='clearfix'>
24
-
25
- <div class='fl pad1y space-right2'>
26
- <span class="strong">100% </span>
27
- <span class="quiet">Statements</span>
28
- <span class='fraction'>17/17</span>
29
- </div>
30
-
31
-
32
- <div class='fl pad1y space-right2'>
33
- <span class="strong">100% </span>
34
- <span class="quiet">Branches</span>
35
- <span class='fraction'>0/0</span>
36
- </div>
37
-
38
-
39
- <div class='fl pad1y space-right2'>
40
- <span class="strong">100% </span>
41
- <span class="quiet">Functions</span>
42
- <span class='fraction'>0/0</span>
43
- </div>
44
-
45
-
46
- <div class='fl pad1y space-right2'>
47
- <span class="strong">100% </span>
48
- <span class="quiet">Lines</span>
49
- <span class='fraction'>17/17</span>
50
- </div>
51
-
52
-
15
+ </head>
16
+
17
+ <body>
18
+ <div class="wrapper">
19
+ <div class="pad1">
20
+ <h1>
21
+ <a href="../../index.html">All files</a> /
22
+ <a href="index.html">src/core</a> onCreateHook.ts
23
+ </h1>
24
+ <div class="clearfix">
25
+ <div class="fl pad1y space-right2">
26
+ <span class="strong">100% </span>
27
+ <span class="quiet">Statements</span>
28
+ <span class="fraction">17/17</span>
29
+ </div>
30
+
31
+ <div class="fl pad1y space-right2">
32
+ <span class="strong">100% </span>
33
+ <span class="quiet">Branches</span>
34
+ <span class="fraction">0/0</span>
35
+ </div>
36
+
37
+ <div class="fl pad1y space-right2">
38
+ <span class="strong">100% </span>
39
+ <span class="quiet">Functions</span>
40
+ <span class="fraction">0/0</span>
41
+ </div>
42
+
43
+ <div class="fl pad1y space-right2">
44
+ <span class="strong">100% </span>
45
+ <span class="quiet">Lines</span>
46
+ <span class="fraction">17/17</span>
47
+ </div>
53
48
  </div>
54
49
  <p class="quiet">
55
- Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
50
+ Press <em>n</em> or <em>j</em> to go to the next uncovered block,
51
+ <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
56
52
  </p>
57
53
  <template id="filterTemplate">
58
- <div class="quiet">
59
- Filter:
60
- <input type="search" id="fileSearch">
61
- </div>
54
+ <div class="quiet">
55
+ Filter:
56
+ <input type="search" id="fileSearch" />
57
+ </div>
62
58
  </template>
63
- </div>
64
- <div class='status-line high'></div>
65
- <pre><table class="coverage">
59
+ </div>
60
+ <div class="status-line high"></div>
61
+ <pre><table class="coverage">
66
62
  <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
67
63
  <a name='L2'></a><a href='#L2'>2</a>
68
64
  <a name='L3'></a><a href='#L3'>3</a>
@@ -100,7 +96,7 @@
100
96
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { hook } from "./hook";
101
97
  import { MutableAtom } from "./types";
102
98
  &nbsp;
103
- export interface AtomCreateInfo {
99
+ export interface CreateInfo {
104
100
  type: "atom";
105
101
  key: string | undefined;
106
102
  atom: MutableAtom&lt;any&gt;;
@@ -113,24 +109,30 @@ export interface ModuleCreateInfo {
113
109
  }
114
110
  &nbsp;
115
111
  export const onCreateHook =
116
- hook&lt;(info: AtomCreateInfo | ModuleCreateInfo) =&gt; void&gt;();
112
+ hook&lt;(info: CreateInfo | ModuleCreateInfo) =&gt; void&gt;();
117
113
  &nbsp;</pre></td></tr></table></pre>
118
114
 
119
- <div class='push'></div><!-- for sticky footer -->
120
- </div><!-- /wrapper -->
121
- <div class='footer quiet pad2 space-top1 center small'>
122
- Code coverage generated by
123
- <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
124
- at 2026-01-16T14:35:38.788Z
125
- </div>
126
- <script src="../../prettify.js"></script>
127
- <script>
128
- window.onload = function () {
129
- prettyPrint();
130
- };
131
- </script>
132
- <script src="../../sorter.js"></script>
133
- <script src="../../block-navigation.js"></script>
134
- </body>
115
+ <div class="push"></div>
116
+ <!-- for sticky footer -->
117
+ </div>
118
+ <!-- /wrapper -->
119
+ <div class="footer quiet pad2 space-top1 center small">
120
+ Code coverage generated by
121
+ <a
122
+ href="https://istanbul.js.org/"
123
+ target="_blank"
124
+ rel="noopener noreferrer"
125
+ >istanbul</a
126
+ >
127
+ at 2026-01-16T14:35:38.788Z
128
+ </div>
129
+ <script src="../../prettify.js"></script>
130
+ <script>
131
+ window.onload = function () {
132
+ prettyPrint();
133
+ };
134
+ </script>
135
+ <script src="../../sorter.js"></script>
136
+ <script src="../../block-navigation.js"></script>
137
+ </body>
135
138
  </html>
136
-
@@ -1,9 +1,27 @@
1
- import { AtomOptions, MutableAtom } from './types';
1
+ import { AtomOptions, MutableAtom, Pipeable, Atom } from './types';
2
+ /**
3
+ * Context object passed to atom initializer functions.
4
+ * Provides utilities for cleanup and cancellation.
5
+ */
6
+ export interface AtomContext extends Pipeable {
7
+ /**
8
+ * AbortSignal that is aborted when the atom value changes (via set or reset).
9
+ * Use this to cancel pending async operations.
10
+ */
11
+ signal: AbortSignal;
12
+ /**
13
+ * Register a cleanup function that runs when the atom value changes or resets.
14
+ * Multiple cleanup functions can be registered; they run in FIFO order.
15
+ *
16
+ * @param cleanup - Function to run during cleanup
17
+ */
18
+ onCleanup(cleanup: VoidFunction): void;
19
+ }
2
20
  /**
3
21
  * Creates a mutable atom - a reactive state container that holds a single value.
4
22
  *
5
23
  * MutableAtom is a raw storage container. It stores values as-is, including Promises.
6
- * If you store a Promise, `.value` returns the Promise object itself.
24
+ * If you store a Promise, `.get()` returns the Promise object itself.
7
25
  *
8
26
  * Features:
9
27
  * - Raw storage: stores any value including Promises
@@ -17,14 +35,14 @@ import { AtomOptions, MutableAtom } from './types';
17
35
  * @param options - Configuration options
18
36
  * @param options.meta - Optional metadata for debugging/devtools
19
37
  * @param options.equals - Equality strategy for change detection (default: strict)
20
- * @returns A mutable atom with value, set/reset methods
38
+ * @returns A mutable atom with get, set/reset methods
21
39
  *
22
40
  * @example Synchronous value
23
41
  * ```ts
24
42
  * const count = atom(0);
25
43
  * count.set(1);
26
44
  * count.set(prev => prev + 1);
27
- * console.log(count.value); // 2
45
+ * console.log(count.get()); // 2
28
46
  * ```
29
47
  *
30
48
  * @example Lazy initialization
@@ -43,7 +61,7 @@ import { AtomOptions, MutableAtom } from './types';
43
61
  * @example Async value (stores Promise as-is)
44
62
  * ```ts
45
63
  * const posts = atom(fetchPosts());
46
- * posts.value; // Promise<Post[]>
64
+ * posts.get(); // Promise<Post[]>
47
65
  *
48
66
  * // Refetch - set a new Promise
49
67
  * posts.set(fetchPosts());
@@ -60,4 +78,63 @@ import { AtomOptions, MutableAtom } from './types';
60
78
  * state.set(prev => ({ ...prev })); // No notification (shallow equal)
61
79
  * ```
62
80
  */
63
- export declare function atom<T>(valueOrInit: T | (() => T), options?: AtomOptions<T>): MutableAtom<T>;
81
+ export declare function atom<T>(valueOrInit: T | ((context: AtomContext) => T), options?: AtomOptions<T>): MutableAtom<T>;
82
+ /**
83
+ * Type utility to expose an atom as read-only when exporting from a module.
84
+ *
85
+ * This function returns the same atom instance but with a narrowed type (`Atom<T>`)
86
+ * that hides mutable methods like `set()` and `reset()`. Use this to encapsulate
87
+ * state mutations within a module while allowing external consumers to only read
88
+ * and subscribe to changes.
89
+ *
90
+ * **Note:** This is a compile-time restriction only. At runtime, the atom is unchanged.
91
+ * Consumers with access to the original reference can still mutate it.
92
+ *
93
+ * @param atom - The atom (or record of atoms) to expose as read-only
94
+ * @returns The same atom(s) with a read-only type signature
95
+ *
96
+ * @example Single atom
97
+ * ```ts
98
+ * const myModule = define(() => {
99
+ * const count$ = atom(0); // Internal mutable atom
100
+ *
101
+ * return {
102
+ * // Expose as read-only - consumers can't call set() or reset()
103
+ * count$: readonly(count$),
104
+ * // Mutations only possible through explicit actions
105
+ * increment: () => count$.set(prev => prev + 1),
106
+ * decrement: () => count$.set(prev => prev - 1),
107
+ * };
108
+ * });
109
+ *
110
+ * // Usage:
111
+ * const { count$, increment } = myModule();
112
+ * count$.get(); // ✅ OK - reading is allowed
113
+ * count$.on(console.log); // ✅ OK - subscribing is allowed
114
+ * count$.set(5); // ❌ TypeScript error - set() not available on Atom<T>
115
+ * increment(); // ✅ OK - use exposed action instead
116
+ * ```
117
+ *
118
+ * @example Record of atoms
119
+ * ```ts
120
+ * const myModule = define(() => {
121
+ * const count$ = atom(0);
122
+ * const name$ = atom('');
123
+ *
124
+ * return {
125
+ * // Expose multiple atoms as read-only at once
126
+ * ...readonly({ count$, name$ }),
127
+ * setName: (name: string) => name$.set(name),
128
+ * };
129
+ * });
130
+ *
131
+ * // Usage:
132
+ * const { count$, name$, setName } = myModule();
133
+ * count$.get(); // ✅ Atom<number>
134
+ * name$.get(); // ✅ Atom<string>
135
+ * name$.set(''); // ❌ TypeScript error
136
+ * ```
137
+ */
138
+ export declare function readonly<T extends Atom<any> | Record<string, Atom<any>>>(atom: T): T extends Atom<infer V> ? Atom<V> : {
139
+ [K in keyof T]: T[K] extends Atom<infer V> ? Atom<V> : never;
140
+ };
@@ -54,7 +54,7 @@
54
54
  * ```ts
55
55
  * const counter = atom(0);
56
56
  *
57
- * counter.on(() => console.log("Counter:", counter.value));
57
+ * counter.on(() => console.log("Counter:", counter.get()));
58
58
  *
59
59
  * batch(() => {
60
60
  * counter.set(1);
@@ -70,7 +70,7 @@
70
70
  * const b = atom(0);
71
71
  *
72
72
  * // Same listener subscribed to both atoms
73
- * const listener = () => console.log("Changed!", a.value, b.value);
73
+ * const listener = () => console.log("Changed!", a.get(), b.get());
74
74
  * a.on(listener);
75
75
  * b.on(listener);
76
76
  *
@@ -98,7 +98,7 @@
98
98
  * ```ts
99
99
  * const result = batch(() => {
100
100
  * counter.set(10);
101
- * return counter.value * 2;
101
+ * return counter.get() * 2;
102
102
  * });
103
103
  * console.log(result); // 20
104
104
  * ```
@@ -1,19 +1,33 @@
1
- import { SelectContext } from './select';
1
+ import { CreateInfo } from './onCreateHook';
2
+ import { ReactiveSelector, SelectContext } from './select';
2
3
  import { DerivedAtom, DerivedOptions } from './types';
4
+ import { WithReadySelectContext } from './withReady';
5
+ /**
6
+ * Internal options for derived atoms.
7
+ * These are not part of the public API.
8
+ * @internal
9
+ */
10
+ export interface DerivedInternalOptions {
11
+ /**
12
+ * Override the error source for onErrorHook.
13
+ * Used by effect() to attribute errors to the effect instead of the internal derived.
14
+ */
15
+ _errorSource?: CreateInfo;
16
+ }
3
17
  /**
4
18
  * Context object passed to derived atom selector functions.
5
- * Provides utilities for reading atoms: `{ get, all, any, race, settled }`.
19
+ * Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
6
20
  *
7
21
  * Currently identical to `SelectContext`, but defined separately to allow
8
22
  * future derived-specific extensions without breaking changes.
9
23
  */
10
- export interface DerivedContext extends SelectContext {
24
+ export interface DerivedContext extends SelectContext, WithReadySelectContext {
11
25
  }
12
26
  /**
13
27
  * Creates a derived (computed) atom from source atom(s).
14
28
  *
15
29
  * Derived atoms are **read-only** and automatically recompute when their
16
- * source atoms change. The `.value` property always returns a `Promise<T>`,
30
+ * source atoms change. The `.get()` method always returns a `Promise<T>`,
17
31
  * even for synchronous computations.
18
32
  *
19
33
  * ## IMPORTANT: Selector Must Return Synchronous Value
@@ -22,35 +36,68 @@ export interface DerivedContext extends SelectContext {
22
36
  *
23
37
  * ```ts
24
38
  * // ❌ WRONG - Don't use async function
25
- * derived(async ({ get }) => {
39
+ * derived(async ({ read }) => {
26
40
  * const data = await fetch('/api');
27
41
  * return data;
28
42
  * });
29
43
  *
30
44
  * // ❌ WRONG - Don't return a Promise
31
- * derived(({ get }) => fetch('/api').then(r => r.json()));
45
+ * derived(({ read }) => fetch('/api').then(r => r.json()));
32
46
  *
33
- * // ✅ CORRECT - Create async atom and read with get()
47
+ * // ✅ CORRECT - Create async atom and read with read()
34
48
  * const data$ = atom(fetch('/api').then(r => r.json()));
35
- * derived(({ get }) => get(data$)); // Suspends until resolved
49
+ * derived(({ read }) => read(data$)); // Suspends until resolved
36
50
  * ```
37
51
  *
52
+ * ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
53
+ *
54
+ * **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
55
+ * Promises when atoms are loading (Suspense pattern). A try/catch will catch
56
+ * these Promises and break the Suspense mechanism.
57
+ *
58
+ * ```ts
59
+ * // ❌ WRONG - Catches Suspense Promise, breaks loading state
60
+ * derived(({ read }) => {
61
+ * try {
62
+ * return read(asyncAtom$);
63
+ * } catch (e) {
64
+ * return 'fallback'; // This catches BOTH errors AND loading promises!
65
+ * }
66
+ * });
67
+ *
68
+ * // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
69
+ * derived(({ read, safe }) => {
70
+ * const [err, data] = safe(() => {
71
+ * const raw = read(asyncAtom$); // Can throw Promise (Suspense)
72
+ * return JSON.parse(raw); // Can throw Error
73
+ * });
74
+ *
75
+ * if (err) return { error: err.message };
76
+ * return { data };
77
+ * });
78
+ * ```
79
+ *
80
+ * The `safe()` utility:
81
+ * - **Catches errors** and returns `[error, undefined]`
82
+ * - **Re-throws Promises** to preserve Suspense behavior
83
+ * - Returns `[undefined, result]` on success
84
+ *
38
85
  * ## Key Features
39
86
  *
40
- * 1. **Always async**: `.value` returns `Promise<T>`
87
+ * 1. **Always async**: `.get()` returns `Promise<T>`
41
88
  * 2. **Lazy computation**: Value is computed on first access
42
89
  * 3. **Automatic updates**: Recomputes when any source atom changes
43
90
  * 4. **Equality checking**: Only notifies if derived value changed
44
91
  * 5. **Fallback support**: Optional fallback for loading/error states
45
- * 6. **Suspense-like async**: `get()` throws promise if loading
92
+ * 6. **Suspense-like async**: `read()` throws promise if loading
46
93
  * 7. **Conditional dependencies**: Only subscribes to atoms accessed
47
94
  *
48
- * ## Suspense-Style get()
95
+ * ## Suspense-Style read()
49
96
  *
50
- * The `get()` function behaves like React Suspense:
51
- * - If source atom is **loading**: `get()` throws the promise
52
- * - If source atom has **error**: `get()` throws the error
53
- * - If source atom has **value**: `get()` returns the value
97
+ * The `read()` function behaves like React Suspense:
98
+ * - If source atom is **loading**: `read()` throws the promise
99
+ * - If source atom has **error**: `read()` throws the error
100
+ * - If source atom has **value**: `read()` returns the value
54
101
  *
55
102
  * @template T - Derived value type
56
103
  * @template F - Whether fallback is provided
@@ -62,9 +109,9 @@ export interface DerivedContext extends SelectContext {
62
109
  * @example Basic derived (no fallback)
63
110
  * ```ts
64
111
  * const count$ = atom(5);
65
- * const doubled$ = derived(({ get }) => get(count$) * 2);
112
+ * const doubled$ = derived(({ read }) => read(count$) * 2);
66
113
  *
67
- * await doubled$.value; // 10
114
+ * await doubled$.get(); // 10
68
115
  * doubled$.staleValue; // undefined (until first resolve) -> 10
69
116
  * doubled$.state(); // { status: "ready", value: 10 }
70
117
  * ```
@@ -72,7 +119,7 @@ export interface DerivedContext extends SelectContext {
72
119
  * @example With fallback
73
120
  * ```ts
74
121
  * const posts$ = atom(fetchPosts());
75
- * const count$ = derived(({ get }) => get(posts$).length, { fallback: 0 });
122
+ * const count$ = derived(({ read }) => read(posts$).length, { fallback: 0 });
76
123
  *
77
124
  * count$.staleValue; // 0 (during loading) -> 42 (after resolve)
78
125
  * count$.state(); // { status: "loading", promise } during loading
@@ -92,11 +139,11 @@ export interface DerivedContext extends SelectContext {
92
139
  *
93
140
  * @example Refresh
94
141
  * ```ts
95
- * const data$ = derived(({ get }) => get(source$));
142
+ * const data$ = derived(({ read }) => read(source$));
96
143
  * data$.refresh(); // Re-run computation
97
144
  * ```
98
145
  */
99
- export declare function derived<T>(fn: (ctx: DerivedContext) => T, options?: DerivedOptions<T>): DerivedAtom<T, false>;
100
- export declare function derived<T>(fn: (ctx: DerivedContext) => T, options: DerivedOptions<T> & {
146
+ export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options?: DerivedOptions<T> & DerivedInternalOptions): DerivedAtom<T, false>;
147
+ export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options: DerivedOptions<T> & {
101
148
  fallback: T;
102
- }): DerivedAtom<T, true>;
149
+ } & DerivedInternalOptions): DerivedAtom<T, true>;
@@ -1,10 +1,11 @@
1
- import { SelectContext } from './select';
2
- import { EffectOptions } from './types';
1
+ import { ReactiveSelector, SelectContext } from './select';
2
+ import { EffectMeta, EffectOptions } from './types';
3
+ import { WithReadySelectContext } from './withReady';
3
4
  /**
4
5
  * Context object passed to effect functions.
5
- * Extends `SelectContext` with cleanup and error handling utilities.
6
+ * Extends `SelectContext` with cleanup utilities.
6
7
  */
7
- export interface EffectContext extends SelectContext {
8
+ export interface EffectContext extends SelectContext, WithReadySelectContext {
8
9
  /**
9
10
  * Register a cleanup function that runs before the next execution or on dispose.
10
11
  * Multiple cleanup functions can be registered; they run in FIFO order.
@@ -13,41 +14,25 @@ export interface EffectContext extends SelectContext {
13
14
  *
14
15
  * @example
15
16
  * ```ts
16
- * effect(({ get, onCleanup }) => {
17
+ * effect(({ read, onCleanup }) => {
17
18
  * const id = setInterval(() => console.log('tick'), 1000);
18
19
  * onCleanup(() => clearInterval(id));
19
20
  * });
20
21
  * ```
21
22
  */
22
23
  onCleanup: (cleanup: VoidFunction) => void;
23
- /**
24
- * Register an error handler for synchronous errors thrown in the effect.
25
- * If registered, prevents errors from propagating to `options.onError`.
26
- *
27
- * @param handler - Function to handle errors
28
- *
29
- * @example
30
- * ```ts
31
- * effect(({ get, onError }) => {
32
- * onError((e) => console.error('Effect failed:', e));
33
- * riskyOperation();
34
- * });
35
- * ```
36
- */
37
- onError: (handler: (error: unknown) => void) => void;
38
24
  }
39
- /**
40
- * Callback function for effects.
41
- * Receives the effect context with `{ get, all, any, race, settled, onCleanup, onError }` utilities.
42
- */
43
- export type EffectFn = (context: EffectContext) => void;
25
+ export interface Effect {
26
+ dispose: VoidFunction;
27
+ meta?: EffectMeta;
28
+ }
44
29
  /**
45
30
  * Creates a side-effect that runs when accessed atom(s) change.
46
31
  *
47
32
  * Effects are similar to derived atoms but for side-effects rather than computed values.
48
33
  * They inherit derived's behavior:
49
34
  * - **Suspense-like async**: Waits for async atoms to resolve before running
50
- * - **Conditional dependencies**: Only tracks atoms actually accessed via `get()`
35
+ * - **Conditional dependencies**: Only tracks atoms actually accessed via `read()`
51
36
  * - **Automatic cleanup**: Previous cleanup runs before next execution
52
37
  * - **Batched updates**: Atom updates within the effect are batched
53
38
  *
@@ -57,23 +42,23 @@ export type EffectFn = (context: EffectContext) => void;
57
42
  *
58
43
  * ```ts
59
44
  * // ❌ WRONG - Don't use async function
60
- * effect(async ({ get }) => {
45
+ * effect(async ({ read }) => {
61
46
  * const data = await fetch('/api');
62
47
  * console.log(data);
63
48
  * });
64
49
  *
65
- * // ✅ CORRECT - Create async atom and read with get()
50
+ * // ✅ CORRECT - Create async atom and read with read()
66
51
  * const data$ = atom(fetch('/api').then(r => r.json()));
67
- * effect(({ get }) => {
68
- * console.log(get(data$)); // Suspends until resolved
52
+ * effect(({ read }) => {
53
+ * console.log(read(data$)); // Suspends until resolved
69
54
  * });
70
55
  * ```
71
56
  *
72
57
  * ## Basic Usage
73
58
  *
74
59
  * ```ts
75
- * const dispose = effect(({ get }) => {
76
- * localStorage.setItem('count', String(get(countAtom)));
60
+ * const dispose = effect(({ read }) => {
61
+ * localStorage.setItem('count', String(read(countAtom)));
77
62
  * });
78
63
  * ```
79
64
  *
@@ -82,39 +67,54 @@ export type EffectFn = (context: EffectContext) => void;
82
67
  * Use `onCleanup` to register cleanup functions that run before the next execution or on dispose:
83
68
  *
84
69
  * ```ts
85
- * const dispose = effect(({ get, onCleanup }) => {
86
- * const interval = get(intervalAtom);
70
+ * const dispose = effect(({ read, onCleanup }) => {
71
+ * const interval = read(intervalAtom);
87
72
  * const id = setInterval(() => console.log('tick'), interval);
88
73
  * onCleanup(() => clearInterval(id));
89
74
  * });
90
75
  * ```
91
76
  *
92
- * ## Error Handling
77
+ * ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
93
78
  *
94
- * Use `onError` callback to handle errors within the effect, or `options.onError` for unhandled errors:
79
+ * **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
80
+ * Promises when atoms are loading (Suspense pattern). A try/catch will catch
81
+ * these Promises and break the Suspense mechanism.
95
82
  *
96
83
  * ```ts
97
- * // Callback-based error handling
98
- * const dispose = effect(({ get, onError }) => {
99
- * onError((e) => console.error('Effect failed:', e));
100
- * const data = get(dataAtom);
101
- * riskyOperation(data);
84
+ * // ❌ WRONG - Catches Suspense Promise, breaks loading state
85
+ * effect(({ read }) => {
86
+ * try {
87
+ * const data = read(asyncAtom$);
88
+ * riskyOperation(data);
89
+ * } catch (e) {
90
+ * console.error(e); // Catches BOTH errors AND loading promises!
91
+ * }
102
92
  * });
103
93
  *
104
- * // Option-based error handling (for unhandled errors)
105
- * const dispose = effect(
106
- * ({ get }) => {
107
- * const data = get(dataAtom);
108
- * riskyOperation(data);
109
- * },
110
- * { onError: (e) => console.error('Effect failed:', e) }
111
- * );
94
+ * // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
95
+ * effect(({ read, safe }) => {
96
+ * const [err, data] = safe(() => {
97
+ * const raw = read(asyncAtom$); // Can throw Promise (Suspense)
98
+ * return riskyOperation(raw); // Can throw Error
99
+ * });
100
+ *
101
+ * if (err) {
102
+ * console.error('Operation failed:', err);
103
+ * return;
104
+ * }
105
+ * // Use data safely
106
+ * });
112
107
  * ```
113
108
  *
114
- * @param fn - Effect callback receiving context with `{ get, all, any, race, settled, onCleanup, onError }`.
109
+ * The `safe()` utility:
110
+ * - **Catches errors** and returns `[error, undefined]`
111
+ * - **Re-throws Promises** to preserve Suspense behavior
112
+ * - Returns `[undefined, result]` on success
113
+ *
114
+ * @param fn - Effect callback receiving context with `{ read, all, any, race, settled, safe, onCleanup }`.
115
115
  * Must be synchronous (not async).
116
- * @param options - Optional configuration (key, onError for unhandled errors)
116
+ * @param options - Optional configuration (key)
117
117
  * @returns Dispose function to stop the effect and run final cleanup
118
118
  * @throws Error if effect function returns a Promise
119
119
  */
120
- export declare function effect(fn: EffectFn, options?: EffectOptions): VoidFunction;
120
+ export declare function effect(fn: ReactiveSelector<void, EffectContext>, options?: EffectOptions): Effect;