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/select.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { isPromiseLike } from "./isPromiseLike";
|
|
2
|
-
import { getAtomState
|
|
3
|
-
import {
|
|
2
|
+
import { getAtomState } from "./getAtomState";
|
|
3
|
+
import { createCombinedPromise } from "./promiseCache";
|
|
4
|
+
import { isAtom } from "./isAtom";
|
|
5
|
+
import {
|
|
6
|
+
Atom,
|
|
7
|
+
AtomValue,
|
|
8
|
+
KeyedResult,
|
|
9
|
+
Pipeable,
|
|
10
|
+
SelectStateResult,
|
|
11
|
+
SettledResult,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { withUse } from "./withUse";
|
|
4
14
|
|
|
5
15
|
// AggregateError polyfill for environments that don't support it
|
|
6
16
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -33,11 +43,19 @@ export interface SelectResult<T> {
|
|
|
33
43
|
dependencies: Set<Atom<unknown>>;
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Result type for safe() - error-first tuple.
|
|
48
|
+
* Either [undefined, T] for success or [unknown, undefined] for error.
|
|
49
|
+
*/
|
|
50
|
+
export type SafeResult<T> =
|
|
51
|
+
| [error: undefined, result: T]
|
|
52
|
+
| [error: unknown, result: undefined];
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* Context object passed to selector functions.
|
|
38
56
|
* Provides utilities for reading atoms and handling async operations.
|
|
39
57
|
*/
|
|
40
|
-
export interface SelectContext {
|
|
58
|
+
export interface SelectContext extends Pipeable {
|
|
41
59
|
/**
|
|
42
60
|
* Read the current value of an atom.
|
|
43
61
|
* Tracks the atom as a dependency.
|
|
@@ -50,79 +68,192 @@ export interface SelectContext {
|
|
|
50
68
|
* @param atom - The atom to read
|
|
51
69
|
* @returns The atom's current value (Awaited<T>)
|
|
52
70
|
*/
|
|
53
|
-
|
|
71
|
+
read<T>(atom: Atom<T>): Awaited<T>;
|
|
54
72
|
|
|
55
73
|
/**
|
|
56
74
|
* Wait for all atoms to resolve (like Promise.all).
|
|
57
|
-
*
|
|
75
|
+
* Array-based - pass atoms as an array.
|
|
58
76
|
*
|
|
59
77
|
* - If all atoms are ready → returns array of values
|
|
60
78
|
* - If any atom has error → throws that error
|
|
61
79
|
* - If any atom is loading (no fallback) → throws Promise
|
|
62
80
|
* - If loading with fallback → uses staleValue
|
|
63
81
|
*
|
|
64
|
-
* @param atoms -
|
|
82
|
+
* @param atoms - Array of atoms to wait for
|
|
65
83
|
* @returns Array of resolved values (same order as input)
|
|
66
84
|
*
|
|
67
85
|
* @example
|
|
68
86
|
* ```ts
|
|
69
|
-
* const [user, posts] = all(user$, posts$);
|
|
87
|
+
* const [user, posts] = all([user$, posts$]);
|
|
70
88
|
* ```
|
|
71
89
|
*/
|
|
72
|
-
all<A extends Atom<unknown>[]>(
|
|
73
|
-
...atoms: A
|
|
74
|
-
): { [K in keyof A]: AtomValue<A[K]> };
|
|
90
|
+
all<A extends Atom<unknown>[]>(atoms: A): { [K in keyof A]: AtomValue<A[K]> };
|
|
75
91
|
|
|
76
92
|
/**
|
|
77
93
|
* Return the first settled value (like Promise.race).
|
|
78
|
-
*
|
|
94
|
+
* Object-based - pass atoms as a record with keys.
|
|
79
95
|
*
|
|
80
|
-
* - If any atom is ready → returns first ready
|
|
96
|
+
* - If any atom is ready → returns `{ key, value }` for first ready
|
|
81
97
|
* - If any atom has error → throws first error
|
|
82
98
|
* - If all atoms are loading → throws first Promise
|
|
83
99
|
*
|
|
100
|
+
* The `key` in the result identifies which atom won the race.
|
|
101
|
+
*
|
|
84
102
|
* Note: race() does NOT use fallback - it's meant for first "real" settled value.
|
|
85
103
|
*
|
|
86
|
-
* @param atoms -
|
|
87
|
-
* @returns
|
|
104
|
+
* @param atoms - Record of atoms to race
|
|
105
|
+
* @returns KeyedResult with winning key and value
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const result = race({ cache: cache$, api: api$ });
|
|
110
|
+
* console.log(result.key); // "cache" or "api"
|
|
111
|
+
* console.log(result.value); // The winning value
|
|
112
|
+
* ```
|
|
88
113
|
*/
|
|
89
|
-
race<
|
|
114
|
+
race<T extends Record<string, Atom<unknown>>>(
|
|
115
|
+
atoms: T
|
|
116
|
+
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
|
|
90
117
|
|
|
91
118
|
/**
|
|
92
119
|
* Return the first ready value (like Promise.any).
|
|
93
|
-
*
|
|
120
|
+
* Object-based - pass atoms as a record with keys.
|
|
94
121
|
*
|
|
95
|
-
* - If any atom is ready → returns first ready
|
|
122
|
+
* - If any atom is ready → returns `{ key, value }` for first ready
|
|
96
123
|
* - If all atoms have errors → throws AggregateError
|
|
97
124
|
* - If any loading (not all errored) → throws Promise
|
|
98
125
|
*
|
|
126
|
+
* The `key` in the result identifies which atom resolved first.
|
|
127
|
+
*
|
|
99
128
|
* Note: any() does NOT use fallback - it waits for a real ready value.
|
|
100
129
|
*
|
|
101
|
-
* @param atoms -
|
|
102
|
-
* @returns
|
|
130
|
+
* @param atoms - Record of atoms to check
|
|
131
|
+
* @returns KeyedResult with winning key and value
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const result = any({ primary: primaryApi$, fallback: fallbackApi$ });
|
|
136
|
+
* console.log(result.key); // "primary" or "fallback"
|
|
137
|
+
* console.log(result.value); // The winning value
|
|
138
|
+
* ```
|
|
103
139
|
*/
|
|
104
|
-
any<
|
|
140
|
+
any<T extends Record<string, Atom<unknown>>>(
|
|
141
|
+
atoms: T
|
|
142
|
+
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
|
|
105
143
|
|
|
106
144
|
/**
|
|
107
145
|
* Get all atom statuses when all are settled (like Promise.allSettled).
|
|
108
|
-
*
|
|
146
|
+
* Array-based - pass atoms as an array.
|
|
109
147
|
*
|
|
110
148
|
* - If all atoms are settled → returns array of statuses
|
|
111
149
|
* - If any atom is loading (no fallback) → throws Promise
|
|
112
150
|
* - If loading with fallback → { status: "ready", value: staleValue }
|
|
113
151
|
*
|
|
114
|
-
* @param atoms -
|
|
152
|
+
* @param atoms - Array of atoms to check
|
|
115
153
|
* @returns Array of settled results
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const [userResult, postsResult] = settled([user$, posts$]);
|
|
158
|
+
* ```
|
|
116
159
|
*/
|
|
117
160
|
settled<A extends Atom<unknown>[]>(
|
|
118
|
-
|
|
161
|
+
atoms: A
|
|
119
162
|
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Safely execute a function, catching errors but preserving Suspense.
|
|
166
|
+
*
|
|
167
|
+
* - If function succeeds → returns [undefined, result]
|
|
168
|
+
* - If function throws Error → returns [error, undefined]
|
|
169
|
+
* - If function throws Promise → re-throws (preserves Suspense)
|
|
170
|
+
*
|
|
171
|
+
* Use this when you need error handling inside selectors without
|
|
172
|
+
* accidentally catching Suspense promises.
|
|
173
|
+
*
|
|
174
|
+
* @param fn - Function to execute safely
|
|
175
|
+
* @returns Error-first tuple: [error, result]
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* const data$ = derived(({ get, safe }) => {
|
|
180
|
+
* const [err, data] = safe(() => {
|
|
181
|
+
* const raw = get(raw$);
|
|
182
|
+
* return JSON.parse(raw); // Can throw SyntaxError
|
|
183
|
+
* });
|
|
184
|
+
*
|
|
185
|
+
* if (err) {
|
|
186
|
+
* return { error: err.message };
|
|
187
|
+
* }
|
|
188
|
+
* return { data };
|
|
189
|
+
* });
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
safe<T>(fn: () => T): SafeResult<T>;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the async state of an atom or selector without throwing.
|
|
196
|
+
*
|
|
197
|
+
* Unlike `read()` which throws promises/errors (Suspense pattern),
|
|
198
|
+
* `state()` always returns a `SelectStateResult<T>` object that you can
|
|
199
|
+
* inspect and handle inline.
|
|
200
|
+
*
|
|
201
|
+
* All properties (`status`, `value`, `error`) are always present,
|
|
202
|
+
* enabling easy destructuring:
|
|
203
|
+
* ```ts
|
|
204
|
+
* const { status, value, error } = state(atom$);
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* @param atom - The atom to get state from
|
|
208
|
+
* @returns SelectStateResult with status, value, error (no promise - for equality)
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* // Get state of single atom
|
|
213
|
+
* const dashboard$ = derived(({ state }) => {
|
|
214
|
+
* const userState = state(user$);
|
|
215
|
+
* const postsState = state(posts$);
|
|
216
|
+
*
|
|
217
|
+
* return {
|
|
218
|
+
* user: userState.value, // undefined if not ready
|
|
219
|
+
* isLoading: userState.status === 'loading' || postsState.status === 'loading',
|
|
220
|
+
* };
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
state<T>(atom: Atom<T>): SelectStateResult<Awaited<T>>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get the async state of a selector function without throwing.
|
|
228
|
+
*
|
|
229
|
+
* Wraps the selector in try/catch and returns the result as a
|
|
230
|
+
* `SelectStateResult<T>` object. Useful for getting state of combined
|
|
231
|
+
* operations like `all()`, `race()`, etc.
|
|
232
|
+
*
|
|
233
|
+
* @param selector - Function that may throw promises or errors
|
|
234
|
+
* @returns SelectStateResult with status, value, error (no promise - for equality)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* // Get state of combined operation
|
|
239
|
+
* const allData$ = derived(({ state, all }) => {
|
|
240
|
+
* const result = state(() => all(a$, b$, c$));
|
|
241
|
+
*
|
|
242
|
+
* if (result.status === 'loading') return { loading: true };
|
|
243
|
+
* if (result.status === 'error') return { error: result.error };
|
|
244
|
+
* return { data: result.value };
|
|
245
|
+
* });
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
state<T>(selector: () => T): SelectStateResult<T>;
|
|
120
249
|
}
|
|
121
250
|
|
|
122
251
|
/**
|
|
123
252
|
* Selector function type for context-based API.
|
|
124
253
|
*/
|
|
125
|
-
export type
|
|
254
|
+
export type ReactiveSelector<T, C extends SelectContext = SelectContext> = (
|
|
255
|
+
context: C
|
|
256
|
+
) => T;
|
|
126
257
|
|
|
127
258
|
/**
|
|
128
259
|
* Custom error for when all atoms in `any()` are rejected.
|
|
@@ -145,7 +276,7 @@ export class AllAtomsRejectedError extends Error {
|
|
|
145
276
|
* Selects/computes a value from atom(s) with dependency tracking.
|
|
146
277
|
*
|
|
147
278
|
* This is the core computation logic used by `derived()`. It:
|
|
148
|
-
* 1. Creates a context with `
|
|
279
|
+
* 1. Creates a context with `read`, `all`, `any`, `race`, `settled`, `safe` utilities
|
|
149
280
|
* 2. Tracks which atoms are accessed during computation
|
|
150
281
|
* 3. Returns a result with value/error/promise and dependencies
|
|
151
282
|
*
|
|
@@ -158,40 +289,128 @@ export class AllAtomsRejectedError extends Error {
|
|
|
158
289
|
* If your selector returns a Promise, it will throw an error. This is because:
|
|
159
290
|
* - `select()` is designed for synchronous derivation from atoms
|
|
160
291
|
* - Async atoms should be created using `atom(Promise)` directly
|
|
161
|
-
* - Use `
|
|
292
|
+
* - Use `read()` to read async atoms - it handles Suspense-style loading
|
|
162
293
|
*
|
|
163
294
|
* ```ts
|
|
164
295
|
* // ❌ WRONG - Don't return a Promise from selector
|
|
165
296
|
* select(({ get }) => fetch('/api/data'));
|
|
166
297
|
*
|
|
167
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
298
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
168
299
|
* const data$ = atom(fetch('/api/data').then(r => r.json()));
|
|
169
|
-
* select(({
|
|
300
|
+
* select(({ read }) => read(data$)); // Suspends until resolved
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
304
|
+
*
|
|
305
|
+
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
306
|
+
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
307
|
+
* these Promises and break the Suspense mechanism.
|
|
308
|
+
*
|
|
309
|
+
* ```ts
|
|
310
|
+
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
311
|
+
* select(({ read }) => {
|
|
312
|
+
* try {
|
|
313
|
+
* return read(asyncAtom$);
|
|
314
|
+
* } catch (e) {
|
|
315
|
+
* return 'fallback'; // This catches BOTH errors AND loading promises!
|
|
316
|
+
* }
|
|
317
|
+
* });
|
|
318
|
+
*
|
|
319
|
+
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
320
|
+
* select(({ read, safe }) => {
|
|
321
|
+
* const [err, data] = safe(() => {
|
|
322
|
+
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
323
|
+
* return JSON.parse(raw); // Can throw Error
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* if (err) return { error: err.message };
|
|
327
|
+
* return { data };
|
|
328
|
+
* });
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* The `safe()` utility:
|
|
332
|
+
* - **Catches errors** and returns `[error, undefined]`
|
|
333
|
+
* - **Re-throws Promises** to preserve Suspense behavior
|
|
334
|
+
* - Returns `[undefined, result]` on success
|
|
335
|
+
*
|
|
336
|
+
* ## IMPORTANT: SelectContext Methods Are Synchronous Only
|
|
337
|
+
*
|
|
338
|
+
* **All context methods (`read`, `all`, `race`, `any`, `settled`, `safe`) must be
|
|
339
|
+
* called synchronously during selector execution.** They cannot be used in async
|
|
340
|
+
* callbacks like `setTimeout`, `Promise.then`, or event handlers.
|
|
341
|
+
*
|
|
342
|
+
* ```ts
|
|
343
|
+
* // ❌ WRONG - Calling read() in async callback
|
|
344
|
+
* select(({ read }) => {
|
|
345
|
+
* setTimeout(() => {
|
|
346
|
+
* read(atom$); // Error: called outside selection context
|
|
347
|
+
* }, 100);
|
|
348
|
+
* return 'value';
|
|
349
|
+
* });
|
|
350
|
+
*
|
|
351
|
+
* // ❌ WRONG - Storing read() for later use
|
|
352
|
+
* let savedRead;
|
|
353
|
+
* select(({ read }) => {
|
|
354
|
+
* savedRead = read; // Don't do this!
|
|
355
|
+
* return read(atom$);
|
|
356
|
+
* });
|
|
357
|
+
* savedRead(atom$); // Error: called outside selection context
|
|
358
|
+
*
|
|
359
|
+
* // ✅ CORRECT - For async access, use atom.get() directly
|
|
360
|
+
* effect(({ read }) => {
|
|
361
|
+
* const config = read(config$);
|
|
362
|
+
* setTimeout(async () => {
|
|
363
|
+
* // Use atom.get() for async access
|
|
364
|
+
* const data = await asyncAtom$.get();
|
|
365
|
+
* console.log(data);
|
|
366
|
+
* }, 100);
|
|
367
|
+
* });
|
|
170
368
|
* ```
|
|
171
369
|
*
|
|
172
370
|
* @template T - The type of the computed value
|
|
173
371
|
* @param fn - Context-based selector function (must return sync value)
|
|
174
372
|
* @returns SelectResult with value, error, promise, and dependencies
|
|
175
373
|
* @throws Error if selector returns a Promise or PromiseLike
|
|
374
|
+
* @throws Error if context methods are called outside selection context
|
|
176
375
|
*
|
|
177
376
|
* @example
|
|
178
377
|
* ```ts
|
|
179
|
-
* select(({
|
|
180
|
-
* const user =
|
|
181
|
-
* const [posts, comments] = all(posts$, comments$);
|
|
378
|
+
* select(({ read, all }) => {
|
|
379
|
+
* const user = read(user$);
|
|
380
|
+
* const [posts, comments] = all([posts$, comments$]);
|
|
182
381
|
* return { user, posts, comments };
|
|
183
382
|
* });
|
|
184
383
|
* ```
|
|
185
384
|
*/
|
|
186
|
-
export function select<T>(fn:
|
|
385
|
+
export function select<T>(fn: ReactiveSelector<T>): SelectResult<T> {
|
|
187
386
|
// Track accessed dependencies during computation
|
|
188
387
|
const dependencies = new Set<Atom<unknown>>();
|
|
189
388
|
|
|
389
|
+
// Flag to detect calls outside selection context (e.g., in async callbacks)
|
|
390
|
+
let isExecuting = true;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Throws an error if called outside the synchronous selection context.
|
|
394
|
+
* This catches common mistakes like using get() in async callbacks.
|
|
395
|
+
*/
|
|
396
|
+
const assertExecuting = (methodName: string) => {
|
|
397
|
+
if (!isExecuting) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`${methodName}() was called outside of the selection context. ` +
|
|
400
|
+
"This usually happens when calling context methods in async callbacks (setTimeout, Promise.then, etc.). " +
|
|
401
|
+
"All atom reads must happen synchronously during selector execution. " +
|
|
402
|
+
"For async access, use atom.get() directly (e.g., myMutableAtom$.get() or await myDerivedAtom$.get())."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
190
407
|
/**
|
|
191
408
|
* Read atom value using getAtomState().
|
|
192
|
-
* Implements
|
|
409
|
+
* Implements Suspense-like behavior.
|
|
193
410
|
*/
|
|
194
|
-
const
|
|
411
|
+
const read = <V>(atom: Atom<V>): Awaited<V> => {
|
|
412
|
+
assertExecuting("read");
|
|
413
|
+
|
|
195
414
|
// Track this atom as accessed dependency
|
|
196
415
|
dependencies.add(atom as Atom<unknown>);
|
|
197
416
|
|
|
@@ -209,12 +428,15 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
209
428
|
|
|
210
429
|
/**
|
|
211
430
|
* all() - like Promise.all
|
|
431
|
+
* Array-based: waits for ALL loading atoms in parallel
|
|
212
432
|
*/
|
|
213
433
|
const all = <A extends Atom<unknown>[]>(
|
|
214
|
-
|
|
434
|
+
atoms: A
|
|
215
435
|
): { [K in keyof A]: AtomValue<A[K]> } => {
|
|
436
|
+
assertExecuting("all");
|
|
437
|
+
|
|
216
438
|
const results: unknown[] = [];
|
|
217
|
-
|
|
439
|
+
const loadingPromises: Promise<unknown>[] = [];
|
|
218
440
|
|
|
219
441
|
for (const atom of atoms) {
|
|
220
442
|
dependencies.add(atom);
|
|
@@ -230,17 +452,15 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
230
452
|
throw state.error;
|
|
231
453
|
|
|
232
454
|
case "loading":
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
loadingPromise = state.promise;
|
|
236
|
-
}
|
|
455
|
+
// Collect ALL loading promises for parallel waiting
|
|
456
|
+
loadingPromises.push(state.promise!);
|
|
237
457
|
break;
|
|
238
458
|
}
|
|
239
459
|
}
|
|
240
460
|
|
|
241
|
-
// If any loading
|
|
242
|
-
if (
|
|
243
|
-
throw
|
|
461
|
+
// If any loading → throw combined Promise.all for parallel waiting
|
|
462
|
+
if (loadingPromises.length > 0) {
|
|
463
|
+
throw createCombinedPromise("all", loadingPromises);
|
|
244
464
|
}
|
|
245
465
|
|
|
246
466
|
return results as { [K in keyof A]: AtomValue<A[K]> };
|
|
@@ -248,36 +468,41 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
248
468
|
|
|
249
469
|
/**
|
|
250
470
|
* race() - like Promise.race
|
|
471
|
+
* Object-based: races all atoms, returns { key, value } for winner
|
|
251
472
|
*/
|
|
252
|
-
const race = <
|
|
253
|
-
|
|
254
|
-
): AtomValue<
|
|
255
|
-
|
|
473
|
+
const race = <T extends Record<string, Atom<unknown>>>(
|
|
474
|
+
atoms: T
|
|
475
|
+
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>> => {
|
|
476
|
+
assertExecuting("race");
|
|
256
477
|
|
|
257
|
-
|
|
478
|
+
const loadingPromises: Promise<unknown>[] = [];
|
|
479
|
+
const entries = Object.entries(atoms);
|
|
480
|
+
|
|
481
|
+
for (const [key, atom] of entries) {
|
|
258
482
|
dependencies.add(atom);
|
|
259
483
|
|
|
260
484
|
// For race(), we need raw state without fallback handling
|
|
261
|
-
const state =
|
|
485
|
+
const state = getAtomState(atom);
|
|
262
486
|
|
|
263
487
|
switch (state.status) {
|
|
264
488
|
case "ready":
|
|
265
|
-
return
|
|
489
|
+
return {
|
|
490
|
+
key: key as keyof T & string,
|
|
491
|
+
value: state.value as AtomValue<T[keyof T]>,
|
|
492
|
+
};
|
|
266
493
|
|
|
267
494
|
case "error":
|
|
268
495
|
throw state.error;
|
|
269
496
|
|
|
270
497
|
case "loading":
|
|
271
|
-
|
|
272
|
-
firstLoadingPromise = state.promise;
|
|
273
|
-
}
|
|
498
|
+
loadingPromises.push(state.promise!);
|
|
274
499
|
break;
|
|
275
500
|
}
|
|
276
501
|
}
|
|
277
502
|
|
|
278
|
-
// All loading →
|
|
279
|
-
if (
|
|
280
|
-
throw
|
|
503
|
+
// All loading → race them (first to settle wins)
|
|
504
|
+
if (loadingPromises.length > 0) {
|
|
505
|
+
throw createCombinedPromise("race", loadingPromises);
|
|
281
506
|
}
|
|
282
507
|
|
|
283
508
|
throw new Error("race() called with no atoms");
|
|
@@ -285,38 +510,43 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
285
510
|
|
|
286
511
|
/**
|
|
287
512
|
* any() - like Promise.any
|
|
513
|
+
* Object-based: returns { key, value } for first ready atom
|
|
288
514
|
*/
|
|
289
|
-
const any = <
|
|
290
|
-
|
|
291
|
-
): AtomValue<
|
|
515
|
+
const any = <T extends Record<string, Atom<unknown>>>(
|
|
516
|
+
atoms: T
|
|
517
|
+
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>> => {
|
|
518
|
+
assertExecuting("any");
|
|
519
|
+
|
|
292
520
|
const errors: unknown[] = [];
|
|
293
|
-
|
|
521
|
+
const loadingPromises: Promise<unknown>[] = [];
|
|
522
|
+
const entries = Object.entries(atoms);
|
|
294
523
|
|
|
295
|
-
for (const atom of
|
|
524
|
+
for (const [key, atom] of entries) {
|
|
296
525
|
dependencies.add(atom);
|
|
297
526
|
|
|
298
527
|
// For any(), we need raw state without fallback handling
|
|
299
|
-
const state =
|
|
528
|
+
const state = getAtomState(atom);
|
|
300
529
|
|
|
301
530
|
switch (state.status) {
|
|
302
531
|
case "ready":
|
|
303
|
-
return
|
|
532
|
+
return {
|
|
533
|
+
key: key as keyof T & string,
|
|
534
|
+
value: state.value as AtomValue<T[keyof T]>,
|
|
535
|
+
};
|
|
304
536
|
|
|
305
537
|
case "error":
|
|
306
538
|
errors.push(state.error);
|
|
307
539
|
break;
|
|
308
540
|
|
|
309
541
|
case "loading":
|
|
310
|
-
|
|
311
|
-
firstLoadingPromise = state.promise;
|
|
312
|
-
}
|
|
542
|
+
loadingPromises.push(state.promise!);
|
|
313
543
|
break;
|
|
314
544
|
}
|
|
315
545
|
}
|
|
316
546
|
|
|
317
|
-
// If any loading →
|
|
318
|
-
if (
|
|
319
|
-
throw
|
|
547
|
+
// If any loading → race them all (first to resolve wins)
|
|
548
|
+
if (loadingPromises.length > 0) {
|
|
549
|
+
throw createCombinedPromise("race", loadingPromises);
|
|
320
550
|
}
|
|
321
551
|
|
|
322
552
|
// All errored → throw AggregateError
|
|
@@ -325,12 +555,15 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
325
555
|
|
|
326
556
|
/**
|
|
327
557
|
* settled() - like Promise.allSettled
|
|
558
|
+
* Array-based: waits for ALL atoms in parallel
|
|
328
559
|
*/
|
|
329
560
|
const settled = <A extends Atom<unknown>[]>(
|
|
330
|
-
|
|
561
|
+
atoms: A
|
|
331
562
|
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> } => {
|
|
563
|
+
assertExecuting("settled");
|
|
564
|
+
|
|
332
565
|
const results: SettledResult<unknown>[] = [];
|
|
333
|
-
|
|
566
|
+
const loadingPromises: Promise<unknown>[] = [];
|
|
334
567
|
|
|
335
568
|
for (const atom of atoms) {
|
|
336
569
|
dependencies.add(atom);
|
|
@@ -346,24 +579,114 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
346
579
|
break;
|
|
347
580
|
|
|
348
581
|
case "loading":
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
pendingPromise = state.promise;
|
|
352
|
-
}
|
|
582
|
+
// Collect ALL loading promises for parallel waiting
|
|
583
|
+
loadingPromises.push(state.promise!);
|
|
353
584
|
break;
|
|
354
585
|
}
|
|
355
586
|
}
|
|
356
587
|
|
|
357
|
-
// If any loading
|
|
358
|
-
|
|
359
|
-
|
|
588
|
+
// If any loading → throw combined Promise.allSettled for parallel waiting
|
|
589
|
+
// (allSettled never rejects, so we wait for ALL to settle regardless of success/failure)
|
|
590
|
+
if (loadingPromises.length > 0) {
|
|
591
|
+
throw createCombinedPromise("allSettled", loadingPromises);
|
|
360
592
|
}
|
|
361
593
|
|
|
362
594
|
return results as { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
363
595
|
};
|
|
364
596
|
|
|
597
|
+
/**
|
|
598
|
+
* safe() - Execute function with error handling, preserving Suspense
|
|
599
|
+
*/
|
|
600
|
+
const safe = <T>(fn: () => T): SafeResult<T> => {
|
|
601
|
+
assertExecuting("safe");
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const result = fn();
|
|
605
|
+
return [undefined, result];
|
|
606
|
+
} catch (e) {
|
|
607
|
+
// Re-throw Promises to preserve Suspense behavior
|
|
608
|
+
if (isPromiseLike(e)) {
|
|
609
|
+
throw e;
|
|
610
|
+
}
|
|
611
|
+
// Return errors as tuple instead of throwing
|
|
612
|
+
return [e, undefined];
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* state() - Get async state without throwing
|
|
618
|
+
* Overloaded: accepts atom or selector function
|
|
619
|
+
* Returns SelectStateResult with placeholder props for equality-friendly comparisons
|
|
620
|
+
*/
|
|
621
|
+
function state<T>(atom: Atom<T>): SelectStateResult<Awaited<T>>;
|
|
622
|
+
function state<T>(selector: () => T): SelectStateResult<T>;
|
|
623
|
+
function state<T>(
|
|
624
|
+
atomOrSelector: Atom<T> | (() => T)
|
|
625
|
+
): SelectStateResult<Awaited<T>> | SelectStateResult<T> {
|
|
626
|
+
assertExecuting("state");
|
|
627
|
+
|
|
628
|
+
// Atom shorthand - get state directly and convert to SelectStateResult
|
|
629
|
+
if (isAtom(atomOrSelector)) {
|
|
630
|
+
dependencies.add(atomOrSelector as Atom<unknown>);
|
|
631
|
+
const atomState = getAtomState(atomOrSelector);
|
|
632
|
+
|
|
633
|
+
// Convert AtomState to SelectStateResult (remove promise, add placeholders)
|
|
634
|
+
switch (atomState.status) {
|
|
635
|
+
case "ready":
|
|
636
|
+
return {
|
|
637
|
+
status: "ready",
|
|
638
|
+
value: atomState.value,
|
|
639
|
+
error: undefined,
|
|
640
|
+
} as SelectStateResult<Awaited<T>>;
|
|
641
|
+
case "error":
|
|
642
|
+
return {
|
|
643
|
+
status: "error",
|
|
644
|
+
value: undefined,
|
|
645
|
+
error: atomState.error,
|
|
646
|
+
} as SelectStateResult<Awaited<T>>;
|
|
647
|
+
case "loading":
|
|
648
|
+
return {
|
|
649
|
+
status: "loading",
|
|
650
|
+
value: undefined,
|
|
651
|
+
error: undefined,
|
|
652
|
+
} as SelectStateResult<Awaited<T>>;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Selector function - wrap in try/catch
|
|
657
|
+
try {
|
|
658
|
+
const value = atomOrSelector();
|
|
659
|
+
return {
|
|
660
|
+
status: "ready",
|
|
661
|
+
value,
|
|
662
|
+
error: undefined,
|
|
663
|
+
} as SelectStateResult<T>;
|
|
664
|
+
} catch (e) {
|
|
665
|
+
if (isPromiseLike(e)) {
|
|
666
|
+
return {
|
|
667
|
+
status: "loading",
|
|
668
|
+
value: undefined,
|
|
669
|
+
error: undefined,
|
|
670
|
+
} as SelectStateResult<T>;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
status: "error",
|
|
674
|
+
value: undefined,
|
|
675
|
+
error: e,
|
|
676
|
+
} as SelectStateResult<T>;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
365
680
|
// Create the context
|
|
366
|
-
const context: SelectContext = {
|
|
681
|
+
const context: SelectContext = withUse({
|
|
682
|
+
read,
|
|
683
|
+
all,
|
|
684
|
+
any,
|
|
685
|
+
race,
|
|
686
|
+
settled,
|
|
687
|
+
safe,
|
|
688
|
+
state,
|
|
689
|
+
});
|
|
367
690
|
|
|
368
691
|
// Execute the selector function
|
|
369
692
|
try {
|
|
@@ -399,56 +722,8 @@ export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
|
399
722
|
dependencies,
|
|
400
723
|
};
|
|
401
724
|
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// ============================================================================
|
|
406
|
-
// Internal helpers
|
|
407
|
-
// ============================================================================
|
|
408
|
-
|
|
409
|
-
// Note: trackPromise is already imported at the top
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Gets raw atom state WITHOUT fallback handling.
|
|
413
|
-
* Used by race() and any() which need the actual loading state.
|
|
414
|
-
*/
|
|
415
|
-
function getAtomStateRaw<T>(
|
|
416
|
-
atom: Atom<T>
|
|
417
|
-
):
|
|
418
|
-
| { status: "ready"; value: Awaited<T> }
|
|
419
|
-
| { status: "error"; error: unknown }
|
|
420
|
-
| { status: "loading"; promise: Promise<Awaited<T>> } {
|
|
421
|
-
const value = atom.value;
|
|
422
|
-
|
|
423
|
-
// 1. Sync value - ready
|
|
424
|
-
if (!isPromiseLike(value)) {
|
|
425
|
-
return {
|
|
426
|
-
status: "ready",
|
|
427
|
-
value: value as Awaited<T>,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// 2. Promise value - check state via promiseCache
|
|
432
|
-
const state = trackPromise(value);
|
|
433
|
-
|
|
434
|
-
switch (state.status) {
|
|
435
|
-
case "fulfilled":
|
|
436
|
-
return {
|
|
437
|
-
status: "ready",
|
|
438
|
-
value: state.value as Awaited<T>,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
case "rejected":
|
|
442
|
-
return {
|
|
443
|
-
status: "error",
|
|
444
|
-
error: state.error,
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
case "pending":
|
|
448
|
-
// Raw - don't use fallback
|
|
449
|
-
return {
|
|
450
|
-
status: "loading",
|
|
451
|
-
promise: state.promise as Promise<Awaited<T>>,
|
|
452
|
-
};
|
|
725
|
+
} finally {
|
|
726
|
+
// Mark execution as complete to catch async misuse
|
|
727
|
+
isExecuting = false;
|
|
453
728
|
}
|
|
454
729
|
}
|