@synnaxlabs/x 0.53.0 → 0.54.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.
Files changed (119) hide show
  1. package/.turbo/turbo-build.log +14 -8
  2. package/dist/src/array/nullable.d.ts +10 -1
  3. package/dist/src/array/nullable.d.ts.map +1 -1
  4. package/dist/src/binary/codec.d.ts +5 -5
  5. package/dist/src/binary/codec.d.ts.map +1 -1
  6. package/dist/src/caseconv/caseconv.bench.d.ts +2 -0
  7. package/dist/src/caseconv/caseconv.bench.d.ts.map +1 -0
  8. package/dist/src/caseconv/caseconv.d.ts +19 -1
  9. package/dist/src/caseconv/caseconv.d.ts.map +1 -1
  10. package/dist/src/color/color.d.ts +22 -24
  11. package/dist/src/color/color.d.ts.map +1 -1
  12. package/dist/src/color/gradient.d.ts +10 -10
  13. package/dist/src/color/palette.d.ts +16 -16
  14. package/dist/src/control/control.d.ts +21 -37
  15. package/dist/src/control/control.d.ts.map +1 -1
  16. package/dist/src/control/external.d.ts +3 -0
  17. package/dist/src/control/external.d.ts.map +1 -0
  18. package/dist/src/control/index.d.ts +1 -1
  19. package/dist/src/control/index.d.ts.map +1 -1
  20. package/dist/src/control/types.gen.d.ts +34 -0
  21. package/dist/src/control/types.gen.d.ts.map +1 -0
  22. package/dist/src/deep/merge.d.ts.map +1 -1
  23. package/dist/src/label/index.d.ts +1 -1
  24. package/dist/src/label/index.d.ts.map +1 -1
  25. package/dist/src/label/types.gen.d.ts +51 -0
  26. package/dist/src/label/types.gen.d.ts.map +1 -0
  27. package/dist/src/math/constants.d.ts +1 -0
  28. package/dist/src/math/constants.d.ts.map +1 -1
  29. package/dist/src/record/record.d.ts +12 -12
  30. package/dist/src/record/record.d.ts.map +1 -1
  31. package/dist/src/spatial/base.d.ts +40 -43
  32. package/dist/src/spatial/base.d.ts.map +1 -1
  33. package/dist/src/spatial/bounds/bounds.d.ts +2 -2
  34. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  35. package/dist/src/spatial/box/box.d.ts +4 -16
  36. package/dist/src/spatial/box/box.d.ts.map +1 -1
  37. package/dist/src/spatial/dimensions/dimensions.d.ts +3 -7
  38. package/dist/src/spatial/dimensions/dimensions.d.ts.map +1 -1
  39. package/dist/src/spatial/direction/direction.d.ts +3 -3
  40. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  41. package/dist/src/spatial/external.d.ts +1 -0
  42. package/dist/src/spatial/external.d.ts.map +1 -1
  43. package/dist/src/spatial/location/location.d.ts +9 -3
  44. package/dist/src/spatial/location/location.d.ts.map +1 -1
  45. package/dist/src/spatial/spatial.d.ts +2 -1
  46. package/dist/src/spatial/spatial.d.ts.map +1 -1
  47. package/dist/src/spatial/types.gen.d.ts +20 -0
  48. package/dist/src/spatial/types.gen.d.ts.map +1 -0
  49. package/dist/src/spatial/xy/xy.d.ts +3 -2
  50. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  51. package/dist/src/status/external.d.ts +3 -0
  52. package/dist/src/status/external.d.ts.map +1 -0
  53. package/dist/src/status/index.d.ts +1 -1
  54. package/dist/src/status/index.d.ts.map +1 -1
  55. package/dist/src/status/status.d.ts +5 -35
  56. package/dist/src/status/status.d.ts.map +1 -1
  57. package/dist/src/status/types.gen.d.ts +74 -0
  58. package/dist/src/status/types.gen.d.ts.map +1 -0
  59. package/dist/src/telem/external.d.ts +4 -0
  60. package/dist/src/telem/external.d.ts.map +1 -0
  61. package/dist/src/telem/index.d.ts +2 -3
  62. package/dist/src/telem/index.d.ts.map +1 -1
  63. package/dist/src/telem/telem.d.ts +62 -0
  64. package/dist/src/telem/telem.d.ts.map +1 -1
  65. package/dist/src/zod/nullToUndefined.d.ts +1 -1
  66. package/dist/src/zod/nullToUndefined.d.ts.map +1 -1
  67. package/dist/src/zod/schemas.d.ts +6 -0
  68. package/dist/src/zod/schemas.d.ts.map +1 -1
  69. package/dist/x.cjs +8 -13
  70. package/dist/x.js +4393 -5591
  71. package/package.json +8 -8
  72. package/src/array/nullable.ts +10 -1
  73. package/src/binary/codec.spec.ts +31 -0
  74. package/src/binary/codec.ts +9 -8
  75. package/src/caseconv/caseconv.bench.ts +270 -0
  76. package/src/caseconv/caseconv.spec.ts +534 -0
  77. package/src/caseconv/caseconv.ts +186 -41
  78. package/src/color/color.spec.ts +51 -36
  79. package/src/color/color.ts +7 -8
  80. package/src/control/control.ts +7 -32
  81. package/src/{label/label.ts → control/external.ts} +2 -13
  82. package/src/control/index.ts +1 -1
  83. package/src/control/types.gen.ts +52 -0
  84. package/src/deep/merge.ts +2 -1
  85. package/src/deep/path.ts +1 -1
  86. package/src/deep/remove.ts +1 -1
  87. package/src/label/index.ts +1 -1
  88. package/src/label/types.gen.ts +35 -0
  89. package/src/math/constants.ts +1 -0
  90. package/src/migrate/migrate.ts +2 -2
  91. package/src/record/record.spec.ts +31 -7
  92. package/src/record/record.ts +17 -17
  93. package/src/spatial/base.ts +63 -39
  94. package/src/spatial/bounds/bounds.ts +2 -2
  95. package/src/spatial/box/box.ts +2 -2
  96. package/src/spatial/dimensions/dimensions.ts +11 -5
  97. package/src/spatial/direction/direction.ts +2 -2
  98. package/src/spatial/external.ts +1 -0
  99. package/src/spatial/location/location.ts +15 -13
  100. package/src/spatial/spatial.ts +29 -2
  101. package/src/spatial/sticky/sticky.ts +1 -1
  102. package/src/spatial/types.gen.ts +28 -0
  103. package/src/spatial/xy/xy.ts +9 -10
  104. package/src/status/external.ts +11 -0
  105. package/src/status/index.ts +1 -1
  106. package/src/status/status.ts +11 -59
  107. package/src/status/types.gen.ts +118 -0
  108. package/src/strings/deduplicateFileName.ts +3 -3
  109. package/src/telem/external.ts +12 -0
  110. package/src/telem/index.ts +2 -3
  111. package/src/telem/telem.ts +30 -5
  112. package/src/zod/nullToUndefined.ts +4 -4
  113. package/src/zod/schemas.ts +9 -2
  114. package/src/zod/util.ts +2 -2
  115. package/tsconfig.json +3 -2
  116. package/tsconfig.tsbuildinfo +1 -1
  117. package/vite.config.ts +8 -1
  118. package/dist/src/label/label.d.ts +0 -25
  119. package/dist/src/label/label.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/x",
