@synnaxlabs/x 0.55.0 → 0.56.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 (146) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/dist/src/array/nullable.d.ts +1 -1
  3. package/dist/src/array/nullable.d.ts.map +1 -1
  4. package/dist/src/caseconv/caseconv.d.ts.map +1 -1
  5. package/dist/src/compare/compare.d.ts +14 -0
  6. package/dist/src/compare/compare.d.ts.map +1 -1
  7. package/dist/src/debounce/debounce.d.ts +7 -2
  8. package/dist/src/debounce/debounce.d.ts.map +1 -1
  9. package/dist/src/deep/merge.d.ts.map +1 -1
  10. package/dist/src/deep/set.d.ts.map +1 -1
  11. package/dist/src/destructor/destructor.d.ts +1 -0
  12. package/dist/src/destructor/destructor.d.ts.map +1 -1
  13. package/dist/src/errors/errors.d.ts +18 -10
  14. package/dist/src/errors/errors.d.ts.map +1 -1
  15. package/dist/src/index.d.ts +4 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/migrate/migrate.d.ts.map +1 -1
  18. package/dist/src/notation/external.d.ts +3 -0
  19. package/dist/src/notation/external.d.ts.map +1 -0
  20. package/dist/src/notation/index.d.ts +1 -1
  21. package/dist/src/notation/notation.d.ts +5 -9
  22. package/dist/src/notation/notation.d.ts.map +1 -1
  23. package/dist/src/notation/types.gen.d.ts +9 -0
  24. package/dist/src/notation/types.gen.d.ts.map +1 -0
  25. package/dist/src/primitive/primitive.d.ts +16 -0
  26. package/dist/src/primitive/primitive.d.ts.map +1 -1
  27. package/dist/src/record/record.d.ts +8 -1
  28. package/dist/src/record/record.d.ts.map +1 -1
  29. package/dist/src/require/index.d.ts +2 -0
  30. package/dist/src/require/index.d.ts.map +1 -0
  31. package/dist/src/require/require.d.ts +2 -0
  32. package/dist/src/require/require.d.ts.map +1 -0
  33. package/dist/src/spatial/base.d.ts +1 -103
  34. package/dist/src/spatial/base.d.ts.map +1 -1
  35. package/dist/src/spatial/bounds/bounds.d.ts +3 -3
  36. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  37. package/dist/src/spatial/box/box.d.ts +7 -13
  38. package/dist/src/spatial/box/box.d.ts.map +1 -1
  39. package/dist/src/spatial/direction/direction.d.ts +17 -16
  40. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  41. package/dist/src/spatial/external.d.ts +1 -2
  42. package/dist/src/spatial/external.d.ts.map +1 -1
  43. package/dist/src/spatial/location/location.d.ts +28 -28
  44. package/dist/src/spatial/location/location.d.ts.map +1 -1
  45. package/dist/src/spatial/scale/scale.d.ts +2 -2
  46. package/dist/src/spatial/scale/scale.d.ts.map +1 -1
  47. package/dist/src/spatial/sticky/sticky.d.ts +15 -15
  48. package/dist/src/spatial/sticky/sticky.d.ts.map +1 -1
  49. package/dist/src/spatial/types.gen.d.ts +179 -2
  50. package/dist/src/spatial/types.gen.d.ts.map +1 -1
  51. package/dist/src/spatial/xy/xy.d.ts +4 -4
  52. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  53. package/dist/src/status/status.d.ts +11 -0
  54. package/dist/src/status/status.d.ts.map +1 -1
  55. package/dist/src/telem/external.d.ts +1 -0
  56. package/dist/src/telem/external.d.ts.map +1 -1
  57. package/dist/src/telem/series.d.ts +5 -2
  58. package/dist/src/telem/series.d.ts.map +1 -1
  59. package/dist/src/telem/telem.d.ts +42 -34
  60. package/dist/src/telem/telem.d.ts.map +1 -1
  61. package/dist/src/telem/types.gen.d.ts +19 -0
  62. package/dist/src/telem/types.gen.d.ts.map +1 -0
  63. package/dist/src/text/external.d.ts +3 -0
  64. package/dist/src/text/external.d.ts.map +1 -0
  65. package/dist/src/text/index.d.ts +2 -0
  66. package/dist/src/text/index.d.ts.map +1 -0
  67. package/dist/src/text/types.d.ts +21 -0
  68. package/dist/src/text/types.d.ts.map +1 -0
  69. package/dist/src/text/types.gen.d.ts +13 -0
  70. package/dist/src/text/types.gen.d.ts.map +1 -0
  71. package/dist/src/throttle/index.d.ts +2 -0
  72. package/dist/src/throttle/index.d.ts.map +1 -0
  73. package/dist/src/throttle/throttle.d.ts +3 -0
  74. package/dist/src/throttle/throttle.d.ts.map +1 -0
  75. package/dist/src/throttle/throttle.spec.d.ts +2 -0
  76. package/dist/src/throttle/throttle.spec.d.ts.map +1 -0
  77. package/dist/src/zod/parse.d.ts.map +1 -1
  78. package/dist/x.cjs +10 -10
  79. package/dist/x.js +1497 -1365
  80. package/package.json +11 -11
  81. package/src/array/nullable.ts +1 -4
  82. package/src/caseconv/caseconv.spec.ts +71 -0
  83. package/src/caseconv/caseconv.ts +15 -2
  84. package/src/compare/compare.spec.ts +115 -0
  85. package/src/compare/compare.ts +29 -0
  86. package/src/debounce/debounce.spec.ts +258 -24
  87. package/src/debounce/debounce.ts +49 -30
  88. package/src/deep/copy.spec.ts +13 -0
  89. package/src/deep/difference.ts +1 -1
  90. package/src/deep/merge.ts +2 -1
  91. package/src/deep/set.ts +2 -1
  92. package/src/destructor/destructor.ts +2 -0
  93. package/src/errors/errors.spec.ts +122 -0
  94. package/src/errors/errors.ts +51 -17
  95. package/src/index.ts +4 -1
  96. package/src/migrate/migrate.ts +2 -1
  97. package/src/notation/external.ts +11 -0
  98. package/src/notation/index.ts +1 -1
  99. package/src/notation/notation.spec.ts +260 -2
  100. package/src/notation/notation.ts +25 -7
  101. package/src/notation/types.gen.ts +16 -0
  102. package/src/primitive/primitive.spec.ts +58 -5
  103. package/src/primitive/primitive.ts +22 -0
  104. package/src/record/record.spec.ts +26 -0
  105. package/src/record/record.ts +20 -5
  106. package/src/require/index.ts +10 -0
  107. package/src/require/require.ts +10 -0
  108. package/src/spatial/base.ts +1 -93
  109. package/src/spatial/bounds/bounds.ts +10 -10
  110. package/src/spatial/box/box.ts +5 -5
  111. package/src/spatial/direction/direction.ts +16 -17
  112. package/src/spatial/external.ts +1 -2
  113. package/src/spatial/location/location.ts +19 -17
  114. package/src/spatial/scale/scale.ts +2 -2
  115. package/src/spatial/sticky/sticky.spec.ts +2 -2
  116. package/src/spatial/sticky/sticky.ts +6 -13
  117. package/src/spatial/types.gen.ts +140 -0
  118. package/src/spatial/xy/xy.ts +7 -7
  119. package/src/status/status.spec.ts +82 -2
  120. package/src/status/status.ts +35 -9
  121. package/src/telem/external.ts +8 -0
  122. package/src/telem/series.spec.ts +183 -0
  123. package/src/telem/series.ts +54 -16
  124. package/src/telem/telem.spec.ts +128 -9
  125. package/src/telem/telem.ts +91 -79
  126. package/src/telem/types.gen.ts +28 -0
  127. package/src/text/external.ts +11 -0
  128. package/src/text/index.ts +10 -0
  129. package/src/text/types.gen.ts +16 -0
  130. package/src/text/types.ts +37 -0
  131. package/src/{worker → throttle}/index.ts +1 -1
  132. package/src/throttle/throttle.spec.ts +147 -0
  133. package/src/throttle/throttle.ts +44 -0
  134. package/src/zod/parse.ts +2 -3
  135. package/tsconfig.tsbuildinfo +1 -1
  136. package/dist/src/spatial/spatial.d.ts +0 -3
  137. package/dist/src/spatial/spatial.d.ts.map +0 -1
  138. package/dist/src/worker/index.d.ts +0 -2
  139. package/dist/src/worker/index.d.ts.map +0 -1
  140. package/dist/src/worker/worker.d.ts +0 -33
  141. package/dist/src/worker/worker.d.ts.map +0 -1
  142. package/dist/src/worker/worker.spec.d.ts +0 -2
  143. package/dist/src/worker/worker.spec.d.ts.map +0 -1
  144. package/src/spatial/spatial.ts +0 -44
  145. package/src/worker/worker.spec.ts +0 -41
  146. package/src/worker/worker.ts +0 -86
