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.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. 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
+ });