@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/.turbo/turbo-build.log +8 -8
- package/dist/src/strings/strings.d.ts +9 -0
- package/dist/src/strings/strings.d.ts.map +1 -1
- package/dist/src/telem/clockSkew.d.ts +17 -0
- package/dist/src/telem/clockSkew.d.ts.map +1 -0
- package/dist/src/telem/clockSkew.spec.d.ts +2 -0
- package/dist/src/telem/clockSkew.spec.d.ts.map +1 -0
- package/dist/src/telem/external.d.ts +1 -0
- package/dist/src/telem/external.d.ts.map +1 -1
- package/dist/src/telem/series.d.ts +1 -1
- package/dist/src/telem/series.d.ts.map +1 -1
- package/dist/src/telem/telem.d.ts +6 -5
- package/dist/src/telem/telem.d.ts.map +1 -1
- package/dist/x.cjs +10 -13
- package/dist/x.js +2060 -1996
- package/package.json +10 -10
- package/src/strings/strings.spec.ts +19 -0
- package/src/strings/strings.ts +16 -0
- package/src/telem/clockSkew.spec.ts +58 -0
- package/src/telem/clockSkew.ts +46 -0
- package/src/telem/external.ts +1 -0
- package/src/telem/series.spec.ts +52 -4
- package/src/telem/series.ts +118 -42
- package/src/telem/telem.spec.ts +19 -0
- package/src/telem/telem.ts +10 -5
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/x",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
23
|
-
"@vitest/coverage-v8": "^4.1.
|
|
24
|
-
"eslint": "^10.1
|
|
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.
|
|
27
|
-
"vite": "^8.0.
|
|
28
|
-
"vitest": "^4.1.
|
|
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("<div>"));
|
|
113
|
+
|
|
114
|
+
it("should escape quotes", () =>
|
|
115
|
+
expect(strings.escapeHTML(`"it's"`)).toBe(""it's""));
|
|
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
|
+
"<a href="x">&",
|
|
123
|
+
));
|
|
124
|
+
});
|
package/src/strings/strings.ts
CHANGED
|
@@ -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, "&")
|
|
123
|
+
.replace(/</g, "<")
|
|
124
|
+
.replace(/>/g, ">")
|
|
125
|
+
.replace(/"/g, """)
|
|
126
|
+
.replace(/'/g, "'");
|
|
@@ -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
|
+
}
|
package/src/telem/external.ts
CHANGED
|
@@ -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";
|
package/src/telem/series.spec.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
218
|
-
expect(
|
|
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:
|
|
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", () => {
|
package/src/telem/series.ts
CHANGED
|
@@ -114,7 +114,7 @@ const nullArrayZ = z
|
|
|
114
114
|
.union([z.null(), z.undefined()])
|
|
115
115
|
.transform(() => new Uint8Array().buffer);
|
|
116
116
|
|
|
117
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
)
|
|
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
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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[] = [
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
815
|
+
len = view.getUint32(start - UINT32_SIZE, true);
|
|
749
816
|
} else {
|
|
750
817
|
if (index < 0) index = this.length + index;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
761
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
this.
|
|
1092
|
-
|
|
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
|
}
|
package/src/telem/telem.spec.ts
CHANGED
|
@@ -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);
|
package/src/telem/telem.ts
CHANGED
|
@@ -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
|
|
2018
|
-
*
|
|
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
|
|
2021
|
-
*
|
|
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
|
|
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> =
|