@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.
Files changed (140) 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/destructor/destructor.d.ts +1 -0
  10. package/dist/src/destructor/destructor.d.ts.map +1 -1
  11. package/dist/src/errors/errors.d.ts +6 -10
  12. package/dist/src/errors/errors.d.ts.map +1 -1
  13. package/dist/src/index.d.ts +4 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/notation/external.d.ts +3 -0
  16. package/dist/src/notation/external.d.ts.map +1 -0
  17. package/dist/src/notation/index.d.ts +1 -1
  18. package/dist/src/notation/notation.d.ts +5 -9
  19. package/dist/src/notation/notation.d.ts.map +1 -1
  20. package/dist/src/notation/types.gen.d.ts +9 -0
  21. package/dist/src/notation/types.gen.d.ts.map +1 -0
  22. package/dist/src/primitive/primitive.d.ts +16 -0
  23. package/dist/src/primitive/primitive.d.ts.map +1 -1
  24. package/dist/src/record/record.d.ts +8 -1
  25. package/dist/src/record/record.d.ts.map +1 -1
  26. package/dist/src/require/index.d.ts +2 -0
  27. package/dist/src/require/index.d.ts.map +1 -0
  28. package/dist/src/require/require.d.ts +2 -0
  29. package/dist/src/require/require.d.ts.map +1 -0
  30. package/dist/src/spatial/base.d.ts +1 -103
  31. package/dist/src/spatial/base.d.ts.map +1 -1
  32. package/dist/src/spatial/bounds/bounds.d.ts +3 -3
  33. package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
  34. package/dist/src/spatial/box/box.d.ts +7 -13
  35. package/dist/src/spatial/box/box.d.ts.map +1 -1
  36. package/dist/src/spatial/direction/direction.d.ts +17 -16
  37. package/dist/src/spatial/direction/direction.d.ts.map +1 -1
  38. package/dist/src/spatial/external.d.ts +1 -2
  39. package/dist/src/spatial/external.d.ts.map +1 -1
  40. package/dist/src/spatial/location/location.d.ts +28 -28
  41. package/dist/src/spatial/location/location.d.ts.map +1 -1
  42. package/dist/src/spatial/scale/scale.d.ts +2 -2
  43. package/dist/src/spatial/scale/scale.d.ts.map +1 -1
  44. package/dist/src/spatial/sticky/sticky.d.ts +15 -15
  45. package/dist/src/spatial/sticky/sticky.d.ts.map +1 -1
  46. package/dist/src/spatial/types.gen.d.ts +179 -2
  47. package/dist/src/spatial/types.gen.d.ts.map +1 -1
  48. package/dist/src/spatial/xy/xy.d.ts +4 -4
  49. package/dist/src/spatial/xy/xy.d.ts.map +1 -1
  50. package/dist/src/status/status.d.ts +11 -0
  51. package/dist/src/status/status.d.ts.map +1 -1
  52. package/dist/src/telem/external.d.ts +1 -0
  53. package/dist/src/telem/external.d.ts.map +1 -1
  54. package/dist/src/telem/series.d.ts +5 -2
  55. package/dist/src/telem/series.d.ts.map +1 -1
  56. package/dist/src/telem/telem.d.ts +42 -34
  57. package/dist/src/telem/telem.d.ts.map +1 -1
  58. package/dist/src/telem/types.gen.d.ts +19 -0
  59. package/dist/src/telem/types.gen.d.ts.map +1 -0
  60. package/dist/src/text/external.d.ts +3 -0
  61. package/dist/src/text/external.d.ts.map +1 -0
  62. package/dist/src/text/index.d.ts +2 -0
  63. package/dist/src/text/index.d.ts.map +1 -0
  64. package/dist/src/text/types.d.ts +21 -0
  65. package/dist/src/text/types.d.ts.map +1 -0
  66. package/dist/src/text/types.gen.d.ts +13 -0
  67. package/dist/src/text/types.gen.d.ts.map +1 -0
  68. package/dist/src/throttle/index.d.ts +2 -0
  69. package/dist/src/throttle/index.d.ts.map +1 -0
  70. package/dist/src/throttle/throttle.d.ts +3 -0
  71. package/dist/src/throttle/throttle.d.ts.map +1 -0
  72. package/dist/src/throttle/throttle.spec.d.ts +2 -0
  73. package/dist/src/throttle/throttle.spec.d.ts.map +1 -0
  74. package/dist/src/zod/parse.d.ts.map +1 -1
  75. package/dist/x.cjs +9 -9
  76. package/dist/x.js +1469 -1346
  77. package/package.json +11 -11
  78. package/src/array/nullable.ts +1 -4
  79. package/src/caseconv/caseconv.spec.ts +71 -0
  80. package/src/caseconv/caseconv.ts +15 -2
  81. package/src/compare/compare.spec.ts +115 -0
  82. package/src/compare/compare.ts +29 -0
  83. package/src/debounce/debounce.spec.ts +258 -24
  84. package/src/debounce/debounce.ts +49 -30
  85. package/src/deep/copy.spec.ts +13 -0
  86. package/src/deep/difference.ts +1 -1
  87. package/src/destructor/destructor.ts +2 -0
  88. package/src/errors/errors.spec.ts +30 -0
  89. package/src/errors/errors.ts +29 -17
  90. package/src/index.ts +4 -1
  91. package/src/notation/external.ts +11 -0
  92. package/src/notation/index.ts +1 -1
  93. package/src/notation/notation.spec.ts +260 -2
  94. package/src/notation/notation.ts +25 -7
  95. package/src/notation/types.gen.ts +16 -0
  96. package/src/primitive/primitive.spec.ts +58 -5
  97. package/src/primitive/primitive.ts +22 -0
  98. package/src/record/record.spec.ts +26 -0
  99. package/src/record/record.ts +20 -5
  100. package/src/require/index.ts +10 -0
  101. package/src/require/require.ts +10 -0
  102. package/src/spatial/base.ts +1 -93
  103. package/src/spatial/bounds/bounds.ts +10 -10
  104. package/src/spatial/box/box.ts +5 -5
  105. package/src/spatial/direction/direction.ts +16 -17
  106. package/src/spatial/external.ts +1 -2
  107. package/src/spatial/location/location.ts +19 -17
  108. package/src/spatial/scale/scale.ts +2 -2
  109. package/src/spatial/sticky/sticky.spec.ts +2 -2
  110. package/src/spatial/sticky/sticky.ts +6 -13
  111. package/src/spatial/types.gen.ts +140 -0
  112. package/src/spatial/xy/xy.ts +7 -7
  113. package/src/status/status.spec.ts +65 -0
  114. package/src/status/status.ts +20 -0
  115. package/src/telem/external.ts +8 -0
  116. package/src/telem/series.spec.ts +183 -0
  117. package/src/telem/series.ts +54 -16
  118. package/src/telem/telem.spec.ts +128 -9
  119. package/src/telem/telem.ts +91 -79
  120. package/src/telem/types.gen.ts +28 -0
  121. package/src/text/external.ts +11 -0
  122. package/src/text/index.ts +10 -0
  123. package/src/text/types.gen.ts +16 -0
  124. package/src/text/types.ts +37 -0
  125. package/src/{worker → throttle}/index.ts +1 -1
  126. package/src/throttle/throttle.spec.ts +147 -0
  127. package/src/throttle/throttle.ts +44 -0
  128. package/src/zod/parse.ts +2 -3
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/src/spatial/spatial.d.ts +0 -3
  131. package/dist/src/spatial/spatial.d.ts.map +0 -1
  132. package/dist/src/worker/index.d.ts +0 -2
  133. package/dist/src/worker/index.d.ts.map +0 -1
  134. package/dist/src/worker/worker.d.ts +0 -33
  135. package/dist/src/worker/worker.d.ts.map +0 -1
  136. package/dist/src/worker/worker.spec.d.ts +0 -2
  137. package/dist/src/worker/worker.spec.d.ts.map +0 -1
  138. package/src/spatial/spatial.ts +0 -44
  139. package/src/worker/worker.spec.ts +0 -41
  140. package/src/worker/worker.ts +0 -86
