atomirx 0.0.7 → 0.1.0

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 -30
  33. package/dist/react/index.js +206 -791
  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 -39
@@ -1,799 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { atom } from "./atom";
3
- import { select } from "./select";
4
- import { promisesEqual } from "./promiseCache";
5
-
6
- describe("select", () => {
7
- describe("read()", () => {
8
- it("should read value from sync atom", () => {
9
- const count$ = atom(5);
10
- const result = select(({ read }) => read(count$));
11
-
12
- expect(result.value).toBe(5);
13
- expect(result.error).toBe(undefined);
14
- expect(result.promise).toBe(undefined);
15
- });
16
-
17
- it("should track dependencies", () => {
18
- const a$ = atom(1);
19
- const b$ = atom(2);
20
-
21
- const result = select(({ read }) => read(a$) + read(b$));
22
-
23
- expect(result.dependencies.size).toBe(2);
24
- expect(result.dependencies.has(a$)).toBe(true);
25
- expect(result.dependencies.has(b$)).toBe(true);
26
- });
27
-
28
- it("should throw error if computation throws", () => {
29
- const count$ = atom(5);
30
- const error = new Error("Test error");
31
-
32
- const result = select(({ read }) => {
33
- read(count$);
34
- throw error;
35
- });
36
-
37
- expect(result.value).toBe(undefined);
38
- expect(result.error).toBe(error);
39
- expect(result.promise).toBe(undefined);
40
- });
41
- });
42
-
43
- describe("all()", () => {
44
- it("should return array of values for all sync atoms", () => {
45
- const a$ = atom(1);
46
- const b$ = atom(2);
47
- const c$ = atom(3);
48
-
49
- const result = select(({ all }) => all([a$, b$, c$]));
50
-
51
- expect(result.value).toEqual([1, 2, 3]);
52
- });
53
-
54
- it("should throw promise if any atom is pending", () => {
55
- const a$ = atom(1);
56
- const b$ = atom(new Promise<number>(() => {}));
57
-
58
- const result = select(({ all }) => all([a$, b$]));
59
-
60
- expect(result.promise).toBeDefined();
61
- expect(result.value).toBe(undefined);
62
- });
63
-
64
- it("should throw error if any atom has rejected promise", async () => {
65
- const error = new Error("Test error");
66
- const a$ = atom(1);
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
- });
72
- rejectedPromise.catch(() => {}); // Prevent unhandled rejection
73
- rejectFn!(error);
74
- const b$ = atom(rejectedPromise);
75
-
76
- // First call to select tracks the promise but returns pending
77
- select(({ all }) => all([a$, b$]));
78
-
79
- // Wait for promise handlers to run
80
- await Promise.resolve();
81
- await Promise.resolve();
82
-
83
- // Now the promise state should be updated
84
- const result = select(({ all }) => all([a$, b$]));
85
-
86
- expect(result.error).toBe(error);
87
- });
88
- });
89
-
90
- describe("race()", () => {
91
- it("should return first fulfilled value with key", () => {
92
- const a$ = atom(1);
93
- const b$ = atom(2);
94
-
95
- const result = select(({ race }) => race({ a: a$, b: b$ }));
96
-
97
- expect(result.value).toEqual({ key: "a", value: 1 });
98
- });
99
-
100
- it("should throw first error if first atom is rejected", async () => {
101
- const error = new Error("Test error");
102
- const rejectedPromise = Promise.reject(error);
103
- rejectedPromise.catch(() => {});
104
- const a$ = atom(rejectedPromise);
105
- const b$ = atom(2);
106
-
107
- // Track the promise first
108
- select(({ race }) => race({ a: a$, b: b$ }));
109
- await Promise.resolve();
110
- await Promise.resolve();
111
-
112
- const result = select(({ race }) => race({ a: a$, b: b$ }));
113
-
114
- expect(result.error).toBe(error);
115
- });
116
-
117
- it("should throw promise if all are pending", () => {
118
- const a$ = atom(new Promise<number>(() => {}));
119
- const b$ = atom(new Promise<number>(() => {}));
120
-
121
- const result = select(({ race }) => race({ a: a$, b: b$ }));
122
-
123
- expect(result.promise).toBeDefined();
124
- });
125
- });
126
-
127
- describe("any()", () => {
128
- it("should return first fulfilled value with key", () => {
129
- const a$ = atom(1);
130
- const b$ = atom(2);
131
-
132
- const result = select(({ any }) => any({ a: a$, b: b$ }));
133
-
134
- expect(result.value).toEqual({ key: "a", value: 1 });
135
- });
136
-
137
- it("should skip rejected and return next fulfilled with key", async () => {
138
- const error = new Error("Test error");
139
- const rejectedPromise = Promise.reject(error);
140
- rejectedPromise.catch(() => {});
141
- const a$ = atom(rejectedPromise);
142
- const b$ = atom(2);
143
-
144
- // Track first, then wait for microtasks
145
- select(({ any }) => any({ a: a$, b: b$ }));
146
- await Promise.resolve();
147
- await Promise.resolve();
148
-
149
- const result = select(({ any }) => any({ a: a$, b: b$ }));
150
-
151
- expect(result.value).toEqual({ key: "b", value: 2 });
152
- });
153
-
154
- it("should throw AggregateError if all rejected", async () => {
155
- const error1 = new Error("Error 1");
156
- const error2 = new Error("Error 2");
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
- });
166
- p1.catch(() => {});
167
- p2.catch(() => {});
168
- reject1!(error1);
169
- reject2!(error2);
170
-
171
- const a$ = atom(p1);
172
- const b$ = atom(p2);
173
-
174
- // Track first, then wait for microtasks
175
- select(({ any }) => any({ a: a$, b: b$ }));
176
- await Promise.resolve();
177
- await Promise.resolve();
178
-
179
- const result = select(({ any }) => any({ a: a$, b: b$ }));
180
-
181
- expect(result.error).toBeDefined();
182
- expect((result.error as Error).name).toBe("AggregateError");
183
- });
184
- });
185
-
186
- describe("settled()", () => {
187
- it("should return array of settled results", async () => {
188
- const a$ = atom(1);
189
- const error = new Error("Test 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
- });
195
- rejectedPromise.catch(() => {});
196
- rejectFn!(error);
197
- const b$ = atom(rejectedPromise);
198
-
199
- // Track first, wait for microtasks
200
- select(({ settled }) => settled([a$, b$]));
201
- await Promise.resolve();
202
- await Promise.resolve();
203
-
204
- const result = select(({ settled }) => settled([a$, b$]));
205
-
206
- expect(result.value).toEqual([
207
- { status: "ready", value: 1 },
208
- { status: "error", error },
209
- ]);
210
- });
211
-
212
- it("should throw promise if any atom is pending", () => {
213
- const a$ = atom(1);
214
- const b$ = atom(new Promise<number>(() => {}));
215
-
216
- const result = select(({ settled }) => settled([a$, b$]));
217
-
218
- expect(result.promise).toBeDefined();
219
- });
220
- });
221
-
222
- describe("conditional dependencies", () => {
223
- it("should only track accessed atoms", () => {
224
- const condition$ = atom(false);
225
- const a$ = atom(1);
226
- const b$ = atom(2);
227
-
228
- const result = select(({ read }) =>
229
- read(condition$) ? read(a$) : read(b$)
230
- );
231
-
232
- expect(result.dependencies.size).toBe(2);
233
- expect(result.dependencies.has(condition$)).toBe(true);
234
- expect(result.dependencies.has(b$)).toBe(true);
235
- // a$ was not accessed because condition was false
236
- expect(result.dependencies.has(a$)).toBe(false);
237
- });
238
- });
239
-
240
- describe("error handling", () => {
241
- it("should throw error if selector returns a Promise", () => {
242
- const result = select(() => Promise.resolve(42));
243
-
244
- expect(result.error).toBeDefined();
245
- expect(result.error).toBeInstanceOf(Error);
246
- expect((result.error as Error).message).toContain(
247
- "select() selector must return a synchronous value"
248
- );
249
- expect(result.value).toBe(undefined);
250
- expect(result.promise).toBe(undefined);
251
- });
252
-
253
- it("should throw error if selector returns a PromiseLike", () => {
254
- // Custom PromiseLike object
255
- const promiseLike = {
256
- then: (resolve: (value: number) => void) => {
257
- resolve(42);
258
- return promiseLike;
259
- },
260
- };
261
-
262
- const result = select(() => promiseLike);
263
-
264
- expect(result.error).toBeDefined();
265
- expect(result.error).toBeInstanceOf(Error);
266
- expect((result.error as Error).message).toContain(
267
- "select() selector must return a synchronous value"
268
- );
269
- });
270
-
271
- it("should work fine with sync values", () => {
272
- const result = select(() => 42);
273
-
274
- expect(result.value).toBe(42);
275
- expect(result.error).toBe(undefined);
276
- expect(result.promise).toBe(undefined);
277
- });
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
- });
799
- });