@evolu/common 8.6.2 → 8.8.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.
- package/dist/src/Array.d.ts +21 -21
- package/dist/src/Array.js +5 -5
- package/dist/src/Assert.d.ts +478 -141
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +644 -99
- package/dist/src/Binary.d.ts +254 -0
- package/dist/src/Binary.d.ts.map +1 -0
- package/dist/src/Binary.js +958 -0
- package/dist/src/Console.d.ts +2 -10
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +6 -12
- package/dist/src/Crypto.d.ts +5 -5
- package/dist/src/Crypto.js +1 -1
- package/dist/src/Eq.d.ts +35 -40
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +51 -87
- package/dist/src/Function.d.ts +9 -14
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +7 -9
- package/dist/src/Http.d.ts +6 -7
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +1 -0
- package/dist/src/LockManager.d.ts +1 -1
- package/dist/src/LockManager.js +1 -1
- package/dist/src/Lookup.d.ts +3 -2
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +4 -3
- package/dist/src/Number.d.ts +1 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +1 -1
- package/dist/src/Object.d.ts +4 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -4
- package/dist/src/Option.d.ts +1 -2
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +1 -2
- package/dist/src/Platform.js +2 -0
- package/dist/src/Polyfills.js +4 -1
- package/dist/src/Random.d.ts +1 -1
- package/dist/src/Result.d.ts +49 -54
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +17 -22
- package/dist/src/Set.d.ts +6 -6
- package/dist/src/Set.js +2 -2
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/String.js +3 -2
- package/dist/src/Task.d.ts +32 -32
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +20 -22
- package/dist/src/Test.d.ts +31 -3
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +46 -3
- package/dist/src/Time.d.ts +3 -11
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +10 -18
- package/dist/src/Type.d.ts +87 -85
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +70 -63
- package/dist/src/Types.d.ts +12 -12
- package/dist/src/Types.js +1 -1
- package/dist/src/WebSocket.d.ts +4 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +1 -1
- package/dist/src/Worker.js +7 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +1 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +1 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +1 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +1 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +1 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +1 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +1 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +1 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +1 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +1 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +1 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +1 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +1 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +1 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +1 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +1 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +1 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +1 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +1 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +1 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +1 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +1 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +1 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +1 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +1 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +1 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +1 -1
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +1 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +1 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +1 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +1 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +1 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +1 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +1 -1
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +1 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +1 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +1 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +1 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +1 -1
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +1 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +1 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +1 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +1 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +1 -1
- package/dist/src/local-first/Evolu.d.ts +12 -12
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/Protocol.d.ts +28 -76
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +45 -229
- package/dist/src/local-first/Query.d.ts +11 -11
- package/dist/src/local-first/Query.js +6 -6
- package/dist/src/local-first/Relay.d.ts +2 -2
- package/dist/src/local-first/Schema.d.ts +3 -5
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +2 -4
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +5 -5
- package/dist/src/local-first/Storage.js +4 -4
- package/dist/src/local-first/Timestamp.d.ts +9 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +13 -10
- package/package.json +7 -5
- package/src/Array.test.ts +809 -0
- package/src/Array.ts +21 -21
- package/src/Assert.test.ts +1181 -0
- package/src/Assert.ts +971 -227
- package/src/BigInt.test.ts +28 -0
- package/src/Binary.test.ts +1128 -0
- package/src/Binary.ts +1377 -0
- package/src/Brand.test.ts +90 -0
- package/src/Cache.test.ts +183 -0
- package/src/Callbacks.test.ts +66 -0
- package/src/Console.test.ts +809 -0
- package/src/Console.ts +9 -17
- package/src/Crypto.test.ts +138 -0
- package/src/Crypto.ts +5 -5
- package/src/Eq.test.ts +333 -0
- package/src/Eq.ts +110 -176
- package/src/Error.test.ts +118 -0
- package/src/Function.test.ts +204 -0
- package/src/Function.ts +10 -15
- package/src/Http.test.ts +816 -0
- package/src/Http.ts +6 -7
- package/src/LeakDetector.test.ts +210 -0
- package/src/LeakDetector.ts +1 -0
- package/src/LockManager.ts +2 -2
- package/src/Lookup.test.ts +418 -0
- package/src/Lookup.ts +4 -3
- package/src/Microtask.test.ts +140 -0
- package/src/Number.test.ts +218 -0
- package/src/Number.ts +2 -2
- package/src/Object.test.ts +293 -0
- package/src/Object.ts +5 -4
- package/src/Option.test.ts +104 -0
- package/src/Option.ts +1 -2
- package/src/Order.test.ts +56 -0
- package/src/Platform.ts +2 -0
- package/src/Polyfills.ts +3 -4
- package/src/Random.test.ts +89 -0
- package/src/Random.ts +1 -1
- package/src/Ref.test.ts +139 -0
- package/src/RefCount.test.ts +184 -0
- package/src/Relation.test.ts +505 -0
- package/src/Resource.test.ts +4623 -0
- package/src/Result.test.ts +1829 -0
- package/src/Result.ts +49 -54
- package/src/Schedule.test.ts +1554 -0
- package/src/Set.test.ts +279 -0
- package/src/Set.ts +6 -6
- package/src/Sqlite.ts +1 -1
- package/src/Store.test.ts +288 -0
- package/src/String.test.ts +54 -0
- package/src/String.ts +3 -3
- package/src/Task.test.ts +10820 -0
- package/src/Task.ts +38 -43
- package/src/Test.test.ts +88 -0
- package/src/Test.ts +49 -3
- package/src/Time.test.ts +926 -0
- package/src/Time.ts +13 -21
- package/src/Type.base64.test.ts +88 -0
- package/src/Type.test.ts +20465 -0
- package/src/Type.ts +114 -111
- package/src/Types.test.ts +324 -0
- package/src/Types.ts +13 -13
- package/src/WebSocket.ts +4 -7
- package/src/Worker.test.ts +755 -0
- package/src/Worker.ts +13 -18
- package/src/index.ts +1 -1
- package/src/intl/_en.ts +1 -1
- package/src/intl/ar.ts +1 -1
- package/src/intl/bn.ts +1 -1
- package/src/intl/ca.ts +1 -1
- package/src/intl/cs.ts +1 -1
- package/src/intl/da.ts +1 -1
- package/src/intl/de.ts +1 -1
- package/src/intl/el.ts +1 -1
- package/src/intl/es.ts +1 -1
- package/src/intl/fa.ts +1 -1
- package/src/intl/fi.ts +1 -1
- package/src/intl/fil.ts +1 -1
- package/src/intl/fr.ts +1 -1
- package/src/intl/he.ts +1 -1
- package/src/intl/hi.ts +1 -1
- package/src/intl/hr.ts +1 -1
- package/src/intl/hu.ts +1 -1
- package/src/intl/id.ts +1 -1
- package/src/intl/intl.test.ts +3106 -0
- package/src/intl/it.ts +1 -1
- package/src/intl/ja.ts +1 -1
- package/src/intl/ko.ts +1 -1
- package/src/intl/ml.ts +1 -1
- package/src/intl/mr.ts +1 -1
- package/src/intl/ms.ts +1 -1
- package/src/intl/nb.ts +1 -1
- package/src/intl/nl.ts +1 -1
- package/src/intl/pa.ts +1 -1
- package/src/intl/pl.ts +1 -1
- package/src/intl/pt-BR.ts +1 -1
- package/src/intl/pt.ts +1 -1
- package/src/intl/ro.ts +1 -1
- package/src/intl/sk.ts +1 -1
- package/src/intl/sl.ts +1 -1
- package/src/intl/sv.ts +1 -1
- package/src/intl/sw.ts +1 -1
- package/src/intl/ta.ts +1 -1
- package/src/intl/te.ts +1 -1
- package/src/intl/th.ts +1 -1
- package/src/intl/tr.ts +1 -1
- package/src/intl/uk.ts +1 -1
- package/src/intl/ur.ts +1 -1
- package/src/intl/vi.ts +1 -1
- package/src/intl/zh-CN.ts +1 -1
- package/src/intl/zh-TW.ts +1 -1
- package/src/local-first/Evolu.test.ts +1921 -0
- package/src/local-first/Evolu.ts +18 -14
- package/src/local-first/Owner.test.ts +83 -0
- package/src/local-first/Protocol.test.ts +1037 -0
- package/src/local-first/Protocol.ts +75 -290
- package/src/local-first/Query.test.ts +349 -0
- package/src/local-first/Query.ts +11 -11
- package/src/local-first/Relay.ts +2 -2
- package/src/local-first/Schema.ts +3 -5
- package/src/local-first/Shared.test.ts +1828 -0
- package/src/local-first/Shared.ts +9 -8
- package/src/local-first/Storage.ts +4 -4
- package/src/local-first/Timestamp.test.ts +374 -0
- package/src/local-first/Timestamp.ts +22 -10
- package/dist/src/Buffer.d.ts +0 -122
- package/dist/src/Buffer.d.ts.map +0 -1
- package/dist/src/Buffer.js +0 -69
- package/src/Buffer.ts +0 -206
package/src/Http.ts
CHANGED
|
@@ -357,8 +357,8 @@ export type FetchConsume<T, E = never> = (
|
|
|
357
357
|
* }),
|
|
358
358
|
* );
|
|
359
359
|
* assertType<
|
|
360
|
-
*
|
|
361
|
-
*
|
|
360
|
+
* typeof metadata,
|
|
361
|
+
* Task<{ status: number; cache: string | null }, FetchTransportError>
|
|
362
362
|
* >();
|
|
363
363
|
* assertOk(user);
|
|
364
364
|
* assertType(Data, user.value);
|
|
@@ -374,7 +374,8 @@ export type FetchConsume<T, E = never> = (
|
|
|
374
374
|
*
|
|
375
375
|
* ```ts
|
|
376
376
|
* import {
|
|
377
|
-
*
|
|
377
|
+
* assertErr,
|
|
378
|
+
* assertType,
|
|
378
379
|
* AbortError,
|
|
379
380
|
* createRun,
|
|
380
381
|
* fetch,
|
|
@@ -396,10 +397,8 @@ export type FetchConsume<T, E = never> = (
|
|
|
396
397
|
* fiber.abort();
|
|
397
398
|
* const result = await fiber;
|
|
398
399
|
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* "Expected an AbortError.",
|
|
402
|
-
* );
|
|
400
|
+
* assertErr(result);
|
|
401
|
+
* assertType(AbortError, result.error);
|
|
403
402
|
* ```
|
|
404
403
|
*
|
|
405
404
|
* @group Fetch
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import { assertEqual, assertLength, assertSame, assertTrue } from "./Assert.ts";
|
|
3
|
+
|
|
4
|
+
import { testCreateConsole } from "./Console.ts";
|
|
5
|
+
import {
|
|
6
|
+
createLeakDetector,
|
|
7
|
+
noopLeakDetector,
|
|
8
|
+
testCreateLeakDetector,
|
|
9
|
+
} from "./LeakDetector.ts";
|
|
10
|
+
import { assertType, String } from "./Type.ts";
|
|
11
|
+
|
|
12
|
+
describe("createLeakDetector", () => {
|
|
13
|
+
it("accepts track and untrack calls", () => {
|
|
14
|
+
const console = testCreateConsole();
|
|
15
|
+
const leakDetector = createLeakDetector({ console });
|
|
16
|
+
const target = {};
|
|
17
|
+
const unregisterToken = {};
|
|
18
|
+
|
|
19
|
+
leakDetector.track(
|
|
20
|
+
target,
|
|
21
|
+
{ name: "Lease", isLeaked: () => true },
|
|
22
|
+
unregisterToken,
|
|
23
|
+
);
|
|
24
|
+
leakDetector.untrack(unregisterToken);
|
|
25
|
+
|
|
26
|
+
assertEqual(console.getEntriesSnapshot(), []);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns the no-op detector without FinalizationRegistry", () => {
|
|
30
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
31
|
+
globalThis,
|
|
32
|
+
"FinalizationRegistry",
|
|
33
|
+
);
|
|
34
|
+
delete (
|
|
35
|
+
globalThis as { FinalizationRegistry?: typeof FinalizationRegistry }
|
|
36
|
+
).FinalizationRegistry;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
assertSame(
|
|
40
|
+
createLeakDetector({ console: testCreateConsole() }),
|
|
41
|
+
noopLeakDetector,
|
|
42
|
+
);
|
|
43
|
+
} finally {
|
|
44
|
+
if (descriptor)
|
|
45
|
+
Object.defineProperty(globalThis, "FinalizationRegistry", descriptor);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("noopLeakDetector", () => {
|
|
51
|
+
it("ignores track and untrack", () => {
|
|
52
|
+
const unregisterToken = {};
|
|
53
|
+
|
|
54
|
+
noopLeakDetector.track(
|
|
55
|
+
{},
|
|
56
|
+
{ name: "Lease", isLeaked: () => true },
|
|
57
|
+
unregisterToken,
|
|
58
|
+
);
|
|
59
|
+
noopLeakDetector.untrack(unregisterToken);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("testCreateLeakDetector", () => {
|
|
64
|
+
it("collect reports tracked handles that are still held", () => {
|
|
65
|
+
const console = testCreateConsole();
|
|
66
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
67
|
+
|
|
68
|
+
leakDetector.track({}, { name: "Lease", isLeaked: () => true }, {});
|
|
69
|
+
|
|
70
|
+
assertEqual(leakDetector.getTrackedCount(), 1);
|
|
71
|
+
assertEqual(leakDetector.collect(), 1);
|
|
72
|
+
assertEqual(leakDetector.getTrackedCount(), 0);
|
|
73
|
+
|
|
74
|
+
const entries = console.getEntriesSnapshot();
|
|
75
|
+
assertLength(entries, 1);
|
|
76
|
+
assertEqual(entries[0]?.method, "warn");
|
|
77
|
+
assertEqual(
|
|
78
|
+
entries[0]?.args[0],
|
|
79
|
+
"Lease was garbage-collected without cleanup. Tracked at:",
|
|
80
|
+
);
|
|
81
|
+
const stack = entries[0]?.args[1];
|
|
82
|
+
assertType(String, stack);
|
|
83
|
+
assertTrue(stack.includes("LeakDetector.test"));
|
|
84
|
+
|
|
85
|
+
// Collected targets are no longer tracked.
|
|
86
|
+
assertEqual(leakDetector.collect(), 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("collect does not report handles that are no longer held", () => {
|
|
90
|
+
const console = testCreateConsole();
|
|
91
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
92
|
+
|
|
93
|
+
leakDetector.track({}, { name: "Lease", isLeaked: () => false }, {});
|
|
94
|
+
|
|
95
|
+
assertEqual(leakDetector.collect(), 0);
|
|
96
|
+
assertEqual(console.getEntriesSnapshot(), []);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("collect counts only leaked handles and clears all", () => {
|
|
100
|
+
const console = testCreateConsole();
|
|
101
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
102
|
+
|
|
103
|
+
leakDetector.track({}, { name: "Lease", isLeaked: () => true }, {});
|
|
104
|
+
leakDetector.track({}, { name: "Lease", isLeaked: () => false }, {});
|
|
105
|
+
|
|
106
|
+
assertEqual(leakDetector.collect(), 1);
|
|
107
|
+
assertLength(console.getEntriesSnapshot(), 1);
|
|
108
|
+
assertEqual(leakDetector.getTrackedCount(), 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("collect counts the same leak decision it reports", () => {
|
|
112
|
+
const console = testCreateConsole();
|
|
113
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
114
|
+
let isLeaked = true;
|
|
115
|
+
|
|
116
|
+
leakDetector.track(
|
|
117
|
+
{},
|
|
118
|
+
{
|
|
119
|
+
name: "Lease",
|
|
120
|
+
isLeaked: () => {
|
|
121
|
+
const result = isLeaked;
|
|
122
|
+
isLeaked = false;
|
|
123
|
+
return result;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
assertEqual(leakDetector.collect(), 1);
|
|
130
|
+
assertLength(console.getEntriesSnapshot(), 1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("untrack stops tracking", () => {
|
|
134
|
+
const console = testCreateConsole();
|
|
135
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
136
|
+
const unregisterToken = {};
|
|
137
|
+
|
|
138
|
+
leakDetector.track(
|
|
139
|
+
{},
|
|
140
|
+
{ name: "Lease", isLeaked: () => true },
|
|
141
|
+
unregisterToken,
|
|
142
|
+
);
|
|
143
|
+
leakDetector.untrack(unregisterToken);
|
|
144
|
+
|
|
145
|
+
assertEqual(leakDetector.getTrackedCount(), 0);
|
|
146
|
+
assertEqual(leakDetector.collect(), 0);
|
|
147
|
+
assertEqual(console.getEntriesSnapshot(), []);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("untrack removes all registrations for duplicate tokens", () => {
|
|
151
|
+
const console = testCreateConsole();
|
|
152
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
153
|
+
const unregisterToken = {};
|
|
154
|
+
|
|
155
|
+
leakDetector.track(
|
|
156
|
+
{},
|
|
157
|
+
{ name: "Lease", isLeaked: () => true },
|
|
158
|
+
unregisterToken,
|
|
159
|
+
);
|
|
160
|
+
leakDetector.track(
|
|
161
|
+
{},
|
|
162
|
+
{ name: "SemaphorePermit", isLeaked: () => true },
|
|
163
|
+
unregisterToken,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
assertEqual(leakDetector.getTrackedCount(), 2);
|
|
167
|
+
|
|
168
|
+
leakDetector.untrack(unregisterToken);
|
|
169
|
+
|
|
170
|
+
assertEqual(leakDetector.getTrackedCount(), 0);
|
|
171
|
+
assertEqual(leakDetector.collect(), 0);
|
|
172
|
+
assertEqual(console.getEntriesSnapshot(), []);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("collect reports every registration under a duplicate token", () => {
|
|
176
|
+
const console = testCreateConsole();
|
|
177
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
178
|
+
const unregisterToken = {};
|
|
179
|
+
|
|
180
|
+
leakDetector.track(
|
|
181
|
+
{},
|
|
182
|
+
{ name: "Lease", isLeaked: () => true },
|
|
183
|
+
unregisterToken,
|
|
184
|
+
);
|
|
185
|
+
leakDetector.track(
|
|
186
|
+
{},
|
|
187
|
+
{ name: "Lease", isLeaked: () => true },
|
|
188
|
+
unregisterToken,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
assertEqual(leakDetector.collect(), 2);
|
|
192
|
+
assertLength(console.getEntriesSnapshot(), 2);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("getTrackedCount filters by leak name", () => {
|
|
196
|
+
const console = testCreateConsole();
|
|
197
|
+
const leakDetector = testCreateLeakDetector({ console });
|
|
198
|
+
|
|
199
|
+
leakDetector.track({}, { name: "Lease", isLeaked: () => true }, {});
|
|
200
|
+
leakDetector.track(
|
|
201
|
+
{},
|
|
202
|
+
{ name: "SemaphorePermit", isLeaked: () => true },
|
|
203
|
+
{},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
assertEqual(leakDetector.getTrackedCount(), 2);
|
|
207
|
+
assertEqual(leakDetector.getTrackedCount({ name: "Lease" }), 1);
|
|
208
|
+
assertEqual(leakDetector.getTrackedCount({ name: "SemaphorePermit" }), 1);
|
|
209
|
+
});
|
|
210
|
+
});
|
package/src/LeakDetector.ts
CHANGED
|
@@ -67,6 +67,7 @@ export const createLeakDetector = (deps: ConsoleDep): LeakDetector => {
|
|
|
67
67
|
return noopLeakDetector;
|
|
68
68
|
|
|
69
69
|
const report = reportLeak(deps);
|
|
70
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- Construct the same global object FinalizationRegistry verified by the feature check.
|
|
70
71
|
const registry = new globalThis.FinalizationRegistry<TrackedLeak>((leak) => {
|
|
71
72
|
report(leak);
|
|
72
73
|
});
|
package/src/LockManager.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { Callback } from "./Types.ts";
|
|
|
23
23
|
* import { assertEqual, type LockManagerDep } from "@evolu/common";
|
|
24
24
|
*
|
|
25
25
|
* const deps: LockManagerDep = {
|
|
26
|
-
* lockManager:
|
|
26
|
+
* lockManager: navigator.locks,
|
|
27
27
|
* };
|
|
28
28
|
*
|
|
29
29
|
* const result = await deps.lockManager.request(
|
|
@@ -67,7 +67,7 @@ export interface LockManagerDep {
|
|
|
67
67
|
* private namespace and returned with the original visible names.
|
|
68
68
|
*/
|
|
69
69
|
export const testCreateLockManager = (
|
|
70
|
-
nativeLockManager: LockManager =
|
|
70
|
+
nativeLockManager: LockManager = navigator.locks,
|
|
71
71
|
): LockManager => {
|
|
72
72
|
const namespace = createId({ randomBytes: createRandomBytes() });
|
|
73
73
|
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertEqual,
|
|
4
|
+
assertFalse,
|
|
5
|
+
assertSame,
|
|
6
|
+
assertThrowsInstanceOf,
|
|
7
|
+
assertTrue,
|
|
8
|
+
} from "./Assert.ts";
|
|
9
|
+
|
|
10
|
+
import { createMutableArray } from "./Array.ts";
|
|
11
|
+
import {
|
|
12
|
+
createLookupMap,
|
|
13
|
+
createLookupSet,
|
|
14
|
+
structuralLookup,
|
|
15
|
+
} from "./Lookup.ts";
|
|
16
|
+
import { installPolyfills } from "./Polyfills.ts";
|
|
17
|
+
|
|
18
|
+
installPolyfills();
|
|
19
|
+
|
|
20
|
+
describe("createLookupMap", () => {
|
|
21
|
+
it("LookupMap is not assignable to native Map", () => {
|
|
22
|
+
const lookupMap = createLookupMap<string, number, string>({
|
|
23
|
+
lookup: (key) => key,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const takesNativeMap = (_map: Map<string, number>) => undefined;
|
|
27
|
+
|
|
28
|
+
// LookupMap intentionally does not substitute for Map because callers may
|
|
29
|
+
// rely on native Map key semantics while LookupMap uses derived lookup keys.
|
|
30
|
+
// @ts-expect-error LookupMap is intentionally not substitutable for Map.
|
|
31
|
+
takesNativeMap(lookupMap);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("uses natural ids for logical equality", () => {
|
|
35
|
+
interface Person {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const map = createLookupMap<Person, string, string>({
|
|
41
|
+
lookup: (person) => person.id,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
map.set({ id: "1", name: "Ada" }, "person");
|
|
45
|
+
|
|
46
|
+
assertEqual(map.get({ id: "1", name: "Grace" }), "person");
|
|
47
|
+
assertEqual(map.getKey({ id: "1", name: "Grace" }), {
|
|
48
|
+
id: "1",
|
|
49
|
+
name: "Ada",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("initializes from entries and preserves the first representative", () => {
|
|
54
|
+
const key1 = { id: "a", name: "Ada" } as const;
|
|
55
|
+
const key2 = { id: "a", name: "Grace" } as const;
|
|
56
|
+
const key3 = { id: "b", name: "Linus" } as const;
|
|
57
|
+
|
|
58
|
+
const map = createLookupMap<
|
|
59
|
+
{ readonly id: string; readonly name: string },
|
|
60
|
+
number,
|
|
61
|
+
string
|
|
62
|
+
>({
|
|
63
|
+
lookup: (key) => key.id,
|
|
64
|
+
entries: [
|
|
65
|
+
[key1, 1],
|
|
66
|
+
[key2, 2],
|
|
67
|
+
[key3, 3],
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assertEqual(map.size, 2);
|
|
72
|
+
assertTrue(map.has(key2));
|
|
73
|
+
assertEqual(map.get(key2), 2);
|
|
74
|
+
assertSame(map.getKey(key2), key1);
|
|
75
|
+
assertEqual([...map.keys()], [key1, key3]);
|
|
76
|
+
assertEqual([...map.values()], [2, 3]);
|
|
77
|
+
assertEqual(
|
|
78
|
+
[...map.entries()],
|
|
79
|
+
[
|
|
80
|
+
[key1, 2],
|
|
81
|
+
[key3, 3],
|
|
82
|
+
],
|
|
83
|
+
);
|
|
84
|
+
assertEqual(
|
|
85
|
+
[...map],
|
|
86
|
+
[
|
|
87
|
+
[key1, 2],
|
|
88
|
+
[key3, 3],
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const seen: Array<
|
|
93
|
+
readonly [{ readonly id: string; readonly name: string }, number]
|
|
94
|
+
> = [];
|
|
95
|
+
map.forEach((value, key) => {
|
|
96
|
+
seen.push([key, value]);
|
|
97
|
+
});
|
|
98
|
+
assertEqual(seen, [
|
|
99
|
+
[key1, 2],
|
|
100
|
+
[key3, 3],
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
assertFalse(map.delete({ id: "missing", name: "x" }));
|
|
104
|
+
assertTrue(map.delete(key2));
|
|
105
|
+
assertEqual(map.size, 1);
|
|
106
|
+
map.clear();
|
|
107
|
+
assertEqual(map.size, 0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("getOrInsert uses lookup equality and preserves the first representative", () => {
|
|
111
|
+
interface Person {
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly name: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ada: Person = { id: "a", name: "Ada" };
|
|
117
|
+
const grace: Person = { id: "a", name: "Grace" };
|
|
118
|
+
|
|
119
|
+
const map = createLookupMap<Person, number, string>({
|
|
120
|
+
lookup: (key) => key.id,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assertEqual(map.getOrInsert(ada, 1), 1);
|
|
124
|
+
assertEqual(map.getOrInsert(grace, 2), 1);
|
|
125
|
+
assertEqual(map.size, 1);
|
|
126
|
+
assertEqual(map.get(grace), 1);
|
|
127
|
+
assertSame(map.getKey(grace), ada);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("getOrInsertComputed computes only for missing logical keys", () => {
|
|
131
|
+
interface Person {
|
|
132
|
+
readonly id: string;
|
|
133
|
+
readonly name: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ada: Person = { id: "a", name: "Ada" };
|
|
137
|
+
const grace: Person = { id: "a", name: "Grace" };
|
|
138
|
+
const linus: Person = { id: "b", name: "Linus" };
|
|
139
|
+
|
|
140
|
+
const map = createLookupMap<Person, number, string>({
|
|
141
|
+
lookup: (key) => key.id,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const computedForKeys: Array<string> = [];
|
|
145
|
+
|
|
146
|
+
assertEqual(
|
|
147
|
+
map.getOrInsertComputed(ada, (key) => {
|
|
148
|
+
computedForKeys.push(key.name);
|
|
149
|
+
return 1;
|
|
150
|
+
}),
|
|
151
|
+
1,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
assertEqual(
|
|
155
|
+
map.getOrInsertComputed(grace, (key) => {
|
|
156
|
+
computedForKeys.push(key.name);
|
|
157
|
+
return 2;
|
|
158
|
+
}),
|
|
159
|
+
1,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
assertEqual(
|
|
163
|
+
map.getOrInsertComputed(linus, (key) => {
|
|
164
|
+
computedForKeys.push(key.name);
|
|
165
|
+
return 3;
|
|
166
|
+
}),
|
|
167
|
+
3,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
assertEqual(computedForKeys, ["Ada", "Linus"]);
|
|
171
|
+
assertSame(map.getKey(grace), ada);
|
|
172
|
+
assertEqual(map.get(grace), 1);
|
|
173
|
+
assertEqual(map.get(linus), 3);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("createLookupSet", () => {
|
|
178
|
+
it("LookupSet is not assignable to native Set", () => {
|
|
179
|
+
const lookupSet = createLookupSet<string, string>({
|
|
180
|
+
lookup: (key) => key,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const takesNativeSet = (_set: Set<string>) => undefined;
|
|
184
|
+
|
|
185
|
+
// LookupSet intentionally does not substitute for Set because callers may
|
|
186
|
+
// rely on native Set value semantics while LookupSet uses derived lookup keys.
|
|
187
|
+
// @ts-expect-error LookupSet is intentionally not substitutable for Set.
|
|
188
|
+
takesNativeSet(lookupSet);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("uses natural ids and supports iteration helpers", () => {
|
|
192
|
+
const key1 = { id: "a", name: "Ada" } as const;
|
|
193
|
+
const key2 = { id: "a", name: "Grace" } as const;
|
|
194
|
+
const key3 = { id: "b", name: "Linus" } as const;
|
|
195
|
+
|
|
196
|
+
const set = createLookupSet<
|
|
197
|
+
{ readonly id: string; readonly name: string },
|
|
198
|
+
string
|
|
199
|
+
>({
|
|
200
|
+
lookup: (key) => key.id,
|
|
201
|
+
values: [key1, key2, key3],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assertEqual(set.size, 2);
|
|
205
|
+
assertTrue(set.has(key2));
|
|
206
|
+
assertSame(set.get(key2), key1);
|
|
207
|
+
assertEqual([...set.keys()], [key1, key3]);
|
|
208
|
+
assertEqual([...set.values()], [key1, key3]);
|
|
209
|
+
assertEqual(
|
|
210
|
+
[...set.entries()],
|
|
211
|
+
[
|
|
212
|
+
[key1, key1],
|
|
213
|
+
[key3, key3],
|
|
214
|
+
],
|
|
215
|
+
);
|
|
216
|
+
assertEqual([...set], [key1, key3]);
|
|
217
|
+
|
|
218
|
+
const seen: Array<
|
|
219
|
+
readonly [
|
|
220
|
+
{ readonly id: string; readonly name: string },
|
|
221
|
+
{ readonly id: string; readonly name: string },
|
|
222
|
+
]
|
|
223
|
+
> = [];
|
|
224
|
+
set.forEach((value, key) => {
|
|
225
|
+
seen.push([key, value]);
|
|
226
|
+
});
|
|
227
|
+
assertEqual(seen, [
|
|
228
|
+
[key1, key1],
|
|
229
|
+
[key3, key3],
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
assertFalse(set.delete({ id: "missing", name: "x" }));
|
|
233
|
+
assertTrue(set.delete(key2));
|
|
234
|
+
assertEqual(set.size, 1);
|
|
235
|
+
set.clear();
|
|
236
|
+
assertEqual(set.size, 0);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("structuralLookup", () => {
|
|
241
|
+
it("serializes primitives with tagged JSON-like semantics", () => {
|
|
242
|
+
assertEqual(structuralLookup("a"), 's:"a"');
|
|
243
|
+
assertEqual(structuralLookup(1), "n:1");
|
|
244
|
+
assertEqual(structuralLookup(0), "n:0");
|
|
245
|
+
assertEqual(structuralLookup(-0), "n:-0");
|
|
246
|
+
assertEqual(structuralLookup(NaN), "n:NaN");
|
|
247
|
+
assertEqual(structuralLookup(Number.POSITIVE_INFINITY), "n:Infinity");
|
|
248
|
+
assertEqual(structuralLookup(Number.NEGATIVE_INFINITY), "n:-Infinity");
|
|
249
|
+
assertEqual(structuralLookup(true), "b:true");
|
|
250
|
+
assertEqual(structuralLookup(false), "b:false");
|
|
251
|
+
assertEqual(structuralLookup(null), "l:null");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("distinguishes signed zero through a Map-backed lookup", () => {
|
|
255
|
+
const map = createLookupMap<number, string, string>({
|
|
256
|
+
lookup: structuralLookup,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
map.set(0, "positive");
|
|
260
|
+
map.set(-0, "negative");
|
|
261
|
+
|
|
262
|
+
assertEqual(map.size, 2);
|
|
263
|
+
assertEqual(map.get(0), "positive");
|
|
264
|
+
assertEqual(map.get(-0), "negative");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("serializes arrays, objects, Uint8Array, and null-prototype objects", () => {
|
|
268
|
+
assertEqual(
|
|
269
|
+
structuralLookup(["a", { count: 1 }]),
|
|
270
|
+
'a:[s:"a",o:{"count":n:1}]',
|
|
271
|
+
);
|
|
272
|
+
assertEqual(
|
|
273
|
+
structuralLookup({ nested: { enabled: true }, id: "a" }),
|
|
274
|
+
'o:{"id":s:"a","nested":o:{"enabled":b:true}}',
|
|
275
|
+
);
|
|
276
|
+
assertEqual(structuralLookup(new Uint8Array([1, 2, 3])), "u:AQID");
|
|
277
|
+
|
|
278
|
+
const nullPrototype = Object.assign(Object.create(null), { id: "a" }) as {
|
|
279
|
+
readonly id: string;
|
|
280
|
+
};
|
|
281
|
+
assertEqual(structuralLookup(nullPrototype), 'o:{"id":s:"a"}');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("memoizes by object identity", () => {
|
|
285
|
+
let accessCount = 0;
|
|
286
|
+
const key = Object.defineProperty({}, "id", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: () => {
|
|
289
|
+
accessCount += 1;
|
|
290
|
+
return "a";
|
|
291
|
+
},
|
|
292
|
+
}) as { readonly id: string };
|
|
293
|
+
|
|
294
|
+
assertEqual(structuralLookup(key), 'o:{"id":s:"a"}');
|
|
295
|
+
assertEqual(structuralLookup(key), 'o:{"id":s:"a"}');
|
|
296
|
+
assertEqual(accessCount, 1);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("rejects symbol-keyed properties at compile time", () => {
|
|
300
|
+
const symbolKey = Symbol("meta");
|
|
301
|
+
const key = { id: "a", [symbolKey]: "x" };
|
|
302
|
+
|
|
303
|
+
// @ts-expect-error - symbol-keyed properties are ignored by runtime serialization.
|
|
304
|
+
structuralLookup(key);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("rejects unsupported values and cycles", () => {
|
|
308
|
+
class Example {
|
|
309
|
+
readonly id = "a";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const cyclicObject: Record<string, unknown> = { id: "a" };
|
|
313
|
+
cyclicObject.self = cyclicObject;
|
|
314
|
+
|
|
315
|
+
const cyclicArray: Array<unknown> = [];
|
|
316
|
+
cyclicArray.push(cyclicArray);
|
|
317
|
+
|
|
318
|
+
const undefinedError = assertThrowsInstanceOf(
|
|
319
|
+
() => structuralLookup(undefined as never),
|
|
320
|
+
Error,
|
|
321
|
+
);
|
|
322
|
+
assertTrue(
|
|
323
|
+
undefinedError.message.includes(
|
|
324
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const functionError = assertThrowsInstanceOf(
|
|
328
|
+
() => structuralLookup((() => undefined) as never),
|
|
329
|
+
Error,
|
|
330
|
+
);
|
|
331
|
+
assertTrue(
|
|
332
|
+
functionError.message.includes(
|
|
333
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
const symbolError = assertThrowsInstanceOf(
|
|
337
|
+
() => structuralLookup(Symbol("x") as never),
|
|
338
|
+
Error,
|
|
339
|
+
);
|
|
340
|
+
assertTrue(
|
|
341
|
+
symbolError.message.includes(
|
|
342
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
const bigintError = assertThrowsInstanceOf(
|
|
346
|
+
() => structuralLookup(10n as never),
|
|
347
|
+
Error,
|
|
348
|
+
);
|
|
349
|
+
assertTrue(
|
|
350
|
+
bigintError.message.includes(
|
|
351
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
const dateError = assertThrowsInstanceOf(
|
|
355
|
+
() => structuralLookup(new Date() as never),
|
|
356
|
+
Error,
|
|
357
|
+
);
|
|
358
|
+
assertTrue(
|
|
359
|
+
dateError.message.includes(
|
|
360
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
const instanceError = assertThrowsInstanceOf(
|
|
364
|
+
() => structuralLookup(new Example() as never),
|
|
365
|
+
Error,
|
|
366
|
+
);
|
|
367
|
+
assertTrue(
|
|
368
|
+
instanceError.message.includes(
|
|
369
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
const arrayError = assertThrowsInstanceOf(
|
|
373
|
+
() => structuralLookup(["a", undefined] as never),
|
|
374
|
+
Error,
|
|
375
|
+
);
|
|
376
|
+
assertTrue(
|
|
377
|
+
arrayError.message.includes(
|
|
378
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
const mutableArrayError = assertThrowsInstanceOf(
|
|
382
|
+
() => structuralLookup(createMutableArray(1) as never),
|
|
383
|
+
Error,
|
|
384
|
+
);
|
|
385
|
+
assertTrue(
|
|
386
|
+
mutableArrayError.message.includes(
|
|
387
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
const objectError = assertThrowsInstanceOf(
|
|
391
|
+
() => structuralLookup({ id: "a", optional: undefined } as never),
|
|
392
|
+
Error,
|
|
393
|
+
);
|
|
394
|
+
assertTrue(
|
|
395
|
+
objectError.message.includes(
|
|
396
|
+
"Structural lookup keys must be JSON-like values or Uint8Array.",
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
const cyclicObjectError = assertThrowsInstanceOf(
|
|
400
|
+
() => structuralLookup(cyclicObject as never),
|
|
401
|
+
Error,
|
|
402
|
+
);
|
|
403
|
+
assertTrue(
|
|
404
|
+
cyclicObjectError.message.includes(
|
|
405
|
+
"Structural lookup keys must not contain cycles.",
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
const cyclicArrayError = assertThrowsInstanceOf(
|
|
409
|
+
() => structuralLookup(cyclicArray as never),
|
|
410
|
+
Error,
|
|
411
|
+
);
|
|
412
|
+
assertTrue(
|
|
413
|
+
cyclicArrayError.message.includes(
|
|
414
|
+
"Structural lookup keys must not contain cycles.",
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
});
|
package/src/Lookup.ts
CHANGED
|
@@ -331,8 +331,9 @@ export type StructuralFunction = (...args: ReadonlyArray<unknown>) => unknown;
|
|
|
331
331
|
*
|
|
332
332
|
* Structural lookup keys are derived from JSON-like values plus `Uint8Array`.
|
|
333
333
|
* Equal structures produce the same lookup key even when they are different
|
|
334
|
-
* JavaScript instances.
|
|
335
|
-
*
|
|
334
|
+
* JavaScript instances. Primitive values use SameValue (`Object.is`), so `NaN`
|
|
335
|
+
* equals itself while `0` and `-0` produce different keys. `undefined` values
|
|
336
|
+
* and sparse array holes are rejected rather than skipped.
|
|
336
337
|
*
|
|
337
338
|
* The derived key is memoized by non-null object identity in a module-scoped
|
|
338
339
|
* `WeakMap` shared by all callers, so keys must be immutable.
|
|
@@ -394,7 +395,7 @@ const structuralLookupInternal = (
|
|
|
394
395
|
if (Number.isNaN(value)) return "n:NaN";
|
|
395
396
|
if (value === Number.POSITIVE_INFINITY) return "n:Infinity";
|
|
396
397
|
if (value === Number.NEGATIVE_INFINITY) return "n:-Infinity";
|
|
397
|
-
return Object.is(value, -0) ? "n
|
|
398
|
+
return Object.is(value, -0) ? "n:-0" : `n:${value}`;
|
|
398
399
|
case "boolean":
|
|
399
400
|
return value ? "b:true" : "b:false";
|
|
400
401
|
case "object": {
|