@synnaxlabs/x 0.55.0 → 0.56.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/.turbo/turbo-build.log +10 -13
- package/dist/src/array/nullable.d.ts +1 -1
- package/dist/src/array/nullable.d.ts.map +1 -1
- package/dist/src/caseconv/caseconv.d.ts.map +1 -1
- package/dist/src/compare/compare.d.ts +14 -0
- package/dist/src/compare/compare.d.ts.map +1 -1
- package/dist/src/debounce/debounce.d.ts +7 -2
- package/dist/src/debounce/debounce.d.ts.map +1 -1
- package/dist/src/destructor/destructor.d.ts +1 -0
- package/dist/src/destructor/destructor.d.ts.map +1 -1
- package/dist/src/errors/errors.d.ts +6 -10
- package/dist/src/errors/errors.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/notation/external.d.ts +3 -0
- package/dist/src/notation/external.d.ts.map +1 -0
- package/dist/src/notation/index.d.ts +1 -1
- package/dist/src/notation/notation.d.ts +5 -9
- package/dist/src/notation/notation.d.ts.map +1 -1
- package/dist/src/notation/types.gen.d.ts +9 -0
- package/dist/src/notation/types.gen.d.ts.map +1 -0
- package/dist/src/primitive/primitive.d.ts +16 -0
- package/dist/src/primitive/primitive.d.ts.map +1 -1
- package/dist/src/record/record.d.ts +8 -1
- package/dist/src/record/record.d.ts.map +1 -1
- package/dist/src/require/index.d.ts +2 -0
- package/dist/src/require/index.d.ts.map +1 -0
- package/dist/src/require/require.d.ts +2 -0
- package/dist/src/require/require.d.ts.map +1 -0
- package/dist/src/spatial/base.d.ts +1 -103
- package/dist/src/spatial/base.d.ts.map +1 -1
- package/dist/src/spatial/bounds/bounds.d.ts +3 -3
- package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
- package/dist/src/spatial/box/box.d.ts +7 -13
- package/dist/src/spatial/box/box.d.ts.map +1 -1
- package/dist/src/spatial/direction/direction.d.ts +17 -16
- package/dist/src/spatial/direction/direction.d.ts.map +1 -1
- package/dist/src/spatial/external.d.ts +1 -2
- package/dist/src/spatial/external.d.ts.map +1 -1
- package/dist/src/spatial/location/location.d.ts +28 -28
- package/dist/src/spatial/location/location.d.ts.map +1 -1
- package/dist/src/spatial/scale/scale.d.ts +2 -2
- package/dist/src/spatial/scale/scale.d.ts.map +1 -1
- package/dist/src/spatial/sticky/sticky.d.ts +15 -15
- package/dist/src/spatial/sticky/sticky.d.ts.map +1 -1
- package/dist/src/spatial/types.gen.d.ts +179 -2
- package/dist/src/spatial/types.gen.d.ts.map +1 -1
- package/dist/src/spatial/xy/xy.d.ts +4 -4
- package/dist/src/spatial/xy/xy.d.ts.map +1 -1
- package/dist/src/status/status.d.ts +11 -0
- package/dist/src/status/status.d.ts.map +1 -1
- package/dist/src/telem/external.d.ts +1 -0
- package/dist/src/telem/external.d.ts.map +1 -1
- package/dist/src/telem/series.d.ts +5 -2
- package/dist/src/telem/series.d.ts.map +1 -1
- package/dist/src/telem/telem.d.ts +42 -34
- package/dist/src/telem/telem.d.ts.map +1 -1
- package/dist/src/telem/types.gen.d.ts +19 -0
- package/dist/src/telem/types.gen.d.ts.map +1 -0
- package/dist/src/text/external.d.ts +3 -0
- package/dist/src/text/external.d.ts.map +1 -0
- package/dist/src/text/index.d.ts +2 -0
- package/dist/src/text/index.d.ts.map +1 -0
- package/dist/src/text/types.d.ts +21 -0
- package/dist/src/text/types.d.ts.map +1 -0
- package/dist/src/text/types.gen.d.ts +13 -0
- package/dist/src/text/types.gen.d.ts.map +1 -0
- package/dist/src/throttle/index.d.ts +2 -0
- package/dist/src/throttle/index.d.ts.map +1 -0
- package/dist/src/throttle/throttle.d.ts +3 -0
- package/dist/src/throttle/throttle.d.ts.map +1 -0
- package/dist/src/throttle/throttle.spec.d.ts +2 -0
- package/dist/src/throttle/throttle.spec.d.ts.map +1 -0
- package/dist/src/zod/parse.d.ts.map +1 -1
- package/dist/x.cjs +9 -9
- package/dist/x.js +1469 -1346
- package/package.json +11 -11
- package/src/array/nullable.ts +1 -4
- package/src/caseconv/caseconv.spec.ts +71 -0
- package/src/caseconv/caseconv.ts +15 -2
- package/src/compare/compare.spec.ts +115 -0
- package/src/compare/compare.ts +29 -0
- package/src/debounce/debounce.spec.ts +258 -24
- package/src/debounce/debounce.ts +49 -30
- package/src/deep/copy.spec.ts +13 -0
- package/src/deep/difference.ts +1 -1
- package/src/destructor/destructor.ts +2 -0
- package/src/errors/errors.spec.ts +30 -0
- package/src/errors/errors.ts +29 -17
- package/src/index.ts +4 -1
- package/src/notation/external.ts +11 -0
- package/src/notation/index.ts +1 -1
- package/src/notation/notation.spec.ts +260 -2
- package/src/notation/notation.ts +25 -7
- package/src/notation/types.gen.ts +16 -0
- package/src/primitive/primitive.spec.ts +58 -5
- package/src/primitive/primitive.ts +22 -0
- package/src/record/record.spec.ts +26 -0
- package/src/record/record.ts +20 -5
- package/src/require/index.ts +10 -0
- package/src/require/require.ts +10 -0
- package/src/spatial/base.ts +1 -93
- package/src/spatial/bounds/bounds.ts +10 -10
- package/src/spatial/box/box.ts +5 -5
- package/src/spatial/direction/direction.ts +16 -17
- package/src/spatial/external.ts +1 -2
- package/src/spatial/location/location.ts +19 -17
- package/src/spatial/scale/scale.ts +2 -2
- package/src/spatial/sticky/sticky.spec.ts +2 -2
- package/src/spatial/sticky/sticky.ts +6 -13
- package/src/spatial/types.gen.ts +140 -0
- package/src/spatial/xy/xy.ts +7 -7
- package/src/status/status.spec.ts +65 -0
- package/src/status/status.ts +20 -0
- package/src/telem/external.ts +8 -0
- package/src/telem/series.spec.ts +183 -0
- package/src/telem/series.ts +54 -16
- package/src/telem/telem.spec.ts +128 -9
- package/src/telem/telem.ts +91 -79
- package/src/telem/types.gen.ts +28 -0
- package/src/text/external.ts +11 -0
- package/src/text/index.ts +10 -0
- package/src/text/types.gen.ts +16 -0
- package/src/text/types.ts +37 -0
- package/src/{worker → throttle}/index.ts +1 -1
- package/src/throttle/throttle.spec.ts +147 -0
- package/src/throttle/throttle.ts +44 -0
- package/src/zod/parse.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/src/spatial/spatial.d.ts +0 -3
- package/dist/src/spatial/spatial.d.ts.map +0 -1
- package/dist/src/worker/index.d.ts +0 -2
- package/dist/src/worker/index.d.ts.map +0 -1
- package/dist/src/worker/worker.d.ts +0 -33
- package/dist/src/worker/worker.d.ts.map +0 -1
- package/dist/src/worker/worker.spec.d.ts +0 -2
- package/dist/src/worker/worker.spec.d.ts.map +0 -1
- package/src/spatial/spatial.ts +0 -44
- package/src/worker/worker.spec.ts +0 -41
- package/src/worker/worker.ts +0 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/x",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Common Utilities for Synnax Labs",
|
|
6
6
|
"repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
|
|
@@ -14,21 +14,21 @@
|
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"async-mutex": "^0.5.0",
|
|
17
|
-
"nanoid": "^5.1.
|
|
18
|
-
"uuid": "^
|
|
19
|
-
"zod": "^4.3
|
|
17
|
+
"nanoid": "^5.1.11",
|
|
18
|
+
"uuid": "^14.0.0",
|
|
19
|
+
"zod": "^4.4.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^25.
|
|
23
|
-
"@vitest/coverage-v8": "^4.1.
|
|
24
|
-
"eslint": "^10.
|
|
22
|
+
"@types/node": "^25.9.0",
|
|
23
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
24
|
+
"eslint": "^10.4.0",
|
|
25
25
|
"madge": "^8.0.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
-
"vite": "^8.0.
|
|
28
|
-
"vitest": "^4.1.
|
|
27
|
+
"vite": "^8.0.13",
|
|
28
|
+
"vitest": "^4.1.6",
|
|
29
29
|
"@synnaxlabs/eslint-config": "^0.0.0",
|
|
30
|
-
"@synnaxlabs/
|
|
31
|
-
"@synnaxlabs/
|
|
30
|
+
"@synnaxlabs/vite-plugin": "^0.0.0",
|
|
31
|
+
"@synnaxlabs/tsconfig": "^0.0.0"
|
|
32
32
|
},
|
|
33
33
|
"main": "dist/x.cjs",
|
|
34
34
|
"module": "dist/x.js",
|
package/src/array/nullable.ts
CHANGED
|
@@ -19,7 +19,4 @@ import z from "zod";
|
|
|
19
19
|
* - [items] → [items]
|
|
20
20
|
*/
|
|
21
21
|
export const nullishToEmpty = <Z extends z.ZodType>(item: Z) =>
|
|
22
|
-
z.union([
|
|
23
|
-
z.union([z.null(), z.undefined()]).transform<z.infer<Z>[]>(() => []),
|
|
24
|
-
item.array(),
|
|
25
|
-
]);
|
|
22
|
+
z.union([z.null().transform<z.infer<Z>[]>(() => []), item.array()]).default(() => []);
|
|
@@ -403,6 +403,35 @@ describe("caseconv", () => {
|
|
|
403
403
|
expect(result.values[0].data.One).toBe(1);
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
+
it("should resolve array element schema through wrapper schemas (default, optional, nullable)", () => {
|
|
407
|
+
const elementZ = z.object({
|
|
408
|
+
data: caseconv.preserveCase(z.record(z.string(), z.unknown())),
|
|
409
|
+
});
|
|
410
|
+
const input = {
|
|
411
|
+
items: [{ data: { CamelKey: 1, snake_key: 2, PascalKey: { Inner_Key: 3 } } }],
|
|
412
|
+
};
|
|
413
|
+
const expectPreserved = (result: R) => {
|
|
414
|
+
expect(result.items[0].data.CamelKey).toBe(1);
|
|
415
|
+
expect(result.items[0].data.snake_key).toBe(2);
|
|
416
|
+
expect(result.items[0].data.PascalKey.Inner_Key).toBe(3);
|
|
417
|
+
};
|
|
418
|
+
expectPreserved(
|
|
419
|
+
caseconv.snakeToCamel(input, {
|
|
420
|
+
schema: z.object({ items: elementZ.array().default(() => []) }),
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
expectPreserved(
|
|
424
|
+
caseconv.snakeToCamel(input, {
|
|
425
|
+
schema: z.object({ items: elementZ.array().optional() }),
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
expectPreserved(
|
|
429
|
+
caseconv.snakeToCamel(input, {
|
|
430
|
+
schema: z.object({ items: elementZ.array().nullable() }),
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
406
435
|
it("should handle array.nullishToEmpty with preserveCase on element field", async () => {
|
|
407
436
|
const { nullishToEmpty } = await import("@/array/nullable");
|
|
408
437
|
const elementZ = z.object({
|
|
@@ -692,5 +721,47 @@ describe("caseconv", () => {
|
|
|
692
721
|
expect(decoded.linePlots[0].data.my_custom_key).toBeUndefined();
|
|
693
722
|
});
|
|
694
723
|
});
|
|
724
|
+
|
|
725
|
+
describe("discriminated union shape merging", () => {
|
|
726
|
+
it("should resolve a per-variant field schema when walking a discriminated union", () => {
|
|
727
|
+
const variantA = z.object({
|
|
728
|
+
type: z.literal("a"),
|
|
729
|
+
fieldA: caseconv.preserveCase(z.record(z.string(), z.unknown())),
|
|
730
|
+
});
|
|
731
|
+
const variantB = z.object({
|
|
732
|
+
type: z.literal("b"),
|
|
733
|
+
fieldB: caseconv.preserveCase(z.record(z.string(), z.unknown())),
|
|
734
|
+
});
|
|
735
|
+
const unionZ = z.discriminatedUnion("type", [variantA, variantB]);
|
|
736
|
+
const wrapperZ = z.object({ envelope: unionZ });
|
|
737
|
+
const inputB = {
|
|
738
|
+
envelope: {
|
|
739
|
+
type: "b",
|
|
740
|
+
fieldB: { CamelKey: 1, snake_key: 2, PascalKey: { Inner_Key: 3 } },
|
|
741
|
+
},
|
|
742
|
+
};
|
|
743
|
+
const encoded = caseconv.camelToSnake(inputB, { schema: wrapperZ }) as R;
|
|
744
|
+
expect(encoded.envelope.field_b.CamelKey).toBe(1);
|
|
745
|
+
expect(encoded.envelope.field_b.snake_key).toBe(2);
|
|
746
|
+
expect(encoded.envelope.field_b.PascalKey.Inner_Key).toBe(3);
|
|
747
|
+
expect(encoded.envelope.field_b.camel_key).toBeUndefined();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("should preserve case through preserveCase wrapping a union (record.nullishToEmpty)", () => {
|
|
751
|
+
const innerZ = caseconv.preserveCase(
|
|
752
|
+
z
|
|
753
|
+
.union([z.null().transform(() => ({})), z.record(z.string(), z.unknown())])
|
|
754
|
+
.default(() => ({})),
|
|
755
|
+
);
|
|
756
|
+
const schema = z.object({ payload: innerZ });
|
|
757
|
+
const input = {
|
|
758
|
+
payload: { CamelKey: 1, snake_key: 2, PascalKey: { Inner_Key: 3 } },
|
|
759
|
+
};
|
|
760
|
+
const encoded = caseconv.camelToSnake(input, { schema }) as R;
|
|
761
|
+
expect(encoded.payload.CamelKey).toBe(1);
|
|
762
|
+
expect(encoded.payload.snake_key).toBe(2);
|
|
763
|
+
expect(encoded.payload.PascalKey.Inner_Key).toBe(3);
|
|
764
|
+
});
|
|
765
|
+
});
|
|
695
766
|
});
|
|
696
767
|
});
|
package/src/caseconv/caseconv.ts
CHANGED
|
@@ -97,6 +97,8 @@ const getArrayElementSchema = (
|
|
|
97
97
|
if (schema == null) return undefined;
|
|
98
98
|
const def = (schema as ZodSchema)._zod?.def;
|
|
99
99
|
if (def?.type === "array" && def.element != null) return def.element as z.ZodType;
|
|
100
|
+
// Traverse through wrappers (optional, nullable, default, catch) to inner schema
|
|
101
|
+
if (def?.innerType != null) return getArrayElementSchema(def.innerType);
|
|
100
102
|
// Handle union types that may contain arrays (e.g., nullishToEmpty)
|
|
101
103
|
if (def?.type === "union" && Array.isArray(def.options))
|
|
102
104
|
for (const option of def.options) {
|
|
@@ -111,6 +113,12 @@ const getArrayElementSchema = (
|
|
|
111
113
|
* Extracts the shape (field name → ZodType map) from a Zod schema.
|
|
112
114
|
* Traverses through wrappers (optional, nullable, default, catch, union, pipe)
|
|
113
115
|
* to find the inner object schema's shape. Returns null for non-object schemas.
|
|
116
|
+
*
|
|
117
|
+
* For unions (including discriminated unions), the shapes of every option are
|
|
118
|
+
* merged, with earlier options taking precedence on key conflicts. This lets a
|
|
119
|
+
* caller looking up a single field name resolve it to the variant that
|
|
120
|
+
* actually declares it — e.g., walking a discriminated union of action
|
|
121
|
+
* payloads where each variant carries a distinct inner field.
|
|
114
122
|
*/
|
|
115
123
|
const getSchemaShape = (
|
|
116
124
|
schema: z.ZodType | z.core.SomeType | undefined,
|
|
@@ -125,11 +133,16 @@ const getSchemaShape = (
|
|
|
125
133
|
const def = s._zod?.def;
|
|
126
134
|
if (def == null) return null;
|
|
127
135
|
if (def.innerType != null) return getSchemaShape(def.innerType);
|
|
128
|
-
if (def.type === "union" && Array.isArray(def.options))
|
|
136
|
+
if (def.type === "union" && Array.isArray(def.options)) {
|
|
137
|
+
let merged: Record<string, z.ZodType> | null = null;
|
|
129
138
|
for (const option of def.options) {
|
|
130
139
|
const result = getSchemaShape(option);
|
|
131
|
-
if (result
|
|
140
|
+
if (result == null) continue;
|
|
141
|
+
if (merged == null) merged = { ...result };
|
|
142
|
+
else for (const k in result) if (!(k in merged)) merged[k] = result[k];
|
|
132
143
|
}
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
133
146
|
|
|
134
147
|
if (def.type === "pipe") return getSchemaShape(def.in) ?? getSchemaShape(def.out);
|
|
135
148
|
return null;
|
|
@@ -46,4 +46,119 @@ describe("compare", () => {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
describe("mapsEqual", () => {
|
|
51
|
+
it("returns true for the same reference", () => {
|
|
52
|
+
const m = new Map<string, number>([["a", 1]]);
|
|
53
|
+
expect(compare.mapsEqual(m, m)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns true for content-equal maps with identity-equal values", () => {
|
|
57
|
+
const a = new Map<string, number>([
|
|
58
|
+
["a", 1],
|
|
59
|
+
["b", 2],
|
|
60
|
+
]);
|
|
61
|
+
const b = new Map<string, number>([
|
|
62
|
+
["a", 1],
|
|
63
|
+
["b", 2],
|
|
64
|
+
]);
|
|
65
|
+
expect(compare.mapsEqual(a, b)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns true regardless of insertion order", () => {
|
|
69
|
+
const a = new Map<string, number>([
|
|
70
|
+
["a", 1],
|
|
71
|
+
["b", 2],
|
|
72
|
+
]);
|
|
73
|
+
const b = new Map<string, number>([
|
|
74
|
+
["b", 2],
|
|
75
|
+
["a", 1],
|
|
76
|
+
]);
|
|
77
|
+
expect(compare.mapsEqual(a, b)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns false when sizes differ", () => {
|
|
81
|
+
const a = new Map<string, number>([["a", 1]]);
|
|
82
|
+
const b = new Map<string, number>([
|
|
83
|
+
["a", 1],
|
|
84
|
+
["b", 2],
|
|
85
|
+
]);
|
|
86
|
+
expect(compare.mapsEqual(a, b)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns false when a key is present in one but missing in the other", () => {
|
|
90
|
+
const a = new Map<string, number>([
|
|
91
|
+
["a", 1],
|
|
92
|
+
["b", 2],
|
|
93
|
+
]);
|
|
94
|
+
const b = new Map<string, number>([
|
|
95
|
+
["a", 1],
|
|
96
|
+
["c", 2],
|
|
97
|
+
]);
|
|
98
|
+
expect(compare.mapsEqual(a, b)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns false when a value differs by identity", () => {
|
|
102
|
+
const a = new Map<string, { v: number }>([["a", { v: 1 }]]);
|
|
103
|
+
const b = new Map<string, { v: number }>([["a", { v: 1 }]]);
|
|
104
|
+
expect(compare.mapsEqual(a, b)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns true when a value is the same object reference", () => {
|
|
108
|
+
const shared = { v: 1 };
|
|
109
|
+
const a = new Map<string, { v: number }>([["a", shared]]);
|
|
110
|
+
const b = new Map<string, { v: number }>([["a", shared]]);
|
|
111
|
+
expect(compare.mapsEqual(a, b)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("treats undefined values as present", () => {
|
|
115
|
+
const a = new Map<string, number | undefined>([["a", undefined]]);
|
|
116
|
+
const b = new Map<string, number | undefined>([["b", undefined]]);
|
|
117
|
+
expect(compare.mapsEqual(a, b)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns true for two empty maps", () => {
|
|
121
|
+
expect(compare.mapsEqual(new Map(), new Map())).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("arraysEqual", () => {
|
|
126
|
+
it("returns true for the same reference", () => {
|
|
127
|
+
const a = [1, 2, 3];
|
|
128
|
+
expect(compare.arraysEqual(a, a)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("returns true for content-equal primitive arrays", () => {
|
|
132
|
+
expect(compare.arraysEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns false when lengths differ", () => {
|
|
136
|
+
expect(compare.arraysEqual([1, 2], [1, 2, 3])).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns false when an element differs by value", () => {
|
|
140
|
+
expect(compare.arraysEqual([1, 2, 3], [1, 9, 3])).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("is order-sensitive", () => {
|
|
144
|
+
expect(compare.arraysEqual([1, 2, 3], [3, 2, 1])).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("uses identity equality for object elements", () => {
|
|
148
|
+
const a = { v: 1 };
|
|
149
|
+
const b = { v: 1 };
|
|
150
|
+
expect(compare.arraysEqual([a], [a])).toBe(true);
|
|
151
|
+
expect(compare.arraysEqual([a], [b])).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns true for two empty arrays", () => {
|
|
155
|
+
expect(compare.arraysEqual([], [])).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("works with readonly arrays", () => {
|
|
159
|
+
const a: readonly number[] = [1, 2, 3];
|
|
160
|
+
const b: readonly number[] = [1, 2, 3];
|
|
161
|
+
expect(compare.arraysEqual(a, b)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
49
164
|
});
|
package/src/compare/compare.ts
CHANGED
|
@@ -104,6 +104,35 @@ export const uniqueUnorderedPrimitiveArrays = <T extends primitive.Value>(
|
|
|
104
104
|
return unorderedPrimitiveArrays(uniqueA, uniqueB);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Returns true if the two maps have the same set of keys and each value is
|
|
109
|
+
* identity-equal (===). Intended for use as a reference-stability comparator
|
|
110
|
+
* (e.g. React's `useSyncExternalStoreWithSelector`) where the values are
|
|
111
|
+
* already reference-stable in storage (e.g. Immer state). For deeper value
|
|
112
|
+
* comparison, compose with `deep.equal` at the call site.
|
|
113
|
+
*/
|
|
114
|
+
export const mapsEqual = <K, V>(a: Map<K, V>, b: Map<K, V>): boolean => {
|
|
115
|
+
if (a === b) return true;
|
|
116
|
+
if (a.size !== b.size) return false;
|
|
117
|
+
for (const [k, v] of a) if (!b.has(k) || b.get(k) !== v) return false;
|
|
118
|
+
return true;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns true if the two arrays have the same length and each element is
|
|
123
|
+
* identity-equal (===). Same use case as `mapsEqual` for selectors that
|
|
124
|
+
* derive arrays from reference-stable storage.
|
|
125
|
+
*/
|
|
126
|
+
export const arraysEqual = <T>(
|
|
127
|
+
a: readonly T[] | T[],
|
|
128
|
+
b: readonly T[] | T[],
|
|
129
|
+
): boolean => {
|
|
130
|
+
if (a === b) return true;
|
|
131
|
+
if (a.length !== b.length) return false;
|
|
132
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
107
136
|
export const order = (a: spatial.Order, b: spatial.Order): number => {
|
|
108
137
|
if (a === b) return 0;
|
|
109
138
|
if (a === "first" && b === "last") return 1;
|
|
@@ -7,45 +7,279 @@
|
|
|
7
7
|
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
8
|
// included in the file licenses/APL.txt.
|
|
9
9
|
|
|
10
|
-
import { describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
11
|
|
|
12
|
-
import { debounce
|
|
12
|
+
import { debounce } from "@/debounce/debounce";
|
|
13
|
+
import { TimeSpan } from "@/telem/telem";
|
|
13
14
|
|
|
14
15
|
describe("debounce", () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.useFakeTimers();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should not execute immediately", () => {
|
|
25
|
+
const fn = vi.fn();
|
|
26
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
27
|
+
|
|
28
|
+
debounced(10);
|
|
29
|
+
|
|
30
|
+
expect(fn).toHaveBeenCalledTimes(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should execute after the wait period", () => {
|
|
34
|
+
const fn = vi.fn();
|
|
35
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
36
|
+
|
|
37
|
+
debounced(10);
|
|
38
|
+
|
|
39
|
+
vi.advanceTimersByTime(99);
|
|
40
|
+
expect(fn).toHaveBeenCalledTimes(0);
|
|
41
|
+
|
|
42
|
+
vi.advanceTimersByTime(1);
|
|
43
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(fn).toHaveBeenLastCalledWith(10);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should reset the timer on repeated calls", () => {
|
|
48
|
+
const fn = vi.fn();
|
|
49
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
50
|
+
|
|
51
|
+
debounced(10);
|
|
52
|
+
vi.advanceTimersByTime(90);
|
|
53
|
+
|
|
54
|
+
debounced(20);
|
|
55
|
+
vi.advanceTimersByTime(90);
|
|
56
|
+
|
|
57
|
+
expect(fn).toHaveBeenCalledTimes(0);
|
|
58
|
+
|
|
59
|
+
vi.advanceTimersByTime(10);
|
|
60
|
+
|
|
61
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(fn).toHaveBeenLastCalledWith(20);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should run with the latest arguments", () => {
|
|
66
|
+
const fn = vi.fn();
|
|
67
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
68
|
+
|
|
69
|
+
debounced(10);
|
|
70
|
+
debounced(20);
|
|
71
|
+
debounced(30);
|
|
72
|
+
debounced(40);
|
|
73
|
+
|
|
74
|
+
vi.advanceTimersByTime(100);
|
|
75
|
+
|
|
76
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(fn).toHaveBeenLastCalledWith(40);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should not execute before the full wait period since the latest call", () => {
|
|
81
|
+
const fn = vi.fn();
|
|
82
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
83
|
+
|
|
84
|
+
debounced(10);
|
|
85
|
+
vi.advanceTimersByTime(50);
|
|
86
|
+
|
|
87
|
+
debounced(20);
|
|
88
|
+
vi.advanceTimersByTime(99);
|
|
89
|
+
|
|
90
|
+
expect(fn).toHaveBeenCalledTimes(0);
|
|
91
|
+
|
|
92
|
+
vi.advanceTimersByTime(1);
|
|
93
|
+
|
|
94
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(fn).toHaveBeenLastCalledWith(20);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should only execute once for a burst of calls", () => {
|
|
99
|
+
const fn = vi.fn();
|
|
100
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
101
|
+
|
|
102
|
+
debounced(10);
|
|
103
|
+
vi.advanceTimersByTime(25);
|
|
104
|
+
debounced(20);
|
|
105
|
+
vi.advanceTimersByTime(25);
|
|
106
|
+
debounced(30);
|
|
107
|
+
vi.advanceTimersByTime(25);
|
|
108
|
+
debounced(40);
|
|
109
|
+
|
|
110
|
+
vi.advanceTimersByTime(99);
|
|
111
|
+
expect(fn).toHaveBeenCalledTimes(0);
|
|
112
|
+
|
|
113
|
+
vi.advanceTimersByTime(1);
|
|
114
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(fn).toHaveBeenLastCalledWith(40);
|
|
116
|
+
|
|
117
|
+
vi.advanceTimersByTime(100);
|
|
118
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should execute again after a previous debounce cycle completes", () => {
|
|
122
|
+
const fn = vi.fn();
|
|
123
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
124
|
+
|
|
125
|
+
debounced(10);
|
|
126
|
+
vi.advanceTimersByTime(100);
|
|
127
|
+
|
|
128
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(fn).toHaveBeenNthCalledWith(1, 10);
|
|
130
|
+
|
|
131
|
+
debounced(20);
|
|
132
|
+
vi.advanceTimersByTime(100);
|
|
133
|
+
|
|
134
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
135
|
+
expect(fn).toHaveBeenNthCalledWith(2, 20);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should preserve null arguments", () => {
|
|
139
|
+
const fn = vi.fn();
|
|
140
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
141
|
+
|
|
142
|
+
debounced(10);
|
|
143
|
+
debounced(null);
|
|
144
|
+
debounced(20);
|
|
145
|
+
debounced(null);
|
|
146
|
+
|
|
147
|
+
vi.advanceTimersByTime(100);
|
|
148
|
+
|
|
149
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(fn).toHaveBeenLastCalledWith(null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should preserve multiple arguments", () => {
|
|
154
|
+
const fn = vi.fn();
|
|
155
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
156
|
+
|
|
157
|
+
debounced(1, "a", false);
|
|
158
|
+
debounced(2, "b", true);
|
|
159
|
+
|
|
160
|
+
vi.advanceTimersByTime(100);
|
|
161
|
+
|
|
162
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(fn).toHaveBeenLastCalledWith(2, "b", true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should clear pending state before invoking the function", () => {
|
|
167
|
+
const fn = vi.fn();
|
|
168
|
+
fn.mockImplementation((value: number) => {
|
|
169
|
+
if (value === 20) debounced(30);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
173
|
+
|
|
174
|
+
debounced(10);
|
|
175
|
+
debounced(20);
|
|
176
|
+
|
|
177
|
+
vi.advanceTimersByTime(100);
|
|
178
|
+
|
|
179
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(fn).toHaveBeenNthCalledWith(1, 20);
|
|
181
|
+
|
|
182
|
+
vi.advanceTimersByTime(99);
|
|
183
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
184
|
+
|
|
185
|
+
vi.advanceTimersByTime(1);
|
|
186
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
187
|
+
expect(fn).toHaveBeenNthCalledWith(2, 30);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should invoke the function synchronously when the wait period is 0", () => {
|
|
191
|
+
const fn = vi.fn();
|
|
192
|
+
const debounced = debounce(fn, TimeSpan.ZERO);
|
|
193
|
+
|
|
194
|
+
debounced(10);
|
|
195
|
+
|
|
196
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(fn).toHaveBeenLastCalledWith(10);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("cancel", () => {
|
|
201
|
+
it("should drop a pending invocation", () => {
|
|
18
202
|
const fn = vi.fn();
|
|
19
|
-
const debounced = debounce(fn, 100);
|
|
203
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
204
|
+
|
|
205
|
+
debounced(10);
|
|
206
|
+
debounced.cancel();
|
|
207
|
+
|
|
208
|
+
vi.advanceTimersByTime(200);
|
|
209
|
+
|
|
210
|
+
expect(fn).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should be a no-op when no invocation is pending", () => {
|
|
214
|
+
const fn = vi.fn();
|
|
215
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
216
|
+
|
|
217
|
+
expect(() => debounced.cancel()).not.toThrow();
|
|
218
|
+
|
|
20
219
|
debounced(10);
|
|
21
|
-
debounced(20);
|
|
22
|
-
debounced(30);
|
|
23
|
-
debounced(40);
|
|
24
|
-
expect(fn).toHaveBeenCalledTimes(0);
|
|
25
220
|
vi.advanceTimersByTime(100);
|
|
221
|
+
|
|
26
222
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
27
|
-
expect(fn).
|
|
223
|
+
expect(fn).toHaveBeenLastCalledWith(10);
|
|
28
224
|
});
|
|
29
|
-
|
|
225
|
+
|
|
226
|
+
it("should be a no-op for a zero-wait debounce", () => {
|
|
30
227
|
const fn = vi.fn();
|
|
31
|
-
const debounced = debounce(fn,
|
|
32
|
-
|
|
33
|
-
expect(debounced).
|
|
228
|
+
const debounced = debounce(fn, TimeSpan.ZERO);
|
|
229
|
+
|
|
230
|
+
expect(() => debounced.cancel()).not.toThrow();
|
|
231
|
+
|
|
232
|
+
debounced(10);
|
|
233
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
34
234
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
it("should throttle the execution of the given function", () => {
|
|
38
|
-
vi.useFakeTimers();
|
|
235
|
+
|
|
236
|
+
it("should allow scheduling a new invocation after cancellation", () => {
|
|
39
237
|
const fn = vi.fn();
|
|
40
|
-
const debounced =
|
|
238
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
239
|
+
|
|
41
240
|
debounced(10);
|
|
241
|
+
debounced.cancel();
|
|
242
|
+
vi.advanceTimersByTime(100);
|
|
243
|
+
expect(fn).not.toHaveBeenCalled();
|
|
244
|
+
|
|
42
245
|
debounced(20);
|
|
43
|
-
debounced(30);
|
|
44
|
-
debounced(40);
|
|
45
246
|
vi.advanceTimersByTime(100);
|
|
247
|
+
|
|
46
248
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
47
|
-
expect(fn).
|
|
48
|
-
|
|
249
|
+
expect(fn).toHaveBeenLastCalledWith(20);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("flush", () => {
|
|
254
|
+
it("should invoke a pending call immediately with the latest arguments", () => {
|
|
255
|
+
const fn = vi.fn();
|
|
256
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
257
|
+
|
|
258
|
+
debounced(10);
|
|
259
|
+
debounced(20);
|
|
260
|
+
debounced.flush();
|
|
261
|
+
|
|
262
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(fn).toHaveBeenLastCalledWith(20);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should be a no-op when no invocation is pending", () => {
|
|
267
|
+
const fn = vi.fn();
|
|
268
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
269
|
+
|
|
270
|
+
debounced.flush();
|
|
271
|
+
|
|
272
|
+
expect(fn).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should clear the pending timer so the function does not fire again later", () => {
|
|
276
|
+
const fn = vi.fn();
|
|
277
|
+
const debounced = debounce(fn, TimeSpan.milliseconds(100));
|
|
278
|
+
|
|
279
|
+
debounced(10);
|
|
280
|
+
debounced.flush();
|
|
281
|
+
vi.advanceTimersByTime(200);
|
|
282
|
+
|
|
49
283
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
50
284
|
});
|
|
51
285
|
});
|