atomirx 0.0.1 → 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 +867 -160
- 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 +17 -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/src/core/derived.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { onCreateHook } from "./onCreateHook";
|
|
|
2
2
|
import { emitter } from "./emitter";
|
|
3
3
|
import { resolveEquality } from "./equality";
|
|
4
4
|
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
5
|
-
import { select, SelectContext } from "./select";
|
|
5
|
+
import { ReactiveSelector, select, SelectContext } from "./select";
|
|
6
6
|
import {
|
|
7
7
|
Atom,
|
|
8
8
|
AtomState,
|
|
@@ -12,21 +12,22 @@ import {
|
|
|
12
12
|
SYMBOL_ATOM,
|
|
13
13
|
SYMBOL_DERIVED,
|
|
14
14
|
} from "./types";
|
|
15
|
+
import { withReady, WithReadySelectContext } from "./withReady";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Context object passed to derived atom selector functions.
|
|
18
|
-
* Provides utilities for reading atoms: `{
|
|
19
|
+
* Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
|
|
19
20
|
*
|
|
20
21
|
* Currently identical to `SelectContext`, but defined separately to allow
|
|
21
22
|
* future derived-specific extensions without breaking changes.
|
|
22
23
|
*/
|
|
23
|
-
export interface DerivedContext extends SelectContext {}
|
|
24
|
+
export interface DerivedContext extends SelectContext, WithReadySelectContext {}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Creates a derived (computed) atom from source atom(s).
|
|
27
28
|
*
|
|
28
29
|
* Derived atoms are **read-only** and automatically recompute when their
|
|
29
|
-
* source atoms change. The `.
|
|
30
|
+
* source atoms change. The `.get()` method always returns a `Promise<T>`,
|
|
30
31
|
* even for synchronous computations.
|
|
31
32
|
*
|
|
32
33
|
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
@@ -35,35 +36,68 @@ export interface DerivedContext extends SelectContext {}
|
|
|
35
36
|
*
|
|
36
37
|
* ```ts
|
|
37
38
|
* // ❌ WRONG - Don't use async function
|
|
38
|
-
* derived(async ({
|
|
39
|
+
* derived(async ({ read }) => {
|
|
39
40
|
* const data = await fetch('/api');
|
|
40
41
|
* return data;
|
|
41
42
|
* });
|
|
42
43
|
*
|
|
43
44
|
* // ❌ WRONG - Don't return a Promise
|
|
44
|
-
* derived(({
|
|
45
|
+
* derived(({ read }) => fetch('/api').then(r => r.json()));
|
|
45
46
|
*
|
|
46
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
47
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
47
48
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
48
|
-
* derived(({
|
|
49
|
+
* derived(({ read }) => read(data$)); // Suspends until resolved
|
|
49
50
|
* ```
|
|
50
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
|
+
*
|
|
51
85
|
* ## Key Features
|
|
52
86
|
*
|
|
53
|
-
* 1. **Always async**: `.
|
|
87
|
+
* 1. **Always async**: `.get()` returns `Promise<T>`
|
|
54
88
|
* 2. **Lazy computation**: Value is computed on first access
|
|
55
89
|
* 3. **Automatic updates**: Recomputes when any source atom changes
|
|
56
90
|
* 4. **Equality checking**: Only notifies if derived value changed
|
|
57
91
|
* 5. **Fallback support**: Optional fallback for loading/error states
|
|
58
|
-
* 6. **Suspense-like async**: `
|
|
92
|
+
* 6. **Suspense-like async**: `read()` throws promise if loading
|
|
59
93
|
* 7. **Conditional dependencies**: Only subscribes to atoms accessed
|
|
60
94
|
*
|
|
61
|
-
* ## Suspense-Style
|
|
95
|
+
* ## Suspense-Style read()
|
|
62
96
|
*
|
|
63
|
-
* The `
|
|
64
|
-
* - If source atom is **loading**: `
|
|
65
|
-
* - If source atom has **error**: `
|
|
66
|
-
* - 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
|
|
67
101
|
*
|
|
68
102
|
* @template T - Derived value type
|
|
69
103
|
* @template F - Whether fallback is provided
|
|
@@ -75,9 +109,9 @@ export interface DerivedContext extends SelectContext {}
|
|
|
75
109
|
* @example Basic derived (no fallback)
|
|
76
110
|
* ```ts
|
|
77
111
|
* const count$ = atom(5);
|
|
78
|
-
* const doubled$ = derived(({
|
|
112
|
+
* const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
79
113
|
*
|
|
80
|
-
* await doubled$.
|
|
114
|
+
* await doubled$.get(); // 10
|
|
81
115
|
* doubled$.staleValue; // undefined (until first resolve) -> 10
|
|
82
116
|
* doubled$.state(); // { status: "ready", value: 10 }
|
|
83
117
|
* ```
|
|
@@ -85,7 +119,7 @@ export interface DerivedContext extends SelectContext {}
|
|
|
85
119
|
* @example With fallback
|
|
86
120
|
* ```ts
|
|
87
121
|
* const posts$ = atom(fetchPosts());
|
|
88
|
-
* const count$ = derived(({
|
|
122
|
+
* const count$ = derived(({ read }) => read(posts$).length, { fallback: 0 });
|
|
89
123
|
*
|
|
90
124
|
* count$.staleValue; // 0 (during loading) -> 42 (after resolve)
|
|
91
125
|
* count$.state(); // { status: "loading", promise } during loading
|
|
@@ -105,26 +139,26 @@ export interface DerivedContext extends SelectContext {}
|
|
|
105
139
|
*
|
|
106
140
|
* @example Refresh
|
|
107
141
|
* ```ts
|
|
108
|
-
* const data$ = derived(({
|
|
142
|
+
* const data$ = derived(({ read }) => read(source$));
|
|
109
143
|
* data$.refresh(); // Re-run computation
|
|
110
144
|
* ```
|
|
111
145
|
*/
|
|
112
146
|
|
|
113
147
|
// Overload: Without fallback - staleValue is T | undefined
|
|
114
148
|
export function derived<T>(
|
|
115
|
-
fn:
|
|
149
|
+
fn: ReactiveSelector<T, DerivedContext>,
|
|
116
150
|
options?: DerivedOptions<T>
|
|
117
151
|
): DerivedAtom<T, false>;
|
|
118
152
|
|
|
119
153
|
// Overload: With fallback - staleValue is guaranteed T
|
|
120
154
|
export function derived<T>(
|
|
121
|
-
fn:
|
|
155
|
+
fn: ReactiveSelector<T, DerivedContext>,
|
|
122
156
|
options: DerivedOptions<T> & { fallback: T }
|
|
123
157
|
): DerivedAtom<T, true>;
|
|
124
158
|
|
|
125
159
|
// Implementation
|
|
126
160
|
export function derived<T>(
|
|
127
|
-
fn:
|
|
161
|
+
fn: ReactiveSelector<T, DerivedContext>,
|
|
128
162
|
options: DerivedOptions<T> & { fallback?: T } = {}
|
|
129
163
|
): DerivedAtom<T, boolean> {
|
|
130
164
|
const changeEmitter = emitter();
|
|
@@ -142,6 +176,10 @@ export function derived<T>(
|
|
|
142
176
|
let isLoading = false;
|
|
143
177
|
let version = 0;
|
|
144
178
|
|
|
179
|
+
// Store resolve/reject to allow reusing the same promise across recomputations
|
|
180
|
+
let resolvePromise: ((value: T) => void) | null = null;
|
|
181
|
+
let rejectPromise: ((error: unknown) => void) | null = null;
|
|
182
|
+
|
|
145
183
|
// Track current subscriptions (atom -> unsubscribe function)
|
|
146
184
|
const subscriptions = new Map<Atom<unknown>, VoidFunction>();
|
|
147
185
|
|
|
@@ -179,65 +217,95 @@ export function derived<T>(
|
|
|
179
217
|
|
|
180
218
|
/**
|
|
181
219
|
* Computes the derived value.
|
|
182
|
-
*
|
|
220
|
+
* Reuses the existing Promise if loading (to prevent orphaned promises
|
|
221
|
+
* that React Suspense might be waiting on).
|
|
183
222
|
*/
|
|
184
223
|
const compute = (silent = false) => {
|
|
185
224
|
const computeVersion = ++version;
|
|
186
225
|
isLoading = true;
|
|
187
226
|
lastError = undefined; // Clear error when starting new computation
|
|
188
227
|
|
|
189
|
-
// Create a new promise
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
result.promise.then(
|
|
201
|
-
() => {
|
|
202
|
-
// Check if we're still the current computation
|
|
203
|
-
if (version !== computeVersion) return;
|
|
204
|
-
attemptCompute();
|
|
205
|
-
},
|
|
206
|
-
(error) => {
|
|
207
|
-
// Check if we're still the current computation
|
|
208
|
-
if (version !== computeVersion) return;
|
|
209
|
-
isLoading = false;
|
|
210
|
-
lastError = error;
|
|
211
|
-
reject(error);
|
|
212
|
-
if (!silent) notify();
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
} else if (result.error !== undefined) {
|
|
216
|
-
// Error thrown
|
|
217
|
-
isLoading = false;
|
|
218
|
-
lastError = result.error;
|
|
219
|
-
reject(result.error);
|
|
220
|
-
if (!silent) notify();
|
|
221
|
-
} else {
|
|
222
|
-
// Success - update lastResolved and resolve
|
|
223
|
-
const newValue = result.value as T;
|
|
224
|
-
isLoading = false;
|
|
225
|
-
lastError = undefined;
|
|
226
|
-
|
|
227
|
-
// Only update and notify if value changed
|
|
228
|
-
if (!lastResolved || !eq(newValue, lastResolved.value)) {
|
|
229
|
-
lastResolved = { value: newValue };
|
|
230
|
-
if (!silent) notify();
|
|
231
|
-
}
|
|
228
|
+
// Create a new promise if:
|
|
229
|
+
// 1. We don't have one yet, OR
|
|
230
|
+
// 2. The previous computation completed (resolved/rejected) and we need a new one
|
|
231
|
+
// This ensures we reuse promises while loading (for Suspense) but create fresh
|
|
232
|
+
// promises for new computations after completion
|
|
233
|
+
if (!resolvePromise) {
|
|
234
|
+
currentPromise = new Promise<T>((resolve, reject) => {
|
|
235
|
+
resolvePromise = resolve;
|
|
236
|
+
rejectPromise = reject;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
232
239
|
|
|
233
|
-
|
|
240
|
+
// Run select to compute value and track dependencies
|
|
241
|
+
const attemptCompute = () => {
|
|
242
|
+
const result = select((context) => fn(context.use(withReady())));
|
|
243
|
+
|
|
244
|
+
// Update subscriptions based on accessed deps
|
|
245
|
+
updateSubscriptions(result.dependencies);
|
|
246
|
+
|
|
247
|
+
if (result.promise) {
|
|
248
|
+
// Notify subscribers that we're now in loading state
|
|
249
|
+
// This allows downstream derived atoms and useSelector to suspend
|
|
250
|
+
if (!silent) notify();
|
|
251
|
+
// Promise thrown - wait for it and retry
|
|
252
|
+
// Note: For never-resolving promises (from ready()), this .then() will never fire.
|
|
253
|
+
// But when a dependency changes, compute() is called again via subscription,
|
|
254
|
+
// and the new computation will run (with a new version).
|
|
255
|
+
result.promise.then(
|
|
256
|
+
() => {
|
|
257
|
+
// Check if we're still the current computation
|
|
258
|
+
if (version !== computeVersion) return;
|
|
259
|
+
attemptCompute();
|
|
260
|
+
},
|
|
261
|
+
(error) => {
|
|
262
|
+
// Check if we're still the current computation
|
|
263
|
+
if (version !== computeVersion) return;
|
|
264
|
+
isLoading = false;
|
|
265
|
+
lastError = error;
|
|
266
|
+
rejectPromise?.(error);
|
|
267
|
+
// Clear resolve/reject so next computation creates new promise
|
|
268
|
+
resolvePromise = null;
|
|
269
|
+
rejectPromise = null;
|
|
270
|
+
// Always notify when promise rejects - subscribers need to know
|
|
271
|
+
// state changed from loading to error
|
|
272
|
+
notify();
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
} else if (result.error !== undefined) {
|
|
276
|
+
// Error thrown
|
|
277
|
+
isLoading = false;
|
|
278
|
+
lastError = result.error;
|
|
279
|
+
rejectPromise?.(result.error);
|
|
280
|
+
// Clear resolve/reject so next computation creates new promise
|
|
281
|
+
resolvePromise = null;
|
|
282
|
+
rejectPromise = null;
|
|
283
|
+
if (!silent) notify();
|
|
284
|
+
} else {
|
|
285
|
+
// Success - update lastResolved and resolve
|
|
286
|
+
const newValue = result.value as T;
|
|
287
|
+
const wasFirstResolve = !lastResolved;
|
|
288
|
+
isLoading = false;
|
|
289
|
+
lastError = undefined;
|
|
290
|
+
|
|
291
|
+
// Only update and notify if value changed
|
|
292
|
+
if (!lastResolved || !eq(newValue, lastResolved.value)) {
|
|
293
|
+
lastResolved = { value: newValue };
|
|
294
|
+
// Always notify on first resolve (loading → ready transition)
|
|
295
|
+
// even if silent, because subscribers need to know state changed
|
|
296
|
+
if (wasFirstResolve || !silent) notify();
|
|
234
297
|
}
|
|
235
|
-
};
|
|
236
298
|
|
|
237
|
-
|
|
238
|
-
|
|
299
|
+
resolvePromise?.(newValue);
|
|
300
|
+
// Clear resolve/reject so next computation creates new promise
|
|
301
|
+
resolvePromise = null;
|
|
302
|
+
rejectPromise = null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
attemptCompute();
|
|
239
307
|
|
|
240
|
-
return currentPromise
|
|
308
|
+
return currentPromise!;
|
|
241
309
|
};
|
|
242
310
|
|
|
243
311
|
/**
|
|
@@ -258,10 +326,10 @@ export function derived<T>(
|
|
|
258
326
|
meta: options.meta,
|
|
259
327
|
|
|
260
328
|
/**
|
|
261
|
-
*
|
|
329
|
+
* Get the computed value as a Promise.
|
|
262
330
|
* Always returns Promise<T>, even for sync computations.
|
|
263
331
|
*/
|
|
264
|
-
get
|
|
332
|
+
get(): Promise<T> {
|
|
265
333
|
init();
|
|
266
334
|
return currentPromise!;
|
|
267
335
|
},
|