@synnaxlabs/x 0.54.2 → 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 (150) 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/strings/strings.d.ts +9 -0
  53. package/dist/src/strings/strings.d.ts.map +1 -1
  54. package/dist/src/telem/clockSkew.d.ts +17 -0
  55. package/dist/src/telem/clockSkew.d.ts.map +1 -0
  56. package/dist/src/telem/clockSkew.spec.d.ts +2 -0
  57. package/dist/src/telem/clockSkew.spec.d.ts.map +1 -0
  58. package/dist/src/telem/external.d.ts +2 -0
  59. package/dist/src/telem/external.d.ts.map +1 -1
  60. package/dist/src/telem/series.d.ts +6 -3
  61. package/dist/src/telem/series.d.ts.map +1 -1
  62. package/dist/src/telem/telem.d.ts +48 -39
  63. package/dist/src/telem/telem.d.ts.map +1 -1
  64. package/dist/src/telem/types.gen.d.ts +19 -0
  65. package/dist/src/telem/types.gen.d.ts.map +1 -0
  66. package/dist/src/text/external.d.ts +3 -0
  67. package/dist/src/text/external.d.ts.map +1 -0
  68. package/dist/src/text/index.d.ts +2 -0
  69. package/dist/src/text/index.d.ts.map +1 -0
  70. package/dist/src/text/types.d.ts +21 -0
  71. package/dist/src/text/types.d.ts.map +1 -0
  72. package/dist/src/text/types.gen.d.ts +13 -0
  73. package/dist/src/text/types.gen.d.ts.map +1 -0
  74. package/dist/src/throttle/index.d.ts +2 -0
  75. package/dist/src/throttle/index.d.ts.map +1 -0
  76. package/dist/src/throttle/throttle.d.ts +3 -0
  77. package/dist/src/throttle/throttle.d.ts.map +1 -0
  78. package/dist/src/throttle/throttle.spec.d.ts +2 -0
  79. package/dist/src/throttle/throttle.spec.d.ts.map +1 -0
  80. package/dist/src/zod/parse.d.ts.map +1 -1
  81. package/dist/x.cjs +10 -13
  82. package/dist/x.js +2271 -2084
  83. package/package.json +12 -12
  84. package/src/array/nullable.ts +1 -4
  85. package/src/caseconv/caseconv.spec.ts +71 -0
  86. package/src/caseconv/caseconv.ts +15 -2
  87. package/src/compare/compare.spec.ts +115 -0
  88. package/src/compare/compare.ts +29 -0
  89. package/src/debounce/debounce.spec.ts +258 -24
  90. package/src/debounce/debounce.ts +49 -30
  91. package/src/deep/copy.spec.ts +13 -0
  92. package/src/deep/difference.ts +1 -1
  93. package/src/destructor/destructor.ts +2 -0
  94. package/src/errors/errors.spec.ts +30 -0
  95. package/src/errors/errors.ts +29 -17
  96. package/src/index.ts +4 -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 +65 -0
  120. package/src/status/status.ts +20 -0
  121. package/src/strings/strings.spec.ts +19 -0
  122. package/src/strings/strings.ts +16 -0
  123. package/src/telem/clockSkew.spec.ts +58 -0
  124. package/src/telem/clockSkew.ts +46 -0
  125. package/src/telem/external.ts +9 -0
  126. package/src/telem/series.spec.ts +235 -4
  127. package/src/telem/series.ts +172 -58
  128. package/src/telem/telem.spec.ts +147 -9
  129. package/src/telem/telem.ts +101 -84
  130. package/src/telem/types.gen.ts +28 -0
  131. package/src/text/external.ts +11 -0
  132. package/src/text/index.ts +10 -0
  133. package/src/text/types.gen.ts +16 -0
  134. package/src/text/types.ts +37 -0
  135. package/src/{worker → throttle}/index.ts +1 -1
  136. package/src/throttle/throttle.spec.ts +147 -0
  137. package/src/throttle/throttle.ts +44 -0
  138. package/src/zod/parse.ts +2 -3
  139. package/tsconfig.tsbuildinfo +1 -1
  140. package/dist/src/spatial/spatial.d.ts +0 -3
  141. package/dist/src/spatial/spatial.d.ts.map +0 -1
  142. package/dist/src/worker/index.d.ts +0 -2
  143. package/dist/src/worker/index.d.ts.map +0 -1
  144. package/dist/src/worker/worker.d.ts +0 -33
  145. package/dist/src/worker/worker.d.ts.map +0 -1
  146. package/dist/src/worker/worker.spec.d.ts +0 -2
  147. package/dist/src/worker/worker.spec.d.ts.map +0 -1
  148. package/src/spatial/spatial.ts +0 -44
  149. package/src/worker/worker.spec.ts +0 -41
  150. package/src/worker/worker.ts +0 -86
