atomirx 0.0.2 → 0.0.5

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 (65) hide show
  1. package/README.md +868 -161
  2. package/coverage/src/core/onCreateHook.ts.html +72 -70
  3. package/dist/core/atom.d.ts +83 -6
  4. package/dist/core/batch.d.ts +3 -3
  5. package/dist/core/derived.d.ts +69 -22
  6. package/dist/core/effect.d.ts +52 -52
  7. package/dist/core/getAtomState.d.ts +29 -0
  8. package/dist/core/hook.d.ts +1 -1
  9. package/dist/core/onCreateHook.d.ts +37 -23
  10. package/dist/core/onErrorHook.d.ts +49 -0
  11. package/dist/core/promiseCache.d.ts +23 -32
  12. package/dist/core/select.d.ts +208 -29
  13. package/dist/core/types.d.ts +107 -22
  14. package/dist/core/withReady.d.ts +115 -0
  15. package/dist/core/withReady.test.d.ts +1 -0
  16. package/dist/index-CBVj1kSj.js +1350 -0
  17. package/dist/index-Cxk9v0um.cjs +1 -0
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +12 -8
  20. package/dist/index.js +18 -15
  21. package/dist/react/index.cjs +10 -10
  22. package/dist/react/index.d.ts +2 -1
  23. package/dist/react/index.js +422 -377
  24. package/dist/react/rx.d.ts +114 -25
  25. package/dist/react/useAction.d.ts +5 -4
  26. package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
  27. package/dist/react/useSelector.test.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/src/core/atom.test.ts +307 -43
  30. package/src/core/atom.ts +144 -22
  31. package/src/core/batch.test.ts +10 -10
  32. package/src/core/batch.ts +3 -3
  33. package/src/core/define.test.ts +12 -11
  34. package/src/core/define.ts +1 -1
  35. package/src/core/derived.test.ts +906 -72
  36. package/src/core/derived.ts +192 -81
  37. package/src/core/effect.test.ts +651 -45
  38. package/src/core/effect.ts +102 -98
  39. package/src/core/getAtomState.ts +69 -0
  40. package/src/core/hook.test.ts +5 -5
  41. package/src/core/hook.ts +1 -1
  42. package/src/core/onCreateHook.ts +38 -23
  43. package/src/core/onErrorHook.test.ts +350 -0
  44. package/src/core/onErrorHook.ts +52 -0
  45. package/src/core/promiseCache.test.ts +5 -3
  46. package/src/core/promiseCache.ts +76 -71
  47. package/src/core/select.ts +405 -130
  48. package/src/core/selector.test.ts +574 -32
  49. package/src/core/types.ts +107 -29
  50. package/src/core/withReady.test.ts +534 -0
  51. package/src/core/withReady.ts +191 -0
  52. package/src/core/withUse.ts +1 -1
  53. package/src/index.test.ts +4 -4
  54. package/src/index.ts +21 -7
  55. package/src/react/index.ts +2 -1
  56. package/src/react/rx.test.tsx +173 -18
  57. package/src/react/rx.tsx +274 -43
  58. package/src/react/useAction.test.ts +12 -14
  59. package/src/react/useAction.ts +11 -9
  60. package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
  61. package/src/react/{useValue.ts → useSelector.ts} +64 -33
  62. package/v2.md +44 -44
  63. package/dist/index-2ok7ilik.js +0 -1217
  64. package/dist/index-B_5SFzfl.cjs +0 -1
  65. /package/dist/{react/useValue.test.d.ts → core/onErrorHook.test.d.ts} +0 -0
@@ -1,12 +1,13 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { atom } from "./atom";
3
3
  import { select } from "./select";
4
+ import { promisesEqual } from "./promiseCache";
4
5
 