@@ -11,10 +11,56 @@
11
11
 
12
12
  import { z } from "zod";
13
13
 
14
+ import { type numeric } from "@/numeric";
15
+
16
+ export const X_LOCATIONS = ["left", "right"] as const;
17
+ export const xLocationZ = z.enum(X_LOCATIONS);
18
+ export type XLocation = z.infer<typeof xLocationZ>;
19
+
20
+ export const Y_LOCATIONS = ["top", "bottom"] as const;
21
+ export const yLocationZ = z.enum(Y_LOCATIONS);
22
+ export type YLocation = z.infer<typeof yLocationZ>;
23
+
24
+ export const STICKY_UNITS = ["px", "decimal"] as const;
25
+ export const stickyUnitZ = z.enum(STICKY_UNITS);
26
+ export type StickyUnit = z.infer<typeof stickyUnitZ>;
27
+
14
28
  export const OUTER_LOCATIONS = ["top", "right", "bottom", "left"] as const;
15
29
  export const outerLocationZ = z.enum(OUTER_LOCATIONS);
16
30
  export type OuterLocation = z.infer<typeof outerLocationZ>;
17
31
 
32
+ export const DIRECTIONS = ["x", "y"] as const;
33
+ export const directionZ = z.enum(DIRECTIONS);
34
+ export type Direction = z.infer<typeof directionZ>;
35
+
36
+ export const ANGULAR_DIRECTIONS = ["clockwise", "counterclockwise"] as const;
37
+ export const angularDirectionZ = z.enum(ANGULAR_DIRECTIONS);
38
+ export type AngularDirection = z.infer<typeof angularDirectionZ>;
39
+
40
+ export const CENTER_LOCATIONS = ["center"] as const;
41
+ export const centerLocationZ = z.enum(CENTER_LOCATIONS);
42
+ export type CenterLocation = z.infer<typeof centerLocationZ>;
43
+
44
+ export const LOCATIONS = ["top", "right", "bottom", "left", "center"] as const;
45
+ export const locationZ = z.enum(LOCATIONS);
46
+ export type Location = z.infer<typeof locationZ>;
47
+
48
+ export const ALIGNMENTS = ["start", "center", "end"] as const;
49
+ export const alignmentZ = z.enum(ALIGNMENTS);
50
+ export type Alignment = z.infer<typeof alignmentZ>;
51
+
52
+ export const ORDERS = ["first", "last"] as const;
53
+ export const orderZ = z.enum(ORDERS);
54
+ export type Order = z.infer<typeof orderZ>;
55
+
56
+ export const DIMENSIONS = ["width", "height"] as const;
57
+ export const dimensionZ = z.enum(DIMENSIONS);
58
+ export type Dimension = z.infer<typeof dimensionZ>;
59
+
60
+ export const SIGNED_DIMENSIONS = ["signedWidth", "signedHeight"] as const;
61
+ export const signedDimensionZ = z.enum(SIGNED_DIMENSIONS);
62
+ export type SignedDimension = z.infer<typeof signedDimensionZ>;
63
+
18
64
  /**
19
65
  * XY is a 2D coordinate point with x and y values. Used for positioning
20
66
  * elements in two-dimensional space.
@@ -26,3 +72,97 @@ export const xyZ = z.object({
26
72
  y: z.number(),
27
73
  });
28
74
  export interface XY extends z.infer<typeof xyZ> {}
75
+
76
+ /** CornerLocation is an anchor corner for positioning. */
77
+ export const cornerLocationZ = z.object({
78
+ /** x is the horizontal anchor. */
79
+ x: xLocationZ,
80
+ /** y is the vertical anchor. */
81
+ y: yLocationZ,
82
+ });
83
+ export interface CornerLocation extends z.infer<typeof cornerLocationZ> {}
84
+
85
+ /** StickyUnits specifies the measurement units for sticky positioning. */
86
+ export const stickyUnitsZ = z.object({
87
+ /** x is the horizontal unit. */
88
+ x: stickyUnitZ,
89
+ /** y is the vertical unit. */
90
+ y: stickyUnitZ,
91
+ });
92
+ export interface StickyUnits extends z.infer<typeof stickyUnitsZ> {}
93
+
94
+ /** Dimensions is a 2D size with width and height values. */
95
+ export const dimensionsZ = z.object({
96
+ /** width is the width in pixels. */
97
+ width: z.number(),
98
+ /** height is the height in pixels. */
99
+ height: z.number(),
100
+ });
101
+ export interface Dimensions extends z.infer<typeof dimensionsZ> {}
102
+
103
+ /**
104
+ * SignedDimensions is a 2D size whose width and height components carry sign, allowing
105
+ * negative values to express direction.
106
+ */
107
+ export const signedDimensionsZ = z.object({
108
+ /** signedWidth is the signed width. */
109
+ signedWidth: z.number(),
110
+ /** signedHeight is the signed height. */
111
+ signedHeight: z.number(),
112
+ });
113
+ export interface SignedDimensions extends z.infer<typeof signedDimensionsZ> {}
114
+
115
+ /**
116
+ * ClientXY is a 2D coordinate point expressed in client (viewport) space, matching
117
+ * the shape of DOM mouse events.
118
+ */
119
+ export const clientXYZ = z.object({
120
+ /** clientX is the horizontal coordinate in client (viewport) space. */
121
+ clientX: z.number(),
122
+ /** clientY is the vertical coordinate in client (viewport) space. */
123
+ clientY: z.number(),
124
+ });
125
+ export interface ClientXY extends z.infer<typeof clientXYZ> {}
126
+
127
+ /**
128
+ * Bounds is a closed-open interval [lower, upper) over an ordered numeric value
129
+ * space. The TypeScript binding is generic over T so callers can express
130
+ * bounds over either number or bigint values; other languages emit a
131
+ * concrete float64-based type.
132
+ */
133
+ export const boundsZ = <T extends numeric.Value = number>(t?: z.ZodType<T>) =>
134
+ z.object({
135
+ /** lower is the inclusive lower bound. */
136
+ lower: t ?? z.number(),
137
+ /** upper is the exclusive upper bound. */
138
+ upper: t ?? z.number(),
139
+ });
140
+ export interface Bounds<T extends numeric.Value = number> {
141
+ lower: T;
142
+ upper: T;
143
+ }
144
+
145
+ /** Viewport is the camera state of a viewport. */
146
+ export const viewportZ = z.object({
147
+ /** zoom is the zoom level where 1.0 equals 100%. */
148
+ zoom: z.number().default(1),
149
+ /** position is the (x, y) pan offset of the viewport. */
150
+ position: xyZ,
151
+ });
152
+ export interface Viewport extends z.infer<typeof viewportZ> {}
153
+
154
+ /**
155
+ * StickyXY is a position that can be anchored to different corners of a
156
+ * container with configurable units (pixels or decimal fractions).
157
+ */
158
+ export const stickyXYZ = z.object({
159
+ /** x is the horizontal coordinate. */
160
+ x: z.number(),
161
+ /** y is the vertical coordinate. */
162
+ y: z.number(),
163
+ /** root is the optional anchor corner for the position. */
164
+ root: cornerLocationZ.optional(),
165
+ /** units is the optional unit specification for the coordinates. */
166
+ units: stickyUnitsZ.optional(),
167
+ });
168
+ export interface StickyXY extends z.infer<typeof stickyXYZ> {}
@@ -12,19 +12,19 @@ import { z } from "zod";
12
12
  import {
13
13
  type AngularDirection,
14
14
  type ClientXY,
15
- clientXyZ,
16
- type CrudeDirection,
15
+ clientXYZ,
17
16
  dimensionsZ,
18
17
  type Direction,
19
18
  type NumberCouple,
20
19
  numberCouple,
21
20
  signedDimensionsZ,
21
+ type XY,
22
+ xyZ,
22
23
  } from "@/spatial/base";
