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.
- package/README.md +868 -161
- package/coverage/src/core/onCreateHook.ts.html +72 -70
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +69 -22
- package/dist/core/effect.d.ts +52 -52
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/hook.d.ts +1 -1
- package/dist/core/onCreateHook.d.ts +37 -23
- package/dist/core/onErrorHook.d.ts +49 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +107 -22
- package/dist/core/withReady.d.ts +115 -0
- package/dist/core/withReady.test.d.ts +1 -0
- package/dist/index-CBVj1kSj.js +1350 -0
- package/dist/index-Cxk9v0um.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +18 -15
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +422 -377
- 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 +144 -22
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/define.test.ts +12 -11
- package/src/core/define.ts +1 -1
- package/src/core/derived.test.ts +906 -72
- package/src/core/derived.ts +192 -81
- package/src/core/effect.test.ts +651 -45
- package/src/core/effect.ts +102 -98
- package/src/core/getAtomState.ts +69 -0
- package/src/core/hook.test.ts +5 -5
- package/src/core/hook.ts +1 -1
- package/src/core/onCreateHook.ts +38 -23
- package/src/core/onErrorHook.test.ts +350 -0
- package/src/core/onErrorHook.ts +52 -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 +107 -29
- package/src/core/withReady.test.ts +534 -0
- package/src/core/withReady.ts +191 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +21 -7
- 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/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=
|
|
13
|
-
|
|
14
|
-
|
|
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=
|
|
21
|
-
|
|
22
|
-
<h1
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
<div class="quiet">
|
|
55
|
+
Filter:
|
|
56
|
+
<input type="search" id="fileSearch" />
|
|
57
|
+
</div>
|
|
62
58
|
</template>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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"> </span></td><td class="text"><pre class="prettyprint lang-js">import { hook } from "./hook";
|
|
101
97
|
import { MutableAtom } from "./types";
|
|
102
98
|
|
|
103
|
-
export interface
|
|
99
|
+
export interface CreateInfo {
|
|
104
100
|
type: "atom";
|
|
105
101
|
key: string | undefined;
|
|
106
102
|
atom: MutableAtom<any>;
|
|
@@ -113,24 +109,30 @@ export interface ModuleCreateInfo {
|
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
export const onCreateHook =
|
|
116
|
-
hook<(info:
|
|
112
|
+
hook<(info: CreateInfo | ModuleCreateInfo) => void>();
|
|
117
113
|
</pre></td></tr></table></pre>
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
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,33 @@
|
|
|
1
|
-
import {
|
|
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: `{
|
|
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 `.
|
|
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 ({
|
|
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(({
|
|
45
|
+
* derived(({ read }) => fetch('/api').then(r => r.json()));
|
|
32
46
|
*
|
|
33
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
47
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
34
48
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
35
|
-
* derived(({
|
|
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**: `.
|
|
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**: `
|
|
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
|
|
95
|
+
* ## Suspense-Style read()
|
|
49
96
|
*
|
|
50
|
-
* The `
|
|
51
|
-
* - If source atom is **loading**: `
|
|
52
|
-
* - If source atom has **error**: `
|
|
53
|
-
* - If source atom has **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(({
|
|
112
|
+
* const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
66
113
|
*
|
|
67
|
-
* await doubled$.
|
|
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(({
|
|
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(({
|
|
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:
|
|
100
|
-
export declare function derived<T>(fn:
|
|
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>;
|
package/dist/core/effect.d.ts
CHANGED
|
@@ -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
|
|
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(({
|
|
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
|
-
|
|
41
|
-
|
|
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 `
|
|
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 ({
|
|
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
|
|
50
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
66
51
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
67
|
-
* effect(({
|
|
68
|
-
* console.log(
|
|
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(({
|
|
76
|
-
* localStorage.setItem('count', String(
|
|
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(({
|
|
86
|
-
* const interval =
|
|
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
|
-
* ##
|
|
77
|
+
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
93
78
|
*
|
|
94
|
-
*
|
|
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
|
-
* //
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
* //
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* const
|
|
108
|
-
* riskyOperation(
|
|
109
|
-
* }
|
|
110
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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:
|
|
120
|
+
export declare function effect(fn: ReactiveSelector<void, EffectContext>, options?: EffectOptions): Effect;
|