@synnaxlabs/x 0.55.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -13
- package/dist/src/array/nullable.d.ts +1 -1
- package/dist/src/array/nullable.d.ts.map +1 -1
- package/dist/src/caseconv/caseconv.d.ts.map +1 -1
- package/dist/src/compare/compare.d.ts +14 -0
- package/dist/src/compare/compare.d.ts.map +1 -1
- package/dist/src/debounce/debounce.d.ts +7 -2
- package/dist/src/debounce/debounce.d.ts.map +1 -1
- package/dist/src/deep/merge.d.ts.map +1 -1
- package/dist/src/deep/set.d.ts.map +1 -1
- package/dist/src/destructor/destructor.d.ts +1 -0
- package/dist/src/destructor/destructor.d.ts.map +1 -1
- package/dist/src/errors/errors.d.ts +18 -10
- package/dist/src/errors/errors.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/migrate/migrate.d.ts.map +1 -1
- package/dist/src/notation/external.d.ts +3 -0
- package/dist/src/notation/external.d.ts.map +1 -0
- package/dist/src/notation/index.d.ts +1 -1
- package/dist/src/notation/notation.d.ts +5 -9
- package/dist/src/notation/notation.d.ts.map +1 -1
- package/dist/src/notation/types.gen.d.ts +9 -0
- package/dist/src/notation/types.gen.d.ts.map +1 -0
- package/dist/src/primitive/primitive.d.ts +16 -0
- package/dist/src/primitive/primitive.d.ts.map +1 -1
- package/dist/src/record/record.d.ts +8 -1
- package/dist/src/record/record.d.ts.map +1 -1
- package/dist/src/require/index.d.ts +2 -0
- package/dist/src/require/index.d.ts.map +1 -0
- package/dist/src/require/require.d.ts +2 -0
- package/dist/src/require/require.d.ts.map +1 -0
- package/dist/src/spatial/base.d.ts +1 -103
- package/dist/src/spatial/base.d.ts.map +1 -1
- package/dist/src/spatial/bounds/bounds.d.ts +3 -3
- package/dist/src/spatial/bounds/bounds.d.ts.map +1 -1
- package/dist/src/spatial/box/box.d.ts +7 -13
- package/dist/src/spatial/box/box.d.ts.map +1 -1
- package/dist/src/spatial/direction/direction.d.ts +17 -16
- package/dist/src/spatial/direction/direction.d.ts.map +1 -1
- package/dist/src/spatial/external.d.ts +1 -2
- package/dist/src/spatial/external.d.ts.map +1 -1
- package/dist/src/spatial/location/location.d.ts +28 -28
- package/dist/src/spatial/location/location.d.ts.map +1 -1
- package/dist/src/spatial/scale/scale.d.ts +2 -2
- package/dist/src/spatial/scale/scale.d.ts.map +1 -1
- package/dist/src/spatial/sticky/sticky.d.ts +15 -15
- package/dist/src/spatial/sticky/sticky.d.ts.map +1 -1
- package/dist/src/spatial/types.gen.d.ts +179 -2
- package/dist/src/spatial/types.gen.d.ts.map +1 -1
- package/dist/src/spatial/xy/xy.d.ts +4 -4
- package/dist/src/spatial/xy/xy.d.ts.map +1 -1
- package/dist/src/status/status.d.ts +11 -0
- package/dist/src/status/status.d.ts.map +1 -1
- 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 +5 -2
- package/dist/src/telem/series.d.ts.map +1 -1
- package/dist/src/telem/telem.d.ts +42 -34
- package/dist/src/telem/telem.d.ts.map +1 -1
- package/dist/src/telem/types.gen.d.ts +19 -0
- package/dist/src/telem/types.gen.d.ts.map +1 -0
- package/dist/src/text/external.d.ts +3 -0
- package/dist/src/text/external.d.ts.map +1 -0
- package/dist/src/text/index.d.ts +2 -0
- package/dist/src/text/index.d.ts.map +1 -0
- package/dist/src/text/types.d.ts +21 -0
- package/dist/src/text/types.d.ts.map +1 -0
- package/dist/src/text/types.gen.d.ts +13 -0
- package/dist/src/text/types.gen.d.ts.map +1 -0
- package/dist/src/throttle/index.d.ts +2 -0
- package/dist/src/throttle/index.d.ts.map +1 -0
- package/dist/src/throttle/throttle.d.ts +3 -0
- package/dist/src/throttle/throttle.d.ts.map +1 -0
- package/dist/src/throttle/throttle.spec.d.ts +2 -0
- package/dist/src/throttle/throttle.spec.d.ts.map +1 -0
- package/dist/src/zod/parse.d.ts.map +1 -1
- package/dist/x.cjs +10 -10
- package/dist/x.js +1497 -1365
- package/package.json +11 -11
- package/src/array/nullable.ts +1 -4
- package/src/caseconv/caseconv.spec.ts +71 -0
- package/src/caseconv/caseconv.ts +15 -2
- package/src/compare/compare.spec.ts +115 -0
- package/src/compare/compare.ts +29 -0
- package/src/debounce/debounce.spec.ts +258 -24
- package/src/debounce/debounce.ts +49 -30
- package/src/deep/copy.spec.ts +13 -0
- package/src/deep/difference.ts +1 -1
- package/src/deep/merge.ts +2 -1
- package/src/deep/set.ts +2 -1
- package/src/destructor/destructor.ts +2 -0
- package/src/errors/errors.spec.ts +122 -0
- package/src/errors/errors.ts +51 -17
- package/src/index.ts +4 -1
- package/src/migrate/migrate.ts +2 -1
- package/src/notation/external.ts +11 -0
- package/src/notation/index.ts +1 -1
- package/src/notation/notation.spec.ts +260 -2
- package/src/notation/notation.ts +25 -7
- package/src/notation/types.gen.ts +16 -0
- package/src/primitive/primitive.spec.ts +58 -5
- package/src/primitive/primitive.ts +22 -0
- package/src/record/record.spec.ts +26 -0
- package/src/record/record.ts +20 -5
- package/src/require/index.ts +10 -0
- package/src/require/require.ts +10 -0
- package/src/spatial/base.ts +1 -93
- package/src/spatial/bounds/bounds.ts +10 -10
- package/src/spatial/box/box.ts +5 -5
- package/src/spatial/direction/direction.ts +16 -17
- package/src/spatial/external.ts +1 -2
- package/src/spatial/location/location.ts +19 -17
- package/src/spatial/scale/scale.ts +2 -2
- package/src/spatial/sticky/sticky.spec.ts +2 -2
- package/src/spatial/sticky/sticky.ts +6 -13
- package/src/spatial/types.gen.ts +140 -0
- package/src/spatial/xy/xy.ts +7 -7
- package/src/status/status.spec.ts +82 -2
- package/src/status/status.ts +35 -9
- package/src/telem/external.ts +8 -0
- package/src/telem/series.spec.ts +183 -0
- package/src/telem/series.ts +54 -16
- package/src/telem/telem.spec.ts +128 -9
- package/src/telem/telem.ts +91 -79
- package/src/telem/types.gen.ts +28 -0
- package/src/text/external.ts +11 -0
- package/src/text/index.ts +10 -0
- package/src/text/types.gen.ts +16 -0
- package/src/text/types.ts +37 -0
- package/src/{worker → throttle}/index.ts +1 -1
- package/src/throttle/throttle.spec.ts +147 -0
- package/src/throttle/throttle.ts +44 -0
- package/src/zod/parse.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/src/spatial/spatial.d.ts +0 -3
- package/dist/src/spatial/spatial.d.ts.map +0 -1
- package/dist/src/worker/index.d.ts +0 -2
- package/dist/src/worker/index.d.ts.map +0 -1
- package/dist/src/worker/worker.d.ts +0 -33
- package/dist/src/worker/worker.d.ts.map +0 -1
- package/dist/src/worker/worker.spec.d.ts +0 -2
- package/dist/src/worker/worker.spec.d.ts.map +0 -1
- package/src/spatial/spatial.ts +0 -44
- package/src/worker/worker.spec.ts +0 -41
- package/src/worker/worker.ts +0 -86
|
@@ -110,9 +110,24 @@ describe("status", () => {
|
|
|
110
110
|
expect(s.details.error).toBe(error);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it("should
|
|
113
|
+
it("should coerce a non-Error throwable into an error status via fromUnknown", () => {
|
|
114
114
|
const notAnError = "just a string";
|
|
115
|
-
|
|
115
|
+
const s = status.fromException(notAnError);
|
|
116
|
+
expect(s.variant).toEqual("error");
|
|
117
|
+
// JSON.stringify of a string quotes it.
|
|
118
|
+
expect(s.message).toEqual('"just a string"');
|
|
119
|
+
expect(s.details.error).toBeInstanceOf(Error);
|
|
120
|
+
expect(s.details.error.cause).toEqual("just a string");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should pick up custom toStatus contributions from non-Error throwables", () => {
|
|
124
|
+
const custom = {
|
|
125
|
+
toStatus: () => ({ message: "custom msg", description: "custom desc" }),
|
|
126
|
+
};
|
|
127
|
+
const s = status.fromException(custom);
|
|
128
|
+
expect(s.variant).toEqual("error");
|
|
129
|
+
expect(s.message).toEqual("custom msg");
|
|
130
|
+
expect(s.description).toEqual("custom desc");
|
|
116
131
|
});
|
|
117
132
|
|
|
118
133
|
it("should include valid key and timestamp", () => {
|
|
@@ -448,4 +463,69 @@ describe("status", () => {
|
|
|
448
463
|
});
|
|
449
464
|
});
|
|
450
465
|
});
|
|
466
|
+
|
|
467
|
+
describe("toError", () => {
|
|
468
|
+
it("should use the wrapped status message as the Error message", () => {
|
|
469
|
+
const inner = new Error("raw");
|
|
470
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
471
|
+
const err = status.toError(s);
|
|
472
|
+
|
|
473
|
+
expect(err.message).toBe("Failed to fetch");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("should copy name from the inner error", () => {
|
|
477
|
+
class NotFoundError extends Error {
|
|
478
|
+
constructor(message: string) {
|
|
479
|
+
super(message);
|
|
480
|
+
this.name = "NotFoundError";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const inner = new NotFoundError("missing");
|
|
484
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
485
|
+
const err = status.toError(s);
|
|
486
|
+
|
|
487
|
+
expect(err.name).toBe("NotFoundError");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should copy stack from the inner error", () => {
|
|
491
|
+
const inner = new Error("raw");
|
|
492
|
+
inner.stack = "Error: raw\n at someFn (file.ts:1:1)";
|
|
493
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
494
|
+
const err = status.toError(s);
|
|
495
|
+
|
|
496
|
+
expect(err.stack).toBe("Error: raw\n at someFn (file.ts:1:1)");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should not leak the toError call-site stack when inner stack is missing", () => {
|
|
500
|
+
const inner = new Error("raw");
|
|
501
|
+
inner.stack = undefined;
|
|
502
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
503
|
+
const err = status.toError(s);
|
|
504
|
+
|
|
505
|
+
expect(err.stack).toBeUndefined();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("should set cause to the original status", () => {
|
|
509
|
+
const inner = new Error("raw");
|
|
510
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
511
|
+
const err = status.toError(s);
|
|
512
|
+
|
|
513
|
+
expect(err.cause).toBe(s);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("should round-trip through throw/catch", () => {
|
|
517
|
+
const inner = new TypeError("not a function");
|
|
518
|
+
const s = status.fromException(inner, "Failed to invoke");
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
throw status.toError(s);
|
|
522
|
+
} catch (caught) {
|
|
523
|
+
expect(caught).toBeInstanceOf(Error);
|
|
524
|
+
const e = caught as Error;
|
|
525
|
+
expect(e.name).toBe("TypeError");
|
|
526
|
+
expect(e.message).toBe("Failed to invoke");
|
|
527
|
+
expect(e.cause).toBe(s);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
});
|
|
451
531
|
});
|
package/src/status/status.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
+
import { errors } from "@/errors";
|
|
12
13
|
import { id } from "@/id";
|
|
13
14
|
import { narrow } from "@/narrow";
|
|
14
15
|
import { type optional } from "@/optional";
|
|
@@ -52,11 +53,14 @@ const customReturnZ = z.object({
|
|
|
52
53
|
details: record.unknownZ().optional(),
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const safeToStatus = (exc: unknown): z.infer<typeof customReturnZ> | undefined => {
|
|
57
|
+
if (
|
|
58
|
+
exc == null ||
|
|
59
|
+
typeof exc !== "object" ||
|
|
60
|
+
!("toStatus" in exc) ||
|
|
61
|
+
typeof exc.toStatus !== "function"
|
|
62
|
+
)
|
|
63
|
+
return undefined;
|
|
60
64
|
let raw: unknown;
|
|
61
65
|
try {
|
|
62
66
|
raw = exc.toStatus();
|
|
@@ -78,13 +82,15 @@ export const fromException = (
|
|
|
78
82
|
exc: unknown,
|
|
79
83
|
message?: string,
|
|
80
84
|
): Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">> => {
|
|
81
|
-
|
|
85
|
+
const err = errors.fromUnknown(exc);
|
|
82
86
|
const crude: Crude<typeof exceptionDetailsSchema, "error"> = {
|
|
83
87
|
variant: "error",
|
|
84
|
-
message: message ??
|
|
85
|
-
description: message != null ?
|
|
86
|
-
details: { stack:
|
|
88
|
+
message: message ?? err.message,
|
|
89
|
+
description: message != null ? err.message : undefined,
|
|
90
|
+
details: { stack: err.stack ?? "", error: err },
|
|
87
91
|
};
|
|
92
|
+
// Probe the original (pre-coercion) value so a non-Error throwable with a custom
|
|
93
|
+
// `toStatus()` method still contributes its status fields.
|
|
88
94
|
const custom = safeToStatus(exc);
|
|
89
95
|
if (custom != null) {
|
|
90
96
|
if (message != null && custom.message != null)
|
|
@@ -97,6 +103,26 @@ export const fromException = (
|
|
|
97
103
|
return create<typeof exceptionDetailsSchema, "error">(crude);
|
|
98
104
|
};
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Converts an exception-shaped status (one built via {@link fromException}) back
|
|
108
|
+
* into a thrown-shaped {@link Error}. The returned error carries the status's
|
|
109
|
+
* wrapped message, copies `name` and `stack` from the inner error preserved on
|
|
110
|
+
* `details.error`, and stashes the full status on `cause` for callers that need
|
|
111
|
+
* the rich shape.
|
|
112
|
+
*
|
|
113
|
+
* Use this when bridging the status pipeline back into a context that expects
|
|
114
|
+
* a real Error — typically before `throw`-ing across an error boundary.
|
|
115
|
+
*/
|
|
116
|
+
export const toError = (
|
|
117
|
+
s: Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">>,
|
|
118
|
+
): Error => {
|
|
119
|
+
const inner = s.details.error;
|
|
120
|
+
const err = new Error(s.message, { cause: s });
|
|
121
|
+
err.name = inner.name;
|
|
122
|
+
err.stack = inner.stack;
|
|
123
|
+
return err;
|
|
124
|
+
};
|
|
125
|
+
|
|
100
126
|
export const create = <
|
|
101
127
|
DetailsSchema extends z.ZodType = z.ZodNever,
|
|
102
128
|
V extends Variant = Variant,
|
package/src/telem/external.ts
CHANGED
|
@@ -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";
|
package/src/telem/series.spec.ts
CHANGED
|
@@ -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({
|
package/src/telem/series.ts
CHANGED
|
@@ -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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|