5
6
  describe("select", () => {
6
- describe("get()", () => {
7
+ describe("read()", () => {
7
8
  it("should read value from sync atom", () => {
8
9
  const count$ = atom(5);
9
- const result = select(({ get }) => get(count$));
10
+ const result = select(({ read }) => read(count$));
10
11
 
11
12
  expect(result.value).toBe(5);
12
13
  expect(result.error).toBe(undefined);
@@ -17,7 +18,7 @@ describe("select", () => {
17
18
  const a$ = atom(1);
18
19
  const b$ = atom(2);
19
20
 
20
- const result = select(({ get }) => get(a$) + get(b$));
21
+ const result = select(({ read }) => read(a$) + read(b$));
21
22
 
22
23
  expect(result.dependencies.size).toBe(2);
23
24
  expect(result.dependencies.has(a$)).toBe(true);
@@ -28,8 +29,8 @@ describe("select", () => {
28
29
  const count$ = atom(5);
29
30
  const error = new Error("Test error");
30
31
 
31
- const result = select(({ get }) => {
32
- get(count$);
32
+ const result = select(({ read }) => {
33
+ read(count$);
33
34
  throw error;
34
35
  });
35
36
 
@@ -45,7 +46,7 @@ describe("select", () => {
45
46
  const b$ = atom(2);
46
47
  const c$ = atom(3);
47
48
 
48
- const result = select(({ all }) => all(a$, b$, c$));
49
+ const result = select(({ all }) => all([a$, b$, c$]));
49
50
 
50
51
  expect(result.value).toEqual([1, 2, 3]);
51
52
  });
@@ -54,7 +55,7 @@ describe("select", () => {
54
55
  const a$ = atom(1);
55
56
  const b$ = atom(new Promise<number>(() => {}));
56
57
 
57
- const result = select(({ all }) => all(a$, b$));
58
+ const result = select(({ all }) => all([a$, b$]));
58
59
 
59
60
  expect(result.promise).toBeDefined();
60
61
  expect(result.value).toBe(undefined);
@@ -63,32 +64,37 @@ describe("select", () => {
63
64
  it("should throw error if any atom has rejected promise", async () => {
64
65
  const error = new Error("Test error");
65
66
  const a$ = atom(1);
66
- const rejectedPromise = Promise.reject(error);
67
+ // Create rejected promise with immediate catch to prevent unhandled rejection warning
68
+ let rejectFn: (e: Error) => void;
69
+ const rejectedPromise = new Promise<number>((_, reject) => {
70
+ rejectFn = reject;
71
+ });
67
72
  rejectedPromise.catch(() => {}); // Prevent unhandled rejection
73
+ rejectFn!(error);
68
74
  const b$ = atom(rejectedPromise);
69
75
 
70
76
  // First call to select tracks the promise but returns pending
71
- select(({ all }) => all(a$, b$));
77
+ select(({ all }) => all([a$, b$]));
72
78
 
73
79
  // Wait for promise handlers to run
74
80
  await Promise.resolve();
75
81
  await Promise.resolve();
76
82
 
77
83
  // Now the promise state should be updated
78
- const result = select(({ all }) => all(a$, b$));
84
+ const result = select(({ all }) => all([a$, b$]));
79
85
 
80
86
  expect(result.error).toBe(error);
81
87
  });
82
88
  });
83
89
 
84
90
  describe("race()", () => {
85
- it("should return first fulfilled value", () => {
91
+ it("should return first fulfilled value with key", () => {
86
92
  const a$ = atom(1);
87
93
  const b$ = atom(2);
88
94
 
89
- const result = select(({ race }) => race(a$, b$));
95
+ const result = select(({ race }) => race({ a: a$, b: b$ }));
90
96
 
91
- expect(result.value).toBe(1);
97
+ expect(result.value).toEqual({ key: "a", value: 1 });
92
98
  });
93
99
 
94
100
  it("should throw first error if first atom is rejected", async () => {
@@ -99,11 +105,11 @@ describe("select", () => {
99
105
  const b$ = atom(2);
100
106
 
101
107
  // Track the promise first
102
- select(({ race }) => race(a$, b$));
108
+ select(({ race }) => race({ a: a$, b: b$ }));
103
109
  await Promise.resolve();
104
110
  await Promise.resolve();
105
111
 
106
- const result = select(({ race }) => race(a$, b$));
112
+ const result = select(({ race }) => race({ a: a$, b: b$ }));
107
113
 
108
114
  expect(result.error).toBe(error);
109
115
  });
@@ -112,23 +118,23 @@ describe("select", () => {
112
118
  const a$ = atom(new Promise<number>(() => {}));
113
119
  const b$ = atom(new Promise<number>(() => {}));
114
120
 
115
- const result = select(({ race }) => race(a$, b$));
121
+ const result = select(({ race }) => race({ a: a$, b: b$ }));
116
122
 
117
123
  expect(result.promise).toBeDefined();
118
124
  });
119
125
  });
120
126
 
121
127
  describe("any()", () => {
122
- it("should return first fulfilled value", () => {
128
+ it("should return first fulfilled value with key", () => {
123
129
  const a$ = atom(1);
124
130
  const b$ = atom(2);
125
131
 
126
- const result = select(({ any }) => any(a$, b$));
132
+ const result = select(({ any }) => any({ a: a$, b: b$ }));
127
133
 
128
- expect(result.value).toBe(1);
134
+ expect(result.value).toEqual({ key: "a", value: 1 });
129
135
  });
130
136
 
131
- it("should skip rejected and return next fulfilled", async () => {
137
+ it("should skip rejected and return next fulfilled with key", async () => {
132
138
  const error = new Error("Test error");
133
139
  const rejectedPromise = Promise.reject(error);
134
140
  rejectedPromise.catch(() => {});
@@ -136,32 +142,41 @@ describe("select", () => {
136
142
  const b$ = atom(2);
137
143
 
138
144
  // Track first, then wait for microtasks
139
- select(({ any }) => any(a$, b$));
145
+ select(({ any }) => any({ a: a$, b: b$ }));
140
146
  await Promise.resolve();
141
147
  await Promise.resolve();
142
148
 
143
- const result = select(({ any }) => any(a$, b$));
149
+ const result = select(({ any }) => any({ a: a$, b: b$ }));
144
150
 
145
- expect(result.value).toBe(2);
151
+ expect(result.value).toEqual({ key: "b", value: 2 });
146
152
  });
147
153
 
148
154
  it("should throw AggregateError if all rejected", async () => {
149
155
  const error1 = new Error("Error 1");
150
156
  const error2 = new Error("Error 2");
151
- const p1 = Promise.reject(error1);
152
- const p2 = Promise.reject(error2);
157
+ // Create rejected promises with immediate catch to prevent unhandled rejection warning
158
+ let reject1: (e: Error) => void;
159
+ let reject2: (e: Error) => void;
160
+ const p1 = new Promise<number>((_, reject) => {
161
+ reject1 = reject;
162
+ });
163
+ const p2 = new Promise<number>((_, reject) => {
164
+ reject2 = reject;
165
+ });
153
166
  p1.catch(() => {});
154
167
  p2.catch(() => {});
168
+ reject1!(error1);
169
+ reject2!(error2);
155
170
 
156
171
  const a$ = atom(p1);
157
172
  const b$ = atom(p2);
158
173
 
159
174
  // Track first, then wait for microtasks
160
- select(({ any }) => any(a$, b$));
175
+ select(({ any }) => any({ a: a$, b: b$ }));
161
176
  await Promise.resolve();
162
177
  await Promise.resolve();
163
178
 
164
- const result = select(({ any }) => any(a$, b$));
179
+ const result = select(({ any }) => any({ a: a$, b: b$ }));
165
180
 
166
181
  expect(result.error).toBeDefined();
167
182
  expect((result.error as Error).name).toBe("AggregateError");
@@ -172,16 +187,21 @@ describe("select", () => {
172
187
  it("should return array of settled results", async () => {
173
188
  const a$ = atom(1);
174
189
  const error = new Error("Test error");
175
- const rejectedPromise = Promise.reject(error);
190
+ // Create rejected promise with immediate catch to prevent unhandled rejection warning
191
+ let rejectFn: (e: Error) => void;
192
+ const rejectedPromise = new Promise<number>((_, reject) => {
193
+ rejectFn = reject;
194
+ });
176
195
  rejectedPromise.catch(() => {});
196
+ rejectFn!(error);
177
197
  const b$ = atom(rejectedPromise);
178
198
 
179
199
  // Track first, wait for microtasks
180
- select(({ settled }) => settled(a$, b$));
200
+ select(({ settled }) => settled([a$, b$]));
181
201
  await Promise.resolve();
182
202
  await Promise.resolve();
183
203
 
184
- const result = select(({ settled }) => settled(a$, b$));
204
+ const result = select(({ settled }) => settled([a$, b$]));
185
205
 
186
206
  expect(result.value).toEqual([
187
207
  { status: "ready", value: 1 },
@@ -193,7 +213,7 @@ describe("select", () => {
193
213
  const a$ = atom(1);
194
214
  const b$ = atom(new Promise<number>(() => {}));
195
215
 
196
- const result = select(({ settled }) => settled(a$, b$));
216
+ const result = select(({ settled }) => settled([a$, b$]));
197
217
 
198
218
  expect(result.promise).toBeDefined();
199
219
  });
@@ -205,7 +225,9 @@ describe("select", () => {
205
225
  const a$ = atom(1);
206
226
  const b$ = atom(2);
207
227
 
208
- const result = select(({ get }) => (get(condition$) ? get(a$) : get(b$)));
228
+ const result = select(({ read }) =>
229
+ read(condition$) ? read(a$) : read(b$)
230
+ );
209
231
 
210
232
  expect(result.dependencies.size).toBe(2);
211
233
  expect(result.dependencies.has(condition$)).toBe(true);
@@ -254,4 +276,524 @@ describe("select", () => {
254
276
  expect(result.promise).toBe(undefined);
255
277
  });
256
278
  });
279
+
280
+ describe("safe()", () => {
281
+ it("should return [undefined, result] on success", () => {
282
+ const count$ = atom(5);
283
+
284
+ const result = select(({ read, safe }) => {
285
+ const [err, value] = safe(() => read(count$) * 2);
286
+ return { err, value };
287
+ });
288
+
289
+ expect(result.value).toEqual({ err: undefined, value: 10 });
290
+ });
291
+
292
+ it("should return [error, undefined] on sync error", () => {
293
+ const result = select(({ safe }) => {
294
+ const [err, value] = safe(() => {
295
+ throw new Error("Test error");
296
+ });
297
+ return { err, value };
298
+ });
299
+
300
+ expect(result.value?.err).toBeInstanceOf(Error);
301
+ expect((result.value?.err as Error).message).toBe("Test error");
302
+ expect(result.value?.value).toBe(undefined);
303
+ });
304
+
305
+ it("should re-throw Promise to preserve Suspense", () => {
306
+ const pending$ = atom(new Promise(() => {})); // Never resolves
307
+
308
+ const result = select(({ read, safe }) => {
309
+ const [err, value] = safe(() => read(pending$));
310
+ return { err, value };
311
+ });
312
+
313
+ // Promise should be thrown, not caught
314
+ expect(result.promise).toBeDefined();
315
+ expect(result.value).toBe(undefined);
316
+ });
317
+
318
+ it("should catch JSON.parse errors", () => {
319
+ const raw$ = atom("invalid json");
320
+
321
+ const result = select(({ read, safe }) => {
322
+ const [err, data] = safe(() => {
323
+ const raw = read(raw$);
324
+ return JSON.parse(raw);
325
+ });
326
+
327
+ if (err) {
328
+ return { error: "Parse failed" };
329
+ }
330
+ return { data };
331
+ });
332
+
333
+ expect(result.value).toEqual({ error: "Parse failed" });
334
+ });
335
+
336
+ it("should allow graceful degradation", () => {
337
+ const user$ = atom({ name: "John" });
338
+
339
+ const result = select(({ read, safe }) => {
340
+ const [err1, user] = safe(() => read(user$));
341
+ if (err1) return { user: null, posts: [] };
342
+
343
+ const [err2] = safe(() => {
344
+ throw new Error("Posts failed");
345
+ });
346
+ if (err2) return { user, posts: [] }; // Graceful degradation
347
+
348
+ return { user, posts: ["post1"] };
349
+ });
350
+
351
+ expect(result.value).toEqual({
352
+ user: { name: "John" },
353
+ posts: [], // Gracefully degraded
354
+ });
355
+ });
356
+
357
+ it("should preserve error type information", () => {
358
+ class CustomError extends Error {
359
+ code = "CUSTOM";
360
+ }
361
+
362
+ const result = select(({ safe }) => {
363
+ const [err] = safe(() => {
364
+ throw new CustomError("Custom error");
365
+ });
366
+
367
+ if (err instanceof CustomError) {
368
+ return { code: err.code };
369
+ }
370
+ return { code: "UNKNOWN" };
371
+ });
372
+
373
+ expect(result.value).toEqual({ code: "CUSTOM" });
374
+ });
375
+ });
376
+
377
+ describe("async context detection", () => {
378
+ it("should throw error when read() is called outside selection context", async () => {
379
+ const count$ = atom(5);
380
+ let capturedRead: ((atom: typeof count$) => number) | null = null;
381
+
382
+ select(({ read }) => {
383
+ capturedRead = read;
384
+ return read(count$);
385
+ });
386
+
387
+ // Calling read() after select() has finished should throw
388
+ expect(() => capturedRead!(count$)).toThrow(
389
+ "read() was called outside of the selection context"
390
+ );
391
+ });
392
+
393
+ it("should throw error when all() is called outside selection context", async () => {
394
+ const a$ = atom(1);
395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
+ let capturedAll: any = null;
397
+
398
+ select(({ all }) => {
399
+ capturedAll = all;
400
+ return all([a$]);
401
+ });
402
+
403
+ expect(() => capturedAll([a$])).toThrow(
404
+ "all() was called outside of the selection context"
405
+ );
406
+ });
407
+
408
+ it("should throw error when race() is called outside selection context", async () => {
409
+ const a$ = atom(1);
410
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
411
+ let capturedRace: any = null;
412
+
413
+ select(({ race }) => {
414
+ capturedRace = race;
415
+ return race({ a: a$ });
416
+ });
417
+
418
+ expect(() => capturedRace({ a: a$ })).toThrow(
419
+ "race() was called outside of the selection context"
420
+ );
421
+ });
422
+
423
+ it("should throw error when any() is called outside selection context", async () => {
424
+ const a$ = atom(1);
425
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
+ let capturedAny: any = null;
427
+
428
+ select(({ any }) => {
429
+ capturedAny = any;
430
+ return any({ a: a$ });
431
+ });
432
+
433
+ expect(() => capturedAny({ a: a$ })).toThrow(
434
+ "any() was called outside of the selection context"
435
+ );
436
+ });
437
+
438
+ it("should throw error when settled() is called outside selection context", async () => {
439
+ const a$ = atom(1);
440
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
441
+ let capturedSettled: any = null;
442
+
443
+ select(({ settled }) => {
444
+ capturedSettled = settled;
445
+ return settled([a$]);
446
+ });
447
+
448
+ expect(() => capturedSettled([a$])).toThrow(
449
+ "settled() was called outside of the selection context"
450
+ );
451
+ });
452
+
453
+ it("should throw error when safe() is called outside selection context", async () => {
454
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
455
+ let capturedSafe: any = null;
456
+
457
+ select(({ safe }) => {
458
+ capturedSafe = safe;
459
+ return safe(() => 42);
460
+ });
461
+
462
+ expect(() => capturedSafe(() => 42)).toThrow(
463
+ "safe() was called outside of the selection context"
464
+ );
465
+ });
466
+ });
467
+
468
+ describe("state()", () => {
469
+ it("should return ready state for sync atom", () => {
470
+ const count$ = atom(5);
471
+
472
+ const result = select(({ state }) => state(count$));
473
+
474
+ expect(result.value).toEqual({ status: "ready", value: 5 });
475
+ });
476
+
477
+ it("should return loading state for pending promise atom", () => {
478
+ const promise = new Promise<number>(() => {});
479
+ const async$ = atom(promise);
480
+
481
+ const result = select(({ state }) => state(async$));
482
+
483
+ expect(result.value).toEqual({
484
+ status: "loading",
485
+ value: undefined,
486
+ error: undefined,
487
+ });
488
+ });
489
+
490
+ it("should return error state for rejected promise atom", async () => {
491
+ const error = new Error("Test error");
492
+ const rejectedPromise = Promise.reject(error);
493
+ rejectedPromise.catch(() => {});
494
+ const async$ = atom(rejectedPromise);
495
+
496
+ // Track first
497
+ select(({ state }) => state(async$));
498
+ await Promise.resolve();
499
+ await Promise.resolve();
500
+
501
+ const result = select(({ state }) => state(async$));
502
+
503
+ expect(result.value).toEqual({ status: "error", error });
504
+ });
505
+
506
+ it("should track dependencies when using state(atom)", () => {
507
+ const a$ = atom(1);
508
+ const b$ = atom(2);
509
+
510
+ const result = select(({ state }) => {
511
+ state(a$);
512
+ state(b$);
513
+ return "done";
514
+ });
515
+
516
+ expect(result.dependencies.size).toBe(2);
517
+ expect(result.dependencies.has(a$)).toBe(true);
518
+ expect(result.dependencies.has(b$)).toBe(true);
519
+ });
520
+
521
+ it("should wrap selector function with try/catch and return ready state", () => {
522
+ const a$ = atom(10);
523
+ const b$ = atom(20);
524
+
525
+ const result = select(({ read, state }) =>
526
+ state(() => read(a$) + read(b$))
527
+ );
528
+
529
+ expect(result.value).toEqual({ status: "ready", value: 30 });
530
+ });
531
+
532
+ it("should wrap selector function and return loading state when promise thrown", () => {
533
+ const async$ = atom(new Promise<number>(() => {}));
534
+
535
+ const result = select(({ read, state }) => state(() => read(async$)));
536
+
537
+ expect(result.value).toEqual({
538
+ status: "loading",
539
+ value: undefined,
540
+ error: undefined,
541
+ });
542
+ });
543
+
544
+ it("should wrap selector function and return error state when error thrown", () => {
545
+ const result = select(({ state }) =>
546
+ state(() => {
547
+ throw new Error("Test error");
548
+ })
549
+ );
550
+
551
+ expect(result.value?.status).toBe("error");
552
+ expect(
553
+ (result.value as { status: "error"; error: unknown }).error
554
+ ).toBeInstanceOf(Error);
555
+ });
556
+
557
+ it("should work with all() inside state()", () => {
558
+ const a$ = atom(1);
559
+ const b$ = atom(2);
560
+
561
+ const result = select(({ all, state }) => state(() => all([a$, b$])));
562
+
563
+ expect(result.value).toEqual({
564
+ status: "ready",
565
+ value: [1, 2],
566
+ error: undefined,
567
+ });
568
+ });
569
+
570
+ it("should return loading state when all() has pending atoms", () => {
571
+ const a$ = atom(1);
572
+ const b$ = atom(new Promise<number>(() => {}));
573
+
574
+ const result = select(({ all, state }) => state(() => all([a$, b$])));
575
+
576
+ expect(result.value?.status).toBe("loading");
577
+ });
578
+
579
+ it("should allow building dashboard-style derived atoms", () => {
580
+ const user$ = atom({ name: "John" });
581
+ const posts$ = atom(new Promise<string[]>(() => {})); // Loading
582
+
583
+ const result = select(({ state }) => {
584
+ const userState = state(user$);
585
+ const postsState = state(posts$);
586
+
587
+ return {
588
+ user: userState.status === "ready" ? userState.value : null,
589
+ posts: postsState.status === "ready" ? postsState.value : [],
590
+ isLoading:
591
+ userState.status === "loading" || postsState.status === "loading",
592
+ };
593
+ });
594
+
595
+ expect(result.value).toEqual({
596
+ user: { name: "John" },
597
+ posts: [],
598
+ isLoading: true,
599
+ });
600
+ });
601
+
602
+ it("should throw error when called outside selection context", () => {
603
+ const a$ = atom(1);
604
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
605
+ let capturedState: any = null;
606
+
607
+ select(({ state }) => {
608
+ capturedState = state;
609
+ return state(a$);
610
+ });
611
+
612
+ expect(() => capturedState(a$)).toThrow(
613
+ "state() was called outside of the selection context"
614
+ );
615
+ });
616
+ });
617
+
618
+ describe("parallel waiting behavior", () => {
619
+ it("all() should throw combined Promise.all for parallel waiting", async () => {
620
+ let resolve1: (value: number) => void;
621
+ let resolve2: (value: number) => void;
622
+ const p1 = new Promise<number>((r) => {
623
+ resolve1 = r;
624
+ });
625
+ const p2 = new Promise<number>((r) => {
626
+ resolve2 = r;
627
+ });
628
+ const a$ = atom(p1);
629
+ const b$ = atom(p2);
630
+
631
+ // First call should throw a combined promise
632
+ const result1 = select(({ all }) => all([a$, b$]));
633
+ expect(result1.promise).toBeDefined();
634
+
635
+ // Resolve promises in reverse order
636
+ resolve2!(20);
637
+ await Promise.resolve();
638
+
639
+ // Still loading (a$ not resolved yet)
640
+ const result2 = select(({ all }) => all([a$, b$]));
641
+ expect(result2.promise).toBeDefined();
642
+
643
+ // Resolve first promise
644
+ resolve1!(10);
645
+ await Promise.resolve();
646
+ await Promise.resolve();
647
+
648
+ // Now should be ready with both values
649
+ const result3 = select(({ all }) => all([a$, b$]));
650
+ expect(result3.value).toEqual([10, 20]);
651
+ });
652
+
653
+ it("all() should return equivalent promises when atoms unchanged", async () => {
654
+ const p1 = new Promise<number>(() => {});
655
+ const p2 = new Promise<number>(() => {});
656
+ const a$ = atom(p1);
657
+ const b$ = atom(p2);
658
+
659
+ // Get promise from first call
660
+ const result1 = select(({ all }) => all([a$, b$]));
661
+ const promise1 = result1.promise;
662
+
663
+ // Second call should return equivalent promise (same source promises)
664
+ const result2 = select(({ all }) => all([a$, b$]));
665
+ const promise2 = result2.promise;
666
+
667
+ // Promises are equivalent via metadata comparison
668
+ expect(promisesEqual(promise1, promise2)).toBe(true);
669
+ });
670
+
671
+ it("race() should throw combined Promise.race for parallel racing", async () => {
672
+ let resolve1: (value: number) => void;
673
+ let resolve2: (value: number) => void;
674
+ const p1 = new Promise<number>((r) => {
675
+ resolve1 = r;
676
+ });
677
+ const p2 = new Promise<number>((r) => {
678
+ resolve2 = r;
679
+ });
680
+ const a$ = atom(p1);
681
+ const b$ = atom(p2);
682
+
683
+ // First call should throw a combined promise
684
+ const result1 = select(({ race }) => race({ a: a$, b: b$ }));
685
+ expect(result1.promise).toBeDefined();
686
+
687
+ // Resolve second promise first (it should win the race)
688
+ resolve2!(20);
689
+ await Promise.resolve();
690
+ await Promise.resolve();
691
+
692
+ // Race should return second value (first to resolve) with key
693
+ const result2 = select(({ race }) => race({ a: a$, b: b$ }));
694
+ expect(result2.value).toEqual({ key: "b", value: 20 });
695
+
696
+ // Clean up: resolve first promise
697
+ resolve1!(10);
698
+ });
699
+
700
+ it("race() should return equivalent promises when atoms unchanged", async () => {
701
+ const p1 = new Promise<number>(() => {});
702
+ const p2 = new Promise<number>(() => {});
703
+ const a$ = atom(p1);
704
+ const b$ = atom(p2);
705
+
706
+ const result1 = select(({ race }) => race({ a: a$, b: b$ }));
707
+ const promise1 = result1.promise;
708
+
709
+ const result2 = select(({ race }) => race({ a: a$, b: b$ }));
710
+ const promise2 = result2.promise;
711
+
712
+ // Promises are equivalent via metadata comparison
713
+ expect(promisesEqual(promise1, promise2)).toBe(true);
714
+ });
715
+
716
+ it("any() should race all loading promises in parallel", async () => {
717
+ let resolve1: (value: number) => void;
718
+ let resolve2: (value: number) => void;
719
+ const p1 = new Promise<number>((r) => {
720
+ resolve1 = r;
721
+ });
722
+ const p2 = new Promise<number>((r) => {
723
+ resolve2 = r;
724
+ });
725
+ const a$ = atom(p1);
726
+ const b$ = atom(p2);
727
+
728
+ // First call should throw combined race promise
729
+ const result1 = select(({ any }) => any({ a: a$, b: b$ }));
730
+ expect(result1.promise).toBeDefined();
731
+
732
+ // Resolve second promise first
733
+ resolve2!(20);
734
+ await Promise.resolve();
735
+ await Promise.resolve();
736
+
737
+ // any() should return second value (first to resolve) with key
738
+ const result2 = select(({ any }) => any({ a: a$, b: b$ }));
739
+ expect(result2.value).toEqual({ key: "b", value: 20 });
740
+
741
+ // Clean up
742
+ resolve1!(10);
743
+ });
744
+
745
+ it("settled() should wait for all in parallel", async () => {
746
+ let resolve1: (value: number) => void;
747
+ let reject2: (error: Error) => void;
748
+ const p1 = new Promise<number>((r) => {
749
+ resolve1 = r;
750
+ });
751
+ const p2 = new Promise<number>((_, reject) => {
752
+ reject2 = reject;
753
+ });
754
+ p2.catch(() => {}); // Prevent unhandled rejection
755
+ const a$ = atom(p1);
756
+ const b$ = atom(p2);
757
+
758
+ // First call should throw combined promise
759
+ const result1 = select(({ settled }) => settled([a$, b$]));
760
+ expect(result1.promise).toBeDefined();
761
+
762
+ // Settle promises in any order
763
+ reject2!(new Error("fail"));
764
+ await Promise.resolve();
765
+
766
+ // Still loading (a$ not settled yet)
767
+ const result2 = select(({ settled }) => settled([a$, b$]));
768
+ expect(result2.promise).toBeDefined();
769
+
770
+ // Settle first
771
+ resolve1!(10);
772
+ await Promise.resolve();
773
+ await Promise.resolve();
774
+
775
+ // Now should have settled results
776
+ const result3 = select(({ settled }) => settled([a$, b$]));
777
+ expect(result3.value).toEqual([
778
+ { status: "ready", value: 10 },
779
+ { status: "error", error: expect.any(Error) },
780
+ ]);
781
+ });
782
+
783
+ it("settled() should return equivalent promises when atoms unchanged", async () => {
784
+ const p1 = new Promise<number>(() => {});
785
+ const p2 = new Promise<number>(() => {});
786
+ const a$ = atom(p1);
787
+ const b$ = atom(p2);
788
+
789
+ const result1 = select(({ settled }) => settled([a$, b$]));
790
+ const promise1 = result1.promise;
791
+
792
+ const result2 = select(({ settled }) => settled([a$, b$]));
793
+ const promise2 = result2.promise;
794
+
795
+ // Promises are equivalent via metadata comparison
796
+ expect(promisesEqual(promise1, promise2)).toBe(true);
797
+ });
798
+ });
257
799
  });