@synnaxlabs/x 0.54.2 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/x",
3
- "version": "0.54.2",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
5
  "description": "Common Utilities for Synnax Labs",
6
6
  "repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
@@ -14,21 +14,21 @@
14
14
  ],
15
15
  "dependencies": {
16
16
  "async-mutex": "^0.5.0",
17
- "nanoid": "^5.1.7",
17
+ "nanoid": "^5.1.9",
18
18
  "uuid": "^13.0.0",
19
19
  "zod": "^4.3.6"
20
20
  },
21
21
  "devDependencies": {
22
- "@types/node": "^25.5.0",
23
- "@vitest/coverage-v8": "^4.1.2",
24
- "eslint": "^10.1.0",
22
+ "@types/node": "^25.6.0",
23
+ "@vitest/coverage-v8": "^4.1.4",
24
+ "eslint": "^10.2.1",
25
25
  "madge": "^8.0.0",
26
- "typescript": "^6.0.2",
27
- "vite": "^8.0.3",
28
- "vitest": "^4.1.2",
26
+ "typescript": "^6.0.3",
27
+ "vite": "^8.0.8",
28
+ "vitest": "^4.1.4",
29
+ "@synnaxlabs/eslint-config": "^0.0.0",
29
30
  "@synnaxlabs/tsconfig": "^0.0.0",
30
- "@synnaxlabs/vite-plugin": "^0.0.0",
31
- "@synnaxlabs/eslint-config": "^0.0.0"
31
+ "@synnaxlabs/vite-plugin": "^0.0.0"
32
32
  },
33
33
  "main": "dist/x.cjs",
34
34
  "module": "dist/x.js",
@@ -103,3 +103,22 @@ describe("trimPrefix", () => {
103
103
  it("should handle numbers in prefix", () =>
104
104
  expect(strings.trimPrefix("123abc", "123")).toBe("abc"));
105
105
  });
106
+
107
+ describe("escapeHTML", () => {
108
+ it("should escape ampersands", () =>
109
+ expect(strings.escapeHTML("a&b")).toBe("a&b"));
110
+
111
+ it("should escape angle brackets", () =>
112
+ expect(strings.escapeHTML("<div>")).toBe("&lt;div&gt;"));
113
+
114
+ it("should escape quotes", () =>
115
+ expect(strings.escapeHTML(`"it's"`)).toBe("&quot;it&#39;s&quot;"));
116
+
117
+ it("should return the original string when no special characters", () =>
118
+ expect(strings.escapeHTML("hello")).toBe("hello"));
119
+
120
+ it("should escape all special characters together", () =>
121
+ expect(strings.escapeHTML(`<a href="x">&`)).toBe(
122
+ "&lt;a href=&quot;x&quot;&gt;&amp;",
123
+ ));
124
+ });
@@ -108,3 +108,19 @@ export const trimPrefix = (str: string, prefix: string): string => {
108
108
  if (str.startsWith(prefix)) return str.slice(prefix.length);
109
109
  return str;
110
110
  };
