atomirx 0.0.8 → 0.1.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 +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.js +191 -151
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -42
package/src/core/select.ts
DELETED
|
@@ -1,729 +0,0 @@
|
|
|
1
|
-
import { isPromiseLike } from "./isPromiseLike";
|
|
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";
|
|
14
|
-
|
|
15
|
-
// AggregateError polyfill for environments that don't support it
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
declare const AggregateError: any;
|
|
18
|
-
const AggregateErrorClass =
|
|
19
|
-
typeof AggregateError !== "undefined"
|
|
20
|
-
? AggregateError
|
|
21
|
-
: class AggregateErrorPolyfill extends Error {
|
|
22
|
-
errors: unknown[];
|
|
23
|
-
constructor(errors: unknown[], message?: string) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = "AggregateError";
|
|
26
|
-
this.errors = errors;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Result of a select computation.
|
|
32
|
-
*
|
|
33
|
-
* @template T - The type of the computed value
|
|
34
|
-
*/
|
|
35
|
-
export interface SelectResult<T> {
|
|
36
|
-
/** The computed value (undefined if error or loading) */
|
|
37
|
-
value: T | undefined;
|
|
38
|
-
/** Error thrown during computation (undefined if success or loading) */
|
|
39
|
-
error: unknown;
|
|
40
|
-
/** Promise thrown during computation - indicates loading state */
|
|
41
|
-
promise: PromiseLike<unknown> | undefined;
|
|
42
|
-
/** Set of atoms that were accessed during computation */
|
|
43
|
-
dependencies: Set<Atom<unknown>>;
|
|
44
|
-
}
|
|
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
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Context object passed to selector functions.
|
|
56
|
-
* Provides utilities for reading atoms and handling async operations.
|
|
57
|
-
*/
|
|
58
|
-
export interface SelectContext extends Pipeable {
|
|
59
|
-
/**
|
|
60
|
-
* Read the current value of an atom.
|
|
61
|
-
* Tracks the atom as a dependency.
|
|
62
|
-
*
|
|
63
|
-
* Suspense-like behavior using getAtomState():
|
|
64
|
-
* - If ready: returns value
|
|
65
|
-
* - If error: throws error
|
|
66
|
-
* - If loading: throws Promise (Suspense)
|
|
67
|
-
*
|
|
68
|
-
* @param atom - The atom to read
|
|
69
|
-
* @returns The atom's current value (Awaited<T>)
|
|
70
|
-
*/
|
|
71
|
-
read<T>(atom: Atom<T>): Awaited<T>;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Wait for all atoms to resolve (like Promise.all).
|
|
75
|
-
* Array-based - pass atoms as an array.
|
|
76
|
-
*
|
|
77
|
-
* - If all atoms are ready → returns array of values
|
|
78
|
-
* - If any atom has error → throws that error
|
|
79
|
-
* - If any atom is loading (no fallback) → throws Promise
|
|
80
|
-
* - If loading with fallback → uses staleValue
|
|
81
|
-
*
|
|
82
|
-
* @param atoms - Array of atoms to wait for
|
|
83
|
-
* @returns Array of resolved values (same order as input)
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* ```ts
|
|
87
|
-
* const [user, posts] = all([user$, posts$]);
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
all<A extends Atom<unknown>[]>(atoms: A): { [K in keyof A]: AtomValue<A[K]> };
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Return the first settled value (like Promise.race).
|
|
94
|
-
* Object-based - pass atoms as a record with keys.
|
|
95
|
-
*
|
|
96
|
-
* - If any atom is ready → returns `{ key, value }` for first ready
|
|
97
|
-
* - If any atom has error → throws first error
|
|
98
|
-
* - If all atoms are loading → throws first Promise
|
|
99
|
-
*
|
|
100
|
-
* The `key` in the result identifies which atom won the race.
|
|
101
|
-
*
|
|
102
|
-
* Note: race() does NOT use fallback - it's meant for first "real" settled value.
|
|
103
|
-
*
|
|
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
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
race<T extends Record<string, Atom<unknown>>>(
|
|
115
|
-
atoms: T
|
|
116
|
-
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Return the first ready value (like Promise.any).
|
|
120
|
-
* Object-based - pass atoms as a record with keys.
|
|
121
|
-
*
|
|
122
|
-
* - If any atom is ready → returns `{ key, value }` for first ready
|
|
123
|
-
* - If all atoms have errors → throws AggregateError
|
|
124
|
-
* - If any loading (not all errored) → throws Promise
|
|
125
|
-
*
|
|
126
|
-
* The `key` in the result identifies which atom resolved first.
|
|
127
|
-
*
|
|
128
|
-
* Note: any() does NOT use fallback - it waits for a real ready value.
|
|
129
|
-
*
|
|
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
|
-
* ```
|
|
139
|
-
*/
|
|
140
|
-
any<T extends Record<string, Atom<unknown>>>(
|
|
141
|
-
atoms: T
|
|
142
|
-
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get all atom statuses when all are settled (like Promise.allSettled).
|
|
146
|
-
* Array-based - pass atoms as an array.
|
|
147
|
-
*
|
|
148
|
-
* - If all atoms are settled → returns array of statuses
|
|
149
|
-
* - If any atom is loading (no fallback) → throws Promise
|
|
150
|
-
* - If loading with fallback → { status: "ready", value: staleValue }
|
|
151
|
-
*
|
|
152
|
-
* @param atoms - Array of atoms to check
|
|
153
|
-
* @returns Array of settled results
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* ```ts
|
|
157
|
-
* const [userResult, postsResult] = settled([user$, posts$]);
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
settled<A extends Atom<unknown>[]>(
|
|
161
|
-
atoms: A
|
|
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>;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Selector function type for context-based API.
|
|
253
|
-
*/
|
|
254
|
-
export type ReactiveSelector<T, C extends SelectContext = SelectContext> = (
|
|
255
|
-
context: C
|
|
256
|
-
) => T;
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Custom error for when all atoms in `any()` are rejected.
|
|
260
|
-
*/
|
|
261
|
-
export class AllAtomsRejectedError extends Error {
|
|
262
|
-
readonly errors: unknown[];
|
|
263
|
-
|
|
264
|
-
constructor(errors: unknown[], message = "All atoms rejected") {
|
|
265
|
-
super(message);
|
|
266
|
-
this.name = "AllAtomsRejectedError";
|
|
267
|
-
this.errors = errors;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ============================================================================
|
|
272
|
-
// select() - Core selection/computation function
|
|
273
|
-
// ============================================================================
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Selects/computes a value from atom(s) with dependency tracking.
|
|
277
|
-
*
|
|
278
|
-
* This is the core computation logic used by `derived()`. It:
|
|
279
|
-
* 1. Creates a context with `read`, `all`, `any`, `race`, `settled`, `safe` utilities
|
|
280
|
-
* 2. Tracks which atoms are accessed during computation
|
|
281
|
-
* 3. Returns a result with value/error/promise and dependencies
|
|
282
|
-
*
|
|
283
|
-
* All context methods use `getAtomState()` internally.
|
|
284
|
-
*
|
|
285
|
-
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
286
|
-
*
|
|
287
|
-
* **The selector function MUST NOT return a Promise or PromiseLike value.**
|
|
288
|
-
*
|
|
289
|
-
* If your selector returns a Promise, it will throw an error. This is because:
|
|
290
|
-
* - `select()` is designed for synchronous derivation from atoms
|
|
291
|
-
* - Async atoms should be created using `atom(Promise)` directly
|
|
292
|
-
* - Use `read()` to read async atoms - it handles Suspense-style loading
|
|
293
|
-
*
|
|
294
|
-
* ```ts
|
|
295
|
-
* // ❌ WRONG - Don't return a Promise from selector
|
|
296
|
-
* select(({ get }) => fetch('/api/data'));
|
|
297
|
-
*
|
|
298
|
-
* // ✅ CORRECT - Create async atom and read with read()
|
|
299
|
-
* const data$ = atom(fetch('/api/data').then(r => r.json()));
|
|
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
|
-
* });
|
|
368
|
-
* ```
|
|
369
|
-
*
|
|
370
|
-
* @template T - The type of the computed value
|
|
371
|
-
* @param fn - Context-based selector function (must return sync value)
|
|
372
|
-
* @returns SelectResult with value, error, promise, and dependencies
|
|
373
|
-
* @throws Error if selector returns a Promise or PromiseLike
|
|
374
|
-
* @throws Error if context methods are called outside selection context
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* ```ts
|
|
378
|
-
* select(({ read, all }) => {
|
|
379
|
-
* const user = read(user$);
|
|
380
|
-
* const [posts, comments] = all([posts$, comments$]);
|
|
381
|
-
* return { user, posts, comments };
|
|
382
|
-
* });
|
|
383
|
-
* ```
|
|
384
|
-
*/
|
|
385
|
-
export function select<T>(fn: ReactiveSelector<T>): SelectResult<T> {
|
|
386
|
-
// Track accessed dependencies during computation
|
|
387
|
-
const dependencies = new Set<Atom<unknown>>();
|
|
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
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Read atom value using getAtomState().
|
|
409
|
-
* Implements Suspense-like behavior.
|
|
410
|
-
*/
|
|
411
|
-
const read = <V>(atom: Atom<V>): Awaited<V> => {
|
|
412
|
-
assertExecuting("read");
|
|
413
|
-
|
|
414
|
-
// Track this atom as accessed dependency
|
|
415
|
-
dependencies.add(atom as Atom<unknown>);
|
|
416
|
-
|
|
417
|
-
const state = getAtomState(atom);
|
|
418
|
-
|
|
419
|
-
switch (state.status) {
|
|
420
|
-
case "ready":
|
|
421
|
-
return state.value;
|
|
422
|
-
case "error":
|
|
423
|
-
throw state.error;
|
|
424
|
-
case "loading":
|
|
425
|
-
throw state.promise; // Suspense pattern
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* all() - like Promise.all
|
|
431
|
-
* Array-based: waits for ALL loading atoms in parallel
|
|
432
|
-
*/
|
|
433
|
-
const all = <A extends Atom<unknown>[]>(
|
|
434
|
-
atoms: A
|
|
435
|
-
): { [K in keyof A]: AtomValue<A[K]> } => {
|
|
436
|
-
assertExecuting("all");
|
|
437
|
-
|
|
438
|
-
const results: unknown[] = [];
|
|
439
|
-
const loadingPromises: Promise<unknown>[] = [];
|
|
440
|
-
|
|
441
|
-
for (const atom of atoms) {
|
|
442
|
-
dependencies.add(atom);
|
|
443
|
-
const state = getAtomState(atom);
|
|
444
|
-
|
|
445
|
-
switch (state.status) {
|
|
446
|
-
case "ready":
|
|
447
|
-
results.push(state.value);
|
|
448
|
-
break;
|
|
449
|
-
|
|
450
|
-
case "error":
|
|
451
|
-
// Any error → throw immediately
|
|
452
|
-
throw state.error;
|
|
453
|
-
|
|
454
|
-
case "loading":
|
|
455
|
-
// Collect ALL loading promises for parallel waiting
|
|
456
|
-
loadingPromises.push(state.promise!);
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// If any loading → throw combined Promise.all for parallel waiting
|
|
462
|
-
if (loadingPromises.length > 0) {
|
|
463
|
-
throw createCombinedPromise("all", loadingPromises);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return results as { [K in keyof A]: AtomValue<A[K]> };
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* race() - like Promise.race
|
|
471
|
-
* Object-based: races all atoms, returns { key, value } for winner
|
|
472
|
-
*/
|
|
473
|
-
const race = <T extends Record<string, Atom<unknown>>>(
|
|
474
|
-
atoms: T
|
|
475
|
-
): KeyedResult<keyof T & string, AtomValue<T[keyof T]>> => {
|
|
476
|
-
assertExecuting("race");
|
|
477
|
-
|
|
478
|
-
const loadingPromises: Promise<unknown>[] = [];
|
|
479
|
-
const entries = Object.entries(atoms);
|
|
480
|
-
|
|
481
|
-
for (const [key, atom] of entries) {
|
|
482
|
-
dependencies.add(atom);
|
|
483
|
-
|
|
484
|
-
// For race(), we need raw state without fallback handling
|
|
485
|
-
const state = getAtomState(atom);
|
|
486
|
-
|
|
487
|
-
switch (state.status) {
|
|
488
|
-
case "ready":
|
|
489
|
-
return {
|
|
490
|
-
key: key as keyof T & string,
|
|
491
|
-
value: state.value as AtomValue<T[keyof T]>,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
case "error":
|
|
495
|
-
throw state.error;
|
|
496
|
-
|
|
497
|
-
case "loading":
|
|
498
|
-
loadingPromises.push(state.promise!);
|
|
499
|
-
break;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// All loading → race them (first to settle wins)
|
|
504
|
-
if (loadingPromises.length > 0) {
|
|
505
|
-
throw createCombinedPromise("race", loadingPromises);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
throw new Error("race() called with no atoms");
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* any() - like Promise.any
|
|
513
|
-
* Object-based: returns { key, value } for first ready atom
|
|
514
|
-
*/
|
|
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
|
-
|
|
520
|
-
const errors: unknown[] = [];
|
|
521
|
-
const loadingPromises: Promise<unknown>[] = [];
|
|
522
|
-
const entries = Object.entries(atoms);
|
|
523
|
-
|
|
524
|
-
for (const [key, atom] of entries) {
|
|
525
|
-
dependencies.add(atom);
|
|
526
|
-
|
|
527
|
-
// For any(), we need raw state without fallback handling
|
|
528
|
-
const state = getAtomState(atom);
|
|
529
|
-
|
|
530
|
-
switch (state.status) {
|
|
531
|
-
case "ready":
|
|
532
|
-
return {
|
|
533
|
-
key: key as keyof T & string,
|
|
534
|
-
value: state.value as AtomValue<T[keyof T]>,
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
case "error":
|
|
538
|
-
errors.push(state.error);
|
|
539
|
-
break;
|
|
540
|
-
|
|
541
|
-
case "loading":
|
|
542
|
-
loadingPromises.push(state.promise!);
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// If any loading → race them all (first to resolve wins)
|
|
548
|
-
if (loadingPromises.length > 0) {
|
|
549
|
-
throw createCombinedPromise("race", loadingPromises);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// All errored → throw AggregateError
|
|
553
|
-
throw new AggregateErrorClass(errors, "All atoms rejected");
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* settled() - like Promise.allSettled
|
|
558
|
-
* Array-based: waits for ALL atoms in parallel
|
|
559
|
-
*/
|
|
560
|
-
const settled = <A extends Atom<unknown>[]>(
|
|
561
|
-
atoms: A
|
|
562
|
-
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> } => {
|
|
563
|
-
assertExecuting("settled");
|
|
564
|
-
|
|
565
|
-
const results: SettledResult<unknown>[] = [];
|
|
566
|
-
const loadingPromises: Promise<unknown>[] = [];
|
|
567
|
-
|
|
568
|
-
for (const atom of atoms) {
|
|
569
|
-
dependencies.add(atom);
|
|
570
|
-
const state = getAtomState(atom);
|
|
571
|
-
|
|
572
|
-
switch (state.status) {
|
|
573
|
-
case "ready":
|
|
574
|
-
results.push({ status: "ready", value: state.value });
|
|
575
|
-
break;
|
|
576
|
-
|
|
577
|
-
case "error":
|
|
578
|
-
results.push({ status: "error", error: state.error });
|
|
579
|
-
break;
|
|
580
|
-
|
|
581
|
-
case "loading":
|
|
582
|
-
// Collect ALL loading promises for parallel waiting
|
|
583
|
-
loadingPromises.push(state.promise!);
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
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);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return results as { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
595
|
-
};
|
|
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
|
-
|
|
680
|
-
// Create the context
|
|
681
|
-
const context: SelectContext = withUse({
|
|
682
|
-
read,
|
|
683
|
-
all,
|
|
684
|
-
any,
|
|
685
|
-
race,
|
|
686
|
-
settled,
|
|
687
|
-
safe,
|
|
688
|
-
state,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
// Execute the selector function
|
|
692
|
-
try {
|
|
693
|
-
const result = fn(context);
|
|
694
|
-
|
|
695
|
-
// Selector must return synchronous value, not a Promise
|
|
696
|
-
if (isPromiseLike(result)) {
|
|
697
|
-
throw new Error(
|
|
698
|
-
"select() selector must return a synchronous value, not a Promise. " +
|
|
699
|
-
"For async data, create an async atom with atom(Promise) and use get() to read it."
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
return {
|
|
704
|
-
value: result,
|
|
705
|
-
error: undefined,
|
|
706
|
-
promise: undefined,
|
|
707
|
-
dependencies,
|
|
708
|
-
};
|
|
709
|
-
} catch (thrown) {
|
|
710
|
-
if (isPromiseLike(thrown)) {
|
|
711
|
-
return {
|
|
712
|
-
value: undefined,
|
|
713
|
-
error: undefined,
|
|
714
|
-
promise: thrown,
|
|
715
|
-
dependencies,
|
|
716
|
-
};
|
|
717
|
-
} else {
|
|
718
|
-
return {
|
|
719
|
-
value: undefined,
|
|
720
|
-
error: thrown,
|
|
721
|
-
promise: undefined,
|
|
722
|
-
dependencies,
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
} finally {
|
|
726
|
-
// Mark execution as complete to catch async misuse
|
|
727
|
-
isExecuting = false;
|
|
728
|
-
}
|
|
729
|
-
}
|