atomirx 0.0.2 → 0.0.4
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.
- package/README.md +866 -159
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +55 -21
- package/dist/core/effect.d.ts +47 -51
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +55 -19
- package/dist/core/withReady.d.ts +69 -0
- package/dist/index-CqO6BDwj.cjs +1 -0
- package/dist/index-D8RDOTB_.js +1319 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +12 -10
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +423 -379
- package/dist/react/rx.d.ts +114 -25
- package/dist/react/useAction.d.ts +5 -4
- package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
- package/dist/react/useSelector.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/atom.test.ts +307 -43
- package/src/core/atom.ts +143 -21
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/derived.test.ts +727 -72
- package/src/core/derived.ts +141 -73
- package/src/core/effect.test.ts +259 -39
- package/src/core/effect.ts +62 -85
- package/src/core/getAtomState.ts +69 -0
- package/src/core/promiseCache.test.ts +5 -3
- package/src/core/promiseCache.ts +76 -71
- package/src/core/select.ts +405 -130
- package/src/core/selector.test.ts +574 -32
- package/src/core/types.ts +54 -26
- package/src/core/withReady.test.ts +360 -0
- package/src/core/withReady.ts +127 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +11 -6
- package/src/react/index.ts +2 -1
- package/src/react/rx.test.tsx +173 -18
- package/src/react/rx.tsx +274 -43
- package/src/react/useAction.test.ts +12 -14
- package/src/react/useAction.ts +11 -9
- package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
- package/src/react/{useValue.ts → useSelector.ts} +64 -33
- package/v2.md +44 -44
- package/dist/index-2ok7ilik.js +0 -1217
- package/dist/index-B_5SFzfl.cjs +0 -1
- /package/dist/{react/useValue.test.d.ts → core/withReady.test.d.ts} +0 -0
package/dist/core/atom.d.ts
CHANGED
|
@@ -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, `.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
};
|
package/dist/core/batch.d.ts
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* ```ts
|
|
55
55
|
* const counter = atom(0);
|
|
56
56
|
*
|
|
57
|
-
* counter.on(() => console.log("Counter:", counter.
|
|
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.
|
|
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.
|
|
101
|
+
* return counter.get() * 2;
|
|
102
102
|
* });
|
|
103
103
|
* console.log(result); // 20
|
|
104
104
|
* ```
|
package/dist/core/derived.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { SelectContext } from './select';
|
|
1
|
+
import { ReactiveSelector, SelectContext } from './select';
|
|
2
2
|
import { DerivedAtom, DerivedOptions } from './types';
|
|
3
|
+
import { WithReadySelectContext } from './withReady';
|
|
3
4
|
/**
|
|
4
5
|
* Context object passed to derived atom selector functions.
|
|
5
|
-
* Provides utilities for reading atoms: `{
|
|
6
|
+
* Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
|
|
6
7
|
*
|
|
7
8
|
* Currently identical to `SelectContext`, but defined separately to allow
|
|
8
9
|
* future derived-specific extensions without breaking changes.
|
|
9
10
|
*/
|
|
10
|
-
export interface DerivedContext extends SelectContext {
|
|
11
|
+
export interface DerivedContext extends SelectContext, WithReadySelectContext {
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Creates a derived (computed) atom from source atom(s).
|
|
14
15
|
*
|
|
15
16
|
* Derived atoms are **read-only** and automatically recompute when their
|
|
16
|
-
* source atoms change. The `.
|
|
17
|
+
* source atoms change. The `.get()` method always returns a `Promise<T>`,
|
|
17
18
|
* even for synchronous computations.
|
|
18
19
|
*
|
|
19
20
|
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
@@ -22,35 +23,68 @@ export interface DerivedContext extends SelectContext {
|
|
|
22
23
|
*
|
|
23
24
|
* ```ts
|
|
24
25
|
* // ❌ WRONG - Don't use async function
|
|
25
|
-
* derived(async ({
|
|
26
|
+
* derived(async ({ read }) => {
|
|
26
27
|
* const data = await fetch('/api');
|
|
27
28
|
* return data;
|
|
28
29
|
* });
|
|
29
30
|
*
|
|
30
31
|
* // ❌ WRONG - Don't return a Promise
|
|
31
|
-
* derived(({
|
|
32
|
+
* derived(({ read }) => fetch('/api').then(r => r.json()));
|
|
32
33
|
*
|
|
33
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
34
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
34
35
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
35
|
-
* derived(({
|
|
36
|
+
* derived(({ read }) => read(data$)); // Suspends until resolved
|
|
36
37
|
* ```
|
|
37
38
|
*
|
|
39
|
+
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
40
|
+
*
|
|
41
|
+
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
42
|
+
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
43
|
+
* these Promises and break the Suspense mechanism.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
47
|
+
* derived(({ read }) => {
|
|
48
|
+
* try {
|
|
49
|
+
* return read(asyncAtom$);
|
|
50
|
+
* } catch (e) {
|
|
51
|
+
* return 'fallback'; // This catches BOTH errors AND loading promises!
|
|
52
|
+
* }
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
56
|
+
* derived(({ read, safe }) => {
|
|
57
|
+
* const [err, data] = safe(() => {
|
|
58
|
+
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
59
|
+
* return JSON.parse(raw); // Can throw Error
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* if (err) return { error: err.message };
|
|
63
|
+
* return { data };
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* The `safe()` utility:
|
|
68
|
+
* - **Catches errors** and returns `[error, undefined]`
|
|
69
|
+
* - **Re-throws Promises** to preserve Suspense behavior
|
|
70
|
+
* - Returns `[undefined, result]` on success
|
|
71
|
+
*
|
|
38
72
|
* ## Key Features
|
|
39
73
|
*
|
|
40
|
-
* 1. **Always async**: `.
|
|
74
|
+
* 1. **Always async**: `.get()` returns `Promise<T>`
|
|
41
75
|
* 2. **Lazy computation**: Value is computed on first access
|
|
42
76
|
* 3. **Automatic updates**: Recomputes when any source atom changes
|
|
43
77
|
* 4. **Equality checking**: Only notifies if derived value changed
|
|
44
78
|
* 5. **Fallback support**: Optional fallback for loading/error states
|
|
45
|
-
* 6. **Suspense-like async**: `
|
|
79
|
+
* 6. **Suspense-like async**: `read()` throws promise if loading
|
|
46
80
|
* 7. **Conditional dependencies**: Only subscribes to atoms accessed
|
|
47
81
|
*
|
|
48
|
-
* ## Suspense-Style
|
|
82
|
+
* ## Suspense-Style read()
|
|
49
83
|
*
|
|
50
|
-
* The `
|
|
51
|
-
* - If source atom is **loading**: `
|
|
52
|
-
* - If source atom has **error**: `
|
|
53
|
-
* - If source atom has **value**: `
|
|
84
|
+
* The `read()` function behaves like React Suspense:
|
|
85
|
+
* - If source atom is **loading**: `read()` throws the promise
|
|
86
|
+
* - If source atom has **error**: `read()` throws the error
|
|
87
|
+
* - If source atom has **value**: `read()` returns the value
|
|
54
88
|
*
|
|
55
89
|
* @template T - Derived value type
|
|
56
90
|
* @template F - Whether fallback is provided
|
|
@@ -62,9 +96,9 @@ export interface DerivedContext extends SelectContext {
|
|
|
62
96
|
* @example Basic derived (no fallback)
|
|
63
97
|
* ```ts
|
|
64
98
|
* const count$ = atom(5);
|
|
65
|
-
* const doubled$ = derived(({
|
|
99
|
+
* const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
66
100
|
*
|
|
67
|
-
* await doubled$.
|
|
101
|
+
* await doubled$.get(); // 10
|
|
68
102
|
* doubled$.staleValue; // undefined (until first resolve) -> 10
|
|
69
103
|
* doubled$.state(); // { status: "ready", value: 10 }
|
|
70
104
|
* ```
|
|
@@ -72,7 +106,7 @@ export interface DerivedContext extends SelectContext {
|
|
|
72
106
|
* @example With fallback
|
|
73
107
|
* ```ts
|
|
74
108
|
* const posts$ = atom(fetchPosts());
|
|
75
|
-
* const count$ = derived(({
|
|
109
|
+
* const count$ = derived(({ read }) => read(posts$).length, { fallback: 0 });
|
|
76
110
|
*
|
|
77
111
|
* count$.staleValue; // 0 (during loading) -> 42 (after resolve)
|
|
78
112
|
* count$.state(); // { status: "loading", promise } during loading
|
|
@@ -92,11 +126,11 @@ export interface DerivedContext extends SelectContext {
|
|
|
92
126
|
*
|
|
93
127
|
* @example Refresh
|
|
94
128
|
* ```ts
|
|
95
|
-
* const data$ = derived(({
|
|
129
|
+
* const data$ = derived(({ read }) => read(source$));
|
|
96
130
|
* data$.refresh(); // Re-run computation
|
|
97
131
|
* ```
|
|
98
132
|
*/
|
|
99
|
-
export declare function derived<T>(fn:
|
|
100
|
-
export declare function derived<T>(fn:
|
|
133
|
+
export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options?: DerivedOptions<T>): DerivedAtom<T, false>;
|
|
134
|
+
export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options: DerivedOptions<T> & {
|
|
101
135
|
fallback: T;
|
|
102
136
|
}): DerivedAtom<T, true>;
|
package/dist/core/effect.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { SelectContext } from './select';
|
|
1
|
+
import { ReactiveSelector, SelectContext } from './select';
|
|
2
2
|
import { EffectOptions } from './types';
|
|
3
|
+
import { WithReadySelectContext } from './withReady';
|
|
3
4
|
/**
|
|
4
5
|
* Context object passed to effect functions.
|
|
5
|
-
* Extends `SelectContext` with cleanup
|
|
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,21 @@ export interface EffectContext extends SelectContext {
|
|
|
13
14
|
*
|
|
14
15
|
* @example
|
|
15
16
|
* ```ts
|
|
16
|
-
* effect(({
|
|
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;
|
|
44
25
|
/**
|
|
45
26
|
* Creates a side-effect that runs when accessed atom(s) change.
|
|
46
27
|
*
|
|
47
28
|
* Effects are similar to derived atoms but for side-effects rather than computed values.
|
|
48
29
|
* They inherit derived's behavior:
|
|
49
30
|
* - **Suspense-like async**: Waits for async atoms to resolve before running
|
|
50
|
-
* - **Conditional dependencies**: Only tracks atoms actually accessed via `
|
|
31
|
+
* - **Conditional dependencies**: Only tracks atoms actually accessed via `read()`
|
|
51
32
|
* - **Automatic cleanup**: Previous cleanup runs before next execution
|
|
52
33
|
* - **Batched updates**: Atom updates within the effect are batched
|
|
53
34
|
*
|
|
@@ -57,23 +38,23 @@ export type EffectFn = (context: EffectContext) => void;
|
|
|
57
38
|
*
|
|
58
39
|
* ```ts
|
|
59
40
|
* // ❌ WRONG - Don't use async function
|
|
60
|
-
* effect(async ({
|
|
41
|
+
* effect(async ({ read }) => {
|
|
61
42
|
* const data = await fetch('/api');
|
|
62
43
|
* console.log(data);
|
|
63
44
|
* });
|
|
64
45
|
*
|
|
65
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
46
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
66
47
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
67
|
-
* effect(({
|
|
68
|
-
* console.log(
|
|
48
|
+
* effect(({ read }) => {
|
|
49
|
+
* console.log(read(data$)); // Suspends until resolved
|
|
69
50
|
* });
|
|
70
51
|
* ```
|
|
71
52
|
*
|
|
72
53
|
* ## Basic Usage
|
|
73
54
|
*
|
|
74
55
|
* ```ts
|
|
75
|
-
* const dispose = effect(({
|
|
76
|
-
* localStorage.setItem('count', String(
|
|
56
|
+
* const dispose = effect(({ read }) => {
|
|
57
|
+
* localStorage.setItem('count', String(read(countAtom)));
|
|
77
58
|
* });
|
|
78
59
|
* ```
|
|
79
60
|
*
|
|
@@ -82,39 +63,54 @@ export type EffectFn = (context: EffectContext) => void;
|
|
|
82
63
|
* Use `onCleanup` to register cleanup functions that run before the next execution or on dispose:
|
|
83
64
|
*
|
|
84
65
|
* ```ts
|
|
85
|
-
* const dispose = effect(({
|
|
86
|
-
* const interval =
|
|
66
|
+
* const dispose = effect(({ read, onCleanup }) => {
|
|
67
|
+
* const interval = read(intervalAtom);
|
|
87
68
|
* const id = setInterval(() => console.log('tick'), interval);
|
|
88
69
|
* onCleanup(() => clearInterval(id));
|
|
89
70
|
* });
|
|
90
71
|
* ```
|
|
91
72
|
*
|
|
92
|
-
* ##
|
|
73
|
+
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
93
74
|
*
|
|
94
|
-
*
|
|
75
|
+
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
76
|
+
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
77
|
+
* these Promises and break the Suspense mechanism.
|
|
95
78
|
*
|
|
96
79
|
* ```ts
|
|
97
|
-
* //
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
80
|
+
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
81
|
+
* effect(({ read }) => {
|
|
82
|
+
* try {
|
|
83
|
+
* const data = read(asyncAtom$);
|
|
84
|
+
* riskyOperation(data);
|
|
85
|
+
* } catch (e) {
|
|
86
|
+
* console.error(e); // Catches BOTH errors AND loading promises!
|
|
87
|
+
* }
|
|
102
88
|
* });
|
|
103
89
|
*
|
|
104
|
-
* //
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* const
|
|
108
|
-
* riskyOperation(
|
|
109
|
-
* }
|
|
110
|
-
*
|
|
111
|
-
* )
|
|
90
|
+
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
91
|
+
* effect(({ read, safe }) => {
|
|
92
|
+
* const [err, data] = safe(() => {
|
|
93
|
+
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
94
|
+
* return riskyOperation(raw); // Can throw Error
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* if (err) {
|
|
98
|
+
* console.error('Operation failed:', err);
|
|
99
|
+
* return;
|
|
100
|
+
* }
|
|
101
|
+
* // Use data safely
|
|
102
|
+
* });
|
|
112
103
|
* ```
|
|
113
104
|
*
|
|
114
|
-
*
|
|
105
|
+
* The `safe()` utility:
|
|
106
|
+
* - **Catches errors** and returns `[error, undefined]`
|
|
107
|
+
* - **Re-throws Promises** to preserve Suspense behavior
|
|
108
|
+
* - Returns `[undefined, result]` on success
|
|
109
|
+
*
|
|
110
|
+
* @param fn - Effect callback receiving context with `{ read, all, any, race, settled, safe, onCleanup }`.
|
|
115
111
|
* Must be synchronous (not async).
|
|
116
|
-
* @param options - Optional configuration (key
|
|
112
|
+
* @param options - Optional configuration (key)
|
|
117
113
|
* @returns Dispose function to stop the effect and run final cleanup
|
|
118
114
|
* @throws Error if effect function returns a Promise
|
|
119
115
|
*/
|
|
120
|
-
export declare function effect(fn:
|
|
116
|
+
export declare function effect(fn: ReactiveSelector<void, EffectContext>, _options?: EffectOptions): VoidFunction;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Atom, AtomState } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the current state of an atom as a discriminated union.
|
|
4
|
+
*
|
|
5
|
+
* For any atom (mutable or derived):
|
|
6
|
+
* - If value is not a Promise: returns ready state
|
|
7
|
+
* - If value is a Promise: tracks and returns its state (ready/error/loading)
|
|
8
|
+
*
|
|
9
|
+
* @param atom - The atom to get state from
|
|
10
|
+
* @returns AtomState discriminated union (ready | error | loading)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const state = getAtomState(myAtom$);
|
|
15
|
+
*
|
|
16
|
+
* switch (state.status) {
|
|
17
|
+
* case "ready":
|
|
18
|
+
* console.log(state.value); // T
|
|
19
|
+
* break;
|
|
20
|
+
* case "error":
|
|
21
|
+
* console.log(state.error);
|
|
22
|
+
* break;
|
|
23
|
+
* case "loading":
|
|
24
|
+
* console.log(state.promise);
|
|
25
|
+
* break;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAtomState<T>(atom: Atom<T>): AtomState<Awaited<T>>;
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DerivedAtom } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Metadata attached to combined promises for comparison.
|
|
4
|
+
*/
|
|
5
|
+
export interface CombinedPromiseMeta {
|
|
6
|
+
type: "all" | "race" | "allSettled";
|
|
7
|
+
promises: Promise<unknown>[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Gets the metadata for a combined promise, if any.
|
|
11
|
+
* Used internally by promisesEqual for comparison.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getCombinedPromiseMetadata(promise: PromiseLike<unknown>): CombinedPromiseMeta | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Create a combined promise with metadata for comparison.
|
|
16
|
+
* If only one promise, returns it directly (no metadata needed).
|
|
17
|
+
*/
|
|
18
|
+
export declare function createCombinedPromise(type: "all" | "race" | "allSettled", promises: Promise<unknown>[]): PromiseLike<unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Compare two promises, considering combined promise metadata.
|
|
21
|
+
* Returns true if promises are considered equal.
|
|
22
|
+
*/
|
|
23
|
+
export declare function promisesEqual(a: PromiseLike<unknown> | undefined, b: PromiseLike<unknown> | undefined): boolean;
|
|
2
24
|
/**
|
|
3
25
|
* Represents the state of a tracked Promise.
|
|
4
26
|
*/
|
|
@@ -51,37 +73,6 @@ export declare function isTracked(promise: PromiseLike<unknown>): boolean;
|
|
|
51
73
|
* Type guard to check if a value is a DerivedAtom.
|
|
52
74
|
*/
|
|
53
75
|
export declare function isDerived<T>(value: unknown): value is DerivedAtom<T, boolean>;
|
|
54
|
-
/**
|
|
55
|
-
* Returns the current state of an atom as a discriminated union.
|
|
56
|
-
*
|
|
57
|
-
* For DerivedAtom:
|
|
58
|
-
* - Returns atom.state() directly (derived atoms track their own state)
|
|
59
|
-
*
|
|
60
|
-
* For MutableAtom:
|
|
61
|
-
* - If value is not a Promise: returns ready state
|
|
62
|
-
* - If value is a Promise: tracks and returns its state (ready/error/loading)
|
|
63
|
-
*
|
|
64
|
-
* @param atom - The atom to get state from
|
|
65
|
-
* @returns AtomState discriminated union (ready | error | loading)
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```ts
|
|
69
|
-
* const state = getAtomState(myAtom$);
|
|
70
|
-
*
|
|
71
|
-
* switch (state.status) {
|
|
72
|
-
* case "ready":
|
|
73
|
-
* console.log(state.value); // T
|
|
74
|
-
* break;
|
|
75
|
-
* case "error":
|
|
76
|
-
* console.log(state.error);
|
|
77
|
-
* break;
|
|
78
|
-
* case "loading":
|
|
79
|
-
* console.log(state.promise);
|
|
80
|
-
* break;
|
|
81
|
-
* }
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export declare function getAtomState<T>(atom: Atom<T>): AtomState<Awaited<T>>;
|
|
85
76
|
/**
|
|
86
77
|
* Unwraps a value that may be a Promise.
|
|
87
78
|
* - If not a Promise, returns the value directly.
|