@synnaxlabs/x 0.52.5 → 0.53.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/x",
3
- "version": "0.52.5",
3
+ "version": "0.53.1",
4
4
  "type": "module",
5
5
  "description": "Common Utilities for Synnax Labs",
6
6
  "repository": "https://github.com/synnaxlabs/synnax/tree/main/x/ts",
@@ -27,8 +27,8 @@
27
27
  "vite": "^7.3.1",
28
28
  "vitest": "^3.2.4",
29
29
  "@synnaxlabs/eslint-config": "^0.0.0",
30
- "@synnaxlabs/tsconfig": "^0.0.0",
31
- "@synnaxlabs/vite-plugin": "^0.0.0"
30
+ "@synnaxlabs/vite-plugin": "^0.0.0",
31
+ "@synnaxlabs/tsconfig": "^0.0.0"
32
32
  },
33
33
  "main": "dist/x.cjs",
34
34
  "module": "dist/x.js",
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export * from "@/destructor";
23
23
  export * from "@/errors";
24
24
  export * from "@/id";
25
25
  export * from "@/instance";
26
+ export * from "@/json";
26
27
  export * from "@/kv";
27
28
  export * from "@/label";
28
29
  export * from "@/link";
@@ -0,0 +1,10 @@
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
+ export * as json from "@/json/json";
@@ -0,0 +1,96 @@
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 { json } from "@/json";
13
+
14
+ describe("json", () => {
15
+ describe("pointerZ", () => {
16
+ it("should accept an empty string", () => {
17
+ expect(json.pointerZ.parse("")).toBe("");
18
+ });
19
+
20
+ it("should accept a simple pointer", () => {
21
+ expect(json.pointerZ.parse("/foo")).toBe("/foo");
22
+ });
23
+
24
+ it("should accept a nested pointer", () => {
25
+ expect(json.pointerZ.parse("/foo/bar/baz")).toBe("/foo/bar/baz");
26
+ });
27
+
28
+ it("should accept a pointer with array indices", () => {
29
+ expect(json.pointerZ.parse("/items/0/name")).toBe("/items/0/name");
30
+ });
31
+
32
+ it("should accept a pointer with escaped tilde", () => {
33
+ expect(json.pointerZ.parse("/a~0b")).toBe("/a~0b");
34
+ });
35
+
36
+ it("should accept a pointer with escaped slash", () => {
37
+ expect(json.pointerZ.parse("/a~1b")).toBe("/a~1b");
38
+ });
39
+
40
+ it("should reject a pointer that does not start with /", () => {
41
+ expect(json.pointerZ.safeParse("foo").success).toBe(false);
42
+ });
43
+
44
+ it("should reject a pointer with a bare tilde", () => {
45
+ expect(json.pointerZ.safeParse("/a~b").success).toBe(false);
46
+ });
47
+ });
48
+
49
+ describe("primitiveZ", () => {
50
+ it("should accept a string", () => {
51
+ expect(json.primitiveZ.parse("hello")).toBe("hello");
52
+ });
53
+
54
+ it("should accept a number", () => {
55
+ expect(json.primitiveZ.parse(42)).toBe(42);
56
+ });
57
+
58
+ it("should accept a boolean", () => {
59
+ expect(json.primitiveZ.parse(true)).toBe(true);
60
+ });
61
+
62
+ it("should accept null", () => {
63
+ expect(json.primitiveZ.parse(null)).toBeNull();
64
+ });
65
+
66
+ it("should reject an object", () => {
67
+ expect(json.primitiveZ.safeParse({}).success).toBe(false);
68
+ });
69
+
70
+ it("should reject an array", () => {
71
+ expect(json.primitiveZ.safeParse([]).success).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe("primitiveTypeZ", () => {
76
+ it("should accept 'string'", () => {
77
+ expect(json.primitiveTypeZ.parse("string")).toBe("string");
78
+ });
79
+
80
+ it("should accept 'number'", () => {
81
+ expect(json.primitiveTypeZ.parse("number")).toBe("number");
82
+ });
83
+
84
+ it("should accept 'boolean'", () => {
85
+ expect(json.primitiveTypeZ.parse("boolean")).toBe("boolean");
86
+ });
87
+
88
+ it("should accept 'null'", () => {
89
+ expect(json.primitiveTypeZ.parse("null")).toBe("null");
90
+ });
91
+
92
+ it("should reject an invalid type name", () => {
93
+ expect(json.primitiveTypeZ.safeParse("object").success).toBe(false);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,32 @@
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 { z } from "zod";
11
+
12
+ /** Validates an RFC 6901 JSON Pointer string (e.g. "/foo/bar/0"). */
13
+ export const pointerZ = z
14
+ .string()
15
+ .regex(/^(?:$|(?:\/(?:[^~/]|~0|~1)*)+)$/, "must be a valid JSON pointer (RFC 6901)");
16
+
17
+ /** A JSON primitive value: string, number, boolean, or null. */
18
+ export const primitiveZ = z.union([z.string(), z.number(), z.boolean(), z.null()]);
19
+
20
+ export type Primitive = z.infer<typeof primitiveZ>;
21
+
22
+ /** The type name of a JSON primitive: "string", "number", "boolean", or "null". */
23
+ export const primitiveTypeZ = z.enum(["string", "number", "boolean", "null"]);
24
+
25
+ export type PrimitiveType = z.infer<typeof primitiveTypeZ>;
26
+
27
+ export const ZERO_PRIMITIVES = {
28
+ string: "",
29
+ number: 0,
30
+ boolean: false,
31
+ null: null,
32
+ } as const satisfies Record<PrimitiveType, Primitive>;
@@ -330,7 +330,7 @@ export class Series<T extends TelemValue = TelemValue>
330
330
  if (typeof first === "string") this.dataType = DataType.STRING;
331
331
  else if (typeof first === "number") this.dataType = DataType.FLOAT64;
332
332
  else if (typeof first === "bigint") this.dataType = DataType.INT64;
333
- else if (typeof first === "boolean") this.dataType = DataType.BOOLEAN;
333
+ else if (typeof first === "boolean") this.dataType = DataType.UINT8;
334
334
  else if (
335
335
  first instanceof TimeStamp ||
336
336
  first instanceof Date ||
@@ -144,6 +144,26 @@ describe("TimeStamp", () => {
144
144
 
145
145
  expect(ts1.valueOf()).toEqual(ts2.valueOf());
146
146
  });
147
+
148
+ test("should round-trip across DST boundaries", () => {
149
+ // 2025-03-09 is US DST spring-forward, 2025-11-02 is US DST fall-back. These
150
+ // dates may have a different UTC offset than the current date, so we verify the
151
+ // round-trip works regardless of DST transitions.
152
+ const dstDates = [
153
+ "2025-03-09T12:00:00.000",
154
+ "2025-03-10T12:00:00.000",
155
+ "2025-11-02T12:00:00.000",
156
+ "2025-11-03T12:00:00.000",
157
+ "2025-06-15T12:00:00.000",
158
+ "2025-01-15T12:00:00.000",
159
+ ];
160
+ for (const input of dstDates) {
161
+ const ts1 = new TimeStamp(input, "local");
162
+ const output = ts1.toString("ISO", "local").slice(0, -1);
163
+ const ts2 = new TimeStamp(output, "local");
164
+ expect(ts1.valueOf()).toEqual(ts2.valueOf());
165
+ }
166
+ });
147
167
  });
148
168
 
149
169
  test("construct from date", () => {
@@ -1832,6 +1852,9 @@ describe("DataType", () => {
1832
1852
  it("should return false if the data type does not have a variable length", () => {
1833
1853
  expect(DataType.STRING.isVariable).toBe(true);
1834
1854
  });
1855
+ it("should return true for BYTES", () => {
1856
+ expect(DataType.BYTES.isVariable).toBe(true);
1857
+ });
1835
1858
  });
1836
1859
 
1837
1860
  describe("construct", () => {
@@ -1877,7 +1900,6 @@ describe("DataType", () => {
1877
1900
  [DataType.INT32, DataType.FLOAT32, false],
1878
1901
  [DataType.INT32, DataType.FLOAT64, true],
1879
1902
  [DataType.INT32, DataType.STRING, false],
1880
- [DataType.INT32, DataType.BOOLEAN, false],
1881
1903
  [DataType.INT32, DataType.INT8, false],
1882
1904
  [DataType.INT64, DataType.INT32, false],
1883
1905
  [DataType.INT64, DataType.INT64, true],
@@ -1887,22 +1909,19 @@ describe("DataType", () => {
1887
1909
  [DataType.FLOAT64, DataType.FLOAT32, false],
1888
1910
  [DataType.FLOAT64, DataType.FLOAT64, true],
1889
1911
  [DataType.FLOAT64, DataType.STRING, false],
1890
- [DataType.FLOAT64, DataType.BOOLEAN, false],
1891
1912
  [DataType.FLOAT32, DataType.FLOAT64, true],
1892
1913
  [DataType.FLOAT32, DataType.FLOAT32, true],
1893
1914
  [DataType.FLOAT32, DataType.STRING, false],
1894
- [DataType.FLOAT32, DataType.BOOLEAN, false],
1895
1915
  [DataType.STRING, DataType.STRING, true],
1896
1916
  [DataType.STRING, DataType.INT32, false],
1897
1917
  [DataType.STRING, DataType.INT64, false],
1898
1918
  [DataType.STRING, DataType.FLOAT32, false],
1899
1919
  [DataType.STRING, DataType.FLOAT64, false],
1900
- [DataType.STRING, DataType.BOOLEAN, false],
1901
1920
  [DataType.STRING, DataType.INT8, false],
1902
- [DataType.BOOLEAN, DataType.BOOLEAN, true],
1903
- [DataType.BOOLEAN, DataType.INT32, false],
1904
- [DataType.BOOLEAN, DataType.INT64, false],
1905
1921
  [DataType.INT8, DataType.FLOAT32, true],
1922
+ [DataType.BYTES, DataType.BYTES, true],
1923
+ [DataType.BYTES, DataType.INT32, false],
1924
+ [DataType.BYTES, DataType.STRING, false],
1906
1925
  ];
1907
1926
  TESTS.forEach(([from, to, expected]) =>
1908
1927
  it(`should return ${expected} when casting from ${from.toString()} to ${to.toString()}`, () => {
@@ -1923,7 +1942,7 @@ describe("DataType", () => {
1923
1942
  for (const to of numericTypes) expect(from.canCastTo(to)).toBe(true);
1924
1943
  });
1925
1944
  it("should return true for non-numeric data types ONLY if they are equal", () => {
1926
- const nonNumericTypes = [DataType.STRING, DataType.BOOLEAN];
1945
+ const nonNumericTypes = [DataType.STRING, DataType.BYTES];
1927
1946
  for (const from of nonNumericTypes)
1928
1947
  for (const to of nonNumericTypes) expect(from.canCastTo(to)).toBe(from === to);
1929
1948
  });
@@ -1969,10 +1988,10 @@ describe("DataType", () => {
1969
1988
  { input: "float32", expected: "float32", short: "f32" },
1970
1989
  { input: "float64", expected: "float64", short: "f64" },
1971
1990
  { input: "string", expected: "string", short: "str" },
1972
- { input: "boolean", expected: "boolean", short: "bool" },
1973
1991
  { input: "timestamp", expected: "timestamp", short: "ts" },
1974
1992
  { input: "uuid", expected: "uuid", short: "uuid" },
1975
1993
  { input: "json", expected: "json", short: "json" },
1994
+ { input: "bytes", expected: "bytes", short: "bytes" },
1976
1995
  ];
1977
1996
 
1978
1997
  testCases.forEach(({ input, expected, short }) => {
@@ -192,7 +192,11 @@ export class TimeStamp
192
192
 
193
193
  private toISOString(tzInfo: TZInfo = "UTC"): string {
194
194
  if (tzInfo === "UTC") return this.date().toISOString();
195
- return this.sub(TimeStamp.utcOffset).date().toISOString();
195
+ const d = this.date();
196
+ const offset = new TimeSpan(
197
+ BigInt(d.getTimezoneOffset()) * TimeStamp.MINUTE.valueOf(),
198
+ );
199
+ return this.sub(offset).date().toISOString();
196
200
  }
197
201
 
198
202
  private timeString(milliseconds: boolean = false, tzInfo: TZInfo = "UTC"): string {
@@ -1852,7 +1856,11 @@ export class DataType
1852
1856
  * @example DataType.INT32.isVariable // false
1853
1857
  */
1854
1858
  get isVariable(): boolean {
1855
- return this.equals(DataType.JSON) || this.equals(DataType.STRING);
1859
+ return (
1860
+ this.equals(DataType.JSON) ||
1861
+ this.equals(DataType.STRING) ||
1862
+ this.equals(DataType.BYTES)
1863
+ );
1856
1864
  }
1857
1865
 
1858
1866
  /**
@@ -1985,8 +1993,6 @@ export class DataType
1985
1993
  static readonly UINT16 = new DataType("uint16");
1986
1994
  /** Represents a 8-bit unsigned integer value. */
1987
1995
  static readonly UINT8 = new DataType("uint8");
1988
- /** Represents a boolean value. Stored as a 8-bit unsigned integer. */
1989
- static readonly BOOLEAN = new DataType("boolean");
1990
1996
  /** Represents a 64-bit unix epoch. */
1991
1997
  static readonly TIMESTAMP = new DataType("timestamp");
1992
1998
  /** Represents a UUID data type. */
@@ -1997,6 +2003,9 @@ export class DataType
1997
2003
  /** Represents a JSON data type. JSON has an unknown density, and is separated by a
1998
2004
  * newline character. */
1999
2005
  static readonly JSON = new DataType("json");
2006
+ /** Represents a bytes data type for arbitrary byte arrays. Bytes have an unknown
2007
+ * density, and are separated by a newline character. */
2008
+ static readonly BYTES = new DataType("bytes");
2000
2009
 
2001
2010
  private static readonly ARRAY_CONSTRUCTORS: Map<string, TypedArrayConstructor> =
2002
2011
  new Map<string, TypedArrayConstructor>([
@@ -2014,6 +2023,7 @@ export class DataType
2014
2023
  [DataType.STRING.toString(), Uint8Array],
2015
2024
  [DataType.JSON.toString(), Uint8Array],
2016
2025
  [DataType.UUID.toString(), Uint8Array],
2026
+ [DataType.BYTES.toString(), Uint8Array],
2017
2027
  ]);
2018
2028
 
2019
2029
  private static readonly ARRAY_CONSTRUCTOR_DATA_TYPES: Map<string, DataType> = new Map<
@@ -2047,25 +2057,27 @@ export class DataType
2047
2057
  [DataType.STRING.toString(), Density.UNKNOWN],
2048
2058
  [DataType.JSON.toString(), Density.UNKNOWN],
2049
2059
  [DataType.UUID.toString(), Density.BIT128],
2060
+ [DataType.BYTES.toString(), Density.UNKNOWN],
2050
2061
  ]);
2051
2062
 
2052
2063
  /** All the data types. */
2053
2064
  static readonly ALL = [
2054
2065
  DataType.UNKNOWN,
2055
- DataType.FLOAT64,
2056
- DataType.FLOAT32,
2057
- DataType.INT64,
2058
- DataType.INT32,
2059
- DataType.INT16,
2060
- DataType.INT8,
2061
- DataType.UINT64,
2062
- DataType.UINT32,
2063
- DataType.UINT16,
2064
2066
  DataType.UINT8,
2067
+ DataType.UINT16,
2068
+ DataType.UINT32,
2069
+ DataType.UINT64,
2070
+ DataType.INT8,
2071
+ DataType.INT16,
2072
+ DataType.INT32,
2073
+ DataType.INT64,
2074
+ DataType.FLOAT32,
2075
+ DataType.FLOAT64,
2065
2076
  DataType.TIMESTAMP,
2066
2077
  DataType.UUID,
2067
2078
  DataType.STRING,
2068
2079
  DataType.JSON,
2080
+ DataType.BYTES,
2069
2081
  ];
2070
2082
 
2071
2083
  private static readonly SHORT_STRINGS = new Map<string, string>([
@@ -2079,11 +2091,11 @@ export class DataType
2079
2091
  [DataType.INT64.toString(), "i64"],
2080
2092
  [DataType.FLOAT32.toString(), "f32"],
2081
2093
  [DataType.FLOAT64.toString(), "f64"],
2082
- [DataType.BOOLEAN.toString(), "bool"],
2083
2094
  [DataType.TIMESTAMP.toString(), "ts"],
2084
2095
  [DataType.UUID.toString(), "uuid"],
2085
2096
  [DataType.STRING.toString(), "str"],
2086
2097
  [DataType.JSON.toString(), "json"],
2098
+ [DataType.BYTES.toString(), "bytes"],
2087
2099
  ]);
2088
2100
 
2089
2101
  static readonly BIG_INT_TYPES = [DataType.INT64, DataType.UINT64, DataType.TIMESTAMP];