@@ -12,12 +12,13 @@ import { describe, expect, it } from "vitest";
12
12
  import { notation } from "@/notation";
13
13
 
14
14
  interface TestCase {
15
- number: number;
15
+ number: number | bigint;
16
16
  precision: number;
17
17
  expected: Record<notation.Notation, string>;
18
18
  }
19
19
 
20
20
  const TEST_CASES: TestCase[] = [
21
+ // === Basic positive numbers ===
21
22
  {
22
23
  number: 12345.678,
23
24
  precision: 1,
@@ -28,6 +29,7 @@ const TEST_CASES: TestCase[] = [
28
29
  precision: 0,
29
30
  expected: { standard: "12346", scientific: "1ᴇ4", engineering: "12ᴇ3" },
30
31
  },
32
+ // === Zero ===
31
33
  {
32
34
  number: 0,
33
35
  precision: 1,
@@ -38,6 +40,18 @@ const TEST_CASES: TestCase[] = [
38
40
  precision: 0,
39
41
  expected: { standard: "0", scientific: "0ᴇ0", engineering: "0ᴇ0" },
40
42
  },
43
+ // === Negative zero — should render identically to positive zero ===
44
+ {
45
+ number: -0,
46
+ precision: 0,
47
+ expected: { standard: "0", scientific: "0ᴇ0", engineering: "0ᴇ0" },
48
+ },
49
+ {
50
+ number: -0,
51
+ precision: 2,
52
+ expected: { standard: "0.00", scientific: "0.00ᴇ0", engineering: "0.00ᴇ0" },
53
+ },
54
+ // === Negative numbers ===
41
55
  {
42
56
  number: -1234.5678,
43
57
  precision: 1,
@@ -48,21 +62,38 @@ const TEST_CASES: TestCase[] = [
48
62
  precision: 0,
49
63
  expected: { standard: "-1235", scientific: "-1ᴇ3", engineering: "-1ᴇ3" },
50
64
  },
65
+ // === Special values (precision must be ignored) ===
51
66
  {
52
67
  number: NaN,
53
68
  precision: 0,
54
69
  expected: { standard: "NaN", scientific: "NaN", engineering: "NaN" },
55
70
  },
71
+ {
72
+ number: NaN,
73
+ precision: 4,
74
+ expected: { standard: "NaN", scientific: "NaN", engineering: "NaN" },
75
+ },
56
76
  {
57
77
  number: Infinity,
58
78
  precision: 0,
59
79
  expected: { standard: "∞", scientific: "∞", engineering: "∞" },
60
80
  },
81
+ {
82
+ number: Infinity,
83
+ precision: 4,
84
+ expected: { standard: "∞", scientific: "∞", engineering: "∞" },
85
+ },
61
86
  {
62
87
  number: -Infinity,
63
88
  precision: 0,
64
89
  expected: { standard: "-∞", scientific: "-∞", engineering: "-∞" },
65
90
  },
91
+ {
92
+ number: -Infinity,
93
+ precision: 4,
94
+ expected: { standard: "-∞", scientific: "-∞", engineering: "-∞" },
95
+ },
96
+ // === Small fractional numbers ===
66
97
  {
67
98
  number: 0.0001234,
68
99
  precision: 1,
@@ -73,11 +104,238 @@ const TEST_CASES: TestCase[] = [
73
104
  precision: 0,
74
105
  expected: { standard: "0", scientific: "1ᴇ-4", engineering: "123ᴇ-6" },
75
106
  },
107
+ // === Negative small fractional numbers ===
108
+ {
109
+ number: -0.0001234,
110
+ precision: 1,
111
+ expected: { standard: "-0.0", scientific: "-1.2ᴇ-4", engineering: "-123.4ᴇ-6" },
112
+ },
113
+ {
114
+ number: -0.0001234,
115
+ precision: 0,
116
+ expected: { standard: "-0", scientific: "-1ᴇ-4", engineering: "-123ᴇ-6" },
117
+ },
118
+ // === Number 1 ===
119
+ {
120
+ number: 1,
121
+ precision: 0,
122
+ expected: { standard: "1", scientific: "1ᴇ0", engineering: "1ᴇ0" },
123
+ },
124
+ {
125
+ number: 1,
126
+ precision: 2,
127
+ expected: { standard: "1.00", scientific: "1.00ᴇ0", engineering: "1.00ᴇ0" },
128
+ },
129
+ // === Power-of-10 boundaries where engineering and scientific differ ===
130
+ {
131
+ number: 10,
132
+ precision: 0,
133
+ expected: { standard: "10", scientific: "1ᴇ1", engineering: "10ᴇ0" },
134
+ },
135
+ {
136
+ number: 100,
137
+ precision: 0,
138
+ expected: { standard: "100", scientific: "1ᴇ2", engineering: "100ᴇ0" },
139
+ },
140
+ {
141
+ number: 1000,
142
+ precision: 0,
143
+ expected: { standard: "1000", scientific: "1ᴇ3", engineering: "1ᴇ3" },
144
+ },
145
+ {
146
+ number: 10000,
147
+ precision: 0,
148
+ expected: { standard: "10000", scientific: "1ᴇ4", engineering: "10ᴇ3" },
149
+ },
150
+ {
151
+ number: 100000,
152
+ precision: 0,
153
+ expected: { standard: "100000", scientific: "1ᴇ5", engineering: "100ᴇ3" },
154
+ },
155
+ {
156
+ number: 1000000,
157
+ precision: 0,
158
+ expected: { standard: "1000000", scientific: "1ᴇ6", engineering: "1ᴇ6" },
159
+ },
160
+ // === Negative power-of-10 boundaries (fractions) ===
161
+ {
162
+ number: 0.1,
163
+ precision: 1,
164
+ expected: { standard: "0.1", scientific: "1.0ᴇ-1", engineering: "100.0ᴇ-3" },
165
+ },
166
+ {
167
+ number: 0.01,
168
+ precision: 1,
169
+ expected: { standard: "0.0", scientific: "1.0ᴇ-2", engineering: "10.0ᴇ-3" },
170
+ },
171
+ {
172
+ number: 0.001,
173
+ precision: 1,
174
+ expected: { standard: "0.0", scientific: "1.0ᴇ-3", engineering: "1.0ᴇ-3" },
175
+ },
176
+ // === Negative power-of-10 boundaries ===
177
+ {
178
+ number: -10,
179
+ precision: 0,
180
+ expected: { standard: "-10", scientific: "-1ᴇ1", engineering: "-10ᴇ0" },
181
+ },
182
+ {
183
+ number: -100,
184
+ precision: 0,
185
+ expected: { standard: "-100", scientific: "-1ᴇ2", engineering: "-100ᴇ0" },
186
+ },
187
+ // === Power of 10 with non-zero precision ===
188
+ {
189
+ number: 100,
190
+ precision: 2,
191
+ expected: { standard: "100.00", scientific: "1.00ᴇ2", engineering: "100.00ᴇ0" },
192
+ },
193
+ {
194
+ number: 1000,
195
+ precision: 3,
196
+ expected: { standard: "1000.000", scientific: "1.000ᴇ3", engineering: "1.000ᴇ3" },
197
+ },
198
+ // === Rounding overflow — toFixed pushes the mantissa past the canonical upper
199
+ // bound (>= 10 for scientific, >= 1000 for engineering). The exponent must be
200
+ // bumped so the mantissa stays in the canonical range. ===
201
+ {
202
+ number: 9.999,
203
+ precision: 1,
204
+ expected: { standard: "10.0", scientific: "1.0ᴇ1", engineering: "10.0ᴇ0" },
205
+ },
206
+ {
207
+ number: 9.999,
208
+ precision: 0,
209
+ expected: { standard: "10", scientific: "1ᴇ1", engineering: "10ᴇ0" },
210
+ },
211
+ {
212
+ number: 99.99,
213
+ precision: 0,
214
+ expected: { standard: "100", scientific: "1ᴇ2", engineering: "100ᴇ0" },
215
+ },
216
+ {
217
+ number: 999.99,
218
+ precision: 0,
219
+ expected: { standard: "1000", scientific: "1ᴇ3", engineering: "1ᴇ3" },
220
+ },
221
+ {
222
+ number: 9999.99,
223
+ precision: 0,
224
+ expected: { standard: "10000", scientific: "1ᴇ4", engineering: "10ᴇ3" },
225
+ },
226
+ {
227
+ number: -9.999,
228
+ precision: 1,
229
+ expected: { standard: "-10.0", scientific: "-1.0ᴇ1", engineering: "-10.0ᴇ0" },
230
+ },
231
+ {
232
+ number: -999.99,
233
+ precision: 0,
234
+ expected: { standard: "-1000", scientific: "-1ᴇ3", engineering: "-1ᴇ3" },
235
+ },
236
+ // === Positive bigint ===
237
+ {
238
+ number: 1n,
239
+ precision: 0,
240
+ expected: { standard: "1", scientific: "1ᴇ0", engineering: "1ᴇ0" },
241
+ },
242
+ {
243
+ number: 1n,
244
+ precision: 2,
245
+ expected: { standard: "1.00", scientific: "1.00ᴇ0", engineering: "1.00ᴇ0" },
246
+ },
247
+ {
248
+ number: 1778020940471336960n,
249
+ precision: 0,
250
+ expected: {
251
+ standard: "1778020940471336960",
252
+ scientific: "2ᴇ18",
253
+ engineering: "2ᴇ18",
254
+ },
255
+ },
256
+ {
257
+ number: 1778020940471336960n,
258
+ precision: 11,
259
+ expected: {
260
+ standard: "1778020940471336960.00000000000",
261
+ scientific: "1.77802094047ᴇ18",
262
+ engineering: "1.77802094047ᴇ18",
263
+ },
264
+ },
265
+ // === Negative bigint ===
266
+ {
267
+ number: -1n,
268
+ precision: 0,
269
+ expected: { standard: "-1", scientific: "-1ᴇ0", engineering: "-1ᴇ0" },
270
+ },
271
+ {
272
+ number: -1778020940471336960n,
273
+ precision: 0,
274
+ expected: {
275
+ standard: "-1778020940471336960",
276
+ scientific: "-2ᴇ18",
277
+ engineering: "-2ᴇ18",
278
+ },
279
+ },
280
+ {
281
+ number: -1778020940471336960n,
282
+ precision: 11,
283
+ expected: {
284
+ standard: "-1778020940471336960.00000000000",
285
+ scientific: "-1.77802094047ᴇ18",
286
+ engineering: "-1.77802094047ᴇ18",
287
+ },
288
+ },
289
+ // === Zero bigint ===
290
+ {
291
+ number: 0n,
292
+ precision: 0,
293
+ expected: { standard: "0", scientific: "0ᴇ0", engineering: "0ᴇ0" },
294
+ },
295
+ {
296
+ number: 0n,
297
+ precision: 11,
298
+ expected: {
299
+ standard: "0.00000000000",
300
+ scientific: "0.00000000000ᴇ0",
301
+ engineering: "0.00000000000ᴇ0",
302
+ },
303
+ },
304
+ // === Large-magnitude floats expressed with e-notation ===
305
+ // For values >= 10^21, JS's toFixed delegates to ToString, so standard output
306
+ // here is itself in e-notation. The scientific and engineering branches keep
307
+ // their canonical forms.
308
+ {
309
+ number: 9e124,
310
+ precision: 0,
311
+ expected: { standard: "9e+124", scientific: "9ᴇ124", engineering: "90ᴇ123" },
312
+ },
313
+ {
314
+ number: 9.99e124,
315
+ precision: 0,
316
+ expected: { standard: "9.99e+124", scientific: "1ᴇ125", engineering: "100ᴇ123" },
317
+ },
318
+ {
319
+ number: -3e10,
320
+ precision: 0,
321
+ expected: { standard: "-30000000000", scientific: "-3ᴇ10", engineering: "-30ᴇ9" },
322
+ },
323
+ // === Tiny-magnitude floats expressed with e-notation ===
324
+ {
325
+ number: 1e-200,
326
+ precision: 0,
327
+ expected: { standard: "0", scientific: "1ᴇ-200", engineering: "10ᴇ-201" },
328
+ },
329
+ {
330
+ number: 5e-50,
331
+ precision: 0,
332
+ expected: { standard: "0", scientific: "5ᴇ-50", engineering: "50ᴇ-51" },
333
+ },
76
334
  ];
77
335
 
78
336
  describe("stringifyNumber", () => {
79
337
  TEST_CASES.forEach(({ number, precision, expected }) =>
80
- describe(`number: ${number}, precision: ${precision}`, () =>
338
+ describe(`${typeof number === "bigint" ? "bigint " : ""}number: ${number}, precision: ${precision}`, () =>
81
339
  notation.NOTATIONS.forEach((n) =>
82
340
  it(`should format correctly in ${n} notation`, () => {
83
341
  const result = notation.stringifyNumber(number, precision, n);
@@ -7,11 +7,7 @@
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 { z } from "zod";
11
-
12
- export const NOTATIONS = ["standard", "scientific", "engineering"] as const;
13
- export const notationZ = z.enum(NOTATIONS);
14
- export type Notation = z.infer<typeof notationZ>;
10
+ import { type Notation } from "@/notation/types.gen";
15
11
 
16
12
  /**
17
13
  * Converts a number to a string representation with a specified precision and notation.
@@ -25,6 +21,9 @@ export type Notation = z.infer<typeof notationZ>;
25
21
  * - If the value is `NaN`, returns "NaN".
26
22
  * - If the value is `Infinity`, returns "∞".
27
23
  * - If the value is `-Infinity`, returns "-∞".
24
+ * - Scientific and engineering output is renormalized after rounding so the mantissa
25
+ * stays in its canonical range (|m| < 10 for scientific, |m| < 1000 for engineering).
26
+ * For example, 9.999 at precision 1 in scientific renders as "1.0ᴇ1", not "10.0ᴇ0".
28
27
  *
29
28
  * Examples:
30
29
  *
@@ -41,10 +40,16 @@ export type Notation = z.infer<typeof notationZ>;
41
40
  * ```
42
41
  */
43
42
  export const stringifyNumber = (
44
- value: number,
43
+ value: number | bigint,
45
44
  precision: number,
46
45
  notation: Notation,
47
46
  ): string => {
47
+ // Standard notation on bigint must preserve full integer precision; coercing through
48
+ // Number() would quantize values above 2^53. All other branches are float-mantissa
49
+ // truncated by definition, so coercing bigint to number is safe.
50
+ if (typeof value === "bigint" && notation === "standard")
51
+ return precision === 0 ? value.toString() : `${value}.${"0".repeat(precision)}`;
52
+ if (typeof value === "bigint") value = Number(value);
48
53
  if (Number.isNaN(value)) return "NaN";
49
54
  if (value === Infinity) return "∞";
50
55
  if (value === -Infinity) return "-∞";
@@ -56,6 +61,19 @@ export const stringifyNumber = (
56
61
  let exp: number;
57
62
  if (notation === "scientific") exp = Math.floor(Math.log10(Math.abs(value)));
58
63
  else exp = Math.floor(Math.log10(Math.abs(value)) / 3) * 3;
59
- const mantissa = value / 10 ** exp;
64
+ let mantissa = value / 10 ** exp;
65
+ // After rounding via toFixed, the mantissa may have crossed the canonical upper
66
+ // bound (>= 10 for scientific, >= 1000 for engineering). Predict the rounded
67
+ // magnitude with Math.round — for non-negative values it matches toFixed's
68
+ // half-away-from-zero semantics, and we're already taking Math.abs — so we avoid
69
+ // having to parseFloat the formatted string back into a number. Bump the exponent
70
+ // if needed so e.g. 9.999 at precision 1 in scientific becomes "1.0ᴇ1" rather
71
+ // than "10.0ᴇ0".
72
+ const upperBound = notation === "scientific" ? 10 : 1000;
73
+ const factor = 10 ** precision;
74
+ if (Math.round(Math.abs(mantissa) * factor) / factor >= upperBound) {
75
+ exp += notation === "scientific" ? 1 : 3;
76
+ mantissa = value / 10 ** exp;
77
+ }
60
78
  return `${mantissa.toFixed(precision)}ᴇ${exp}`;
61
79
  };
@@ -0,0 +1,16 @@
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
+ // Code generated by Oracle. DO NOT EDIT.
11
+
12
+ import { z } from "zod";
13
+
14
+ export const NOTATIONS = ["standard", "scientific", "engineering"] as const;
15
+ export const notationZ = z.enum(NOTATIONS);
16
+ export type Notation = z.infer<typeof notationZ>;
@@ -54,13 +54,13 @@ describe("primitive", () => {
54
54
 
55
55
  describe("isStringer", () => {
56
56
  it("should return true for a stringer", () => {
57
- expect(primitive.isStringer(new ExampleStringer("cat"))).toEqual(true);
57
+ expect(primitive.isStringer(new ExampleStringer("cat"))).toBe(true);
58
58
  });
59
59
  it("should return false for a non-stringer", () => {
60
- expect(primitive.isStringer(0)).toEqual(false);
60
+ expect(primitive.isStringer(0)).toBe(false);
61
61
  });
62
62
  it("should return false for null", () => {
63
- expect(primitive.isStringer(null)).toEqual(false);
63
+ expect(primitive.isStringer(null)).toBe(false);
64
64
  });
65
65
  });
66
66
 
@@ -178,12 +178,65 @@ describe("primitive", () => {
178
178
 
179
179
  describe("isCrudeValueExtension", () => {
180
180
  it("should return true for a CrudeValueExtension", () => {
181
- expect(primitive.isCrudeValueExtension({ value: 12n })).toEqual(true);
181
+ expect(primitive.isCrudeValueExtension({ value: 12n })).toBe(true);
182
182
  });
183
183
  it("should return false for a non-CrudeValueExtension", () => {
184
- expect(primitive.isCrudeValueExtension(12n)).toEqual(false);
184
+ expect(primitive.isCrudeValueExtension(12n)).toBe(false);
185
185
  });
186
186
  });
187
187
  });
188
188
  });
189
+
190
+ describe("isHashable", () => {
191
+ class HashableThing implements primitive.Hashable {
192
+ constructor(private readonly v: string) {}
193
+ hash(): string {
194
+ return this.v;
195
+ }
196
+ }
197
+
198
+ it("returns true for an object with a hash() method", () => {
199
+ expect(primitive.isHashable(new HashableThing("x"))).toBe(true);
200
+ });
201
+
202
+ it("returns true for a plain object literal with a hash function", () => {
203
+ expect(primitive.isHashable({ hash: () => "x" })).toBe(true);
204
+ });
205
+
206
+ it("narrows the type for downstream calls", () => {
207
+ const v: unknown = new HashableThing("abc");
208
+ if (primitive.isHashable(v)) expect(v.hash()).toEqual("abc");
209
+ else throw new Error("expected isHashable to narrow");
210
+ });
211
+
212
+ it("returns false for null", () => {
213
+ expect(primitive.isHashable(null)).toBe(false);
214
+ });
215
+
216
+ it("returns false for undefined", () => {
217
+ expect(primitive.isHashable(undefined)).toBe(false);
218
+ });
219
+
220
+ it("returns false for primitives", () => {
221
+ expect(primitive.isHashable("x")).toBe(false);
222
+ expect(primitive.isHashable(42)).toBe(false);
223
+ expect(primitive.isHashable(42n)).toBe(false);
224
+ expect(primitive.isHashable(true)).toBe(false);
225
+ });
226
+
227
+ it("returns false for plain objects without a hash function", () => {
228
+ expect(primitive.isHashable({})).toBe(false);
229
+ expect(primitive.isHashable({ key: "x" })).toBe(false);
230
+ });
231
+
232
+ it("returns false when hash is not a function", () => {
233
+ expect(primitive.isHashable({ hash: "not a function" })).toBe(false);
234
+ expect(primitive.isHashable({ hash: 42 })).toBe(false);
235
+ expect(primitive.isHashable({ hash: null })).toBe(false);
236
+ });
237
+
238
+ it("returns false for arrays", () => {
239
+ expect(primitive.isHashable([1, 2, 3])).toBe(false);
240
+ });
241
+ });
189
242
  });
@@ -61,6 +61,28 @@ export interface Stringer {
61
61
  export const isStringer = (value: unknown): boolean =>
62
62
  value != null && typeof value === "object" && "toString" in value;
63
63
 
64
+ /**
65
+ * Hashable is a duck-typed protocol for class instances that can produce a stable,
66
+ * canonical string representation of themselves. Implementers commit to: (1) the
67
+ * returned string uniquely identifies the value's logical content, and (2) two
68
+ * instances representing the same logical value return equal strings.
69
+ *
70
+ * Used by cache-key derivation paths (e.g. flux queryCache) so non-primitive query
71
+ * fields hash to a stable identifier rather than relying on enumerable own-property
72
+ * iteration, which is fragile for class instances.
73
+ */
74
+ export interface Hashable {
75
+ /** @returns a stable, canonical string representation of the value. */
76
+ hash: () => string;
77
+ }
78
+
79
+ /** @returns true if the value implements primitive.Hashable, otherwise returns false. */
80
+ export const isHashable = (value: unknown): value is Hashable =>
81
+ value != null &&
82
+ typeof value === "object" &&
83
+ "hash" in value &&
84
+ typeof value.hash === "function";
85
+
64
86
  /**
65
87
  * Type representing zero values for each primitive type
66
88
  */
@@ -353,6 +353,32 @@ describe("record", () => {
353
353
  const obj = { nested: { key: "value" }, arr: [1, 2, 3] };
354
354
  expect(record.nullishToEmpty().parse(obj)).toEqual(obj);
355
355
  });
356
+
357
+ describe("with typed key and value schemas", () => {
358
+ const schema = record.nullishToEmpty(z.string(), z.number());
359
+
360
+ it("should coerce null to empty object", () => {
361
+ expect(schema.parse(null)).toEqual({});
362
+ });
363
+
364
+ it("should coerce undefined to empty object", () => {
365
+ expect(schema.parse(undefined)).toEqual({});
366
+ });
367
+
368
+ it("should pass through populated record matching the value schema", () => {
369
+ expect(schema.parse({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
370
+ });
371
+
372
+ it("should reject values that don't match the value schema", () => {
373
+ expect(() => schema.parse({ a: "not a number" })).toThrow();
374
+ });
375
+
376
+ it("should preserve the inferred typed shape", () => {
377
+ const parsed = schema.parse({ a: 1 });
378
+ const value: number = parsed.a;
379
+ expect(value).toBe(1);
380
+ });
381
+ });
356
382
  });
357
383
 
358
384
  describe("omit", () => {
@@ -141,6 +141,14 @@ export const omit = <T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> =>
141
141
  return result;
142
142
  };
143
143
 
144
+ export interface NullishToEmpty {
145
+ (): z.ZodType<Unknown>;
146
+ <K extends z.ZodType<Key>, V extends z.ZodType>(
147
+ key: K,
148
+ value: V,
149
+ ): z.ZodType<Record<z.infer<K>, z.infer<V>>>;
150
+ }
151
+
144
152
  /**
145
153
  * For required JSON/record fields: coerces null/undefined to empty object {}.
146
154
  * Use when the record must always be present and iterable.
@@ -149,9 +157,16 @@ export const omit = <T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> =>
149
157
  * - undefined → {}
150
158
  * - {} → {}
151
159
  * - {data} → {data}
160
+ *
161
+ * Without arguments the value type is unknown. Pass key and value zod schemas
162
+ * to type the resulting record (e.g. for `map<string, Color>` fields).
152
163
  */
153
- export const nullishToEmpty = (): z.ZodType<Unknown> =>
154
- z.union([
155
- z.union([z.null(), z.undefined()]).transform<Unknown>(() => ({})),
156
- unknownZ(),
157
- ]);
164
+ export const nullishToEmpty = ((key?: z.ZodType, value?: z.ZodType) => {
165
+ if (key === undefined || value === undefined)
166
+ return z
167
+ .union([z.null().transform<Unknown>(() => ({})), unknownZ()])
168
+ .default(() => ({}));
169
+ return z
170
+ .union([z.null().transform(() => ({})), z.record(key as z.ZodType<Key>, value)])
171
+ .default(() => ({}));
172
+ }) as NullishToEmpty;
@@ -0,0 +1,10 @@
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
+ export * as require from "@/require/require";
@@ -0,0 +1,10 @@
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
+ export type Require<T, K extends keyof T> = Pick<Required<T>, K> & Omit<T, K>;