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.
Files changed (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -1
  33. package/dist/react/index.js +191 -151
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. package/vite.config.ts +0 -42
@@ -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
- }