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
@@ -0,0 +1,534 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { withReady } from "./withReady";
3
+ import { atom } from "./atom";
4
+ import { select } from "./select";
5
+ describe("withReady", () => {
6
+ describe("basic functionality", () => {
7
+ it("should add ready method to context", () => {
8
+ select((context) => {
9
+ const enhanced = context.use(withReady());
10
+ expect(typeof enhanced.ready).toBe("function");
11
+ return null;
12
+ });
13
+ });
14
+
15
+ it("should preserve original context methods", () => {
16
+ select((context) => {
17
+ const enhanced = context.use(withReady());
18
+ expect(typeof enhanced.read).toBe("function");
19
+ expect(typeof enhanced.all).toBe("function");
20
+ expect(typeof enhanced.any).toBe("function");
21
+ expect(typeof enhanced.race).toBe("function");
22
+ expect(typeof enhanced.settled).toBe("function");
23
+ expect(typeof enhanced.safe).toBe("function");
24
+ expect(typeof enhanced.use).toBe("function");
25
+ return null;
26
+ });
27
+ });
28
+ });
29
+
30
+ describe("ready() with non-null values", () => {
31
+ it("should return value when atom has non-null value", () => {
32
+ const count$ = atom(42);
33
+
34
+ const result = select((context) => {
35
+ const ctx = context.use(withReady());
36
+ return ctx.ready(count$);
37
+ });
38
+
39
+ expect(result.value).toBe(42);
40
+ expect(result.error).toBeUndefined();
41
+ expect(result.promise).toBeUndefined();
42
+ });
43
+
44
+ it("should return value when atom has zero", () => {
45
+ const count$ = atom(0);
46
+
47
+ const result = select((context) => {
48
+ const ctx = context.use(withReady());
49
+ return ctx.ready(count$);
50
+ });
51
+
52
+ expect(result.value).toBe(0);
53
+ });
54
+
55
+ it("should return value when atom has empty string", () => {
56
+ const str$ = atom("");
57
+
58
+ const result = select((context) => {
59
+ const ctx = context.use(withReady());
60
+ return ctx.ready(str$);
61
+ });
62
+
63
+ expect(result.value).toBe("");
64
+ });
65
+
66
+ it("should return value when atom has false", () => {
67
+ const bool$ = atom(false);
68
+
69
+ const result = select((context) => {
70
+ const ctx = context.use(withReady());
71
+ return ctx.ready(bool$);
72
+ });
73
+
74
+ expect(result.value).toBe(false);
75
+ });
76
+
77
+ it("should return value when atom has object", () => {
78
+ const obj$ = atom({ name: "test" });
79
+
80
+ const result = select((context) => {
81
+ const ctx = context.use(withReady());
82
+ return ctx.ready(obj$);
83
+ });
84
+
85
+ expect(result.value).toEqual({ name: "test" });
86
+ });
87
+ });
88
+
89
+ describe("ready() with null/undefined values", () => {
90
+ it("should throw never-resolve promise when atom value is null", () => {
91
+ const nullable$ = atom<string | null>(null);
92
+
93
+ const result = select((context) => {
94
+ const ctx = context.use(withReady());
95
+ return ctx.ready(nullable$);
96
+ });
97
+
98
+ expect(result.value).toBeUndefined();
99
+ expect(result.error).toBeUndefined();
100
+ expect(result.promise).toBeInstanceOf(Promise);
101
+ });
102
+
103
+ it("should throw never-resolve promise when atom value is undefined", () => {
104
+ const undefinedAtom$ = atom<string | undefined>(undefined);
105
+
106
+ const result = select((context) => {
107
+ const ctx = context.use(withReady());
108
+ return ctx.ready(undefinedAtom$);
109
+ });
110
+
111
+ expect(result.value).toBeUndefined();
112
+ expect(result.error).toBeUndefined();
113
+ expect(result.promise).toBeInstanceOf(Promise);
114
+ });
115
+ });
116
+
117
+ describe("ready() with selector", () => {
118
+ it("should apply selector and return result when non-null", () => {
119
+ const user$ = atom({ id: 1, name: "John" });
120
+
121
+ const result = select((context) => {
122
+ const ctx = context.use(withReady());
123
+ return ctx.ready(user$, (user) => user.name);
124
+ });
125
+
126
+ expect(result.value).toBe("John");
127
+ });
128
+
129
+ it("should throw never-resolve promise when selector returns null", () => {
130
+ const user$ = atom<{ id: number; email: string | null }>({
131
+ id: 1,
132
+ email: null,
133
+ });
134
+
135
+ const result = select((context) => {
136
+ const ctx = context.use(withReady());
137
+ return ctx.ready(user$, (user) => user.email);
138
+ });
139
+
140
+ expect(result.value).toBeUndefined();
141
+ expect(result.promise).toBeInstanceOf(Promise);
142
+ });
143
+
144
+ it("should throw never-resolve promise when selector returns undefined", () => {
145
+ const data$ = atom<{ value?: string }>({});
146
+
147
+ const result = select((context) => {
148
+ const ctx = context.use(withReady());
149
+ return ctx.ready(data$, (data) => data.value);
150
+ });
151
+
152
+ expect(result.value).toBeUndefined();
153
+ expect(result.promise).toBeInstanceOf(Promise);
154
+ });
155
+
156
+ it("should return zero from selector", () => {
157
+ const data$ = atom({ count: 0 });
158
+
159
+ const result = select((context) => {
160
+ const ctx = context.use(withReady());
161
+ return ctx.ready(data$, (data) => data.count);
162
+ });
163
+
164
+ expect(result.value).toBe(0);
165
+ });
166
+
167
+ it("should return empty string from selector", () => {
168
+ const data$ = atom({ name: "" });
169
+
170
+ const result = select((context) => {
171
+ const ctx = context.use(withReady());
172
+ return ctx.ready(data$, (data) => data.name);
173
+ });
174
+
175
+ expect(result.value).toBe("");
176
+ });
177
+ });
178
+
179
+ describe("dependency tracking", () => {
180
+ it("should track atom as dependency", () => {
181
+ const count$ = atom(42);
182
+
183
+ const result = select((context) => {
184
+ const ctx = context.use(withReady());
185
+ return ctx.ready(count$);
186
+ });
187
+
188
+ expect(result.dependencies.has(count$)).toBe(true);
189
+ });
190
+
191
+ it("should track atom as dependency even when throwing promise", () => {
192
+ const nullable$ = atom<string | null>(null);
193
+
194
+ const result = select((context) => {
195
+ const ctx = context.use(withReady());
196
+ return ctx.ready(nullable$);
197
+ });
198
+
199
+ expect(result.dependencies.has(nullable$)).toBe(true);
200
+ });
201
+ });
202
+
203
+ describe("never-resolve promise behavior", () => {
204
+ it("should return a promise that never resolves", async () => {
205
+ const nullable$ = atom<string | null>(null);
206
+
207
+ const result = select((context) => {
208
+ const ctx = context.use(withReady());
209
+ return ctx.ready(nullable$);
210
+ });
211
+
212
+ // The promise should never resolve
213
+ // We test this by racing with a timeout
214
+ const timeoutPromise = new Promise<"timeout">((resolve) =>
215
+ setTimeout(() => resolve("timeout"), 50)
216
+ );
217
+
218
+ const raceResult = await Promise.race([result.promise, timeoutPromise]);
219
+ expect(raceResult).toBe("timeout");
220
+ });
221
+ });
222
+
223
+ describe("ready() with function", () => {
224
+ it("should return value when function returns non-null value", () => {
225
+ const result = select((context) => {
226
+ const ctx = context.use(withReady());
227
+ return ctx.ready(() => 42);
228
+ });
229
+
230
+ expect(result.value).toBe(42);
231
+ expect(result.error).toBeUndefined();
232
+ expect(result.promise).toBeUndefined();
233
+ });
234
+
235
+ it("should return zero from function", () => {
236
+ const result = select((context) => {
237
+ const ctx = context.use(withReady());
238
+ return ctx.ready(() => 0);
239
+ });
240
+
241
+ expect(result.value).toBe(0);
242
+ });
243
+
244
+ it("should return false from function", () => {
245
+ const result = select((context) => {
246
+ const ctx = context.use(withReady());
247
+ return ctx.ready(() => false);
248
+ });
249
+
250
+ expect(result.value).toBe(false);
251
+ });
252
+
253
+ it("should return empty string from function", () => {
254
+ const result = select((context) => {
255
+ const ctx = context.use(withReady());
256
+ return ctx.ready(() => "");
257
+ });
258
+
259
+ expect(result.value).toBe("");
260
+ });
261
+
262
+ it("should return object from function", () => {
263
+ const result = select((context) => {
264
+ const ctx = context.use(withReady());
265
+ return ctx.ready(() => ({ name: "test" }));
266
+ });
267
+
268
+ expect(result.value).toEqual({ name: "test" });
269
+ });
270
+
271
+ it("should throw never-resolve promise when function returns null", () => {
272
+ const result = select((context) => {
273
+ const ctx = context.use(withReady());
274
+ return ctx.ready(() => null);
275
+ });
276
+
277
+ expect(result.value).toBeUndefined();
278
+ expect(result.error).toBeUndefined();
279
+ expect(result.promise).toBeInstanceOf(Promise);
280
+ });
281
+
282
+ it("should throw never-resolve promise when function returns undefined", () => {
283
+ const result = select((context) => {
284
+ const ctx = context.use(withReady());
285
+ return ctx.ready(() => undefined);
286
+ });
287
+
288
+ expect(result.value).toBeUndefined();
289
+ expect(result.error).toBeUndefined();
290
+ expect(result.promise).toBeInstanceOf(Promise);
291
+ });
292
+
293
+ it("should throw error when function returns a pending promise", () => {
294
+ const pendingPromise = new Promise<string>(() => {
295
+ // Never resolves - stays pending
296
+ });
297
+
298
+ const result = select((context) => {
299
+ const ctx = context.use(withReady());
300
+ return ctx.ready(() => pendingPromise);
301
+ });
302
+
303
+ expect(result.value).toBeUndefined();
304
+ expect(result.error).toBeInstanceOf(Error);
305
+ expect((result.error as Error).message).toBe(
306
+ "ready(callback) overload does not support async callbacks. Use ready(atom, selector?) instead."
307
+ );
308
+ });
309
+
310
+ it("should throw error when function returns a resolved promise", () => {
311
+ const resolvedPromise = Promise.resolve("async result");
312
+
313
+ const result = select((context) => {
314
+ const ctx = context.use(withReady());
315
+ return ctx.ready(() => resolvedPromise);
316
+ });
317
+
318
+ expect(result.value).toBeUndefined();
319
+ expect(result.error).toBeInstanceOf(Error);
320
+ expect((result.error as Error).message).toBe(
321
+ "ready(callback) overload does not support async callbacks. Use ready(atom, selector?) instead."
322
+ );
323
+ });
324
+
325
+ it("should throw error when function returns a rejected promise", () => {
326
+ const testError = new Error("async error");
327
+ const rejectedPromise = Promise.reject(testError);
328
+
329
+ // Prevent unhandled rejection warning
330
+ rejectedPromise.catch(() => {});
331
+
332
+ const result = select((context) => {
333
+ const ctx = context.use(withReady());
334
+ return ctx.ready(() => rejectedPromise);
335
+ });
336
+
337
+ // Should throw the "async not supported" error, not the rejection error
338
+ expect(result.value).toBeUndefined();
339
+ expect(result.error).toBeInstanceOf(Error);
340
+ expect((result.error as Error).message).toBe(
341
+ "ready(callback) overload does not support async callbacks. Use ready(atom, selector?) instead."
342
+ );
343
+ });
344
+
345
+ it("should throw error when function returns a promise-like object", () => {
346
+ // Custom thenable (promise-like)
347
+ const thenable = {
348
+ then(resolve: (value: string) => void) {
349
+ resolve("thenable result");
350
+ },
351
+ };
352
+
353
+ const result = select((context) => {
354
+ const ctx = context.use(withReady());
355
+ return ctx.ready(() => thenable);
356
+ });
357
+
358
+ expect(result.value).toBeUndefined();
359
+ expect(result.error).toBeInstanceOf(Error);
360
+ expect((result.error as Error).message).toBe(
361
+ "ready(callback) overload does not support async callbacks. Use ready(atom, selector?) instead."
362
+ );
363
+ });
364
+
365
+ it("should propagate error when function throws synchronously", () => {
366
+ const testError = new Error("sync error");
367
+
368
+ const result = select((context) => {
369
+ const ctx = context.use(withReady());
370
+ return ctx.ready(() => {
371
+ throw testError;
372
+ });
373
+ });
374
+
375
+ expect(result.value).toBeUndefined();
376
+ expect(result.error).toBe(testError);
377
+ expect(result.promise).toBeUndefined();
378
+ });
379
+
380
+ it("should work with function that reads atoms", () => {
381
+ const count$ = atom(10);
382
+ const multiplier$ = atom(2);
383
+
384
+ const result = select((context) => {
385
+ const ctx = context.use(withReady());
386
+ return ctx.ready(() => {
387
+ const count = ctx.read(count$);
388
+ const mult = ctx.read(multiplier$);
389
+ return count * mult;
390
+ });
391
+ });
392
+
393
+ expect(result.value).toBe(20);
394
+ });
395
+ });
396
+
397
+ describe("ready() with async selector", () => {
398
+ it("should suspend when selector returns a pending promise", () => {
399
+ const data$ = atom({ id: 1 });
400
+ const pendingPromise = new Promise<string>(() => {
401
+ // Never resolves - stays pending
402
+ });
403
+
404
+ const result = select((context) => {
405
+ const ctx = context.use(withReady());
406
+ return ctx.ready(data$, () => pendingPromise);
407
+ });
408
+
409
+ // Should suspend with the tracked promise
410
+ expect(result.value).toBeUndefined();
411
+ expect(result.error).toBeUndefined();
412
+ expect(result.promise).toBeInstanceOf(Promise);
413
+ });
414
+
415
+ it("should return value when selector returns a resolved promise", async () => {
416
+ const data$ = atom({ id: 1 });
417
+ const resolvedPromise = Promise.resolve("async result");
418
+
419
+ // First call to track the promise and trigger state tracking
420
+ select((context) => {
421
+ const ctx = context.use(withReady());
422
+ return ctx.ready(data$, () => resolvedPromise);
423
+ });
424
+
425
+ // Wait for the promise .then() handlers to run and update cache
426
+ await resolvedPromise;
427
+ await new Promise<void>((r) => queueMicrotask(() => r()));
428
+
429
+ // Second call - promise should now be fulfilled in cache
430
+ const result = select((context) => {
431
+ const ctx = context.use(withReady());
432
+ return ctx.ready(data$, () => resolvedPromise);
433
+ });
434
+
435
+ expect(result.value).toBe("async result");
436
+ expect(result.error).toBeUndefined();
437
+ expect(result.promise).toBeUndefined();
438
+ });
439
+
440
+ it("should throw error when selector returns a rejected promise", async () => {
441
+ const data$ = atom({ id: 1 });
442
+ const testError = new Error("async error");
443
+ const rejectedPromise = Promise.reject(testError);
444
+
445
+ // Prevent unhandled rejection warning
446
+ rejectedPromise.catch(() => {});
447
+
448
+ // First call to track the promise
449
+ select((context) => {
450
+ const ctx = context.use(withReady());
451
+ return ctx.ready(data$, () => rejectedPromise);
452
+ });
453
+
454
+ // Wait for the promise rejection handlers to run
455
+ await new Promise<void>((r) => queueMicrotask(() => r()));
456
+ await new Promise<void>((r) => queueMicrotask(() => r()));
457
+
458
+ // Second call - promise should now be rejected in cache
459
+ const result = select((context) => {
460
+ const ctx = context.use(withReady());
461
+ return ctx.ready(data$, () => rejectedPromise);
462
+ });
463
+
464
+ expect(result.value).toBeUndefined();
465
+ expect(result.error).toBe(testError);
466
+ expect(result.promise).toBeUndefined();
467
+ });
468
+
469
+ it("should return null when async selector resolves to null (bypasses null check)", async () => {
470
+ const data$ = atom({ id: 1 });
471
+ const resolvedToNull = Promise.resolve(null);
472
+
473
+ // First call to track the promise
474
+ select((context) => {
475
+ const ctx = context.use(withReady());
476
+ return ctx.ready(data$, () => resolvedToNull);
477
+ });
478
+
479
+ // Wait for the promise to be tracked as fulfilled
480
+ await resolvedToNull;
481
+ await new Promise<void>((r) => queueMicrotask(() => r()));
482
+
483
+ // Second call - promise should now be fulfilled in cache
484
+ const result = select((context) => {
485
+ const ctx = context.use(withReady());
486
+ return ctx.ready(data$, () => resolvedToNull);
487
+ });
488
+
489
+ // Async selectors bypass null/undefined checking - value is returned as-is
490
+ expect(result.value).toBe(null);
491
+ expect(result.error).toBeUndefined();
492
+ expect(result.promise).toBeUndefined();
493
+ });
494
+
495
+ it("should return undefined when async selector resolves to undefined (bypasses undefined check)", async () => {
496
+ const data$ = atom({ id: 1 });
497
+ const resolvedToUndefined = Promise.resolve(undefined);
498
+
499
+ // First call to track the promise
500
+ select((context) => {
501
+ const ctx = context.use(withReady());
502
+ return ctx.ready(data$, () => resolvedToUndefined);
503
+ });
504
+
505
+ // Wait for the promise to be tracked as fulfilled
506
+ await resolvedToUndefined;
507
+ await new Promise<void>((r) => queueMicrotask(() => r()));
508
+
509
+ // Second call - promise should now be fulfilled in cache
510
+ const result = select((context) => {
511
+ const ctx = context.use(withReady());
512
+ return ctx.ready(data$, () => resolvedToUndefined);
513
+ });
514
+
515
+ // Async selectors bypass null/undefined checking - value is returned as-is
516
+ expect(result.value).toBe(undefined);
517
+ expect(result.error).toBeUndefined();
518
+ expect(result.promise).toBeUndefined();
519
+ });
520
+
521
+ it("should track atom as dependency when using async selector", () => {
522
+ const data$ = atom({ id: 1 });
523
+ const pendingPromise = new Promise<string>(() => {});
524
+
525
+ const result = select((context) => {
526
+ const ctx = context.use(withReady());
527
+ return ctx.ready(data$, () => pendingPromise);
528
+ });
529
+
530
+ // Dependency is tracked even when suspending
531
+ expect(result.dependencies.has(data$)).toBe(true);
532
+ });
533
+ });
534
+ });