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.
- package/README.md +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batches multiple state updates into a single reactive update cycle.
|
|
3
|
+
*
|
|
4
|
+
* Without batching, each `atom.set()` call triggers immediate notifications to all
|
|
5
|
+
* subscribers. With `batch()`, all updates are collected and subscribers are notified
|
|
6
|
+
* once at the end with the final values.
|
|
7
|
+
*
|
|
8
|
+
* ## Key Behavior
|
|
9
|
+
*
|
|
10
|
+
* 1. **Multiple updates to same atom**: Only 1 notification with final value
|
|
11
|
+
* 2. **Listener deduplication**: Same listener subscribed to multiple atoms = 1 call
|
|
12
|
+
* 3. **Nested batches**: Inner batches are merged into outer batch
|
|
13
|
+
* 4. **Cascading updates**: Updates triggered by listeners are also batched
|
|
14
|
+
*
|
|
15
|
+
* ## When to Use
|
|
16
|
+
*
|
|
17
|
+
* - Updating multiple related atoms together
|
|
18
|
+
* - Preventing intermediate render states
|
|
19
|
+
* - Performance optimization for bulk updates
|
|
20
|
+
* - Ensuring consistent state during complex operations
|
|
21
|
+
*
|
|
22
|
+
* ## How It Works
|
|
23
|
+
*
|
|
24
|
+
* ```
|
|
25
|
+
* batch(() => {
|
|
26
|
+
* a.set(1); // Queued, no notification yet
|
|
27
|
+
* b.set(2); // Queued, no notification yet
|
|
28
|
+
* c.set(3); // Queued, no notification yet
|
|
29
|
+
* });
|
|
30
|
+
* // All listeners notified once here (deduped)
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @template T - Return type of the batched function
|
|
34
|
+
* @param fn - Function containing multiple state updates
|
|
35
|
+
* @returns The return value of fn
|
|
36
|
+
*
|
|
37
|
+
* @example Basic batching - prevent intermediate states
|
|
38
|
+
* ```ts
|
|
39
|
+
* const firstName = atom("John");
|
|
40
|
+
* const lastName = atom("Doe");
|
|
41
|
+
*
|
|
42
|
+
* // Without batch: component renders twice (once per set)
|
|
43
|
+
* firstName.set("Jane");
|
|
44
|
+
* lastName.set("Smith");
|
|
45
|
+
*
|
|
46
|
+
* // With batch: component renders once with final state
|
|
47
|
+
* batch(() => {
|
|
48
|
+
* firstName.set("Jane");
|
|
49
|
+
* lastName.set("Smith");
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Multiple updates to same atom
|
|
54
|
+
* ```ts
|
|
55
|
+
* const counter = atom(0);
|
|
56
|
+
*
|
|
57
|
+
* counter.on(() => console.log("Counter:", counter.value));
|
|
58
|
+
*
|
|
59
|
+
* batch(() => {
|
|
60
|
+
* counter.set(1);
|
|
61
|
+
* counter.set(2);
|
|
62
|
+
* counter.set(3);
|
|
63
|
+
* });
|
|
64
|
+
* // Logs once: "Counter: 3"
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Listener deduplication
|
|
68
|
+
* ```ts
|
|
69
|
+
* const a = atom(0);
|
|
70
|
+
* const b = atom(0);
|
|
71
|
+
*
|
|
72
|
+
* // Same listener subscribed to both atoms
|
|
73
|
+
* const listener = () => console.log("Changed!", a.value, b.value);
|
|
74
|
+
* a.on(listener);
|
|
75
|
+
* b.on(listener);
|
|
76
|
+
*
|
|
77
|
+
* batch(() => {
|
|
78
|
+
* a.set(1);
|
|
79
|
+
* b.set(2);
|
|
80
|
+
* });
|
|
81
|
+
* // Logs once: "Changed! 1 2" (not twice)
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example Nested batches
|
|
85
|
+
* ```ts
|
|
86
|
+
* batch(() => {
|
|
87
|
+
* a.set(1);
|
|
88
|
+
* batch(() => {
|
|
89
|
+
* b.set(2);
|
|
90
|
+
* c.set(3);
|
|
91
|
+
* });
|
|
92
|
+
* d.set(4);
|
|
93
|
+
* });
|
|
94
|
+
* // All updates batched together, listeners notified once at outer batch end
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example Return value
|
|
98
|
+
* ```ts
|
|
99
|
+
* const result = batch(() => {
|
|
100
|
+
* counter.set(10);
|
|
101
|
+
* return counter.value * 2;
|
|
102
|
+
* });
|
|
103
|
+
* console.log(result); // 20
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example With async operations (be careful!)
|
|
107
|
+
* ```ts
|
|
108
|
+
* // ❌ Wrong: async operations escape the batch
|
|
109
|
+
* batch(async () => {
|
|
110
|
+
* a.set(1);
|
|
111
|
+
* await delay(100);
|
|
112
|
+
* b.set(2); // This is OUTSIDE the batch!
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* // ✅ Correct: batch sync operations only
|
|
116
|
+
* batch(() => {
|
|
117
|
+
* a.set(1);
|
|
118
|
+
* b.set(2);
|
|
119
|
+
* });
|
|
120
|
+
* await delay(100);
|
|
121
|
+
* batch(() => {
|
|
122
|
+
* c.set(3);
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare function batch<T>(fn: () => T): T;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { ModuleMeta } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A factory function that creates a swappable lazy singleton store.
|
|
4
|
+
*
|
|
5
|
+
* @template TModule The type of the store instance
|
|
6
|
+
*/
|
|
7
|
+
export interface Define<TModule> {
|
|
8
|
+
readonly key: string | undefined;
|
|
9
|
+
/** Get the current service instance (creates lazily on first call) */
|
|
10
|
+
(): TModule;
|
|
11
|
+
/**
|
|
12
|
+
* Override the service implementation with a lazy factory.
|
|
13
|
+
* Useful for testing, platform-specific implementations, or feature flags.
|
|
14
|
+
* The factory is called lazily on first access after override.
|
|
15
|
+
*
|
|
16
|
+
* **IMPORTANT**: Must be called **before** the service is initialized.
|
|
17
|
+
* Throws an error if called after the service has been accessed.
|
|
18
|
+
*
|
|
19
|
+
* @param factory - Factory function that creates the replacement implementation.
|
|
20
|
+
* Receives the original factory as argument for extending.
|
|
21
|
+
*
|
|
22
|
+
* @throws {Error} If called after the service has been initialized
|
|
23
|
+
*
|
|
24
|
+
* @example Full replacement
|
|
25
|
+
* ```ts
|
|
26
|
+
* myService.override(() => ({ value: 'mock' }));
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example Extend original
|
|
30
|
+
* ```ts
|
|
31
|
+
* myService.override((original) => ({
|
|
32
|
+
* ...original(),
|
|
33
|
+
* extraMethod() { ... }
|
|
34
|
+
* }));
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
override(factory: (original: () => TModule) => TModule): void;
|
|
38
|
+
/**
|
|
39
|
+
* Reset to the original implementation.
|
|
40
|
+
* Clears any override set via `.override()` and disposes the current instance.
|
|
41
|
+
* Next access will create a fresh original instance.
|
|
42
|
+
*/
|
|
43
|
+
reset(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Invalidate the cached instance. Next call will create a fresh instance.
|
|
46
|
+
* If the current instance has a `dispose()` method, it will be called before clearing.
|
|
47
|
+
*
|
|
48
|
+
* Unlike `reset()` which only clears overrides, `invalidate()` clears everything
|
|
49
|
+
* so the next access creates a completely fresh instance from the factory.
|
|
50
|
+
*/
|
|
51
|
+
invalidate(): void;
|
|
52
|
+
/** Returns true if currently using an overridden implementation via `.override()` */
|
|
53
|
+
isOverridden(): boolean;
|
|
54
|
+
/** Returns true if the lazy instance has been created */
|
|
55
|
+
isInitialized(): boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface DefineOptions {
|
|
58
|
+
key?: string;
|
|
59
|
+
meta?: ModuleMeta;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a swappable lazy singleton store.
|
|
63
|
+
*
|
|
64
|
+
* Unlike `once()` from lodash, `define()` allows you to:
|
|
65
|
+
* - Override the implementation at runtime with `.override()`
|
|
66
|
+
* - Reset to the original with `.reset()`
|
|
67
|
+
* - Invalidate and recreate fresh with `.invalidate()`
|
|
68
|
+
*
|
|
69
|
+
* This is useful for:
|
|
70
|
+
* - **Testing** - inject mocks without module mocking
|
|
71
|
+
* - **Platform-specific** - mobile vs web implementations
|
|
72
|
+
* - **Feature flags** - swap implementations at runtime
|
|
73
|
+
*
|
|
74
|
+
* @param creator - Factory function that creates the store instance
|
|
75
|
+
* @returns A callable store with `.override()`, `.reset()`, and `.invalidate()` methods
|
|
76
|
+
*
|
|
77
|
+
* @example Basic usage
|
|
78
|
+
* ```ts
|
|
79
|
+
* const counterStore = define(() => {
|
|
80
|
+
* const [count, setCount] = atom(0);
|
|
81
|
+
* return {
|
|
82
|
+
* count,
|
|
83
|
+
* increment: () => setCount((c) => c + 1),
|
|
84
|
+
* };
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* // Normal usage - lazy singleton
|
|
88
|
+
* const store = counterStore();
|
|
89
|
+
* store.increment();
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example Platform-specific implementation
|
|
93
|
+
* ```ts
|
|
94
|
+
* const storageStore = define(() => ({
|
|
95
|
+
* get: (key) => localStorage.getItem(key),
|
|
96
|
+
* set: (key, value) => localStorage.setItem(key, value),
|
|
97
|
+
* }));
|
|
98
|
+
*
|
|
99
|
+
* // On mobile, swap to secure storage BEFORE first access
|
|
100
|
+
* if (isMobile()) {
|
|
101
|
+
* storageStore.override(() => ({
|
|
102
|
+
* get: (key) => SecureStore.getItem(key),
|
|
103
|
+
* set: (key, value) => SecureStore.setItem(key, value),
|
|
104
|
+
* }));
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @example Extending original with extra methods
|
|
109
|
+
* ```ts
|
|
110
|
+
* apiStore.override((original) => ({
|
|
111
|
+
* ...original(),
|
|
112
|
+
* mockFetch: vi.fn(),
|
|
113
|
+
* }));
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example Wrapping original behavior
|
|
117
|
+
* ```ts
|
|
118
|
+
* loggerStore.override((original) => {
|
|
119
|
+
* const base = original();
|
|
120
|
+
* return {
|
|
121
|
+
* ...base,
|
|
122
|
+
* log: (msg) => {
|
|
123
|
+
* console.log('[DEBUG]', msg);
|
|
124
|
+
* base.log(msg);
|
|
125
|
+
* },
|
|
126
|
+
* };
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example Testing with reset (creates fresh instances)
|
|
131
|
+
* ```ts
|
|
132
|
+
* beforeEach(() => {
|
|
133
|
+
* counterStore.override(() => ({
|
|
134
|
+
* count: () => 999,
|
|
135
|
+
* increment: vi.fn(),
|
|
136
|
+
* }));
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* afterEach(() => {
|
|
140
|
+
* counterStore.reset(); // Clears override, next call creates fresh original
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* @example Testing with invalidate (fresh instance each test)
|
|
145
|
+
* ```ts
|
|
146
|
+
* afterEach(() => {
|
|
147
|
+
* counterStore.invalidate(); // Next call creates fresh instance
|
|
148
|
+
* });
|
|
149
|
+
*
|
|
150
|
+
* it('test 1', () => {
|
|
151
|
+
* counterStore().increment(); // count = 1
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* it('test 2', () => {
|
|
155
|
+
* // Fresh instance, count starts at 0 again
|
|
156
|
+
* expect(counterStore().count()).toBe(0);
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* @example Store with dispose cleanup
|
|
161
|
+
* ```ts
|
|
162
|
+
* const connectionStore = define(() => {
|
|
163
|
+
* const connection = createConnection();
|
|
164
|
+
* return {
|
|
165
|
+
* query: (sql) => connection.query(sql),
|
|
166
|
+
* dispose: () => connection.close(), // Called on invalidate()
|
|
167
|
+
* };
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* connectionStore.invalidate(); // Closes connection, next call creates new
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export declare function define<T>(creator: () => T, options?: DefineOptions): Define<T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SelectContext } from './select';
|
|
2
|
+
import { DerivedAtom, DerivedOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Context object passed to derived atom selector functions.
|
|
5
|
+
* Provides utilities for reading atoms: `{ get, all, any, race, settled }`.
|
|
6
|
+
*
|
|
7
|
+
* Currently identical to `SelectContext`, but defined separately to allow
|
|
8
|
+
* future derived-specific extensions without breaking changes.
|
|
9
|
+
*/
|
|
10
|
+
export interface DerivedContext extends SelectContext {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a derived (computed) atom from source atom(s).
|
|
14
|
+
*
|
|
15
|
+
* Derived atoms are **read-only** and automatically recompute when their
|
|
16
|
+
* source atoms change. The `.value` property always returns a `Promise<T>`,
|
|
17
|
+
* even for synchronous computations.
|
|
18
|
+
*
|
|
19
|
+
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
20
|
+
*
|
|
21
|
+
* **The selector function MUST NOT be async or return a Promise.**
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* // ❌ WRONG - Don't use async function
|
|
25
|
+
* derived(async ({ get }) => {
|
|
26
|
+
* const data = await fetch('/api');
|
|
27
|
+
* return data;
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // ❌ WRONG - Don't return a Promise
|
|
31
|
+
* derived(({ get }) => fetch('/api').then(r => r.json()));
|
|
32
|
+
*
|
|
33
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
34
|
+
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
35
|
+
* derived(({ get }) => get(data$)); // Suspends until resolved
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* ## Key Features
|
|
39
|
+
*
|
|
40
|
+
* 1. **Always async**: `.value` returns `Promise<T>`
|
|
41
|
+
* 2. **Lazy computation**: Value is computed on first access
|
|
42
|
+
* 3. **Automatic updates**: Recomputes when any source atom changes
|
|
43
|
+
* 4. **Equality checking**: Only notifies if derived value changed
|
|
44
|
+
* 5. **Fallback support**: Optional fallback for loading/error states
|
|
45
|
+
* 6. **Suspense-like async**: `get()` throws promise if loading
|
|
46
|
+
* 7. **Conditional dependencies**: Only subscribes to atoms accessed
|
|
47
|
+
*
|
|
48
|
+
* ## Suspense-Style get()
|
|
49
|
+
*
|
|
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
|
|
54
|
+
*
|
|
55
|
+
* @template T - Derived value type
|
|
56
|
+
* @template F - Whether fallback is provided
|
|
57
|
+
* @param fn - Context-based derivation function (must return sync value, not Promise)
|
|
58
|
+
* @param options - Optional configuration (meta, equals, fallback)
|
|
59
|
+
* @returns A read-only derived atom
|
|
60
|
+
* @throws Error if selector returns a Promise or PromiseLike
|
|
61
|
+
*
|
|
62
|
+
* @example Basic derived (no fallback)
|
|
63
|
+
* ```ts
|
|
64
|
+
* const count$ = atom(5);
|
|
65
|
+
* const doubled$ = derived(({ get }) => get(count$) * 2);
|
|
66
|
+
*
|
|
67
|
+
* await doubled$.value; // 10
|
|
68
|
+
* doubled$.staleValue; // undefined (until first resolve) -> 10
|
|
69
|
+
* doubled$.state(); // { status: "ready", value: 10 }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example With fallback
|
|
73
|
+
* ```ts
|
|
74
|
+
* const posts$ = atom(fetchPosts());
|
|
75
|
+
* const count$ = derived(({ get }) => get(posts$).length, { fallback: 0 });
|
|
76
|
+
*
|
|
77
|
+
* count$.staleValue; // 0 (during loading) -> 42 (after resolve)
|
|
78
|
+
* count$.state(); // { status: "loading", promise } during loading
|
|
79
|
+
* // { status: "ready", value: 42 } after resolve
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example Async dependencies
|
|
83
|
+
* ```ts
|
|
84
|
+
* const user$ = atom(fetchUser());
|
|
85
|
+
* const posts$ = atom(fetchPosts());
|
|
86
|
+
*
|
|
87
|
+
* const dashboard$ = derived(({ all }) => {
|
|
88
|
+
* const [user, posts] = all(user$, posts$);
|
|
89
|
+
* return { user, posts };
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @example Refresh
|
|
94
|
+
* ```ts
|
|
95
|
+
* const data$ = derived(({ get }) => get(source$));
|
|
96
|
+
* data$.refresh(); // Re-run computation
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
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> & {
|
|
101
|
+
fallback: T;
|
|
102
|
+
}): DerivedAtom<T, true>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SelectContext } from './select';
|
|
2
|
+
import { EffectOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Context object passed to effect functions.
|
|
5
|
+
* Extends `SelectContext` with cleanup and error handling utilities.
|
|
6
|
+
*/
|
|
7
|
+
export interface EffectContext extends SelectContext {
|
|
8
|
+
/**
|
|
9
|
+
* Register a cleanup function that runs before the next execution or on dispose.
|
|
10
|
+
* Multiple cleanup functions can be registered; they run in FIFO order.
|
|
11
|
+
*
|
|
12
|
+
* @param cleanup - Function to run during cleanup
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* effect(({ get, onCleanup }) => {
|
|
17
|
+
* const id = setInterval(() => console.log('tick'), 1000);
|
|
18
|
+
* onCleanup(() => clearInterval(id));
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
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
|
+
}
|
|
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
|
+
/**
|
|
45
|
+
* Creates a side-effect that runs when accessed atom(s) change.
|
|
46
|
+
*
|
|
47
|
+
* Effects are similar to derived atoms but for side-effects rather than computed values.
|
|
48
|
+
* They inherit derived's behavior:
|
|
49
|
+
* - **Suspense-like async**: Waits for async atoms to resolve before running
|
|
50
|
+
* - **Conditional dependencies**: Only tracks atoms actually accessed via `get()`
|
|
51
|
+
* - **Automatic cleanup**: Previous cleanup runs before next execution
|
|
52
|
+
* - **Batched updates**: Atom updates within the effect are batched
|
|
53
|
+
*
|
|
54
|
+
* ## IMPORTANT: Effect Function Must Be Synchronous
|
|
55
|
+
*
|
|
56
|
+
* **The effect function MUST NOT be async or return a Promise.**
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* // ❌ WRONG - Don't use async function
|
|
60
|
+
* effect(async ({ get }) => {
|
|
61
|
+
* const data = await fetch('/api');
|
|
62
|
+
* console.log(data);
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
66
|
+
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
67
|
+
* effect(({ get }) => {
|
|
68
|
+
* console.log(get(data$)); // Suspends until resolved
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* ## Basic Usage
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* const dispose = effect(({ get }) => {
|
|
76
|
+
* localStorage.setItem('count', String(get(countAtom)));
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ## With Cleanup
|
|
81
|
+
*
|
|
82
|
+
* Use `onCleanup` to register cleanup functions that run before the next execution or on dispose:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* const dispose = effect(({ get, onCleanup }) => {
|
|
86
|
+
* const interval = get(intervalAtom);
|
|
87
|
+
* const id = setInterval(() => console.log('tick'), interval);
|
|
88
|
+
* onCleanup(() => clearInterval(id));
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ## Error Handling
|
|
93
|
+
*
|
|
94
|
+
* Use `onError` callback to handle errors within the effect, or `options.onError` for unhandled errors:
|
|
95
|
+
*
|
|
96
|
+
* ```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);
|
|
102
|
+
* });
|
|
103
|
+
*
|
|
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
|
+
* );
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @param fn - Effect callback receiving context with `{ get, all, any, race, settled, onCleanup, onError }`.
|
|
115
|
+
* Must be synchronous (not async).
|
|
116
|
+
* @param options - Optional configuration (key, onError for unhandled errors)
|
|
117
|
+
* @returns Dispose function to stop the effect and run final cleanup
|
|
118
|
+
* @throws Error if effect function returns a Promise
|
|
119
|
+
*/
|
|
120
|
+
export declare function effect(fn: EffectFn, options?: EffectOptions): VoidFunction;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|