23
- import { direction as dir } from "@/spatial/direction";
24
+ import { direction as dir, type direction } from "@/spatial/direction";
24
25
  import { type location } from "@/spatial/location";
25
- import { type XY, xyZ } from "@/spatial/types.gen";
26
26
 
27
- export { type ClientXY as Client, clientXyZ, type XY, xyZ };
27
+ export { type ClientXY as Client, clientXYZ, type XY, xyZ };
28
28
 
29
29
  /** A crude representation of a {@link XY} coordinate as a zod schema. */
30
30
  export const crudeZ = z.union([
@@ -33,7 +33,7 @@ export const crudeZ = z.union([
33
33
  numberCouple,
34
34
  dimensionsZ,
35
35
  signedDimensionsZ,
36
- clientXyZ,
36
+ clientXYZ,
37
37
  ]);
38
38
 
39
39
  /** A crude representation of a {@link XY} coordinate. */
@@ -141,7 +141,7 @@ export const translate: Translate = (a, b, v, ...cb): XY => {
141
141
  * @returns the given coordinate the given direction set to the given value.
142
142
  * @example set({ x: 1, y: 2 }, "x", 3) // { x: 3, y: 2 }
143
143
  */
144
- export const set = (c: Crude, direction: CrudeDirection, value: number): XY => {
144
+ export const set = (c: Crude, direction: direction.Crude, value: number): XY => {
145
145
  const xy = construct(c);
146
146
  const d = dir.construct(direction);
147
147
  if (d === "x") return { x: value, y: xy.y };
@@ -448,4 +448,69 @@ describe("status", () => {
448
448
  });
449
449
  });
450
450
  });
451
+
452
+ describe("toError", () => {
453
+ it("should use the wrapped status message as the Error message", () => {
454
+ const inner = new Error("raw");
455
+ const s = status.fromException(inner, "Failed to fetch");
456
+ const err = status.toError(s);
457
+
458
+ expect(err.message).toBe("Failed to fetch");
459
+ });
460
+
461
+ it("should copy name from the inner error", () => {
462
+ class NotFoundError extends Error {
463
+ constructor(message: string) {
464
+ super(message);
465
+ this.name = "NotFoundError";
466
+ }
467
+ }
468
+ const inner = new NotFoundError("missing");
469
+ const s = status.fromException(inner, "Failed to fetch");
470
+ const err = status.toError(s);
471
+
472
+ expect(err.name).toBe("NotFoundError");
473
+ });
474
+
475
+ it("should copy stack from the inner error", () => {
476
+ const inner = new Error("raw");
477
+ inner.stack = "Error: raw\n at someFn (file.ts:1:1)";
478
+ const s = status.fromException(inner, "Failed to fetch");
479
+ const err = status.toError(s);
480
+
481
+ expect(err.stack).toBe("Error: raw\n at someFn (file.ts:1:1)");
482
+ });
483
+
484
+ it("should not leak the toError call-site stack when inner stack is missing", () => {
485
+ const inner = new Error("raw");
486
+ inner.stack = undefined;
487
+ const s = status.fromException(inner, "Failed to fetch");
488
+ const err = status.toError(s);
489
+
490
+ expect(err.stack).toBeUndefined();
491
+ });
492
+
493
+ it("should set cause to the original status", () => {
494
+ const inner = new Error("raw");
495
+ const s = status.fromException(inner, "Failed to fetch");
496
+ const err = status.toError(s);
497
+
498
+ expect(err.cause).toBe(s);
499
+ });
500
+
501
+ it("should round-trip through throw/catch", () => {
502
+ const inner = new TypeError("not a function");
503
+ const s = status.fromException(inner, "Failed to invoke");
504
+
505
+ try {
506
+ throw status.toError(s);
507
+ } catch (caught) {
508
+ expect(caught).toBeInstanceOf(Error);
509
+ const e = caught as Error;
510
+ expect(e.name).toBe("TypeError");
511
+ expect(e.message).toBe("Failed to invoke");
512
+ expect(e.cause).toBe(s);
513
+ }
514
+ });
515
+ });
451
516
  });