@@ -141,7 +141,7 @@ describe("Series", () => {
141
141
  const s = new Series({ data: [{ a: 1, b: "apple" }] });
142
142
  expect(s.dataType.equals(DataType.JSON));
143
143
  expect(s.length).toEqual(1);
144
- expect(s.data.at(-1)).toEqual(10);
144
+ expect(s.at(0)).toEqual({ a: 1, b: "apple" });
145
145
  });
146
146
 
147
147
  it("should correctly interpret a bigint as an int64", () => {
@@ -214,8 +214,8 @@ describe("Series", () => {
214
214
 
215
215
  it("should convert encoded keys to snake_case", () => {
216
216
  const a = new Series({ data: [{ aB: 1, bC: "apple" }], dataType: DataType.JSON });
217
- const strContent = new TextDecoder().decode(a.data);
218
- expect(strContent).toBe('{"a_b":1,"b_c":"apple"}\n');
217
+ expect(a.length).toEqual(1);
218
+ expect(a.at(0)).toEqual({ aB: 1, bC: "apple" });
219
219
  });
220
220
 
221
221
  it("should throw an error when an empty JS array is provided and no data type is provided", () => {
@@ -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({
@@ -502,7 +685,7 @@ describe("Series", () => {
502
685
  });
503
686
 
504
687
  it("should recompute the length of a variable density array", () => {
505
- const series = Series.alloc({ capacity: 12, dataType: DataType.STRING });
688
+ const series = Series.alloc({ capacity: 18, dataType: DataType.STRING });
506
689
  expect(series.length).toEqual(0);
507
690
  const writeOne = new Series({ data: ["apple"] });
508
691
  expect(series.write(writeOne)).toEqual(1);
@@ -689,6 +872,54 @@ describe("Series", () => {
689
872
  { a: 3, b: "carrot" },
690
873
  ]);
691
874
  });
875
+
876
+ it("should handle a single JSON value", () => {
877
+ const s = new Series([{ key: "val" }]);
878
+ expect(s.length).toEqual(1);
879
+ expect(s.at(0)).toEqual({ key: "val" });
880
+ });
881
+
882
+ it("should handle empty JSON objects", () => {
883
+ const s = new Series([{}, {}, {}]);
884
+ expect(s.length).toEqual(3);
885
+ expect(s.at(0)).toEqual({});
886
+ expect(s.at(2)).toEqual({});
887
+ });
888
+
889
+ it("should support negative indexing", () => {
890
+ const s = new Series([{ a: 1 }, { a: 2 }, { a: 3 }]);
891
+ expect(s.at(-1)).toEqual({ a: 3 });
892
+ expect(s.at(-2)).toEqual({ a: 2 });
893
+ });
894
+
895
+ it("should handle an empty JSON series", () => {
896
+ const s = new Series({ data: new ArrayBuffer(0), dataType: DataType.JSON });
897
+ expect(s.length).toEqual(0);
898
+ expect(Array.from(s)).toEqual([]);
899
+ });
900
+ });
901
+
902
+ describe("bytes series", () => {
903
+ it("should handle an empty bytes series", () => {
904
+ const s = new Series({ data: new ArrayBuffer(0), dataType: DataType.BYTES });
905
+ expect(s.length).toEqual(0);
906
+ expect(Array.from(s)).toEqual([]);
907
+ });
908
+
909
+ it("should correctly compute the length of a bytes series from a length-prefixed buffer", () => {
910
+ const payload1 = new Uint8Array([1, 2, 3]);
911
+ const payload2 = new Uint8Array([4, 5]);
912
+ const totalBytes = 4 + payload1.byteLength + 4 + payload2.byteLength;
913
+ const buf = new ArrayBuffer(totalBytes);
914
+ const view = new DataView(buf);
915
+ const bytes = new Uint8Array(buf);
916
+ view.setUint32(0, payload1.byteLength, true);
917
+ bytes.set(payload1, 4);
918
+ view.setUint32(4 + payload1.byteLength, payload2.byteLength, true);
919
+ bytes.set(payload2, 4 + payload1.byteLength + 4);
920
+ const s = new Series({ data: buf, dataType: DataType.BYTES });
921
+ expect(s.length).toEqual(2);
922
+ });
692
923
  });
693
924
 
694
925
  describe("binarySearch", () => {
@@ -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;
@@ -114,7 +125,7 @@ const nullArrayZ = z
114
125
  .union([z.null(), z.undefined()])
115
126
  .transform(() => new Uint8Array().buffer);
116
127
 
117
- const NEW_LINE = 10;
128
+ const UINT32_SIZE = 4;
118
129
 
119
130
  type JSType = "string" | "number" | "bigint";
120
131
 
@@ -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(
@@ -358,12 +371,42 @@ export class Series<T extends TelemValue = TelemValue>
358
371
  data_ = data_.map((v) => new TimeStamp(v as CrudeTimeStamp).valueOf());
359
372
  if (this.dataType.equals(DataType.STRING)) {
360
373
  this.cachedLength = data_.length;
361
- this._data = new TextEncoder().encode(`${data_.join("\n")}\n`).buffer;
374
+ const encoded = (data_ as string[]).map((s) => new TextEncoder().encode(s));
375
+ const totalBytes = encoded.reduce(
376
+ (acc, e) => acc + UINT32_SIZE + e.byteLength,
377
+ 0,
378
+ );
379
+ const buf = new ArrayBuffer(totalBytes);
380
+ const view = new DataView(buf);
381
+ const bytes = new Uint8Array(buf);
382
+ let offset = 0;
383
+ for (const e of encoded) {
384
+ view.setUint32(offset, e.byteLength, true);
385
+ offset += UINT32_SIZE;
386
+ bytes.set(e, offset);
387
+ offset += e.byteLength;
388
+ }
389
+ this._data = buf;
362
390
  } else if (this.dataType.equals(DataType.JSON)) {
363
391
  this.cachedLength = data_.length;
364
- this._data = new TextEncoder().encode(
365
- `${data_.map((d) => binary.JSON_CODEC.encodeString(d)).join("\n")}\n`,
366
- ).buffer;
392
+ const encoded = data_.map((d) =>
393
+ new TextEncoder().encode(binary.JSON_CODEC.encodeString(d)),
394
+ );
395
+ const totalBytes = encoded.reduce(
396
+ (acc, e) => acc + UINT32_SIZE + e.byteLength,
397
+ 0,
398
+ );
399
+ const buf = new ArrayBuffer(totalBytes);
400
+ const view = new DataView(buf);
401
+ const bytes = new Uint8Array(buf);
402
+ let offset = 0;
403
+ for (const e of encoded) {
404
+ view.setUint32(offset, e.byteLength, true);
405
+ offset += UINT32_SIZE;
406
+ bytes.set(e, offset);
407
+ offset += e.byteLength;
408
+ }
409
+ this._data = buf;
367
410
  } else if (this.dataType.usesBigInt && typeof first === "number")
368
411
  this._data = new this.dataType.Array(
369
412
  data_.map((v) => BigInt(Math.round(v as number))),
@@ -456,14 +499,25 @@ export class Series<T extends TelemValue = TelemValue>
456
499
  private writeVariable(other: Series): number {
457
500
  if (this.writePos === FULL_BUFFER) return 0;
458
501
  const available = this.byteCapacity.valueOf() - this.writePos;
459
- const toWrite = other.subBytes(0, available);
460
- this.writeToUnderlyingData(toWrite);
461
- this.writePos += toWrite.byteLength.valueOf();
462
- if (this.cachedLength != null) {
463
- this.cachedLength += toWrite.length;
464
- this.calculateCachedLength();
502
+ const otherBuf = other.buffer;
503
+ const otherByteLen = other.byteLength.valueOf();
504
+ const view = new DataView(otherBuf);
505
+ let offset = 0;
506
+ let samplesWritten = 0;
507
+ while (offset + UINT32_SIZE <= otherByteLen) {
508
+ const sampleLen = view.getUint32(offset, true);
509
+ const recordSize = UINT32_SIZE + sampleLen;
510
+ if (offset + recordSize > available) break;
511
+ offset += recordSize;
512
+ samplesWritten++;
465
513
  }
466
- return toWrite.length;
514
+ if (offset === 0) return 0;
515
+ const toWrite = other.subBytes(0, offset);
516
+ this.writeToUnderlyingData(toWrite);
517
+ this.writePos += offset;
518
+ this.cachedLength = (this.cachedLength ?? 0) + samplesWritten;
519
+ this._cachedIndexes = undefined;
520
+ return samplesWritten;
467
521
  }
468
522
 
469
523
  private writeFixed(other: Series): number {
@@ -511,8 +565,21 @@ export class Series<T extends TelemValue = TelemValue>
511
565
  * @returns An array of string representations of the series values.
512
566
  */
513
567
  toStrings(): string[] {
514
- if (this.dataType.isVariable)
515
- return new TextDecoder().decode(this.underlyingData).split("\n").slice(0, -1);
568
+ if (this.dataType.isVariable) {
569
+ const result: string[] = [];
570
+ const buf = this.buffer;
571
+ const byteLen = this.byteLength.valueOf();
572
+ const view = new DataView(buf);
573
+ const decoder = new TextDecoder();
574
+ let offset = 0;
575
+ while (offset + UINT32_SIZE <= byteLen) {
576
+ const len = view.getUint32(offset, true);
577
+ offset += UINT32_SIZE;
578
+ result.push(decoder.decode(new Uint8Array(buf, offset, len)));
579
+ offset += len;
580
+ }
581
+ return result;
582
+ }
516
583
  return Array.from(this).map((d) => d.toString());
517
584
  }
518
585
 
@@ -560,7 +627,7 @@ export class Series<T extends TelemValue = TelemValue>
560
627
 
561
628
  /**
562
629
  * Returns the number of samples in this array.
563
- * For variable length data types, this is calculated by counting newlines.
630
+ * For variable length data types, this is calculated by scanning uint32 length prefixes.
564
631
  * @returns The number of samples in the series.
565
632
  */
566
633
  get length(): number {
@@ -575,12 +642,18 @@ export class Series<T extends TelemValue = TelemValue>
575
642
  if (!this.dataType.isVariable)
576
643
  throw new Error("cannot calculate length of a non-variable length data type");
577
644
  let cl = 0;
578
- const ci: number[] = [0];
579
- this.data.forEach((v, i) => {
580
- if (v !== NEW_LINE) return;
645
+ const ci: number[] = [];
646
+ const buf = this.buffer;
647
+ const byteLen = this.byteLength.valueOf();
648
+ const view = new DataView(buf);
649
+ let offset = 0;
650
+ while (offset + UINT32_SIZE <= byteLen) {
651
+ const len = view.getUint32(offset, true);
652
+ offset += UINT32_SIZE;
653
+ ci.push(offset);
654
+ offset += len;
581
655
  cl++;
582
- ci.push(i + 1);
583
- });
656
+ }
584
657
  this._cachedIndexes = ci;
585
658
  this.cachedLength = cl;
586
659
  return cl;
@@ -634,7 +707,7 @@ export class Series<T extends TelemValue = TelemValue>
634
707
  throw new Error("cannot calculate maximum on a variable length data type");
635
708
  if (this.writePos === 0) return -Infinity;
636
709
  this.cachedMax ??= this.calcRawMax();
637
- return math.add(this.cachedMax, this.sampleOffset);
710
+ return this.applyOffset(this.cachedMax);
638
711
  }
639
712
 
640
713
  private calcRawMin(): math.Numeric {
@@ -660,7 +733,7 @@ export class Series<T extends TelemValue = TelemValue>
660
733
  throw new Error("cannot calculate minimum on a variable length data type");
661
734
  if (this.writePos === 0) return Infinity;
662
735
  this.cachedMin ??= this.calcRawMin();
663
- return math.add(this.cachedMin, this.sampleOffset);
736
+ return this.applyOffset(this.cachedMin);
664
737
  }
665
738
 
666
739
  /** @returns the bounds of the series. */
@@ -717,7 +790,12 @@ export class Series<T extends TelemValue = TelemValue>
717
790
  at(index: number, required?: false): T | undefined;
718
791
 
719
792
  at(index: number, required: boolean = false): T | undefined {
720
- 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
+ }
721
799
  if (this.dataType.equals(DataType.UUID)) return this.atUUID(index, required) as T;
722
800
  if (index < 0) index = this.length + index;
723
801
  const v = this.data[index];
@@ -725,7 +803,16 @@ export class Series<T extends TelemValue = TelemValue>
725
803
  if (required === true) throw new Error(`[series] - no value at index ${index}`);
726
804
  return undefined;
727
805
  }
728
- 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);
729
816
  }
730
817
 
731
818
  private atUUID(index: number, required: boolean): string | undefined {
@@ -740,33 +827,56 @@ export class Series<T extends TelemValue = TelemValue>
740
827
  return uuidString;
741
828
  }
742
829
 
743
- 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 {
744
844
  let start = 0;
745
- let end = 0;
845
+ let len = 0;
846
+ const buf = this.buffer;
847
+ const view = new DataView(buf);
746
848
  if (this._cachedIndexes != null) {
849
+ if (index < 0) index = this._cachedIndexes.length + index;
850
+ if (index < 0 || index >= this._cachedIndexes.length) {
851
+ if (required) throw new Error(`[series] - no value at index ${index}`);
852
+ return undefined;
853
+ }
747
854
  start = this._cachedIndexes[index];
748
- end = this._cachedIndexes[index + 1] - 1;
855
+ len = view.getUint32(start - UINT32_SIZE, true);
749
856
  } else {
750
857
  if (index < 0) index = this.length + index;
751
- for (let i = 0; i < this.data.length; i++)
752
- if (this.data[i] === NEW_LINE) {
753
- if (index === 0) {
754
- end = i;
755
- break;
756
- }
757
- start = i + 1;
758
- index--;
858
+ const byteLen = this.byteLength.valueOf();
859
+ let offset = 0;
860
+ let found = false;
861
+ while (offset + UINT32_SIZE <= byteLen) {
862
+ const sampleLen = view.getUint32(offset, true);
863
+ offset += UINT32_SIZE;
864
+ if (index === 0) {
865
+ start = offset;
866
+ len = sampleLen;
867
+ found = true;
868
+ break;
759
869
  }
760
- if (end === 0) end = this.data.length;
761
- if (start >= end || index > 0) {
870
+ offset += sampleLen;
871
+ index--;
872
+ }
873
+ if (!found) {
762
874
  if (required) throw new Error(`[series] - no value at index ${index}`);
763
875
  return undefined;
764
876
  }
765
877
  }
766
- const slice = this.data.slice(start, end);
767
- if (this.dataType.equals(DataType.STRING))
768
- return new TextDecoder().decode(slice) as T;
769
- return caseconv.snakeToCamel(JSON.parse(new TextDecoder().decode(slice))) as T;
878
+ const slice = new Uint8Array(buf, start, len);
879
+ return new TextDecoder().decode(slice);
770
880
  }
771
881
 
772
882
  /**
@@ -1069,8 +1179,9 @@ class SubIterator<T> implements Iterator<T> {
1069
1179
 
1070
1180
  class StringSeriesIterator implements Iterator<string> {
1071
1181
  private readonly series: Series;
1072
- private index: number;
1182
+ private byteOffset: number;
1073
1183
  private readonly decoder: TextDecoder;
1184
+ private readonly view: DataView;
1074
1185
 
1075
1186
  constructor(series: Series) {
1076
1187
  if (!series.dataType.isVariable)
@@ -1078,18 +1189,21 @@ class StringSeriesIterator implements Iterator<string> {
1078
1189
  "cannot create a variable series iterator for a non-variable series",
1079
1190
  );
1080
1191
  this.series = series;
1081
- this.index = 0;
1192
+ this.byteOffset = 0;
1082
1193
  this.decoder = new TextDecoder();
1194
+ this.view = new DataView(series.buffer);
1083
1195
  }
1084
1196
 
1085
1197
  next(): IteratorResult<string> {
1086
- const start = this.index;
1087
- const data = this.series.data;
1088
- while (this.index < data.length && data[this.index] !== NEW_LINE) this.index++;
1089
- const end = this.index;
1090
- if (start === end) return { done: true, value: undefined };
1091
- this.index++;
1092
- const s = this.decoder.decode(this.series.buffer.slice(start, end));
1198
+ const byteLen = this.series.byteLength.valueOf();
1199
+ if (this.byteOffset + UINT32_SIZE > byteLen)
1200
+ return { done: true, value: undefined };
1201
+ const len = this.view.getUint32(this.byteOffset, true);
1202
+ this.byteOffset += UINT32_SIZE;
1203
+ const s = this.decoder.decode(
1204
+ new Uint8Array(this.series.buffer, this.byteOffset, len),
1205
+ );
1206
+ this.byteOffset += len;
1093
1207
  return { done: false, value: s };
1094
1208
  }
1095
1209
  }