@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.
- 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/destructor/destructor.d.ts +1 -0
- package/dist/src/destructor/destructor.d.ts.map +1 -1
- package/dist/src/errors/errors.d.ts +6 -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/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/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 +2 -0
- package/dist/src/telem/external.d.ts.map +1 -1
- package/dist/src/telem/series.d.ts +6 -3
- package/dist/src/telem/series.d.ts.map +1 -1
- package/dist/src/telem/telem.d.ts +48 -39
- 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 -13
- package/dist/x.js +2271 -2084
- package/package.json +12 -12
- 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/destructor/destructor.ts +2 -0
- package/src/errors/errors.spec.ts +30 -0
- package/src/errors/errors.ts +29 -17
- package/src/index.ts +4 -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 +65 -0
- package/src/status/status.ts +20 -0
- 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 +9 -0
- package/src/telem/series.spec.ts +235 -4
- package/src/telem/series.ts +172 -58
- package/src/telem/telem.spec.ts +147 -9
- package/src/telem/telem.ts +101 -84
- 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
package/src/spatial/types.gen.ts
CHANGED
|
@@ -11,10 +11,56 @@
|
|
|
11
11
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
|
+
import { type numeric } from "@/numeric";
|
|
15
|
+
|
|
16
|
+
export const X_LOCATIONS = ["left", "right"] as const;
|
|
17
|
+
export const xLocationZ = z.enum(X_LOCATIONS);
|
|
18
|
+
export type XLocation = z.infer<typeof xLocationZ>;
|
|
19
|
+
|
|
20
|
+
export const Y_LOCATIONS = ["top", "bottom"] as const;
|
|
21
|
+
export const yLocationZ = z.enum(Y_LOCATIONS);
|
|
22
|
+
export type YLocation = z.infer<typeof yLocationZ>;
|
|
23
|
+
|
|
24
|
+
export const STICKY_UNITS = ["px", "decimal"] as const;
|
|
25
|
+
export const stickyUnitZ = z.enum(STICKY_UNITS);
|
|
26
|
+
export type StickyUnit = z.infer<typeof stickyUnitZ>;
|
|
27
|
+
|
|
14
28
|
export const OUTER_LOCATIONS = ["top", "right", "bottom", "left"] as const;
|
|
15
29
|
export const outerLocationZ = z.enum(OUTER_LOCATIONS);
|
|
16
30
|
export type OuterLocation = z.infer<typeof outerLocationZ>;
|
|
17
31
|
|
|
32
|
+
export const DIRECTIONS = ["x", "y"] as const;
|
|
33
|
+
export const directionZ = z.enum(DIRECTIONS);
|
|
34
|
+
export type Direction = z.infer<typeof directionZ>;
|
|
35
|
+
|
|
36
|
+
export const ANGULAR_DIRECTIONS = ["clockwise", "counterclockwise"] as const;
|
|
37
|
+
export const angularDirectionZ = z.enum(ANGULAR_DIRECTIONS);
|
|
38
|
+
export type AngularDirection = z.infer<typeof angularDirectionZ>;
|
|
39
|
+
|
|
40
|
+
export const CENTER_LOCATIONS = ["center"] as const;
|
|
41
|
+
export const centerLocationZ = z.enum(CENTER_LOCATIONS);
|
|
42
|
+
export type CenterLocation = z.infer<typeof centerLocationZ>;
|
|
43
|
+
|
|
44
|
+
export const LOCATIONS = ["top", "right", "bottom", "left", "center"] as const;
|
|
45
|
+
export const locationZ = z.enum(LOCATIONS);
|
|
46
|
+
export type Location = z.infer<typeof locationZ>;
|
|
47
|
+
|
|
48
|
+
export const ALIGNMENTS = ["start", "center", "end"] as const;
|
|
49
|
+
export const alignmentZ = z.enum(ALIGNMENTS);
|
|
50
|
+
export type Alignment = z.infer<typeof alignmentZ>;
|
|
51
|
+
|
|
52
|
+
export const ORDERS = ["first", "last"] as const;
|
|
53
|
+
export const orderZ = z.enum(ORDERS);
|
|
54
|
+
export type Order = z.infer<typeof orderZ>;
|
|
55
|
+
|
|
56
|
+
export const DIMENSIONS = ["width", "height"] as const;
|
|
57
|
+
export const dimensionZ = z.enum(DIMENSIONS);
|
|
58
|
+
export type Dimension = z.infer<typeof dimensionZ>;
|
|
59
|
+
|
|
60
|
+
export const SIGNED_DIMENSIONS = ["signedWidth", "signedHeight"] as const;
|
|
61
|
+
export const signedDimensionZ = z.enum(SIGNED_DIMENSIONS);
|
|
62
|
+
export type SignedDimension = z.infer<typeof signedDimensionZ>;
|
|
63
|
+
|
|
18
64
|
/**
|
|
19
65
|
* XY is a 2D coordinate point with x and y values. Used for positioning
|
|
20
66
|
* elements in two-dimensional space.
|
|
@@ -26,3 +72,97 @@ export const xyZ = z.object({
|
|
|
26
72
|
y: z.number(),
|
|
27
73
|
});
|
|
28
74
|
export interface XY extends z.infer<typeof xyZ> {}
|
|
75
|
+
|
|
76
|
+
/** CornerLocation is an anchor corner for positioning. */
|
|
77
|
+
export const cornerLocationZ = z.object({
|
|
78
|
+
/** x is the horizontal anchor. */
|
|
79
|
+
x: xLocationZ,
|
|
80
|
+
/** y is the vertical anchor. */
|
|
81
|
+
y: yLocationZ,
|
|
82
|
+
});
|
|
83
|
+
export interface CornerLocation extends z.infer<typeof cornerLocationZ> {}
|
|
84
|
+
|
|
85
|
+
/** StickyUnits specifies the measurement units for sticky positioning. */
|
|
86
|
+
export const stickyUnitsZ = z.object({
|
|
87
|
+
/** x is the horizontal unit. */
|
|
88
|
+
x: stickyUnitZ,
|
|
89
|
+
/** y is the vertical unit. */
|
|
90
|
+
y: stickyUnitZ,
|
|
91
|
+
});
|
|
92
|
+
export interface StickyUnits extends z.infer<typeof stickyUnitsZ> {}
|
|
93
|
+
|
|
94
|
+
/** Dimensions is a 2D size with width and height values. */
|
|
95
|
+
export const dimensionsZ = z.object({
|
|
96
|
+
/** width is the width in pixels. */
|
|
97
|
+
width: z.number(),
|
|
98
|
+
/** height is the height in pixels. */
|
|
99
|
+
height: z.number(),
|
|
100
|
+
});
|
|
101
|
+
export interface Dimensions extends z.infer<typeof dimensionsZ> {}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* SignedDimensions is a 2D size whose width and height components carry sign, allowing
|
|
105
|
+
* negative values to express direction.
|
|
106
|
+
*/
|
|
107
|
+
export const signedDimensionsZ = z.object({
|
|
108
|
+
/** signedWidth is the signed width. */
|
|
109
|
+
signedWidth: z.number(),
|
|
110
|
+
/** signedHeight is the signed height. */
|
|
111
|
+
signedHeight: z.number(),
|
|
112
|
+
});
|
|
113
|
+
export interface SignedDimensions extends z.infer<typeof signedDimensionsZ> {}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* ClientXY is a 2D coordinate point expressed in client (viewport) space, matching
|
|
117
|
+
* the shape of DOM mouse events.
|
|
118
|
+
*/
|
|
119
|
+
export const clientXYZ = z.object({
|
|
120
|
+
/** clientX is the horizontal coordinate in client (viewport) space. */
|
|
121
|
+
clientX: z.number(),
|
|
122
|
+
/** clientY is the vertical coordinate in client (viewport) space. */
|
|
123
|
+
clientY: z.number(),
|
|
124
|
+
});
|
|
125
|
+
export interface ClientXY extends z.infer<typeof clientXYZ> {}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Bounds is a closed-open interval [lower, upper) over an ordered numeric value
|
|
129
|
+
* space. The TypeScript binding is generic over T so callers can express
|
|
130
|
+
* bounds over either number or bigint values; other languages emit a
|
|
131
|
+
* concrete float64-based type.
|
|
132
|
+
*/
|
|
133
|
+
export const boundsZ = <T extends numeric.Value = number>(t?: z.ZodType<T>) =>
|
|
134
|
+
z.object({
|
|
135
|
+
/** lower is the inclusive lower bound. */
|
|
136
|
+
lower: t ?? z.number(),
|
|
137
|
+
/** upper is the exclusive upper bound. */
|
|
138
|
+
upper: t ?? z.number(),
|
|
139
|
+
});
|
|
140
|
+
export interface Bounds<T extends numeric.Value = number> {
|
|
141
|
+
lower: T;
|
|
142
|
+
upper: T;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Viewport is the camera state of a viewport. */
|
|
146
|
+
export const viewportZ = z.object({
|
|
147
|
+
/** zoom is the zoom level where 1.0 equals 100%. */
|
|
148
|
+
zoom: z.number().default(1),
|
|
149
|
+
/** position is the (x, y) pan offset of the viewport. */
|
|
150
|
+
position: xyZ,
|
|
151
|
+
});
|
|
152
|
+
export interface Viewport extends z.infer<typeof viewportZ> {}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* StickyXY is a position that can be anchored to different corners of a
|
|
156
|
+
* container with configurable units (pixels or decimal fractions).
|
|
157
|
+
*/
|
|
158
|
+
export const stickyXYZ = z.object({
|
|
159
|
+
/** x is the horizontal coordinate. */
|
|
160
|
+
x: z.number(),
|
|
161
|
+
/** y is the vertical coordinate. */
|
|
162
|
+
y: z.number(),
|
|
163
|
+
/** root is the optional anchor corner for the position. */
|
|
164
|
+
root: cornerLocationZ.optional(),
|
|
165
|
+
/** units is the optional unit specification for the coordinates. */
|
|
166
|
+
units: stickyUnitsZ.optional(),
|
|
167
|
+
});
|
|
168
|
+
export interface StickyXY extends z.infer<typeof stickyXYZ> {}
|
package/src/spatial/xy/xy.ts
CHANGED
|
@@ -12,19 +12,19 @@ import { z } from "zod";
|
|
|
12
12
|
import {
|
|
13
13
|
type AngularDirection,
|
|
14
14
|
type ClientXY,
|
|
15
|
-
|
|
16
|
-
type CrudeDirection,
|
|
15
|
+
clientXYZ,
|
|
17
16
|
dimensionsZ,
|
|
18
17
|
type Direction,
|
|
19
18
|
type NumberCouple,
|
|
20
19
|
numberCouple,
|
|
21
20
|
signedDimensionsZ,
|
|
21
|
+
type XY,
|
|
22
|
+
xyZ,
|
|
22
23
|
} from "@/spatial/base";
|
|
23
|
-
import { direction as dir } from "@/spatial/direction";
|
|
24
|
+
import { direction as dir, type direction } from "@/spatial/direction";
|
|
24
25
|
import { type location } from "@/spatial/location";
|
|
25
|
-
import { type XY, xyZ } from "@/spatial/types.gen";
|
|
26
26
|
|
|
27
|
-
export { type ClientXY as Client,
|
|
27
|
+
export { type ClientXY as Client, clientXYZ, type XY, xyZ };
|
|
28
28
|
|
|
29
29
|
/** A crude representation of a {@link XY} coordinate as a zod schema. */
|
|
30
30
|
export const crudeZ = z.union([
|
|
@@ -33,7 +33,7 @@ export const crudeZ = z.union([
|
|
|
33
33
|
numberCouple,
|
|
34
34
|
dimensionsZ,
|
|
35
35
|
signedDimensionsZ,
|
|
36
|
-
|
|
36
|
+
clientXYZ,
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
39
|
/** A crude representation of a {@link XY} coordinate. */
|
|
@@ -141,7 +141,7 @@ export const translate: Translate = (a, b, v, ...cb): XY => {
|
|
|
141
141
|
* @returns the given coordinate the given direction set to the given value.
|
|
142
142
|
* @example set({ x: 1, y: 2 }, "x", 3) // { x: 3, y: 2 }
|
|
143
143
|
*/
|
|
144
|
-
export const set = (c: Crude, direction:
|
|
144
|
+
export const set = (c: Crude, direction: direction.Crude, value: number): XY => {
|
|
145
145
|
const xy = construct(c);
|
|
146
146
|
const d = dir.construct(direction);
|
|
147
147
|
if (d === "x") return { x: value, y: xy.y };
|
|
@@ -448,4 +448,69 @@ describe("status", () => {
|
|
|
448
448
|
});
|
|
449
449
|
});
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
describe("toError", () => {
|
|
453
|
+
it("should use the wrapped status message as the Error message", () => {
|
|
454
|
+
const inner = new Error("raw");
|
|
455
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
456
|
+
const err = status.toError(s);
|
|
457
|
+
|
|
458
|
+
expect(err.message).toBe("Failed to fetch");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should copy name from the inner error", () => {
|
|
462
|
+
class NotFoundError extends Error {
|
|
463
|
+
constructor(message: string) {
|
|
464
|
+
super(message);
|
|
465
|
+
this.name = "NotFoundError";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const inner = new NotFoundError("missing");
|
|
469
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
470
|
+
const err = status.toError(s);
|
|
471
|
+
|
|
472
|
+
expect(err.name).toBe("NotFoundError");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("should copy stack from the inner error", () => {
|
|
476
|
+
const inner = new Error("raw");
|
|
477
|
+
inner.stack = "Error: raw\n at someFn (file.ts:1:1)";
|
|
478
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
479
|
+
const err = status.toError(s);
|
|
480
|
+
|
|
481
|
+
expect(err.stack).toBe("Error: raw\n at someFn (file.ts:1:1)");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("should not leak the toError call-site stack when inner stack is missing", () => {
|
|
485
|
+
const inner = new Error("raw");
|
|
486
|
+
inner.stack = undefined;
|
|
487
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
488
|
+
const err = status.toError(s);
|
|
489
|
+
|
|
490
|
+
expect(err.stack).toBeUndefined();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should set cause to the original status", () => {
|
|
494
|
+
const inner = new Error("raw");
|
|
495
|
+
const s = status.fromException(inner, "Failed to fetch");
|
|
496
|
+
const err = status.toError(s);
|
|
497
|
+
|
|
498
|
+
expect(err.cause).toBe(s);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("should round-trip through throw/catch", () => {
|
|
502
|
+
const inner = new TypeError("not a function");
|
|
503
|
+
const s = status.fromException(inner, "Failed to invoke");
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
throw status.toError(s);
|
|
507
|
+
} catch (caught) {
|
|
508
|
+
expect(caught).toBeInstanceOf(Error);
|
|
509
|
+
const e = caught as Error;
|
|
510
|
+
expect(e.name).toBe("TypeError");
|
|
511
|
+
expect(e.message).toBe("Failed to invoke");
|
|
512
|
+
expect(e.cause).toBe(s);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
451
516
|
});
|
package/src/status/status.ts
CHANGED
|
@@ -97,6 +97,26 @@ export const fromException = (
|
|
|
97
97
|
return create<typeof exceptionDetailsSchema, "error">(crude);
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Converts an exception-shaped status (one built via {@link fromException}) back
|
|
102
|
+
* into a thrown-shaped {@link Error}. The returned error carries the status's
|
|
103
|
+
* wrapped message, copies `name` and `stack` from the inner error preserved on
|
|
104
|
+
* `details.error`, and stashes the full status on `cause` for callers that need
|
|
105
|
+
* the rich shape.
|
|
106
|
+
*
|
|
107
|
+
* Use this when bridging the status pipeline back into a context that expects
|
|
108
|
+
* a real Error — typically before `throw`-ing across an error boundary.
|
|
109
|
+
*/
|
|
110
|
+
export const toError = (
|
|
111
|
+
s: Status<typeof exceptionDetailsSchema, z.ZodLiteral<"error">>,
|
|
112
|
+
): Error => {
|
|
113
|
+
const inner = s.details.error;
|
|
114
|
+
const err = new Error(s.message, { cause: s });
|
|
115
|
+
err.name = inner.name;
|
|
116
|
+
err.stack = inner.stack;
|
|
117
|
+
return err;
|
|
118
|
+
};
|
|
119
|
+
|
|
100
120
|
export const create = <
|
|
101
121
|
DetailsSchema extends z.ZodType = z.ZodNever,
|
|
102
122
|
V extends Variant = Variant,
|
|
@@ -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,15 @@
|
|
|
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";
|
|
14
|
+
export {
|
|
15
|
+
TIME_ZONES,
|
|
16
|
+
TIMESTAMP_FORMATS,
|
|
17
|
+
type TimestampFormat,
|
|
18
|
+
timestampFormatZ,
|
|
19
|
+
type TimeZone,
|
|
20
|
+
timeZoneZ,
|
|
21
|
+
} from "@/telem/types.gen";
|