@@ -97,6 +97,26 @@ export const fromException = (
97
97
  return create<typeof exceptionDetailsSchema, "error">(crude);
98
98
  };
99
99
 
100
+ /**
101
+ * Converts an exception-shaped status (one built via {@link fromException}) back
102
+ * into a thrown-shaped {@link Error}. The returned error carries the status's
103
+ * wrapped message, copies `name` and `stack` from the inner error preserved on
104
+ * `details.error`, and stashes the full status on `cause` for callers that need
105
+ * the rich shape.
106
+ *
107
+ * Use this when bridging the status pipeline back into a context that expects
108
+ * a real Error — typically before `throw`-ing across an error boundary.
109
+ */
110
+ export const toError = (
111
+ s: Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">>,
112
+ ): Error => {
113
+ const inner = s.details.error;
114
+ const err = new Error(s.message, { cause: s });
115
+ err.name = inner.name;
116
+ err.stack = inner.stack;
117
+ return err;
118
+ };
119
+
100
120
  export const create = <
101
121
  DetailsSchema extends z.ZodType = z.ZodNever,
102
122
  V extends Variant = Variant,
@@ -11,3 +11,11 @@ export * from "@/telem/clockSkew";
11
11
  export { type GLBufferController } from "@/telem/gl";
12
12
  export * from "@/telem/series";
