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,392 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ strictEqual,
4
+ shallowEqual,
5
+ shallow2Equal,
6
+ shallow3Equal,
7
+ deepEqual,
8
+ resolveEquality,
9
+ equality,
10
+ createStableFn,
11
+ isStableFn,
12
+ tryStabilize,
13
+ } from "./equality";
14
+
15
+ describe("equality", () => {
16
+ describe("strictEqual", () => {
17
+ it("should return true for same reference", () => {
18
+ const obj = { a: 1 };
19
+ expect(strictEqual(obj, obj)).toBe(true);
20
+ });
21
+
22
+ it("should return false for different references with same content", () => {
23
+ expect(strictEqual({ a: 1 }, { a: 1 })).toBe(false);
24
+ });
25
+
26
+ it("should return true for same primitives", () => {
27
+ expect(strictEqual(1, 1)).toBe(true);
28
+ expect(strictEqual("hello", "hello")).toBe(true);
29
+ expect(strictEqual(true, true)).toBe(true);
30
+ });
31
+
32
+ it("should handle NaN correctly (Object.is behavior)", () => {
33
+ expect(strictEqual(NaN, NaN)).toBe(true);
34
+ });
35
+
36
+ it("should distinguish +0 and -0", () => {
37
+ expect(strictEqual(0, -0)).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe("shallowEqual", () => {
42
+ it("should return true for same reference", () => {
43
+ const obj = { a: 1 };
44
+ expect(shallowEqual(obj, obj)).toBe(true);
45
+ });
46
+
47
+ it("should return true for objects with same keys and values", () => {
48
+ expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
49
+ });
50
+
51
+ it("should return false for objects with different keys", () => {
52
+ expect(shallowEqual({ a: 1 }, { b: 1 })).toBe(false);
53
+ });
54
+
55
+ it("should return false for objects with different values", () => {
56
+ expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
57
+ });
58
+
59
+ it("should return false for objects with different number of keys", () => {
60
+ expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
61
+ });
62
+
63
+ it("should return true for arrays with same elements", () => {
64
+ expect(shallowEqual([1, 2, 3], [1, 2, 3])).toBe(true);
65
+ });
66
+
67
+ it("should return false for arrays with different elements", () => {
68
+ expect(shallowEqual([1, 2, 3], [1, 2, 4])).toBe(false);
69
+ });
70
+
71
+ it("should return false for arrays with different lengths", () => {
72
+ expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
73
+ });
74
+
75
+ it("should return false for nested objects with different references", () => {
76
+ expect(shallowEqual({ nested: { a: 1 } }, { nested: { a: 1 } })).toBe(
77
+ false
78
+ );
79
+ });
80
+
81
+ it("should return true for nested objects with same reference", () => {
82
+ const nested = { a: 1 };
83
+ expect(shallowEqual({ nested }, { nested })).toBe(true);
84
+ });
85
+
86
+ it("should return false when comparing object to null", () => {
87
+ expect(shallowEqual({ a: 1 }, null as any)).toBe(false);
88
+ });
89
+
90
+ it("should return false when comparing object to primitive", () => {
91
+ expect(shallowEqual({ a: 1 }, 1 as any)).toBe(false);
92
+ });
93
+
94
+ it("should support custom item comparator", () => {
95
+ const customEqual = (a: unknown, b: unknown) =>
96
+ JSON.stringify(a) === JSON.stringify(b);
97
+ expect(
98
+ shallowEqual({ nested: { a: 1 } }, { nested: { a: 1 } }, customEqual)
99
+ ).toBe(true);
100
+ });
101
+ });
102
+
103
+ describe("shallow2Equal", () => {
104
+ it("should compare 2 levels deep", () => {
105
+ expect(
106
+ shallow2Equal({ nested: { a: 1 } }, { nested: { a: 1 } })
107
+ ).toBe(true);
108
+ });
109
+
110
+ it("should return false for 3 levels deep difference", () => {
111
+ expect(
112
+ shallow2Equal(
113
+ { nested: { deep: { a: 1 } } },
114
+ { nested: { deep: { a: 1 } } }
115
+ )
116
+ ).toBe(false);
117
+ });
118
+
119
+ it("should work with arrays of objects", () => {
120
+ expect(shallow2Equal([{ id: 1 }], [{ id: 1 }])).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe("shallow3Equal", () => {
125
+ it("should compare 3 levels deep", () => {
126
+ expect(
127
+ shallow3Equal(
128
+ { nested: { deep: { a: 1 } } },
129
+ { nested: { deep: { a: 1 } } }
130
+ )
131
+ ).toBe(true);
132
+ });
133
+
134
+ it("should return false for 4 levels deep difference", () => {
135
+ expect(
136
+ shallow3Equal(
137
+ { l1: { l2: { l3: { a: 1 } } } },
138
+ { l1: { l2: { l3: { a: 1 } } } }
139
+ )
140
+ ).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("deepEqual", () => {
145
+ it("should compare deeply nested objects", () => {
146
+ expect(
147
+ deepEqual(
148
+ { a: { b: { c: { d: 1 } } } },
149
+ { a: { b: { c: { d: 1 } } } }
150
+ )
151
+ ).toBe(true);
152
+ });
153
+
154
+ it("should return false for deeply nested differences", () => {
155
+ expect(
156
+ deepEqual(
157
+ { a: { b: { c: { d: 1 } } } },
158
+ { a: { b: { c: { d: 2 } } } }
159
+ )
160
+ ).toBe(false);
161
+ });
162
+
163
+ it("should handle arrays", () => {
164
+ expect(deepEqual([1, [2, [3]]], [1, [2, [3]]])).toBe(true);
165
+ });
166
+
167
+ it("should handle Date objects", () => {
168
+ const date = new Date("2024-01-01");
169
+ expect(deepEqual(date, new Date("2024-01-01"))).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe("resolveEquality", () => {
174
+ it("should return strictEqual for undefined", () => {
175
+ expect(resolveEquality(undefined)).toBe(strictEqual);
176
+ });
177
+
178
+ it("should return strictEqual for 'strict'", () => {
179
+ expect(resolveEquality("strict")).toBe(strictEqual);
180
+ });
181
+
182
+ it("should return shallowEqual for 'shallow'", () => {
183
+ expect(resolveEquality("shallow")).toBe(shallowEqual);
184
+ });
185
+
186
+ it("should return shallow2Equal for 'shallow2'", () => {
187
+ expect(resolveEquality("shallow2")).toBe(shallow2Equal);
188
+ });
189
+
190
+ it("should return shallow3Equal for 'shallow3'", () => {
191
+ expect(resolveEquality("shallow3")).toBe(shallow3Equal);
192
+ });
193
+
194
+ it("should return deepEqual for 'deep'", () => {
195
+ expect(resolveEquality("deep")).toBe(deepEqual);
196
+ });
197
+
198
+ it("should return custom function as-is", () => {
199
+ const customFn = (a: number, b: number) => a === b;
200
+ expect(resolveEquality(customFn)).toBe(customFn);
201
+ });
202
+ });
203
+
204
+ describe("equality helper", () => {
205
+ it("should be an alias for resolveEquality with shorthand", () => {
206
+ expect(equality("strict")).toBe(strictEqual);
207
+ expect(equality("shallow")).toBe(shallowEqual);
208
+ expect(equality("deep")).toBe(deepEqual);
209
+ });
210
+ });
211
+ });
212
+
213
+ describe("StableFn", () => {
214
+ describe("createStableFn", () => {
215
+ it("should create a callable wrapper", () => {
216
+ const fn = (x: number) => x * 2;
217
+ const stable = createStableFn(fn);
218
+
219
+ expect(stable(5)).toBe(10);
220
+ });
221
+
222
+ it("should preserve original function via getOriginal", () => {
223
+ const fn = (x: number) => x * 2;
224
+ const stable = createStableFn(fn);
225
+
226
+ expect(stable.getOriginal()).toBe(fn);
227
+ });
228
+
229
+ it("should return current function via getCurrent", () => {
230
+ const fn = (x: number) => x * 2;
231
+ const stable = createStableFn(fn);
232
+
233
+ expect(stable.getCurrent()).toBe(fn);
234
+ });
235
+
236
+ it("should allow updating current function via setCurrent", () => {
237
+ const fn1 = (x: number) => x * 2;
238
+ const fn2 = (x: number) => x * 3;
239
+ const stable = createStableFn(fn1);
240
+
241
+ stable.setCurrent(fn2);
242
+
243
+ expect(stable(5)).toBe(15);
244
+ expect(stable.getCurrent()).toBe(fn2);
245
+ expect(stable.getOriginal()).toBe(fn1);
246
+ });
247
+ });
248
+
249
+ describe("isStableFn", () => {
250
+ it("should return true for StableFn", () => {
251
+ const stable = createStableFn(() => 42);
252
+ expect(isStableFn(stable)).toBe(true);
253
+ });
254
+
255
+ it("should return false for regular function", () => {
256
+ expect(isStableFn(() => 42)).toBe(false);
257
+ });
258
+
259
+ it("should return false for non-function", () => {
260
+ expect(isStableFn({ a: 1 })).toBe(false);
261
+ expect(isStableFn(42)).toBe(false);
262
+ expect(isStableFn("string")).toBe(false);
263
+ });
264
+
265
+ it("should return false for function with partial StableFn interface", () => {
266
+ const partial = Object.assign(() => 42, { getOriginal: () => {} });
267
+ expect(isStableFn(partial)).toBe(false);
268
+ });
269
+ });
270
+ });
271
+
272
+ describe("tryStabilize", () => {
273
+ describe("first call (no previous value)", () => {
274
+ it("should return value as-is for non-function", () => {
275
+ const [result, wasStable] = tryStabilize(undefined, 42, strictEqual);
276
+ expect(result).toBe(42);
277
+ expect(wasStable).toBe(false);
278
+ });
279
+
280
+ it("should wrap function in StableFn", () => {
281
+ const fn = () => 42;
282
+ const [result, wasStable] = tryStabilize(undefined, fn, strictEqual);
283
+
284
+ expect(isStableFn(result)).toBe(true);
285
+ expect(result()).toBe(42);
286
+ expect(wasStable).toBe(false);
287
+ });
288
+ });
289
+
290
+ describe("subsequent calls with functions", () => {
291
+ it("should update existing StableFn and return stable", () => {
292
+ const fn1 = () => 1;
293
+ const fn2 = () => 2;
294
+
295
+ const [stable1] = tryStabilize(undefined, fn1, strictEqual);
296
+ const [stable2, wasStable] = tryStabilize(
297
+ { value: stable1 },
298
+ fn2,
299
+ strictEqual
300
+ );
301
+
302
+ expect(stable2).toBe(stable1); // Same reference
303
+ expect(stable2()).toBe(2); // But calls new function
304
+ expect(wasStable).toBe(true);
305
+ });
306
+
307
+ it("should create new StableFn if previous was not StableFn", () => {
308
+ const fn = () => 42;
309
+ const [result, wasStable] = tryStabilize(
310
+ { value: "not a stable fn" as any },
311
+ fn,
312
+ strictEqual
313
+ );
314
+
315
+ expect(isStableFn(result)).toBe(true);
316
+ expect(wasStable).toBe(false);
317
+ });
318
+ });
319
+
320
+ describe("Date handling", () => {
321
+ it("should return previous Date if timestamps match", () => {
322
+ const date1 = new Date("2024-01-01");
323
+ const date2 = new Date("2024-01-01");
324
+
325
+ const [result, wasStable] = tryStabilize(
326
+ { value: date1 },
327
+ date2,
328
+ strictEqual
329
+ );
330
+
331
+ expect(result).toBe(date1);
332
+ expect(wasStable).toBe(true);
333
+ });
334
+
335
+ it("should return new Date if timestamps differ", () => {
336
+ const date1 = new Date("2024-01-01");
337
+ const date2 = new Date("2024-01-02");
338
+
339
+ const [result, wasStable] = tryStabilize(
340
+ { value: date1 },
341
+ date2,
342
+ strictEqual
343
+ );
344
+
345
+ expect(result).toBe(date2);
346
+ expect(wasStable).toBe(false);
347
+ });
348
+
349
+ it("should return new Date if previous was not a Date", () => {
350
+ const date = new Date("2024-01-01");
351
+
352
+ const [result, wasStable] = tryStabilize(
353
+ { value: "not a date" as any },
354
+ date,
355
+ strictEqual
356
+ );
357
+
358
+ expect(result).toBe(date);
359
+ expect(wasStable).toBe(false);
360
+ });
361
+ });
362
+
363
+ describe("equality-based stabilization", () => {
364
+ it("should return previous value if equal", () => {
365
+ const obj1 = { a: 1 };
366
+ const obj2 = { a: 1 };
367
+
368
+ const [result, wasStable] = tryStabilize(
369
+ { value: obj1 },
370
+ obj2,
371
+ shallowEqual
372
+ );
373
+
374
+ expect(result).toBe(obj1);
375
+ expect(wasStable).toBe(true);
376
+ });
377
+
378
+ it("should return new value if not equal", () => {
379
+ const obj1 = { a: 1 };
380
+ const obj2 = { a: 2 };
381
+
382
+ const [result, wasStable] = tryStabilize(
383
+ { value: obj1 },
384
+ obj2,
385
+ shallowEqual
386
+ );
387
+
388
+ expect(result).toBe(obj2);
389
+ expect(wasStable).toBe(false);
390
+ });
391
+ });
392
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Equality utilities for comparing values.
3
+ */
4
+
5
+ import type { AnyFunc, Equality, EqualityShorthand } from "./types";
6
+ import isEqual from "lodash/isEqual";
7
+
8
+ /**
9
+ * Strict equality (Object.is).
10
+ */
11
+ export function strictEqual<T>(a: T, b: T): boolean {
12
+ return Object.is(a, b);
13
+ }
14
+
15
+ /**
16
+ * Shallow equality for objects/arrays.
17
+ * Compares by reference for each top-level key/index.
18
+ *
19
+ * @param itemEqual - Optional comparator for each item/value (defaults to Object.is)
20
+ */
21
+ export function shallowEqual<T>(
22
+ a: T,
23
+ b: T,
24
+ itemEqual: (a: unknown, b: unknown) => boolean = Object.is
25
+ ): boolean {
26
+ if (Object.is(a, b)) return true;
27
+ if (typeof a !== "object" || a === null) return false;
28
+ if (typeof b !== "object" || b === null) return false;
29
+
30
+ const keysA = Object.keys(a);
31
+ const keysB = Object.keys(b);
32
+
33
+ if (keysA.length !== keysB.length) return false;
34
+
35
+ for (const key of keysA) {
36
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
37
+ if (!itemEqual((a as any)[key], (b as any)[key])) return false;
38
+ }
39
+
40
+ return true;
41
+ }
42
+
43
+ /**
44
+ * 2-level shallow equality.
45
+ * Compares keys/length, then shallow compares each item/value.
46
+ *
47
+ * @example
48
+ * [{ id: 1, data: obj }] vs [{ id: 1, data: obj }] // true (same obj ref)
49
+ */
50
+ export function shallow2Equal<T>(a: T, b: T): boolean {
51
+ return shallowEqual(a, b, shallowEqual);
52
+ }
53
+
54
+ /**
55
+ * 3-level shallow equality.
56
+ * Compares keys/length, then shallow2 compares each item/value.
57
+ *
58
+ * @example
59
+ * [{ id: 1, nested: { data: obj } }] vs [{ id: 1, nested: { data: obj } }] // true
60
+ */
61
+ export function shallow3Equal<T>(a: T, b: T): boolean {
62
+ return shallowEqual(a, b, shallow2Equal);
63
+ }
64
+
65
+ /**
66
+ * Deep equality.
67
+ */
68
+ export const deepEqual = isEqual;
69
+
70
+ /**
71
+ * Resolve equality strategy to a function.
72
+ */
73
+ export function resolveEquality<T>(
74
+ e: Equality<T> | undefined
75
+ ): (a: T, b: T) => boolean {
76
+ if (!e || e === "strict") return strictEqual;
77
+ if (e === "shallow") return shallowEqual;
78
+ if (e === "shallow2") return shallow2Equal;
79
+ if (e === "shallow3") return shallow3Equal;
80
+ if (e === "deep") return deepEqual;
81
+ return e;
82
+ }
83
+
84
+ export function equality(shorthand: EqualityShorthand) {
85
+ return resolveEquality(shorthand);
86
+ }
87
+
88
+ // =============================================================================
89
+ // Value Stabilization
90
+ // =============================================================================
91
+
92
+ export type StableFn<TArgs extends any[], TResult> = ((...args: TArgs) => TResult) & {
93
+ getOriginal: () => (...args: TArgs) => TResult;
94
+ getCurrent: () => (...args: TArgs) => TResult;
95
+ setCurrent: (newFn: (...args: TArgs) => TResult) => void;
96
+ };
97
+
98
+ export function createStableFn<TArgs extends any[], TResult>(
99
+ fn: (...args: TArgs) => TResult
100
+ ): StableFn<TArgs, TResult> {
101
+ const originalFn = fn;
102
+ let currentFn = fn;
103
+ return Object.assign(
104
+ (...args: TArgs) => {
105
+ return currentFn(...args);
106
+ },
107
+ {
108
+ getOriginal: () => originalFn,
109
+ getCurrent: () => currentFn,
110
+ setCurrent(newFn: (...args: TArgs) => TResult) {
111
+ currentFn = newFn;
112
+ },
113
+ }
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Check if a value is a stable function wrapper.
119
+ */
120
+ export function isStableFn<TArgs extends any[], TResult>(
121
+ value: unknown
122
+ ): value is StableFn<TArgs, TResult> {
123
+ return (
124
+ typeof value === "function" &&
125
+ "getOriginal" in value &&
126
+ "getCurrent" in value &&
127
+ "setCurrent" in value
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Stabilize a value with automatic function wrapper support.
133
+ *
134
+ * - Functions: Creates/updates stable wrapper (reference never changes)
135
+ * - Date objects: Compared by timestamp (uses deepEqual)
136
+ * - Other values: Returns previous if equal per equalityFn
137
+ *
138
+ * @param prev - Previous value container (or undefined for first call)
139
+ * @param next - New value
140
+ * @param equalityFn - Equality function for non-function/non-date values
141
+ * @returns Tuple of [stabilized value, wasStable]
142
+ */
143
+ export function tryStabilize<T>(
144
+ prev: { value: T } | undefined,
145
+ next: T,
146
+ equalityFn: (a: T, b: T) => boolean
147
+ ): [T, boolean] {
148
+ // First time - no previous value
149
+ if (!prev) {
150
+ if (typeof next === "function") {
151
+ return [createStableFn(next as AnyFunc) as T, false];
152
+ }
153
+ return [next, false];
154
+ }
155
+
156
+ // Handle functions with stable wrapper pattern
157
+ if (typeof next === "function") {
158
+ if (isStableFn(prev.value)) {
159
+ // Update existing stable wrapper with new function
160
+ prev.value.setCurrent(next as AnyFunc);
161
+ return [prev.value as T, true];
162
+ }
163
+ // Previous wasn't a stable fn, create new wrapper
164
+ return [createStableFn(next as AnyFunc) as T, false];
165
+ }
166
+
167
+ if (next && next instanceof Date) {
168
+ if (prev.value && prev.value instanceof Date) {
169
+ if (next.getTime() === prev.value.getTime()) {
170
+ return [prev.value, true];
171
+ }
172
+ }
173
+ return [next, false];
174
+ }
175
+
176
+ // Non-functions: use equality comparison
177
+ if (equalityFn(prev.value, next)) {
178
+ return [prev.value, true];
179
+ }
180
+
181
+ return [next, false];
182
+ }