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,454 @@
|
|
|
1
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
2
|
+
import { getAtomState, trackPromise } from "./promiseCache";
|
|
3
|
+
import { Atom, AtomValue, SettledResult } from "./types";
|
|
4
|
+
|
|
5
|
+
// AggregateError polyfill for environments that don't support it
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
declare const AggregateError: any;
|
|
8
|
+
const AggregateErrorClass =
|
|
9
|
+
typeof AggregateError !== "undefined"
|
|
10
|
+
? AggregateError
|
|
11
|
+
: class AggregateErrorPolyfill extends Error {
|
|
12
|
+
errors: unknown[];
|
|
13
|
+
constructor(errors: unknown[], message?: string) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AggregateError";
|
|
16
|
+
this.errors = errors;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of a select computation.
|
|
22
|
+
*
|
|
23
|
+
* @template T - The type of the computed value
|
|
24
|
+
*/
|
|
25
|
+
export interface SelectResult<T> {
|
|
26
|
+
/** The computed value (undefined if error or loading) */
|
|
27
|
+
value: T | undefined;
|
|
28
|
+
/** Error thrown during computation (undefined if success or loading) */
|
|
29
|
+
error: unknown;
|
|
30
|
+
/** Promise thrown during computation - indicates loading state */
|
|
31
|
+
promise: PromiseLike<unknown> | undefined;
|
|
32
|
+
/** Set of atoms that were accessed during computation */
|
|
33
|
+
dependencies: Set<Atom<unknown>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Context object passed to selector functions.
|
|
38
|
+
* Provides utilities for reading atoms and handling async operations.
|
|
39
|
+
*/
|
|
40
|
+
export interface SelectContext {
|
|
41
|
+
/**
|
|
42
|
+
* Read the current value of an atom.
|
|
43
|
+
* Tracks the atom as a dependency.
|
|
44
|
+
*
|
|
45
|
+
* Suspense-like behavior using getAtomState():
|
|
46
|
+
* - If ready: returns value
|
|
47
|
+
* - If error: throws error
|
|
48
|
+
* - If loading: throws Promise (Suspense)
|
|
49
|
+
*
|
|
50
|
+
* @param atom - The atom to read
|
|
51
|
+
* @returns The atom's current value (Awaited<T>)
|
|
52
|
+
*/
|
|
53
|
+
get<T>(atom: Atom<T>): Awaited<T>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wait for all atoms to resolve (like Promise.all).
|
|
57
|
+
* Variadic form - pass atoms as arguments.
|
|
58
|
+
*
|
|
59
|
+
* - If all atoms are ready → returns array of values
|
|
60
|
+
* - If any atom has error → throws that error
|
|
61
|
+
* - If any atom is loading (no fallback) → throws Promise
|
|
62
|
+
* - If loading with fallback → uses staleValue
|
|
63
|
+
*
|
|
64
|
+
* @param atoms - Atoms to wait for (variadic)
|
|
65
|
+
* @returns Array of resolved values (same order as input)
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const [user, posts] = all(user$, posts$);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
all<A extends Atom<unknown>[]>(
|
|
73
|
+
...atoms: A
|
|
74
|
+
): { [K in keyof A]: AtomValue<A[K]> };
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Return the first settled value (like Promise.race).
|
|
78
|
+
* Variadic form - pass atoms as arguments.
|
|
79
|
+
*
|
|
80
|
+
* - If any atom is ready → returns first ready value
|
|
81
|
+
* - If any atom has error → throws first error
|
|
82
|
+
* - If all atoms are loading → throws first Promise
|
|
83
|
+
*
|
|
84
|
+
* Note: race() does NOT use fallback - it's meant for first "real" settled value.
|
|
85
|
+
*
|
|
86
|
+
* @param atoms - Atoms to race (variadic)
|
|
87
|
+
* @returns First settled value
|
|
88
|
+
*/
|
|
89
|
+
race<A extends Atom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Return the first ready value (like Promise.any).
|
|
93
|
+
* Variadic form - pass atoms as arguments.
|
|
94
|
+
*
|
|
95
|
+
* - If any atom is ready → returns first ready value
|
|
96
|
+
* - If all atoms have errors → throws AggregateError
|
|
97
|
+
* - If any loading (not all errored) → throws Promise
|
|
98
|
+
*
|
|
99
|
+
* Note: any() does NOT use fallback - it waits for a real ready value.
|
|
100
|
+
*
|
|
101
|
+
* @param atoms - Atoms to check (variadic)
|
|
102
|
+
* @returns First ready value
|
|
103
|
+
*/
|
|
104
|
+
any<A extends Atom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get all atom statuses when all are settled (like Promise.allSettled).
|
|
108
|
+
* Variadic form - pass atoms as arguments.
|
|
109
|
+
*
|
|
110
|
+
* - If all atoms are settled → returns array of statuses
|
|
111
|
+
* - If any atom is loading (no fallback) → throws Promise
|
|
112
|
+
* - If loading with fallback → { status: "ready", value: staleValue }
|
|
113
|
+
*
|
|
114
|
+
* @param atoms - Atoms to check (variadic)
|
|
115
|
+
* @returns Array of settled results
|
|
116
|
+
*/
|
|
117
|
+
settled<A extends Atom<unknown>[]>(
|
|
118
|
+
...atoms: A
|
|
119
|
+
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Selector function type for context-based API.
|
|
124
|
+
*/
|
|
125
|
+
export type ContextSelectorFn<T> = (context: SelectContext) => T;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Custom error for when all atoms in `any()` are rejected.
|
|
129
|
+
*/
|
|
130
|
+
export class AllAtomsRejectedError extends Error {
|
|
131
|
+
readonly errors: unknown[];
|
|
132
|
+
|
|
133
|
+
constructor(errors: unknown[], message = "All atoms rejected") {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "AllAtomsRejectedError";
|
|
136
|
+
this.errors = errors;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// select() - Core selection/computation function
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Selects/computes a value from atom(s) with dependency tracking.
|
|
146
|
+
*
|
|
147
|
+
* This is the core computation logic used by `derived()`. It:
|
|
148
|
+
* 1. Creates a context with `get`, `all`, `any`, `race`, `settled` utilities
|
|
149
|
+
* 2. Tracks which atoms are accessed during computation
|
|
150
|
+
* 3. Returns a result with value/error/promise and dependencies
|
|
151
|
+
*
|
|
152
|
+
* All context methods use `getAtomState()` internally.
|
|
153
|
+
*
|
|
154
|
+
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
155
|
+
*
|
|
156
|
+
* **The selector function MUST NOT return a Promise or PromiseLike value.**
|
|
157
|
+
*
|
|
158
|
+
* If your selector returns a Promise, it will throw an error. This is because:
|
|
159
|
+
* - `select()` is designed for synchronous derivation from atoms
|
|
160
|
+
* - Async atoms should be created using `atom(Promise)` directly
|
|
161
|
+
* - Use `get()` to read async atoms - it handles Suspense-style loading
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* // ❌ WRONG - Don't return a Promise from selector
|
|
165
|
+
* select(({ get }) => fetch('/api/data'));
|
|
166
|
+
*
|
|
167
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
168
|
+
* const data$ = atom(fetch('/api/data').then(r => r.json()));
|
|
169
|
+
* select(({ get }) => get(data$)); // Suspends until resolved
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @template T - The type of the computed value
|
|
173
|
+
* @param fn - Context-based selector function (must return sync value)
|
|
174
|
+
* @returns SelectResult with value, error, promise, and dependencies
|
|
175
|
+
* @throws Error if selector returns a Promise or PromiseLike
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* select(({ get, all }) => {
|
|
180
|
+
* const user = get(user$);
|
|
181
|
+
* const [posts, comments] = all(posts$, comments$);
|
|
182
|
+
* return { user, posts, comments };
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function select<T>(fn: ContextSelectorFn<T>): SelectResult<T> {
|
|
187
|
+
// Track accessed dependencies during computation
|
|
188
|
+
const dependencies = new Set<Atom<unknown>>();
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read atom value using getAtomState().
|
|
192
|
+
* Implements the v2 get() behavior from spec.
|
|
193
|
+
*/
|
|
194
|
+
const get = <V>(atom: Atom<V>): Awaited<V> => {
|
|
195
|
+
// Track this atom as accessed dependency
|
|
196
|
+
dependencies.add(atom as Atom<unknown>);
|
|
197
|
+
|
|
198
|
+
const state = getAtomState(atom);
|
|
199
|
+
|
|
200
|
+
switch (state.status) {
|
|
201
|
+
case "ready":
|
|
202
|
+
return state.value;
|
|
203
|
+
case "error":
|
|
204
|
+
throw state.error;
|
|
205
|
+
case "loading":
|
|
206
|
+
throw state.promise; // Suspense pattern
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* all() - like Promise.all
|
|
212
|
+
*/
|
|
213
|
+
const all = <A extends Atom<unknown>[]>(
|
|
214
|
+
...atoms: A
|
|
215
|
+
): { [K in keyof A]: AtomValue<A[K]> } => {
|
|
216
|
+
const results: unknown[] = [];
|
|
217
|
+
let loadingPromise: Promise<unknown> | null = null;
|
|
218
|
+
|
|
219
|
+
for (const atom of atoms) {
|
|
220
|
+
dependencies.add(atom);
|
|
221
|
+
const state = getAtomState(atom);
|
|
222
|
+
|
|
223
|
+
switch (state.status) {
|
|
224
|
+
case "ready":
|
|
225
|
+
results.push(state.value);
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case "error":
|
|
229
|
+
// Any error → throw immediately
|
|
230
|
+
throw state.error;
|
|
231
|
+
|
|
232
|
+
case "loading":
|
|
233
|
+
// First loading without fallback → will throw
|
|
234
|
+
if (!loadingPromise) {
|
|
235
|
+
loadingPromise = state.promise;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// If any loading without fallback → throw Promise
|
|
242
|
+
if (loadingPromise) {
|
|
243
|
+
throw loadingPromise;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return results as { [K in keyof A]: AtomValue<A[K]> };
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* race() - like Promise.race
|
|
251
|
+
*/
|
|
252
|
+
const race = <A extends Atom<unknown>[]>(
|
|
253
|
+
...atoms: A
|
|
254
|
+
): AtomValue<A[number]> => {
|
|
255
|
+
let firstLoadingPromise: Promise<unknown> | null = null;
|
|
256
|
+
|
|
257
|
+
for (const atom of atoms) {
|
|
258
|
+
dependencies.add(atom);
|
|
259
|
+
|
|
260
|
+
// For race(), we need raw state without fallback handling
|
|
261
|
+
const state = getAtomStateRaw(atom);
|
|
262
|
+
|
|
263
|
+
switch (state.status) {
|
|
264
|
+
case "ready":
|
|
265
|
+
return state.value as AtomValue<A[number]>;
|
|
266
|
+
|
|
267
|
+
case "error":
|
|
268
|
+
throw state.error;
|
|
269
|
+
|
|
270
|
+
case "loading":
|
|
271
|
+
if (!firstLoadingPromise) {
|
|
272
|
+
firstLoadingPromise = state.promise;
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// All loading → throw first Promise
|
|
279
|
+
if (firstLoadingPromise) {
|
|
280
|
+
throw firstLoadingPromise;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new Error("race() called with no atoms");
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* any() - like Promise.any
|
|
288
|
+
*/
|
|
289
|
+
const any = <A extends Atom<unknown>[]>(
|
|
290
|
+
...atoms: A
|
|
291
|
+
): AtomValue<A[number]> => {
|
|
292
|
+
const errors: unknown[] = [];
|
|
293
|
+
let firstLoadingPromise: Promise<unknown> | null = null;
|
|
294
|
+
|
|
295
|
+
for (const atom of atoms) {
|
|
296
|
+
dependencies.add(atom);
|
|
297
|
+
|
|
298
|
+
// For any(), we need raw state without fallback handling
|
|
299
|
+
const state = getAtomStateRaw(atom);
|
|
300
|
+
|
|
301
|
+
switch (state.status) {
|
|
302
|
+
case "ready":
|
|
303
|
+
return state.value as AtomValue<A[number]>;
|
|
304
|
+
|
|
305
|
+
case "error":
|
|
306
|
+
errors.push(state.error);
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
case "loading":
|
|
310
|
+
if (!firstLoadingPromise) {
|
|
311
|
+
firstLoadingPromise = state.promise;
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If any loading → throw Promise (might still fulfill)
|
|
318
|
+
if (firstLoadingPromise) {
|
|
319
|
+
throw firstLoadingPromise;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// All errored → throw AggregateError
|
|
323
|
+
throw new AggregateErrorClass(errors, "All atoms rejected");
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* settled() - like Promise.allSettled
|
|
328
|
+
*/
|
|
329
|
+
const settled = <A extends Atom<unknown>[]>(
|
|
330
|
+
...atoms: A
|
|
331
|
+
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> } => {
|
|
332
|
+
const results: SettledResult<unknown>[] = [];
|
|
333
|
+
let pendingPromise: Promise<unknown> | null = null;
|
|
334
|
+
|
|
335
|
+
for (const atom of atoms) {
|
|
336
|
+
dependencies.add(atom);
|
|
337
|
+
const state = getAtomState(atom);
|
|
338
|
+
|
|
339
|
+
switch (state.status) {
|
|
340
|
+
case "ready":
|
|
341
|
+
results.push({ status: "ready", value: state.value });
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case "error":
|
|
345
|
+
results.push({ status: "error", error: state.error });
|
|
346
|
+
break;
|
|
347
|
+
|
|
348
|
+
case "loading":
|
|
349
|
+
// Loading without fallback → will throw
|
|
350
|
+
if (!pendingPromise) {
|
|
351
|
+
pendingPromise = state.promise;
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If any loading without fallback → throw Promise
|
|
358
|
+
if (pendingPromise) {
|
|
359
|
+
throw pendingPromise;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return results as { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Create the context
|
|
366
|
+
const context: SelectContext = { get, all, any, race, settled };
|
|
367
|
+
|
|
368
|
+
// Execute the selector function
|
|
369
|
+
try {
|
|
370
|
+
const result = fn(context);
|
|
371
|
+
|
|
372
|
+
// Selector must return synchronous value, not a Promise
|
|
373
|
+
if (isPromiseLike(result)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"select() selector must return a synchronous value, not a Promise. " +
|
|
376
|
+
"For async data, create an async atom with atom(Promise) and use get() to read it."
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
value: result,
|
|
382
|
+
error: undefined,
|
|
383
|
+
promise: undefined,
|
|
384
|
+
dependencies,
|
|
385
|
+
};
|
|
386
|
+
} catch (thrown) {
|
|
387
|
+
if (isPromiseLike(thrown)) {
|
|
388
|
+
return {
|
|
389
|
+
value: undefined,
|
|
390
|
+
error: undefined,
|
|
391
|
+
promise: thrown,
|
|
392
|
+
dependencies,
|
|
393
|
+
};
|
|
394
|
+
} else {
|
|
395
|
+
return {
|
|
396
|
+
value: undefined,
|
|
397
|
+
error: thrown,
|
|
398
|
+
promise: undefined,
|
|
399
|
+
dependencies,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
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
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { atom } from "./atom";
|
|
3
|
+
import { select } from "./select";
|
|
4
|
+
|
|
5
|
+
describe("select", () => {
|
|
6
|
+
describe("get()", () => {
|
|
7
|
+
it("should read value from sync atom", () => {
|
|
8
|
+
const count$ = atom(5);
|
|
9
|
+
const result = select(({ get }) => get(count$));
|
|
10
|
+
|
|
11
|
+
expect(result.value).toBe(5);
|
|
12
|
+
expect(result.error).toBe(undefined);
|
|
13
|
+
expect(result.promise).toBe(undefined);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should track dependencies", () => {
|
|
17
|
+
const a$ = atom(1);
|
|
18
|
+
const b$ = atom(2);
|
|
19
|
+
|
|
20
|
+
const result = select(({ get }) => get(a$) + get(b$));
|
|
21
|
+
|
|
22
|
+
expect(result.dependencies.size).toBe(2);
|
|
23
|
+
expect(result.dependencies.has(a$)).toBe(true);
|
|
24
|
+
expect(result.dependencies.has(b$)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should throw error if computation throws", () => {
|
|
28
|
+
const count$ = atom(5);
|
|
29
|
+
const error = new Error("Test error");
|
|
30
|
+
|
|
31
|
+
const result = select(({ get }) => {
|
|
32
|
+
get(count$);
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result.value).toBe(undefined);
|
|
37
|
+
expect(result.error).toBe(error);
|
|
38
|
+
expect(result.promise).toBe(undefined);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("all()", () => {
|
|
43
|
+
it("should return array of values for all sync atoms", () => {
|
|
44
|
+
const a$ = atom(1);
|
|
45
|
+
const b$ = atom(2);
|
|
46
|
+
const c$ = atom(3);
|
|
47
|
+
|
|
48
|
+
const result = select(({ all }) => all(a$, b$, c$));
|
|
49
|
+
|
|
50
|
+
expect(result.value).toEqual([1, 2, 3]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should throw promise if any atom is pending", () => {
|
|
54
|
+
const a$ = atom(1);
|
|
55
|
+
const b$ = atom(new Promise<number>(() => {}));
|
|
56
|
+
|
|
57
|
+
const result = select(({ all }) => all(a$, b$));
|
|
58
|
+
|
|
59
|
+
expect(result.promise).toBeDefined();
|
|
60
|
+
expect(result.value).toBe(undefined);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should throw error if any atom has rejected promise", async () => {
|
|
64
|
+
const error = new Error("Test error");
|
|
65
|
+
const a$ = atom(1);
|
|
66
|
+
const rejectedPromise = Promise.reject(error);
|
|
67
|
+
rejectedPromise.catch(() => {}); // Prevent unhandled rejection
|
|
68
|
+
const b$ = atom(rejectedPromise);
|
|
69
|
+
|
|
70
|
+
// First call to select tracks the promise but returns pending
|
|
71
|
+
select(({ all }) => all(a$, b$));
|
|
72
|
+
|
|
73
|
+
// Wait for promise handlers to run
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
await Promise.resolve();
|
|
76
|
+
|
|
77
|
+
// Now the promise state should be updated
|
|
78
|
+
const result = select(({ all }) => all(a$, b$));
|
|
79
|
+
|
|
80
|
+
expect(result.error).toBe(error);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("race()", () => {
|
|
85
|
+
it("should return first fulfilled value", () => {
|
|
86
|
+
const a$ = atom(1);
|
|
87
|
+
const b$ = atom(2);
|
|
88
|
+
|
|
89
|
+
const result = select(({ race }) => race(a$, b$));
|
|
90
|
+
|
|
91
|
+
expect(result.value).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should throw first error if first atom is rejected", async () => {
|
|
95
|
+
const error = new Error("Test error");
|
|
96
|
+
const rejectedPromise = Promise.reject(error);
|
|
97
|
+
rejectedPromise.catch(() => {});
|
|
98
|
+
const a$ = atom(rejectedPromise);
|
|
99
|
+
const b$ = atom(2);
|
|
100
|
+
|
|
101
|
+
// Track the promise first
|
|
102
|
+
select(({ race }) => race(a$, b$));
|
|
103
|
+
await Promise.resolve();
|
|
104
|
+
await Promise.resolve();
|
|
105
|
+
|
|
106
|
+
const result = select(({ race }) => race(a$, b$));
|
|
107
|
+
|
|
108
|
+
expect(result.error).toBe(error);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should throw promise if all are pending", () => {
|
|
112
|
+
const a$ = atom(new Promise<number>(() => {}));
|
|
113
|
+
const b$ = atom(new Promise<number>(() => {}));
|
|
114
|
+
|
|
115
|
+
const result = select(({ race }) => race(a$, b$));
|
|
116
|
+
|
|
117
|
+
expect(result.promise).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("any()", () => {
|
|
122
|
+
it("should return first fulfilled value", () => {
|
|
123
|
+
const a$ = atom(1);
|
|
124
|
+
const b$ = atom(2);
|
|
125
|
+
|
|
126
|
+
const result = select(({ any }) => any(a$, b$));
|
|
127
|
+
|
|
128
|
+
expect(result.value).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should skip rejected and return next fulfilled", async () => {
|
|
132
|
+
const error = new Error("Test error");
|
|
133
|
+
const rejectedPromise = Promise.reject(error);
|
|
134
|
+
rejectedPromise.catch(() => {});
|
|
135
|
+
const a$ = atom(rejectedPromise);
|
|
136
|
+
const b$ = atom(2);
|
|
137
|
+
|
|
138
|
+
// Track first, then wait for microtasks
|
|
139
|
+
select(({ any }) => any(a$, b$));
|
|
140
|
+
await Promise.resolve();
|
|
141
|
+
await Promise.resolve();
|
|
142
|
+
|
|
143
|
+
const result = select(({ any }) => any(a$, b$));
|
|
144
|
+
|
|
145
|
+
expect(result.value).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should throw AggregateError if all rejected", async () => {
|
|
149
|
+
const error1 = new Error("Error 1");
|
|
150
|
+
const error2 = new Error("Error 2");
|
|
151
|
+
const p1 = Promise.reject(error1);
|
|
152
|
+
const p2 = Promise.reject(error2);
|
|
153
|
+
p1.catch(() => {});
|
|
154
|
+
p2.catch(() => {});
|
|
155
|
+
|
|
156
|
+
const a$ = atom(p1);
|
|
157
|
+
const b$ = atom(p2);
|
|
158
|
+
|
|
159
|
+
// Track first, then wait for microtasks
|
|
160
|
+
select(({ any }) => any(a$, b$));
|
|
161
|
+
await Promise.resolve();
|
|
162
|
+
await Promise.resolve();
|
|
163
|
+
|
|
164
|
+
const result = select(({ any }) => any(a$, b$));
|
|
165
|
+
|
|
166
|
+
expect(result.error).toBeDefined();
|
|
167
|
+
expect((result.error as Error).name).toBe("AggregateError");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("settled()", () => {
|
|
172
|
+
it("should return array of settled results", async () => {
|
|
173
|
+
const a$ = atom(1);
|
|
174
|
+
const error = new Error("Test error");
|
|
175
|
+
const rejectedPromise = Promise.reject(error);
|
|
176
|
+
rejectedPromise.catch(() => {});
|
|
177
|
+
const b$ = atom(rejectedPromise);
|
|
178
|
+
|
|
179
|
+
// Track first, wait for microtasks
|
|
180
|
+
select(({ settled }) => settled(a$, b$));
|
|
181
|
+
await Promise.resolve();
|
|
182
|
+
await Promise.resolve();
|
|
183
|
+
|
|
184
|
+
const result = select(({ settled }) => settled(a$, b$));
|
|
185
|
+
|
|
186
|
+
expect(result.value).toEqual([
|
|
187
|
+
{ status: "ready", value: 1 },
|
|
188
|
+
{ status: "error", error },
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should throw promise if any atom is pending", () => {
|
|
193
|
+
const a$ = atom(1);
|
|
194
|
+
const b$ = atom(new Promise<number>(() => {}));
|
|
195
|
+
|
|
196
|
+
const result = select(({ settled }) => settled(a$, b$));
|
|
197
|
+
|
|
198
|
+
expect(result.promise).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("conditional dependencies", () => {
|
|
203
|
+
it("should only track accessed atoms", () => {
|
|
204
|
+
const condition$ = atom(false);
|
|
205
|
+
const a$ = atom(1);
|
|
206
|
+
const b$ = atom(2);
|
|
207
|
+
|
|
208
|
+
const result = select(({ get }) => (get(condition$) ? get(a$) : get(b$)));
|
|
209
|
+
|
|
210
|
+
expect(result.dependencies.size).toBe(2);
|
|
211
|
+
expect(result.dependencies.has(condition$)).toBe(true);
|
|
212
|
+
expect(result.dependencies.has(b$)).toBe(true);
|
|
213
|
+
// a$ was not accessed because condition was false
|
|
214
|
+
expect(result.dependencies.has(a$)).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("error handling", () => {
|
|
219
|
+
it("should throw error if selector returns a Promise", () => {
|
|
220
|
+
const result = select(() => Promise.resolve(42));
|
|
221
|
+
|
|
222
|
+
expect(result.error).toBeDefined();
|
|
223
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
224
|
+
expect((result.error as Error).message).toContain(
|
|
225
|
+
"select() selector must return a synchronous value"
|
|
226
|
+
);
|
|
227
|
+
expect(result.value).toBe(undefined);
|
|
228
|
+
expect(result.promise).toBe(undefined);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should throw error if selector returns a PromiseLike", () => {
|
|
232
|
+
// Custom PromiseLike object
|
|
233
|
+
const promiseLike = {
|
|
234
|
+
then: (resolve: (value: number) => void) => {
|
|
235
|
+
resolve(42);
|
|
236
|
+
return promiseLike;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const result = select(() => promiseLike);
|
|
241
|
+
|
|
242
|
+
expect(result.error).toBeDefined();
|
|
243
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
244
|
+
expect((result.error as Error).message).toContain(
|
|
245
|
+
"select() selector must return a synchronous value"
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should work fine with sync values", () => {
|
|
250
|
+
const result = select(() => 42);
|
|
251
|
+
|
|
252
|
+
expect(result.value).toBe(42);
|
|
253
|
+
expect(result.error).toBe(undefined);
|
|
254
|
+
expect(result.promise).toBe(undefined);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|