13
13
  export * from "@/telem/telem";
14
+ export {
15
+ TIME_ZONES,
16
+ TIMESTAMP_FORMATS,
17
+ type TimestampFormat,
18
+ timestampFormatZ,
19
+ type TimeZone,
20
+ timeZoneZ,
21
+ } from "@/telem/types.gen";
@@ -369,6 +369,155 @@ describe("Series", () => {
369
369
  });
370
370
  });
371
371
 
372
+ describe("asString", () => {
373
+ it("should return the string value for a string series", () => {
374
+ const series = new Series({
375
+ data: ["apple", "banana"],
376
+ dataType: DataType.STRING,
377
+ });
378
+ expect(series.asString(0, true)).toEqual("apple");
379
+ expect(series.asString(1, true)).toEqual("banana");
380
+ });
381
+
382
+ it("should return the raw JSON string without parsing for a JSON series", () => {
383
+ const series = new Series({
384
+ data: [{ a_b: 1, c_d: "apple" }],
385
+ dataType: DataType.JSON,
386
+ });
387
+ const result = series.asString(0, true);
388
+ expect(result).toEqual('{"a_b":1,"c_d":"apple"}');
389
+ });
390
+
391
+ it("should preserve raw snake_case keys while at() converts them to camelCase", () => {
392
+ const series = new Series({
393
+ data: [{ user_id: 1, first_name: "alice" }],
394
+ dataType: DataType.JSON,
395
+ });
396
+ expect(series.asString(0, true)).toEqual('{"user_id":1,"first_name":"alice"}');
397
+ expect(series.at(0, true)).toEqual({ userId: 1, firstName: "alice" });
398
+ });
399
+
400
+ it("should decode UTF-8 bytes for a BYTES series", () => {
401
+ const payload = new TextEncoder().encode("hello world");
402
+ const buf = new ArrayBuffer(4 + payload.byteLength);
403
+ new DataView(buf).setUint32(0, payload.byteLength, true);
404
+ new Uint8Array(buf).set(payload, 4);
405
+ const series = new Series({ data: buf, dataType: DataType.BYTES });
406
+ expect(series.asString(0, true)).toEqual("hello world");
407
+ });
408
+
409
+ it("should emit the U+FFFD replacement character for invalid UTF-8 in a BYTES series", () => {
410
+ const payload = new Uint8Array([0xff, 0xfe]);
411
+ const buf = new ArrayBuffer(4 + payload.byteLength);
412
+ new DataView(buf).setUint32(0, payload.byteLength, true);
413
+ new Uint8Array(buf).set(payload, 4);
414
+ const series = new Series({ data: buf, dataType: DataType.BYTES });
415
+ expect(series.asString(0, true)).toEqual("��");
416
+ });
417
+
418
+ it("should return a string representation for a numeric series", () => {
419
+ const series = new Series({
420
+ data: new Float32Array([3.5, 7]),
421
+ dataType: DataType.FLOAT32,
422
+ });
423
+ expect(series.asString(0, true)).toEqual("3.5");
424
+ expect(series.asString(1, true)).toEqual("7");
425
+ });
426
+
427
+ it("should return the shortest f32-roundtrippable decimal for typical FLOAT32 values", () => {
428
+ const series = new Series({
429
+ data: new Float32Array([1.234, 0.1, 100.5, -1.234, 3.14, -0]),
430
+ dataType: DataType.FLOAT32,
431
+ });
432
+ expect(series.asString(0, true)).toEqual("1.234");
433
+ expect(series.asString(1, true)).toEqual("0.1");
434
+ expect(series.asString(2, true)).toEqual("100.5");
435
+ expect(series.asString(3, true)).toEqual("-1.234");
436
+ expect(series.asString(4, true)).toEqual("3.14");
437
+ expect(series.asString(5, true)).toEqual("0");
438
+ });
439
+
440
+ it("should stringify integer-valued FLOAT32 values without decimals", () => {
441
+ const series = new Series({
442
+ data: new Float32Array([0, 1, -1, 1000000, -1000000]),
443
+ dataType: DataType.FLOAT32,
444
+ });
445
+ expect(series.asString(0, true)).toEqual("0");
446
+ expect(series.asString(1, true)).toEqual("1");
447
+ expect(series.asString(2, true)).toEqual("-1");
448
+ expect(series.asString(3, true)).toEqual("1000000");
449
+ expect(series.asString(4, true)).toEqual("-1000000");
450
+ });
451
+
452
+ it("should preserve f32 representation for very small FLOAT32 values", () => {
453
+ const subnormal = Math.fround(1e-40);
454
+ const series = new Series({
455
+ data: new Float32Array([Number.MIN_VALUE, subnormal]),
456
+ dataType: DataType.FLOAT32,
457
+ });
458
+ // Number.MIN_VALUE (5e-324) underflows to 0 in f32.
459
+ expect(series.asString(0, true)).toEqual("0");
460
+ expect(Math.fround(parseFloat(series.asString(1, true)))).toBe(subnormal);
461
+ });
462
+
463
+ it("should preserve f32 representation for very large FLOAT32 values", () => {
464
+ const maxF32 = Math.fround(3.4e38);
465
+ const large = Math.fround(1e30);
466
+ const series = new Series({
467
+ data: new Float32Array([maxF32, large]),
468
+ dataType: DataType.FLOAT32,
469
+ });
470
+ expect(Math.fround(parseFloat(series.asString(0, true)))).toBe(maxF32);
471
+ expect(Math.fround(parseFloat(series.asString(1, true)))).toBe(large);
472
+ });
473
+
474
+ it("should stringify non-finite FLOAT32 values", () => {
475
+ const series = new Series({
476
+ data: new Float32Array([NaN, Infinity, -Infinity]),
477
+ dataType: DataType.FLOAT32,
478
+ });
479
+ expect(series.asString(0, true)).toEqual("NaN");
480
+ expect(series.asString(1, true)).toEqual("Infinity");
481
+ expect(series.asString(2, true)).toEqual("-Infinity");
482
+ });
483
+
484
+ it("should produce idempotent stringification under FLOAT32 round-trip", () => {
485
+ const cases = [1.234, 0.1, 100.5, -1.234, 1.2339999675750732, Math.PI, 1e-20];
486
+ for (const v of cases) {
487
+ const s1 = new Series({
488
+ data: new Float32Array([v]),
489
+ dataType: DataType.FLOAT32,
490
+ }).asString(0, true);
491
+ const s2 = new Series({
492
+ data: new Float32Array([parseFloat(s1)]),
493
+ dataType: DataType.FLOAT32,
494
+ }).asString(0, true);
495
+ expect(s2).toBe(s1);
496
+ }
497
+ });
498
+
499
+ it("should return undefined when index is out of bounds", () => {
500
+ const series = new Series({
501
+ data: ["apple"],
502
+ dataType: DataType.STRING,
503
+ });
504
+ expect(series.asString(5)).toBeUndefined();
505
+ });
506
+
507
+ it("should throw when index is out of bounds and required is true", () => {
508
+ const series = new Series({
509
+ data: ["apple"],
510
+ dataType: DataType.STRING,
511
+ });
512
+ expect(() => series.asString(5, true)).toThrow();
513
+ });
514
+
515
+ it("should return the UUID string for a UUID series", () => {
516
+ const series = new Series({ data: SAMPLE_UUID_BYTES, dataType: DataType.UUID });
517
+ expect(series.asString(0, true)).toEqual("123e4567-e89b-40d3-8056-426614174000");
518
+ });
519
+ });
520
+
372
521
  describe("atAlignment", () => {
373
522
  it("should return the value at a particular alignment", () => {
374
523
  const series = new Series({
@@ -382,6 +531,40 @@ describe("Series", () => {
382
531
  });
383
532
  });
384
533
 
534
+ describe("bigint sampleOffset precision", () => {
535
+ it("preserves precision in at when sampleOffset is a bigint above 2^53", () => {
536
+ const offset = 1778020940471336960n;
537
+ const series = new Series({
538
+ data: new Float32Array([0, 1, 2]),
539
+ dataType: DataType.FLOAT32,
540
+ sampleOffset: offset,
541
+ });
542
+ expect(series.at(0)).toBe(offset);
543
+ expect(series.at(1)).toBe(offset + 1n);
544
+ expect(series.at(2)).toBe(offset + 2n);
545
+ });
546
+
547
+ it("preserves precision in max when sampleOffset is a bigint above 2^53", () => {
548
+ const offset = 1778020940471336960n;
549
+ const series = new Series({
550
+ data: new Float32Array([0, 1, 2]),
551
+ dataType: DataType.FLOAT32,
552
+ sampleOffset: offset,
553
+ });
554
+ expect(series.max).toBe(offset + 2n);
555
+ });
556
+
557
+ it("preserves precision in min when sampleOffset is a bigint above 2^53", () => {
558
+ const offset = 1778020940471336960n;
559
+ const series = new Series({
560
+ data: new Float32Array([0, 1, 2]),
561
+ dataType: DataType.FLOAT32,
562
+ sampleOffset: offset,
563
+ });
564
+ expect(series.min).toBe(offset);
565
+ });
566
+ });
567
+
385
568
  describe("slice", () => {
386
569
  it("should slice a lazy array", () => {
387
570
  const a = new Series({
@@ -45,6 +45,17 @@ interface GL {
45
45
 
46
46
  interface IterableIterator<T> extends Iterator<T>, Iterable<T> {}
47
47
 
48
+ /** Shortest decimal string that round-trips through f32 — JS analogue of Go's strconv.FormatFloat(_, 'g', -1, 32). */
49
+ const stringifyFloat32 = (value: number): string => {
50
+ const f32 = Math.fround(value);
51
+ if (!Number.isFinite(f32)) return f32.toString();
52
+ for (let p = 1; p <= 9; p++) {
53
+ const parsed = parseFloat(f32.toPrecision(p));
54
+ if (Math.fround(parsed) === f32) return parsed.toString();
55
+ }
56
+ return f32.toString();
57
+ };
58
+
48
59
  /** A condensed set of information describing the layout of a series. */
49
60
  export interface SeriesDigest {
50
61
  key: string;
@@ -193,12 +204,14 @@ export class Series<T extends TelemValue = TelemValue>
193
204
  timeRange: TimeRange.z.optional(),
194
205
  dataType: DataType.z,
195
206
  alignment: z.coerce.bigint().optional(),
196
- data: z.union([
197
- stringArrayZ,
198
- nullArrayZ,
199
- z.instanceof(ArrayBuffer),
200
- z.instanceof(Uint8Array),
201
- ]),
207
+ data: z
208
+ .union([
209
+ stringArrayZ,
210
+ nullArrayZ,
211
+ z.instanceof(ArrayBuffer),
212
+ z.instanceof(Uint8Array),
213
+ ])
214
+ .default(() => new Uint8Array().buffer),
202
215
  glBufferUsage: glBufferUsageZ.default("static").optional(),
203
216
  });
204
217
 
@@ -294,7 +307,7 @@ export class Series<T extends TelemValue = TelemValue>
294
307
  data,
295
308
  } = props;
296
309
  if (isSeries(data)) {
297
- const data_ = data as Series;
310
+ const data_ = data;
298
311
  this.key = data_.key;
299
312
  this.dataType = data_.dataType;
300
313
  this.sampleOffset = data_.sampleOffset;
@@ -319,7 +332,7 @@ export class Series<T extends TelemValue = TelemValue>
319
332
  "cannot infer data type from an ArrayBuffer instance when constructing a Series. Please provide a data type.",
320
333
  );
321
334
  else if (isArray || isSingle) {
322
- let first: TelemValue | unknown = data as TelemValue;
335
+ let first: TelemValue | unknown = data;
323
336
  if (!isSingle) {
324
337
  if (data.length === 0)
325
338
  throw new Error(
@@ -694,7 +707,7 @@ export class Series<T extends TelemValue = TelemValue>
694
707
  throw new Error("cannot calculate maximum on a variable length data type");
695
708
  if (this.writePos === 0) return -Infinity;
696
709
  this.cachedMax ??= this.calcRawMax();
697
- return math.add(this.cachedMax, this.sampleOffset);
710
+ return this.applyOffset(this.cachedMax);
698
711
  }
699
712
 
700
713
  private calcRawMin(): math.Numeric {
@@ -720,7 +733,7 @@ export class Series<T extends TelemValue = TelemValue>
720
733
  throw new Error("cannot calculate minimum on a variable length data type");
721
734
  if (this.writePos === 0) return Infinity;
722
735
  this.cachedMin ??= this.calcRawMin();
723
- return math.add(this.cachedMin, this.sampleOffset);
736
+ return this.applyOffset(this.cachedMin);
724
737
  }
725
738
 
726
739
  /** @returns the bounds of the series. */
@@ -777,7 +790,12 @@ export class Series<T extends TelemValue = TelemValue>
777
790
  at(index: number, required?: false): T | undefined;
778
791
 
779
792
  at(index: number, required: boolean = false): T | undefined {
780
- if (this.dataType.isVariable) return this.atVariable(index, required ?? false);
793
+ if (this.dataType.isVariable) {
794
+ const str = this.atVariable(index, required);
795
+ if (str == null) return undefined;
796
+ if (this.dataType.equals(DataType.STRING)) return str as T;
797
+ return caseconv.snakeToCamel(JSON.parse(str)) as T;
798
+ }
781
799
  if (this.dataType.equals(DataType.UUID)) return this.atUUID(index, required) as T;
782
800
  if (index < 0) index = this.length + index;
783
801
  const v = this.data[index];
@@ -785,7 +803,16 @@ export class Series<T extends TelemValue = TelemValue>
785
803
  if (required === true) throw new Error(`[series] - no value at index ${index}`);
786
804
  return undefined;
787
805
  }
788
- return math.add(v, this.sampleOffset) as T;
806
+ return this.applyOffset(v) as T;
807
+ }
808
+
809
+ // For huge bigint offsets (i64 timestamps narrowed to float32 for GL), math.add would
810
+ // coerce through Number() and lose precision above 2^53; do the addition in bigint
811
+ // space when the offset is a bigint.
812
+ private applyOffset(v: math.Numeric): math.Numeric {
813
+ if (typeof this.sampleOffset === "bigint" && typeof v === "number")
814
+ return BigInt(Math.round(v)) + this.sampleOffset;
815
+ return math.add(v, this.sampleOffset);
789
816
  }
790
817
 
791
818
  private atUUID(index: number, required: boolean): string | undefined {
@@ -800,7 +827,20 @@ export class Series<T extends TelemValue = TelemValue>
800
827
  return uuidString;
801
828
  }
802
829
 
803
- private atVariable(index: number, required: boolean): T | undefined {
830
+ asString(index: number, required: true): string;
831
+
832
+ asString(index: number, required?: false): string | undefined;
833
+
834
+ asString(index: number, required: boolean = false): string | undefined {
835
+ if (this.dataType.isVariable) return this.atVariable(index, required);
836
+ if (this.dataType.equals(DataType.UUID)) return this.atUUID(index, required);
837
+ const v = this.at(index, required as true);
838
+ if (v == null) return undefined;
839
+ if (this.dataType.equals(DataType.FLOAT32)) return stringifyFloat32(v as number);
840
+ return String(v);
841
+ }
842
+
843
+ private atVariable(index: number, required: boolean): string | undefined {
804
844
  let start = 0;
805
845
  let len = 0;
806
846
  const buf = this.buffer;
@@ -836,9 +876,7 @@ export class Series<T extends TelemValue = TelemValue>
836
876
  }
837
877
  }
838
878
  const slice = new Uint8Array(buf, start, len);
839
- if (this.dataType.equals(DataType.STRING))
840
- return new TextDecoder().decode(slice) as T;
841
- return caseconv.snakeToCamel(JSON.parse(new TextDecoder().decode(slice))) as T;
879
+ return new TextDecoder().decode(slice);
842
880
  }
843
881
 
844
882
  /**