3
- "version": "0.53.0",
3
+ "version": "0.54.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,18 +14,18 @@
14
14
  ],
15
15
  "dependencies": {
16
16
  "async-mutex": "^0.5.0",
17
- "nanoid": "^5.1.6",
17
+ "nanoid": "^5.1.7",
18
18
  "uuid": "^13.0.0",
19
19
  "zod": "^4.3.6"
20
20
  },
21
21
  "devDependencies": {
22
- "@types/node": "^25.1.0",
23
- "@vitest/coverage-v8": "^3.2.4",
24
- "eslint": "^9.39.2",
22
+ "@types/node": "^25.5.0",
23
+ "@vitest/coverage-v8": "^4.1.2",
24
+ "eslint": "^10.1.0",
25
25
  "madge": "^8.0.0",
26
- "typescript": "^5.9.3",
27
- "vite": "^7.3.1",
28
- "vitest": "^3.2.4",
26
+ "typescript": "^6.0.2",
27
+ "vite": "^8.0.3",
28
+ "vitest": "^4.1.2",
29
29
  "@synnaxlabs/eslint-config": "^0.0.0",
30
30
  "@synnaxlabs/tsconfig": "^0.0.0",
31
31
  "@synnaxlabs/vite-plugin": "^0.0.0"
@@ -9,7 +9,16 @@
9
9
 
10
10
  import z from "zod";
11
11
 
12
- export const nullableZ = <Z extends z.ZodType>(item: Z) =>
12
+ /**
13
+ * For required arrays: coerces null/undefined to empty array [].
14
+ * Use when the array must always be present and iterable.
15
+ *
16
+ * - null → []
17
+ * - undefined → []
18
+ * - [] → []
19
+ * - [items] → [items]
20
+ */
21
+ export const nullishToEmpty = <Z extends z.ZodType>(item: Z) =>
13
22
  z.union([
14
23
  z.union([z.null(), z.undefined()]).transform<z.infer<Z>[]>(() => []),
15
24
  item.array(),
@@ -55,6 +55,37 @@ describe("Codec", () => {
55
55
  const decoded = binary.JSON_CODEC.decodeString(encoded, sampleSchema);
56
56
  expect(decoded.channelKey).toEqual("test");
57
57
  });
58
+
59
+ it("should validate with schema when encoding", () => {
60
+ const schema = z.object({
61
+ name: z.string(),
62
+ count: z.number(),
63
+ });
64
+ const sample = { name: "test", count: 42 };
65
+ const encoded = binary.JSON_CODEC.encodeString(sample, schema);
66
+ const parsed = JSON.parse(encoded);
67
+ expect(parsed.name).toEqual("test");
68
+ expect(parsed.count).toEqual(42);
69
+ });
70
+
71
+ it("should throw on invalid data when schema is provided to encode", () => {
72
+ const schema = z.object({
73
+ name: z.string(),
74
+ count: z.number(),
75
+ });
76
+ const sample = { name: "test", count: "not a number" };
77
+ expect(() => binary.JSON_CODEC.encodeString(sample, schema)).toThrow();
78
+ });
79
+
80
+ it("should apply transforms when encoding with schema", () => {
81
+ const schema = z.object({
82
+ value: z.coerce.number(),
83
+ });
84
+ const sample = { value: "42" };
85
+ const encoded = binary.JSON_CODEC.encodeString(sample, schema);
86
+ const parsed = JSON.parse(encoded);
87
+ expect(parsed.value).toEqual(42);
88
+ });
58
89
  });
59
90
 
60
91
  describe("CSV", () => {
@@ -26,7 +26,7 @@ export interface Codec {
26
26
  * @param payload - The payload to encode.
27
27
  * @returns An ArrayBuffer containing the encoded payload.
28
28
  */
29
- encode: (payload: unknown) => Uint8Array;
29
+ encode: (payload: unknown, schema?: z.ZodType) => Uint8Array<ArrayBuffer>;
30
30
 
31
31
  /**
32
32
  * Decodes the given binary representation into a type checked payload.
@@ -51,8 +51,8 @@ export class JSONCodec implements Codec {
51
51
  this.encoder = new TextEncoder();
52
52
  }
53
53
 
54
- encode(payload: unknown): Uint8Array {
55
- return this.encoder.encode(this.encodeString(payload));
54
+ encode(payload: unknown, schema?: z.ZodType): Uint8Array<ArrayBuffer> {
55
+ return this.encoder.encode(this.encodeString(payload, schema));
56
56
  }
57
57
 
58
58
  decode<P extends z.ZodType>(data: Uint8Array | ArrayBuffer, schema?: P): z.infer<P> {
@@ -61,12 +61,13 @@ export class JSONCodec implements Codec {
61
61
 
62
62
  decodeString<P extends z.ZodType>(data: string, schema?: P): z.infer<P> {
63
63
  const parsed = JSON.parse(data);
64
- const unpacked = caseconv.snakeToCamel(parsed);
64
+ const unpacked = caseconv.snakeToCamel(parsed, { schema });
65
65
  return schema != null ? schema.parse(unpacked) : (unpacked as z.infer<P>);
66
66
  }
67
67
 
68
- encodeString(payload: unknown): string {
69
- const caseConverted = caseconv.camelToSnake(payload);
68
+ encodeString(payload: unknown, schema?: z.ZodType): string {
69
+ const parsed = schema != null ? schema.parse(payload) : payload;
70
+ const caseConverted = caseconv.camelToSnake(parsed ?? {}, { schema });
70
71
  return JSON.stringify(caseConverted, (_, v) => {
71
72
  if (ArrayBuffer.isView(v)) return Array.from(v as Uint8Array);
72
73
  if (typeof v === "bigint") return v.toString();
@@ -81,7 +82,7 @@ export class JSONCodec implements Codec {
81
82
  export class CSVCodec implements Codec {
82
83
  contentType = "text/csv";
83
84
 
84
- encode(payload: unknown): Uint8Array {
85
+ encode(payload: unknown): Uint8Array<ArrayBuffer> {
85
86
  const csvString = this.encodeString(payload);
86
87
  return new TextEncoder().encode(csvString);
87
88
  }
@@ -144,7 +145,7 @@ export class CSVCodec implements Codec {
144
145
  export class TextCodec implements Codec {
145
146
  contentType = "text/plain";
146
147
 
147
- encode(payload: unknown): Uint8Array {
148
+ encode(payload: unknown): Uint8Array<ArrayBuffer> {
148
149
  if (typeof payload !== "string")
149
150
  throw new Error("TextCodec.encode payload must be a string");
150
151
  return new TextEncoder().encode(payload);
@@ -0,0 +1,270 @@
1
+ // Copyright 2026 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { bench, describe } from "vitest";
11
+ import { z } from "zod";
12
+
13
+ import { caseconv } from "@/caseconv";
14
+ import { record } from "@/record";
15
+
16
+ describe("caseconv string conversion", () => {
17
+ bench("snakeToCamel - no conversion needed", () => {
18
+ caseconv.snakeToCamel("alreadyCamelCase");
19
+ });
20
+
21
+ bench("camelToSnake - no conversion needed", () => {
22
+ caseconv.camelToSnake("already_snake_case");
23
+ });
24
+
25
+ bench("snakeToCamel - short string", () => {
26
+ caseconv.snakeToCamel("channel_key_name");
27
+ });
28
+
29
+ bench("camelToSnake - short string", () => {
30
+ caseconv.camelToSnake("channelKeyName");
31
+ });
32
+
33
+ const longSnake = Array.from({ length: 20 }, (_, i) => `segment_${i}`).join("_");
34
+ const longCamel = Array.from({ length: 20 }, (_, i) => `Segment${i}`).join("");
35
+
36
+ bench("snakeToCamel - long string (20 segments)", () => {
37
+ caseconv.snakeToCamel(longSnake);
38
+ });
39
+
40
+ bench("camelToSnake - long string (20 segments)", () => {
41
+ caseconv.camelToSnake(longCamel);
42
+ });
43
+
44
+ bench("snakeToCamel - OPC UA NodeId", () => {
45
+ caseconv.snakeToCamel("ns=2;s=Temperature.Sensor.Value");
46
+ });
47
+
48
+ bench("toKebab - from camelCase", () => {
49
+ caseconv.toKebab("channelKeyName");
50
+ });
51
+
52
+ bench("toKebab - from snake_case", () => {
53
+ caseconv.toKebab("channel_key_name");
54
+ });
55
+
56
+ bench("toProperNoun - from snake_case", () => {
57
+ caseconv.toProperNoun("temperature_sensor_value");
58
+ });
59
+
60
+ bench("toProperNoun - consecutive capitals", () => {
61
+ caseconv.toProperNoun("XMLParserDocument");
62
+ });
63
+ });
64
+
65
+ describe("caseconv object conversion", () => {
66
+ const shallowObject = {
67
+ channel_key: "test",
68
+ data_type: "float32",
69
+ is_index: false,
70
+ sample_rate: 1000,
71
+ };
72
+
73
+ bench("snakeToCamel - shallow object (4 keys)", () => {
74
+ caseconv.snakeToCamel(shallowObject);
75
+ });
76
+
77
+ const nestedObject = {
78
+ channel_config: {
79
+ read_settings: { sample_rate: 1000, buffer_size: 4096 },
80
+ write_settings: { enable_ack: true, timeout_ms: 5000 },
81
+ },
82
+ };
83
+
84
+ bench("snakeToCamel - nested object (3 levels)", () => {
85
+ caseconv.snakeToCamel(nestedObject);
86
+ });
87
+
88
+ const arrayOfObjects = Array.from({ length: 10 }, (_, i) => ({
89
+ channel_key: `channel_${i}`,
90
+ data_type: "float32",
91
+ is_index: i === 0,
92
+ }));
93
+
94
+ bench("snakeToCamel - array of 10 objects", () => {
95
+ caseconv.snakeToCamel(arrayOfObjects);
96
+ });
97
+
98
+ const largeObject: Record<string, number> = {};
99
+ for (let i = 0; i < 100; i++) largeObject[`property_name_${i}`] = i;
100
+
101
+ bench("snakeToCamel - large object (100 keys)", () => {
102
+ caseconv.snakeToCamel(largeObject);
103
+ });
104
+
105
+ const objectWithPrimitiveArrays = {
106
+ channel_key: "test",
107
+ data_values: [1.0, 2.0, 3.0, 4.0, 5.0],
108
+ time_stamps: [100, 200, 300, 400, 500],
109
+ };
110
+
111
+ bench("snakeToCamel - object with primitive arrays", () => {
112
+ caseconv.snakeToCamel(objectWithPrimitiveArrays);
113
+ });
114
+
115
+ const objectWithUint8Array = {
116
+ channel_key: "binary_data",
117
+ raw_bytes: new Uint8Array([0x01, 0x02, 0x03, 0x04]),
118
+ };
119
+
120
+ bench("snakeToCamel - object with Uint8Array", () => {
121
+ caseconv.snakeToCamel(objectWithUint8Array);
122
+ });
123
+ });
124
+
125
+ describe("caseconv schema-based conversion", () => {
126
+ const schemaWithPreserve = z.object({
127
+ task_config: z.object({
128
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
129
+ }),
130
+ });
131
+
132
+ const inputWithOpcKeys = {
133
+ task_config: {
134
+ channels: {
135
+ "ns=2;s=Temperature": 123,
136
+ "ns=2;s=Pressure": 456,
137
+ holding_register_input: 789,
138
+ },
139
+ },
140
+ };
141
+
142
+ bench("snakeToCamel - with preserveCase schema", () => {
143
+ caseconv.snakeToCamel(inputWithOpcKeys, { schema: schemaWithPreserve });
144
+ });
145
+
146
+ bench("snakeToCamel - same input without schema", () => {
147
+ caseconv.snakeToCamel(inputWithOpcKeys);
148
+ });
149
+
150
+ const schemaWithoutPreserve = z.object({
151
+ task_config: z.object({
152
+ channels: z.record(z.string(), z.number()),
153
+ }),
154
+ });
155
+
156
+ bench("snakeToCamel - schema without preserveCase", () => {
157
+ caseconv.snakeToCamel(inputWithOpcKeys, { schema: schemaWithoutPreserve });
158
+ });
159
+
160
+ const complexSchema = z.object({
161
+ read: z.object({
162
+ index: z.number(),
163
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
164
+ }),
165
+ write: z.object({
166
+ index: z.number(),
167
+ channels: caseconv.preserveCase(z.record(z.string(), z.number())),
168
+ }),
169
+ });
170
+
171
+ const complexInput = {
172
+ read: { index: 0, channels: { "ns=2;s=Temp1": 1, "ns=2;s=Temp2": 2 } },
173
+ write: { index: 1, channels: { "ns=2;s=Valve1": 3, "ns=2;s=Valve2": 4 } },
174
+ };
175
+
176
+ bench("snakeToCamel - multiple preserveCase markers", () => {
177
+ caseconv.snakeToCamel(complexInput, { schema: complexSchema });
178
+ });
179
+
180
+ const deepSchema = z.object({
181
+ level1: z.object({
182
+ level2: z.object({
183
+ level3: z.object({
184
+ data: caseconv.preserveCase(z.record(z.string(), z.number())),
185
+ }),
186
+ }),
187
+ }),
188
+ });
189
+
190
+ const deepInput = {
191
+ level1: { level2: { level3: { data: { "ns=2;s=DeepValue": 42 } } } },
192
+ };
193
+
194
+ bench("snakeToCamel - deeply nested schema", () => {
195
+ caseconv.snakeToCamel(deepInput, { schema: deepSchema });
196
+ });
197
+
198
+ bench("camelToSnake - with preserveCase schema", () => {
199
+ caseconv.camelToSnake(inputWithOpcKeys, { schema: schemaWithPreserve });
200
+ });
201
+ });
202
+
203
+ describe("caseconv wrapper traversal performance", () => {
204
+ const directPreserve = z.object({
205
+ data: caseconv.preserveCase(z.record(z.string(), z.number())),
206
+ });
207
+
208
+ const optionalWrapped = z.object({
209
+ data: caseconv.preserveCase(z.record(z.string(), z.number())).optional(),
210
+ });
211
+
212
+ const nullableWrapped = z.object({
213
+ data: caseconv.preserveCase(z.record(z.string(), z.number())).nullable(),
214
+ });
215
+
216
+ const defaultWrapped = z.object({
217
+ data: caseconv.preserveCase(z.record(z.string(), z.number())).default({}),
218
+ });
219
+
220
+ const transformWrapped = z.object({
221
+ data: caseconv.preserveCase(z.record(z.string(), z.number())).transform((v) => v),
222
+ });
223
+
224
+ const deeplyNested = z.object({
225
+ data: caseconv
226
+ .preserveCase(z.record(z.string(), z.number()))
227
+ .optional()
228
+ .nullable()
229
+ .default(null),
230
+ });
231
+
232
+ // Union traversal (like nullishToEmpty)
233
+ const unionSchema = z.object({
234
+ data: record.nullishToEmpty(),
235
+ });
236
+
237
+ const dataInput = { data: { key_one: 1, key_two: 2 } };
238
+
239
+ bench("direct preserveCase (no traversal)", () => {
240
+ caseconv.snakeToCamel(dataInput, { schema: directPreserve });
241
+ });
242
+
243
+ bench("optional wrapped (1 level)", () => {
244
+ caseconv.snakeToCamel(dataInput, { schema: optionalWrapped });
245
+ });
246
+
247
+ bench("nullable wrapped (1 level)", () => {
248
+ caseconv.snakeToCamel(dataInput, { schema: nullableWrapped });
249
+ });
250
+
251
+ bench("default wrapped (1 level)", () => {
252
+ caseconv.snakeToCamel(dataInput, { schema: defaultWrapped });
253
+ });
254
+
255
+ bench("transform wrapped (1 level pipe)", () => {
256
+ caseconv.snakeToCamel(dataInput, { schema: transformWrapped });
257
+ });
258
+
259
+ bench("deeply nested (4 levels)", () => {
260
+ caseconv.snakeToCamel(dataInput, { schema: deeplyNested });
261
+ });
262
+
263
+ bench("union (nullishToEmpty)", () => {
264
+ caseconv.snakeToCamel(dataInput, { schema: unionSchema });
265
+ });
266
+
267
+ bench("no schema (baseline)", () => {
268
+ caseconv.snakeToCamel(dataInput);
269
+ });
270
+ });