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,553 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { useStable } from "./useStable";
3
+ import { isStableFn } from "../core/equality";
4
+ import { wrappers } from "./strictModeTest";
5
+
6
+ describe.each(wrappers)("useStable - $mode", ({ renderHook }) => {
7
+ describe("basic stabilization", () => {
8
+ it("should return stable object reference across renders", () => {
9
+ const { result, rerender } = renderHook(
10
+ (props) =>
11
+ useStable({
12
+ name: props.name,
13
+ count: props.count,
14
+ }),
15
+ { initialProps: { name: "John", count: 1 } }
16
+ );
17
+
18
+ const firstResult = result.current;
19
+
20
+ rerender({ name: "John", count: 1 });
21
+
22
+ expect(result.current).toBe(firstResult);
23
+ });
24
+
25
+ it("should update property when value changes", () => {
26
+ const { result, rerender } = renderHook(
27
+ (props) =>
28
+ useStable({
29
+ name: props.name,
30
+ }),
31
+ { initialProps: { name: "John" } }
32
+ );
33
+
34
+ expect(result.current.name).toBe("John");
35
+
36
+ rerender({ name: "Jane" });
37
+
38
+ expect(result.current.name).toBe("Jane");
39
+ });
40
+ });
41
+
42
+ describe("function stabilization", () => {
43
+ it("should wrap functions in stable wrappers", () => {
44
+ const callback = vi.fn(() => 42);
45
+
46
+ const { result } = renderHook(() =>
47
+ useStable({
48
+ callback,
49
+ })
50
+ );
51
+
52
+ expect(isStableFn(result.current.callback)).toBe(true);
53
+ expect(result.current.callback()).toBe(42);
54
+ expect(callback).toHaveBeenCalled();
55
+ });
56
+
57
+ it("should maintain stable function reference across renders", () => {
58
+ const { result, rerender } = renderHook(
59
+ (props: { callback: () => number }) =>
60
+ useStable({
61
+ callback: props.callback,
62
+ }),
63
+ { initialProps: { callback: () => 1 } }
64
+ );
65
+
66
+ const firstCallback = result.current.callback;
67
+
68
+ rerender({ callback: () => 2 });
69
+
70
+ expect(result.current.callback).toBe(firstCallback);
71
+ });
72
+
73
+ it("should call latest function implementation", () => {
74
+ const { result, rerender } = renderHook(
75
+ (props: { callback: () => number }) =>
76
+ useStable({
77
+ callback: props.callback,
78
+ }),
79
+ { initialProps: { callback: () => 1 } }
80
+ );
81
+
82
+ expect(result.current.callback()).toBe(1);
83
+
84
+ rerender({ callback: () => 2 });
85
+
86
+ expect(result.current.callback()).toBe(2);
87
+ });
88
+
89
+ it("should handle multiple functions", () => {
90
+ const { result, rerender } = renderHook(
91
+ (props: { onClick: () => string; onSubmit: () => string }) =>
92
+ useStable({
93
+ onClick: props.onClick,
94
+ onSubmit: props.onSubmit,
95
+ }),
96
+ {
97
+ initialProps: {
98
+ onClick: () => "click1",
99
+ onSubmit: () => "submit1",
100
+ },
101
+ }
102
+ );
103
+
104
+ const firstOnClick = result.current.onClick;
105
+ const firstOnSubmit = result.current.onSubmit;
106
+
107
+ rerender({
108
+ onClick: () => "click2",
109
+ onSubmit: () => "submit2",
110
+ });
111
+
112
+ expect(result.current.onClick).toBe(firstOnClick);
113
+ expect(result.current.onSubmit).toBe(firstOnSubmit);
114
+ expect(result.current.onClick()).toBe("click2");
115
+ expect(result.current.onSubmit()).toBe("submit2");
116
+ });
117
+ });
118
+
119
+ describe("array stabilization (default: shallow)", () => {
120
+ it("should stabilize array with same items (shallow equal)", () => {
121
+ const { result, rerender } = renderHook(
122
+ (props) =>
123
+ useStable({
124
+ items: props.items,
125
+ }),
126
+ { initialProps: { items: [1, 2, 3] } }
127
+ );
128
+
129
+ const firstItems = result.current.items;
130
+
131
+ rerender({ items: [1, 2, 3] });
132
+
133
+ expect(result.current.items).toBe(firstItems);
134
+ });
135
+
136
+ it("should update array when items change", () => {
137
+ const { result, rerender } = renderHook(
138
+ (props) =>
139
+ useStable({
140
+ items: props.items,
141
+ }),
142
+ { initialProps: { items: [1, 2, 3] } }
143
+ );
144
+
145
+ const firstItems = result.current.items;
146
+
147
+ rerender({ items: [1, 2, 4] });
148
+
149
+ expect(result.current.items).not.toBe(firstItems);
150
+ expect(result.current.items).toEqual([1, 2, 4]);
151
+ });
152
+
153
+ it("should update array when length changes", () => {
154
+ const { result, rerender } = renderHook(
155
+ (props) =>
156
+ useStable({
157
+ items: props.items,
158
+ }),
159
+ { initialProps: { items: [1, 2, 3] } }
160
+ );
161
+
162
+ const firstItems = result.current.items;
163
+
164
+ rerender({ items: [1, 2] });
165
+
166
+ expect(result.current.items).not.toBe(firstItems);
167
+ });
168
+
169
+ it("should work as useEffect dependency", () => {
170
+ const effectFn = vi.fn();
171
+
172
+ const { result, rerender } = renderHook(
173
+ (props) => {
174
+ const stable = useStable({
175
+ items: props.items,
176
+ });
177
+
178
+ // Simulate useEffect behavior - track if dependency changed
179
+ effectFn(stable.items);
180
+
181
+ return stable;
182
+ },
183
+ { initialProps: { items: [1, 2, 3] } }
184
+ );
185
+
186
+ const firstItems = result.current.items;
187
+
188
+ // Same content - should be stable
189
+ rerender({ items: [1, 2, 3] });
190
+
191
+ // Effect should receive same reference
192
+ expect(effectFn).toHaveBeenLastCalledWith(firstItems);
193
+ });
194
+ });
195
+
196
+ describe("object stabilization (default: shallow)", () => {
197
+ it("should stabilize object with same shallow values", () => {
198
+ const { result, rerender } = renderHook(
199
+ (props) =>
200
+ useStable({
201
+ person: props.person,
202
+ }),
203
+ { initialProps: { person: { name: "John", age: 30 } } }
204
+ );
205
+
206
+ const firstPerson = result.current.person;
207
+
208
+ rerender({ person: { name: "John", age: 30 } });
209
+
210
+ expect(result.current.person).toBe(firstPerson);
211
+ });
212
+
213
+ it("should update object when shallow values change", () => {
214
+ const { result, rerender } = renderHook(
215
+ (props) =>
216
+ useStable({
217
+ person: props.person,
218
+ }),
219
+ { initialProps: { person: { name: "John", age: 30 } } }
220
+ );
221
+
222
+ const firstPerson = result.current.person;
223
+
224
+ rerender({ person: { name: "Jane", age: 30 } });
225
+
226
+ expect(result.current.person).not.toBe(firstPerson);
227
+ });
228
+
229
+ it("should NOT stabilize nested objects by default (shallow comparison)", () => {
230
+ const { result, rerender } = renderHook(
231
+ (props) =>
232
+ useStable({
233
+ data: props.data,
234
+ }),
235
+ {
236
+ initialProps: {
237
+ data: { nested: { value: 1 } },
238
+ },
239
+ }
240
+ );
241
+
242
+ const firstData = result.current.data;
243
+
244
+ // Different nested object reference, even with same content
245
+ rerender({ data: { nested: { value: 1 } } });
246
+
247
+ // Should NOT be stable because nested object has different reference
248
+ expect(result.current.data).not.toBe(firstData);
249
+ });
250
+ });
251
+
252
+ describe("Date stabilization (default: deep/timestamp)", () => {
253
+ it("should stabilize Date with same timestamp", () => {
254
+ const { result, rerender } = renderHook(
255
+ (props) =>
256
+ useStable({
257
+ date: props.date,
258
+ }),
259
+ { initialProps: { date: new Date("2024-01-01") } }
260
+ );
261
+
262
+ const firstDate = result.current.date;
263
+
264
+ rerender({ date: new Date("2024-01-01") });
265
+
266
+ expect(result.current.date).toBe(firstDate);
267
+ });
268
+
269
+ it("should update Date when timestamp changes", () => {
270
+ const { result, rerender } = renderHook(
271
+ (props) =>
272
+ useStable({
273
+ date: props.date,
274
+ }),
275
+ { initialProps: { date: new Date("2024-01-01") } }
276
+ );
277
+
278
+ const firstDate = result.current.date;
279
+
280
+ rerender({ date: new Date("2024-01-02") });
281
+
282
+ expect(result.current.date).not.toBe(firstDate);
283
+ });
284
+ });
285
+
286
+ describe("primitive stabilization (default: strict)", () => {
287
+ it("should stabilize primitives with strict equality", () => {
288
+ const { result, rerender } = renderHook(
289
+ (props) =>
290
+ useStable({
291
+ count: props.count,
292
+ name: props.name,
293
+ active: props.active,
294
+ }),
295
+ { initialProps: { count: 42, name: "test", active: true } }
296
+ );
297
+
298
+ const firstResult = result.current;
299
+
300
+ rerender({ count: 42, name: "test", active: true });
301
+
302
+ expect(result.current).toBe(firstResult);
303
+ expect(result.current.count).toBe(42);
304
+ expect(result.current.name).toBe("test");
305
+ expect(result.current.active).toBe(true);
306
+ });
307
+ });
308
+
309
+ describe("custom equals option", () => {
310
+ it("should use custom equals for specified properties", () => {
311
+ const { result, rerender } = renderHook(
312
+ (props) =>
313
+ useStable(
314
+ {
315
+ data: props.data,
316
+ },
317
+ { data: "deep" }
318
+ ),
319
+ {
320
+ initialProps: {
321
+ data: { nested: { value: 1 } },
322
+ },
323
+ }
324
+ );
325
+
326
+ const firstData = result.current.data;
327
+
328
+ // With deep equals, nested objects with same content should be stable
329
+ rerender({ data: { nested: { value: 1 } } });
330
+
331
+ expect(result.current.data).toBe(firstData);
332
+ });
333
+
334
+ it("should override default equals", () => {
335
+ const { result, rerender } = renderHook(
336
+ (props) =>
337
+ useStable(
338
+ {
339
+ items: props.items,
340
+ },
341
+ { items: "strict" }
342
+ ),
343
+ { initialProps: { items: [1, 2, 3] } }
344
+ );
345
+
346
+ const firstItems = result.current.items;
347
+
348
+ // With strict equals, same content but different reference should NOT be stable
349
+ rerender({ items: [1, 2, 3] });
350
+
351
+ expect(result.current.items).not.toBe(firstItems);
352
+ });
353
+
354
+ it("should support custom equals function", () => {
355
+ const { result, rerender } = renderHook(
356
+ (props) =>
357
+ useStable(
358
+ {
359
+ user: props.user,
360
+ },
361
+ { user: (a, b) => a?.id === b?.id }
362
+ ),
363
+ { initialProps: { user: { id: 1, name: "John" } } }
364
+ );
365
+
366
+ const firstUser = result.current.user;
367
+
368
+ // Same id, different name - should be stable with custom equals
369
+ rerender({ user: { id: 1, name: "Jane" } });
370
+
371
+ expect(result.current.user).toBe(firstUser);
372
+
373
+ // Different id - should NOT be stable
374
+ rerender({ user: { id: 2, name: "Jane" } });
375
+
376
+ expect(result.current.user).not.toBe(firstUser);
377
+ });
378
+
379
+ it("should ignore equals option for functions", () => {
380
+ const { result, rerender } = renderHook(
381
+ (props: { callback: () => number }) =>
382
+ useStable(
383
+ {
384
+ callback: props.callback,
385
+ },
386
+ // Functions are excluded from equals type, so this is ignored at runtime
387
+ { callback: "deep" } as any
388
+ ),
389
+ { initialProps: { callback: () => 1 } }
390
+ );
391
+
392
+ const firstCallback = result.current.callback;
393
+
394
+ rerender({ callback: () => 2 });
395
+
396
+ // Function should still be stabilized regardless of equals option
397
+ expect(result.current.callback).toBe(firstCallback);
398
+ expect(isStableFn(result.current.callback)).toBe(true);
399
+ });
400
+ });
401
+
402
+ describe("mixed properties", () => {
403
+ it("should handle mixed property types correctly", () => {
404
+ type Props = {
405
+ person: { name: string; address: { city: string } };
406
+ date: Date;
407
+ items: number[];
408
+ callback: () => string;
409
+ count: number;
410
+ };
411
+
412
+ const { result, rerender } = renderHook(
413
+ (props: Props) =>
414
+ useStable(
415
+ {
416
+ person: props.person,
417
+ date: props.date,
418
+ items: props.items,
419
+ callback: props.callback,
420
+ count: props.count,
421
+ },
422
+ { person: "deep" }
423
+ ),
424
+ {
425
+ initialProps: {
426
+ person: { name: "John", address: { city: "NYC" } },
427
+ date: new Date("2024-01-01"),
428
+ items: [1, 2, 3],
429
+ callback: () => "hello",
430
+ count: 42,
431
+ },
432
+ }
433
+ );
434
+
435
+ const first = {
436
+ person: result.current.person,
437
+ date: result.current.date,
438
+ items: result.current.items,
439
+ callback: result.current.callback,
440
+ count: result.current.count,
441
+ };
442
+
443
+ // Rerender with same logical values but new references
444
+ rerender({
445
+ person: { name: "John", address: { city: "NYC" } },
446
+ date: new Date("2024-01-01"),
447
+ items: [1, 2, 3],
448
+ callback: () => "world",
449
+ count: 42,
450
+ });
451
+
452
+ // person: deep equals - should be stable
453
+ expect(result.current.person).toBe(first.person);
454
+
455
+ // date: timestamp comparison - should be stable
456
+ expect(result.current.date).toBe(first.date);
457
+
458
+ // items: shallow equals - should be stable
459
+ expect(result.current.items).toBe(first.items);
460
+
461
+ // callback: always stabilized - should be stable reference
462
+ expect(result.current.callback).toBe(first.callback);
463
+
464
+ // count: strict equals - should be stable
465
+ expect(result.current.count).toBe(first.count);
466
+
467
+ // But callback should call new implementation
468
+ expect(result.current.callback()).toBe("world");
469
+ });
470
+ });
471
+
472
+ describe("edge cases", () => {
473
+ it("should handle null values", () => {
474
+ const { result, rerender } = renderHook(
475
+ (props) =>
476
+ useStable({
477
+ value: props.value,
478
+ }),
479
+ { initialProps: { value: null as string | null } }
480
+ );
481
+
482
+ expect(result.current.value).toBe(null);
483
+
484
+ rerender({ value: "hello" });
485
+
486
+ expect(result.current.value).toBe("hello");
487
+
488
+ rerender({ value: null });
489
+
490
+ expect(result.current.value).toBe(null);
491
+ });
492
+
493
+ it("should handle undefined values", () => {
494
+ const { result, rerender } = renderHook(
495
+ (props) =>
496
+ useStable({
497
+ value: props.value,
498
+ }),
499
+ { initialProps: { value: undefined as string | undefined } }
500
+ );
501
+
502
+ expect(result.current.value).toBe(undefined);
503
+
504
+ rerender({ value: "hello" });
505
+
506
+ expect(result.current.value).toBe("hello");
507
+ });
508
+
509
+ it("should handle empty object", () => {
510
+ const { result } = renderHook(() => useStable({}));
511
+
512
+ expect(result.current).toEqual({});
513
+ });
514
+
515
+ it("should handle adding new properties on rerender", () => {
516
+ const { result, rerender } = renderHook((props) => useStable(props), {
517
+ initialProps: { a: 1 } as { a: number; b?: number },
518
+ });
519
+
520
+ expect(result.current.a).toBe(1);
521
+ expect(result.current.b).toBeUndefined();
522
+
523
+ rerender({ a: 1, b: 2 });
524
+
525
+ expect(result.current.a).toBe(1);
526
+ expect(result.current.b).toBe(2);
527
+ });
528
+ });
529
+
530
+ describe("type safety", () => {
531
+ it("should preserve property types", () => {
532
+ const { result } = renderHook(() =>
533
+ useStable({
534
+ name: "John",
535
+ age: 30,
536
+ items: [1, 2, 3],
537
+ callback: (x: number) => x * 2,
538
+ })
539
+ );
540
+
541
+ // TypeScript should infer these types correctly
542
+ const name: string = result.current.name;
543
+ const age: number = result.current.age;
544
+ const items: number[] = result.current.items;
545
+ const doubled: number = result.current.callback(5);
546
+
547
+ expect(name).toBe("John");
548
+ expect(age).toBe(30);
549
+ expect(items).toEqual([1, 2, 3]);
550
+ expect(doubled).toBe(10);
551
+ });
552
+ });
553
+ });