111
+
112
+ /**
113
+ * Escapes HTML special characters in a string to prevent XSS and ensure
114
+ * correct rendering in HTML contexts.
115
+ *
116
+ * @param s - The string to escape.
117
+ * @returns The escaped string with &, <, >, ", and ' replaced by their
118
+ * HTML entity equivalents.
119
+ */
120
+ export const escapeHTML = (s: string): string =>
121
+ s
122
+ .replace(/&/g, "&amp;")
123
+ .replace(/</g, "&lt;")
124
+ .replace(/>/g, "&gt;")
125
+ .replace(/"/g, "&quot;")
126
+ .replace(/'/g, "&#39;");
@@ -0,0 +1,58 @@
1
+ // Copyright 2026 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import { ClockSkewCalculator, TimeSpan, TimeStamp } from "@/telem";
13
+
14
+ describe("ClockSkewCalculator", () => {
15
+ it("should correctly calculate clock skew from a single measurement", () => {
16
+ let mockTime = TimeStamp.seconds(0);
17
+ const calc = new ClockSkewCalculator(() => mockTime);
18
+ calc.start();
19
+ mockTime = TimeStamp.seconds(10);
20
+ // Remote midpoint is 3s, local midpoint is 5s, so skew is 2s
21
+ calc.end(TimeStamp.seconds(3));
22
+ expect(calc.skew).toEqual(TimeSpan.seconds(2));
23
+ expect(calc.exceeds(TimeSpan.seconds(1))).toBe(true);
24
+ expect(calc.exceeds(TimeSpan.seconds(3))).toBe(false);
25
+ });
26
+
27
+ it("should report zero skew when times match perfectly", () => {
28
+ let mockTime = TimeStamp.seconds(0);
29
+ const calc = new ClockSkewCalculator(() => mockTime);
30
+ calc.start();
31
+ mockTime = TimeStamp.seconds(10);
32
+ // Remote midpoint matches local midpoint at 5s
33
+ calc.end(TimeStamp.seconds(5));
34
+ expect(calc.skew).toEqual(TimeSpan.ZERO);
35
+ expect(calc.exceeds(TimeSpan.seconds(1))).toBe(false);
36
+ });
37
+
38
+ it("should return the most recent measurement", () => {
39
+ let mockTime = TimeStamp.seconds(0);
40
+ const calc = new ClockSkewCalculator(() => mockTime);
41
+ calc.start();
42
+ mockTime = TimeStamp.seconds(10);
43
+ calc.end(TimeStamp.seconds(3));
44
+ expect(calc.skew).toEqual(TimeSpan.seconds(2));
45
+ mockTime = TimeStamp.seconds(0);
46
+ calc.start();
47
+ mockTime = TimeStamp.seconds(10);
48
+ // Remote midpoint is 7s, local midpoint is 5s, so skew is -2s
49
+ calc.end(TimeStamp.seconds(7));
50
+ expect(calc.skew).toEqual(TimeSpan.seconds(-2));
51
+ });
52
+
53
+ it("should return zero skew when no measurements taken", () => {
54
+ const calc = new ClockSkewCalculator();
55
+ expect(calc.skew).toEqual(TimeSpan.ZERO);
56
+ expect(calc.exceeds(TimeSpan.seconds(1))).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,46 @@
1
+ // Copyright 2026 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
10
+ import { type CrudeTimeSpan, TimeSpan, TimeStamp } from "@/telem/telem";
11
+
12
+ /**
13
+ * Calculates and tracks clock skew between two systems using a midpoint
14
+ * synchronization algorithm. Useful for distributed systems where clock
15
+ * synchronization is critical.
16
+ */
17
+ export class ClockSkewCalculator {
18
+ private readonly now: () => TimeStamp;
19
+ private localStartT: TimeStamp = new TimeStamp(0);
20
+ private lastSkew: TimeSpan = TimeSpan.ZERO;
21
+
22
+ constructor(now: () => TimeStamp = () => TimeStamp.now()) {
23
+ this.now = now;
24
+ }
25
+
26
+ start(): void {
27
+ this.localStartT = this.now();
28
+ }
29
+
30
+ end(remoteMidpointT: TimeStamp): void {
31
+ const localEndT = this.now();
32
+ const halfSpan = localEndT.span(this.localStartT).valueOf() / 2n;
33
+ const thisMidpointT = this.localStartT.add(halfSpan);
34
+ this.lastSkew = new TimeSpan(thisMidpointT.valueOf() - remoteMidpointT.valueOf());
35
+ }
36
+
37
+ get skew(): TimeSpan {
38
+ return this.lastSkew;
39
+ }
40
+
41
+ exceeds(threshold: CrudeTimeSpan): boolean {
42
+ const skewVal = this.skew.valueOf();
43
+ const abs = skewVal < 0n ? -skewVal : skewVal;
44
+ return abs > new TimeSpan(threshold).valueOf();
45
+ }
46
+ }
@@ -7,6 +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
+ export * from "@/telem/clockSkew";
10
11
  export { type GLBufferController } from "@/telem/gl";
11
12
  export * from "@/telem/series";
12
13
  export * from "@/telem/telem";
@@ -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", () => {
@@ -502,7 +502,7 @@ describe("Series", () => {
502
502
  });
503
503
 
504
504
  it("should recompute the length of a variable density array", () => {
505
- const series = Series.alloc({ capacity: 12, dataType: DataType.STRING });
505
+ const series = Series.alloc({ capacity: 18, dataType: DataType.STRING });
506
506
  expect(series.length).toEqual(0);
507
507
  const writeOne = new Series({ data: ["apple"] });
508
508
  expect(series.write(writeOne)).toEqual(1);
@@ -689,6 +689,54 @@ describe("Series", () => {
689
689
  { a: 3, b: "carrot" },
690
690
  ]);
691
691
  });
692
+
693
+ it("should handle a single JSON value", () => {
694
+ const s = new Series([{ key: "val" }]);
695
+ expect(s.length).toEqual(1);
696
+ expect(s.at(0)).toEqual({ key: "val" });
697
+ });
698
+
699
+ it("should handle empty JSON objects", () => {
700
+ const s = new Series([{}, {}, {}]);
701
+ expect(s.length).toEqual(3);
702
+ expect(s.at(0)).toEqual({});
703
+ expect(s.at(2)).toEqual({});
704
+ });
705
+
706
+ it("should support negative indexing", () => {
707
+ const s = new Series([{ a: 1 }, { a: 2 }, { a: 3 }]);
708
+ expect(s.at(-1)).toEqual({ a: 3 });
709
+ expect(s.at(-2)).toEqual({ a: 2 });
710
+ });
711
+
712
+ it("should handle an empty JSON series", () => {
713
+ const s = new Series({ data: new ArrayBuffer(0), dataType: DataType.JSON });
714
+ expect(s.length).toEqual(0);
715
+ expect(Array.from(s)).toEqual([]);
716
+ });
717
+ });
718
+
719
+ describe("bytes series", () => {
720
+ it("should handle an empty bytes series", () => {
721
+ const s = new Series({ data: new ArrayBuffer(0), dataType: DataType.BYTES });
722
+ expect(s.length).toEqual(0);
723
+ expect(Array.from(s)).toEqual([]);
724
+ });
725
+
726
+ it("should correctly compute the length of a bytes series from a length-prefixed buffer", () => {
727
+ const payload1 = new Uint8Array([1, 2, 3]);
728
+ const payload2 = new Uint8Array([4, 5]);
729
+ const totalBytes = 4 + payload1.byteLength + 4 + payload2.byteLength;
730
+ const buf = new ArrayBuffer(totalBytes);
731
+ const view = new DataView(buf);
732
+ const bytes = new Uint8Array(buf);
733
+ view.setUint32(0, payload1.byteLength, true);
734
+ bytes.set(payload1, 4);
735
+ view.setUint32(4 + payload1.byteLength, payload2.byteLength, true);
736
+ bytes.set(payload2, 4 + payload1.byteLength + 4);
737
+ const s = new Series({ data: buf, dataType: DataType.BYTES });
738
+ expect(s.length).toEqual(2);
739
+ });
692
740
  });
693
741
 
694
742
  describe("binarySearch", () => {
@@ -114,7 +114,7 @@ const nullArrayZ = z
114
114
  .union([z.null(), z.undefined()])
115
115
  .transform(() => new Uint8Array().buffer);
116
116
 
117
- const NEW_LINE = 10;
117
+ const UINT32_SIZE = 4;
118
118
 
119
119
  type JSType = "string" | "number" | "bigint";
120
120
 
@@ -358,12 +358,42 @@ export class Series<T extends TelemValue = TelemValue>
358
358
  data_ = data_.map((v) => new TimeStamp(v as CrudeTimeStamp).valueOf());
359
359
  if (this.dataType.equals(DataType.STRING)) {
360
360
  this.cachedLength = data_.length;
361
- this._data = new TextEncoder().encode(`${data_.join("\n")}\n`).buffer;
361
+ const encoded = (data_ as string[]).map((s) => new TextEncoder().encode(s));
362
+ const totalBytes = encoded.reduce(
363
+ (acc, e) => acc + UINT32_SIZE + e.byteLength,
364
+ 0,
365
+ );
366
+ const buf = new ArrayBuffer(totalBytes);
367
+ const view = new DataView(buf);
368
+ const bytes = new Uint8Array(buf);
369
+ let offset = 0;
370
+ for (const e of encoded) {
371
+ view.setUint32(offset, e.byteLength, true);
372
+ offset += UINT32_SIZE;
373
+ bytes.set(e, offset);
374
+ offset += e.byteLength;
375
+ }
376
+ this._data = buf;
362
377
  } else if (this.dataType.equals(DataType.JSON)) {
363
378
  this.cachedLength = data_.length;
364
- this._data = new TextEncoder().encode(
365
- `${data_.map((d) => binary.JSON_CODEC.encodeString(d)).join("\n")}\n`,
366
- ).buffer;
379
+ const encoded = data_.map((d) =>
380
+ new TextEncoder().encode(binary.JSON_CODEC.encodeString(d)),
381
+ );
382
+ const totalBytes = encoded.reduce(
383
+ (acc, e) => acc + UINT32_SIZE + e.byteLength,
384
+ 0,
385
+ );
386
+ const buf = new ArrayBuffer(totalBytes);
387
+ const view = new DataView(buf);
388
+ const bytes = new Uint8Array(buf);
389
+ let offset = 0;
390
+ for (const e of encoded) {
391
+ view.setUint32(offset, e.byteLength, true);
392
+ offset += UINT32_SIZE;
393
+ bytes.set(e, offset);
394
+ offset += e.byteLength;
395
+ }
396
+ this._data = buf;
367
397
  } else if (this.dataType.usesBigInt && typeof first === "number")
368
398
  this._data = new this.dataType.Array(
369
399
  data_.map((v) => BigInt(Math.round(v as number))),
@@ -456,14 +486,25 @@ export class Series<T extends TelemValue = TelemValue>
456
486
  private writeVariable(other: Series): number {
457
487
  if (this.writePos === FULL_BUFFER) return 0;
458
488
  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();
489
+ const otherBuf = other.buffer;
490
+ const otherByteLen = other.byteLength.valueOf();
491
+ const view = new DataView(otherBuf);
492
+ let offset = 0;
493
+ let samplesWritten = 0;
494
+ while (offset + UINT32_SIZE <= otherByteLen) {
495
+ const sampleLen = view.getUint32(offset, true);
496
+ const recordSize = UINT32_SIZE + sampleLen;
497
+ if (offset + recordSize > available) break;
498
+ offset += recordSize;
499
+ samplesWritten++;
465
500
  }
466
- return toWrite.length;
501
+ if (offset === 0) return 0;
502
+ const toWrite = other.subBytes(0, offset);
503
+ this.writeToUnderlyingData(toWrite);
504
+ this.writePos += offset;
505
+ this.cachedLength = (this.cachedLength ?? 0) + samplesWritten;
506
+ this._cachedIndexes = undefined;
507
+ return samplesWritten;
467
508
  }
468
509
 
469
510
  private writeFixed(other: Series): number {
@@ -511,8 +552,21 @@ export class Series<T extends TelemValue = TelemValue>
511
552
  * @returns An array of string representations of the series values.
512
553
  */
513
554
  toStrings(): string[] {
514
- if (this.dataType.isVariable)
515
- return new TextDecoder().decode(this.underlyingData).split("\n").slice(0, -1);
555
+ if (this.dataType.isVariable) {
556
+ const result: string[] = [];
557
+ const buf = this.buffer;
558
+ const byteLen = this.byteLength.valueOf();
559
+ const view = new DataView(buf);
560
+ const decoder = new TextDecoder();
561
+ let offset = 0;
562
+ while (offset + UINT32_SIZE <= byteLen) {
563
+ const len = view.getUint32(offset, true);
564
+ offset += UINT32_SIZE;
565
+ result.push(decoder.decode(new Uint8Array(buf, offset, len)));
566
+ offset += len;
567
+ }
568
+ return result;
569
+ }
516
570
  return Array.from(this).map((d) => d.toString());
517
571
  }
518
572
 
@@ -560,7 +614,7 @@ export class Series<T extends TelemValue = TelemValue>
560
614
 
561
615
  /**
562
616
  * Returns the number of samples in this array.
563
- * For variable length data types, this is calculated by counting newlines.
617
+ * For variable length data types, this is calculated by scanning uint32 length prefixes.
564
618
  * @returns The number of samples in the series.
565
619
  */
566
620
  get length(): number {
@@ -575,12 +629,18 @@ export class Series<T extends TelemValue = TelemValue>
575
629
  if (!this.dataType.isVariable)
576
630
  throw new Error("cannot calculate length of a non-variable length data type");
577
631
  let cl = 0;
578
- const ci: number[] = [0];
579
- this.data.forEach((v, i) => {
580
- if (v !== NEW_LINE) return;
632
+ const ci: number[] = [];
633
+ const buf = this.buffer;
634
+ const byteLen = this.byteLength.valueOf();
635
+ const view = new DataView(buf);
636
+ let offset = 0;
637
+ while (offset + UINT32_SIZE <= byteLen) {
638
+ const len = view.getUint32(offset, true);
639
+ offset += UINT32_SIZE;
640
+ ci.push(offset);
641
+ offset += len;
581
642
  cl++;
582
- ci.push(i + 1);
583
- });
643
+ }
584
644
  this._cachedIndexes = ci;
585
645
  this.cachedLength = cl;
586
646
  return cl;
@@ -742,28 +802,40 @@ export class Series<T extends TelemValue = TelemValue>
742
802
 
743
803
  private atVariable(index: number, required: boolean): T | undefined {
744
804
  let start = 0;
745
- let end = 0;
805
+ let len = 0;
806
+ const buf = this.buffer;
807
+ const view = new DataView(buf);
746
808
  if (this._cachedIndexes != null) {
809
+ if (index < 0) index = this._cachedIndexes.length + index;
810
+ if (index < 0 || index >= this._cachedIndexes.length) {
811
+ if (required) throw new Error(`[series] - no value at index ${index}`);
812
+ return undefined;
813
+ }
747
814
  start = this._cachedIndexes[index];
748
- end = this._cachedIndexes[index + 1] - 1;
815
+ len = view.getUint32(start - UINT32_SIZE, true);
749
816
  } else {
750
817
  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--;
818
+ const byteLen = this.byteLength.valueOf();
819
+ let offset = 0;
820
+ let found = false;
821
+ while (offset + UINT32_SIZE <= byteLen) {
822
+ const sampleLen = view.getUint32(offset, true);
823
+ offset += UINT32_SIZE;
824
+ if (index === 0) {
825
+ start = offset;
826
+ len = sampleLen;
827
+ found = true;
828
+ break;
759
829
  }
760
- if (end === 0) end = this.data.length;
761
- if (start >= end || index > 0) {
830
+ offset += sampleLen;
831
+ index--;
832
+ }
833
+ if (!found) {
762
834
  if (required) throw new Error(`[series] - no value at index ${index}`);
763
835
  return undefined;
764
836
  }
765
837
  }
766
- const slice = this.data.slice(start, end);
838
+ const slice = new Uint8Array(buf, start, len);
767
839
  if (this.dataType.equals(DataType.STRING))
768
840
  return new TextDecoder().decode(slice) as T;
769
841
  return caseconv.snakeToCamel(JSON.parse(new TextDecoder().decode(slice))) as T;
@@ -1069,8 +1141,9 @@ class SubIterator<T> implements Iterator<T> {
1069
1141
 
1070
1142
  class StringSeriesIterator implements Iterator<string> {
1071
1143
  private readonly series: Series;
1072
- private index: number;
1144
+ private byteOffset: number;
1073
1145
  private readonly decoder: TextDecoder;
1146
+ private readonly view: DataView;
1074
1147
 
1075
1148
  constructor(series: Series) {
1076
1149
  if (!series.dataType.isVariable)
@@ -1078,18 +1151,21 @@ class StringSeriesIterator implements Iterator<string> {
1078
1151
  "cannot create a variable series iterator for a non-variable series",
1079
1152
  );
1080
1153
  this.series = series;
1081
- this.index = 0;
1154
+ this.byteOffset = 0;
1082
1155
  this.decoder = new TextDecoder();
1156
+ this.view = new DataView(series.buffer);
1083
1157
  }
1084
1158
 
1085
1159
  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));
1160
+ const byteLen = this.series.byteLength.valueOf();
1161
+ if (this.byteOffset + UINT32_SIZE > byteLen)
1162
+ return { done: true, value: undefined };
1163
+ const len = this.view.getUint32(this.byteOffset, true);
1164
+ this.byteOffset += UINT32_SIZE;
1165
+ const s = this.decoder.decode(
1166
+ new Uint8Array(this.series.buffer, this.byteOffset, len),
1167
+ );
1168
+ this.byteOffset += len;
1093
1169
  return { done: false, value: s };
1094
1170
  }
1095
1171
  }
@@ -1057,6 +1057,25 @@ describe("TimeSpan", () => {
1057
1057
  expect(result.valueOf()).toBe(700n);
1058
1058
  });
1059
1059
 
1060
+ test("abs of positive value", () => {
1061
+ const ts = new TimeSpan(1000);
1062
+ expect(ts.abs().valueOf()).toBe(1000n);
1063
+ });
1064
+
1065
+ test("abs of negative value", () => {
1066
+ const ts = new TimeSpan(-500);
1067
+ expect(ts.abs().valueOf()).toBe(500n);
1068
+ });
1069
+
1070
+ test("abs of zero", () => {
1071
+ expect(TimeSpan.ZERO.abs().valueOf()).toBe(0n);
1072
+ });
1073
+
1074
+ test("abs returns TimeSpan instance", () => {
1075
+ const ts = new TimeSpan(-1000);
1076
+ expect(ts.abs()).toBeInstanceOf(TimeSpan);
1077
+ });
1078
+
1060
1079
  test("mult", () => {
1061
1080
  const ts = new TimeSpan(1000);
1062
1081
  const result = ts.mult(2);
@@ -1190,6 +1190,11 @@ export class TimeSpan
1190
1190
  return new TimeSpan(this.valueOf() - new TimeSpan(other).valueOf());
1191
1191
  }
1192
1192
 
1193
+ abs(): TimeSpan {
1194
+ const v = this.valueOf();
1195
+ return new TimeSpan(v < 0n ? -v : v);
1196
+ }
1197
+
1193
1198
  /**
1194
1199
  * Creates a TimeSpan representing the given number of nanoseconds.
1195
1200
  *
@@ -2014,14 +2019,14 @@ export class DataType
2014
2019
  static readonly TIMESTAMP = new DataType("timestamp");
2015
2020
  /** Represents a UUID data type. */
2016
2021
  static readonly UUID = new DataType("uuid");
2017
- /** Represents a string data type. Strings have an unknown density, and are separate
2018
- * by a newline character. */
2022
+ /** Represents a string data type. Strings have an unknown density and are encoded
2023
+ * as uint32-length-prefixed samples. */
2019
2024
  static readonly STRING = new DataType("string");
2020
- /** Represents a JSON data type. JSON has an unknown density, and is separated by a
2021
- * newline character. */
2025
+ /** Represents a JSON data type. JSON has an unknown density and is encoded as
2026
+ * uint32-length-prefixed samples. */
2022
2027
  static readonly JSON = new DataType("json");
2023
2028
  /** Represents a bytes data type for arbitrary byte arrays. Bytes have an unknown
2024
- * density, and are separated by a newline character. */
2029
+ * density and are encoded as uint32-length-prefixed samples. */
2025
2030
  static readonly BYTES = new DataType("bytes");
2026
2031
 
2027
2032
  private static readonly ARRAY_CONSTRUCTORS: Map<string, TypedArrayConstructor> =