@synnaxlabs/x 0.41.0 → 0.42.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 +23 -23
- package/dist/binary.cjs +1 -1
- package/dist/binary.js +2 -2
- package/dist/{bounds-M-SZ3X1Z.cjs → bounds-BQo7rvs9.cjs} +1 -1
- package/dist/{bounds-DQrjn60Q.js → bounds-Bn5_l4Z3.js} +10 -9
- package/dist/bounds.cjs +1 -1
- package/dist/bounds.js +1 -1
- package/dist/compare.cjs +1 -1
- package/dist/compare.js +1 -1
- package/dist/deep.cjs +1 -1
- package/dist/deep.js +84 -77
- package/dist/{dimensions-PWy5QZoM.cjs → dimensions-D2QGoNXO.cjs} +1 -1
- package/dist/dimensions.cjs +1 -1
- package/dist/{external-CvWr1nhS.cjs → external-DWQITF5_.cjs} +1 -1
- package/dist/index-BywOGO8U.js +1074 -0
- package/dist/index-CYYjI7Uf.cjs +1 -0
- package/dist/index-C_6NXBlg.cjs +3 -0
- package/dist/{index-BVC_8Cg9.js → index-QGplUHuy.js} +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +702 -243
- package/dist/record.js +3 -1
- package/dist/{scale-DL9VFGhL.cjs → scale-BtZINJ-A.cjs} +1 -1
- package/dist/{scale-DQwBWnwc.js → scale-DfJe9755.js} +1 -1
- package/dist/scale.cjs +1 -1
- package/dist/scale.js +1 -1
- package/dist/{series-D0zxMWxP.js → series-B9JERcqi.js} +571 -492
- package/dist/series-DqJ6f97G.cjs +11 -0
- package/dist/spatial.cjs +1 -1
- package/dist/spatial.js +2 -2
- package/dist/src/binary/{encoder.d.ts → codec.d.ts} +14 -8
- package/dist/src/binary/codec.d.ts.map +1 -0
- package/dist/src/binary/codec.spec.d.ts +2 -0
- package/dist/src/binary/codec.spec.d.ts.map +1 -0
- package/dist/src/binary/index.d.ts +1 -1
- package/dist/src/binary/index.d.ts.map +1 -1
- package/dist/src/breaker/breaker.d.ts +14 -21
- package/dist/src/breaker/breaker.d.ts.map +1 -1
- package/dist/src/change/change.d.ts +5 -18
- package/dist/src/change/change.d.ts.map +1 -1
- package/dist/src/color/color.d.ts +126 -0
- package/dist/src/color/color.d.ts.map +1 -0
- package/dist/src/color/color.spec.d.ts +2 -0
- package/dist/src/color/color.spec.d.ts.map +1 -0
- package/dist/src/color/external.d.ts +5 -0
- package/dist/src/color/external.d.ts.map +1 -0
- package/dist/src/color/gradient.d.ts +18 -0
- package/dist/src/color/gradient.d.ts.map +1 -0
- package/dist/src/color/index.d.ts +2 -0
- package/dist/src/color/index.d.ts.map +1 -0
- package/dist/src/color/palette.d.ts +19 -0
- package/dist/src/color/palette.d.ts.map +1 -0
- package/dist/src/color/transformColorsToHex.d.ts +6 -0
- package/dist/src/color/transformColorsToHex.d.ts.map +1 -0
- package/dist/src/control/control.d.ts +69 -74
- package/dist/src/control/control.d.ts.map +1 -1
- package/dist/src/deep/merge.d.ts +1 -1
- package/dist/src/deep/merge.d.ts.map +1 -1
- package/dist/src/errors/errors.d.ts +127 -7
- package/dist/src/errors/errors.d.ts.map +1 -1
- package/dist/src/errors/errors.spec.d.ts +2 -0
- package/dist/src/errors/errors.spec.d.ts.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/jsonrpc/jsonrpc.d.ts +10 -7
- package/dist/src/jsonrpc/jsonrpc.d.ts.map +1 -1
- package/dist/src/kv/types.d.ts +1 -7
- package/dist/src/kv/types.d.ts.map +1 -1
- package/dist/src/migrate/migrate.d.ts +1 -1
- package/dist/src/notation/notation.d.ts +5 -1
- package/dist/src/notation/notation.d.ts.map +1 -1
- package/dist/src/record.d.ts +2 -1
- package/dist/src/record.d.ts.map +1 -1
- package/dist/src/replace.d.ts +2 -0
- package/dist/src/replace.d.ts.map +1 -0
- package/dist/src/runtime/os.d.ts +9 -1
- package/dist/src/runtime/os.d.ts.map +1 -1
- package/dist/src/singleton/define.d.ts +9 -0
- package/dist/src/singleton/define.d.ts.map +1 -0
- package/dist/src/singleton/define.spec.d.ts +2 -0
- package/dist/src/singleton/define.spec.d.ts.map +1 -0
- package/dist/src/singleton/index.d.ts +2 -0
- package/dist/src/singleton/index.d.ts.map +1 -0
- package/dist/src/spatial/base.d.ts +74 -70
- package/dist/src/spatial/base.d.ts.map +1 -1
- package/dist/src/spatial/box/box.d.ts +18 -76
- package/dist/src/spatial/box/box.d.ts.map +1 -1
- package/dist/src/spatial/dimensions/dimensions.d.ts +5 -29
- package/dist/src/spatial/dimensions/dimensions.d.ts.map +1 -1
- package/dist/src/spatial/direction/direction.d.ts +9 -1
- package/dist/src/spatial/direction/direction.d.ts.map +1 -1
- package/dist/src/spatial/location/location.d.ts +43 -22
- package/dist/src/spatial/location/location.d.ts.map +1 -1
- package/dist/src/spatial/scale/scale.d.ts +12 -120
- package/dist/src/spatial/scale/scale.d.ts.map +1 -1
- package/dist/src/spatial/xy/xy.d.ts +5 -29
- package/dist/src/spatial/xy/xy.d.ts.map +1 -1
- package/dist/src/sync/index.d.ts +2 -0
- package/dist/src/sync/index.d.ts.map +1 -0
- package/dist/src/sync/mutex.d.ts +8 -0
- package/dist/src/sync/mutex.d.ts.map +1 -0
- package/dist/src/telem/gl.d.ts +4 -1
- package/dist/src/telem/gl.d.ts.map +1 -1
- package/dist/src/telem/series.d.ts +46 -125
- package/dist/src/telem/series.d.ts.map +1 -1
- package/dist/src/telem/telem.d.ts +101 -86
- package/dist/src/telem/telem.d.ts.map +1 -1
- package/dist/src/toArray.d.ts +1 -1
- package/dist/src/toArray.d.ts.map +1 -1
- package/dist/src/zod/util.d.ts.map +1 -1
- package/dist/telem.cjs +1 -1
- package/dist/telem.js +1 -1
- package/dist/toArray.cjs +1 -1
- package/dist/toArray.js +1 -1
- package/dist/zod.cjs +1 -1
- package/package.json +5 -2
- package/src/binary/codec.spec.ts +370 -0
- package/src/binary/{encoder.ts → codec.ts} +55 -11
- package/src/binary/index.ts +1 -1
- package/src/breaker/breaker.spec.ts +16 -25
- package/src/breaker/breaker.ts +36 -19
- package/src/color/color.spec.ts +673 -0
- package/src/color/color.ts +317 -0
- package/src/color/external.ts +13 -0
- package/src/color/gradient.ts +78 -0
- package/src/color/index.ts +10 -0
- package/src/color/palette.ts +28 -0
- package/src/color/transformColorsToHex.ts +30 -0
- package/src/control/control.ts +30 -22
- package/src/deep/merge.spec.ts +60 -0
- package/src/deep/merge.ts +13 -8
- package/src/errors/errors.spec.ts +152 -0
- package/src/errors/errors.ts +225 -10
- package/src/index.ts +4 -0
- package/src/jsonrpc/jsonrpc.ts +12 -8
- package/src/migrate/migrate.ts +2 -2
- package/src/primitive.ts +1 -1
- package/src/record.ts +5 -1
- package/src/replace.ts +1 -0
- package/src/singleton/define.spec.ts +93 -0
- package/src/singleton/define.ts +27 -0
- package/src/singleton/index.ts +10 -0
- package/src/sync/index.ts +1 -0
- package/src/sync/mutex.ts +16 -0
- package/src/telem/series.spec.ts +32 -0
- package/src/telem/series.ts +54 -19
- package/src/telem/telem.spec.ts +151 -10
- package/src/telem/telem.ts +126 -73
- package/src/toArray.ts +2 -2
- package/src/zod/util.spec.ts +17 -1
- package/src/zod/util.ts +4 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/index-BG3Scw3G.cjs +0 -1
- package/dist/index-C3QzbIwt.js +0 -101
- package/dist/index-CnclyYpG.cjs +0 -3
- package/dist/series-BMma2b5q.cjs +0 -11
- package/dist/src/binary/encoder.d.ts.map +0 -1
- package/dist/src/binary/encoder.spec.d.ts +0 -2
- package/dist/src/binary/encoder.spec.d.ts.map +0 -1
- package/src/binary/encoder.spec.ts +0 -174
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// Copyright 2025 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
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { binary } from "@/binary";
|
|
14
|
+
import { JSON_CODEC, MSGPACK_CODEC } from "@/binary/codec";
|
|
15
|
+
|
|
16
|
+
const sampleSchema = z.object({
|
|
17
|
+
channelKey: z.string(),
|
|
18
|
+
timeStamp: z.number(),
|
|
19
|
+
value: z.unknown(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
[JSON_CODEC, MSGPACK_CODEC].forEach((codec) => {
|
|
23
|
+
describe(`encoder ${codec.contentType}`, () => {
|
|
24
|
+
it("should correctly encode and decode items", () => {
|
|
25
|
+
const sample = {
|
|
26
|
+
channelKey: "test",
|
|
27
|
+
timeStamp: 123,
|
|
28
|
+
value: [1, 2, 3],
|
|
29
|
+
};
|
|
30
|
+
const encoded = codec.encode(sample);
|
|
31
|
+
expect(codec.decode(encoded, sampleSchema)).toEqual(sample);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("JSON", () => {
|
|
37
|
+
it("should correctly convert keys to snake case", () => {
|
|
38
|
+
const sample = {
|
|
39
|
+
channelKey: "test",
|
|
40
|
+
timeStamp: 123,
|
|
41
|
+
value: new Array([1, 2, 3]),
|
|
42
|
+
};
|
|
43
|
+
const encoded = binary.JSON_CODEC.encodeString(sample);
|
|
44
|
+
const parse = JSON.parse(encoded);
|
|
45
|
+
expect(parse.channel_key).toEqual("test");
|
|
46
|
+
});
|
|
47
|
+
it("should correctly decode keys from snake case", () => {
|
|
48
|
+
const sample = {
|
|
49
|
+
channel_key: "test",
|
|
50
|
+
time_stamp: 123,
|
|
51
|
+
value: new Array([1, 2, 3]),
|
|
52
|
+
};
|
|
53
|
+
const encoded = JSON.stringify(sample);
|
|
54
|
+
const decoded = binary.JSON_CODEC.decodeString(encoded, sampleSchema);
|
|
55
|
+
expect(decoded.channelKey).toEqual("test");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("CSV", () => {
|
|
60
|
+
it("should correctly decode CSV data with valid input", () => {
|
|
61
|
+
const sample = `
|
|
62
|
+
channelKey,timeStamp,value
|
|
63
|
+
test,123,5
|
|
64
|
+
test2,124,6
|
|
65
|
+
`;
|
|
66
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
67
|
+
expect(decoded).toEqual({
|
|
68
|
+
channelKey: ["test", "test2"],
|
|
69
|
+
timeStamp: [123, 124],
|
|
70
|
+
value: [5, 6],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should handle empty CSV data", () => {
|
|
75
|
+
const sample = `
|
|
76
|
+
`;
|
|
77
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
78
|
+
expect(decoded).toEqual({});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle CSV with only headers", () => {
|
|
82
|
+
const sample = `
|
|
83
|
+
channelKey,timeStamp,value
|
|
84
|
+
`;
|
|
85
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
86
|
+
expect(decoded).toEqual({
|
|
87
|
+
channelKey: [],
|
|
88
|
+
timeStamp: [],
|
|
89
|
+
value: [],
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle CSV with missing values", () => {
|
|
94
|
+
const sample = `
|
|
95
|
+
channelKey,timeStamp,value
|
|
96
|
+
test,123,
|
|
97
|
+
test2,124,6
|
|
98
|
+
`;
|
|
99
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
100
|
+
expect(decoded).toEqual({
|
|
101
|
+
channelKey: ["test", "test2"],
|
|
102
|
+
timeStamp: [123, 124],
|
|
103
|
+
value: [6],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should handle CSV with extra values", () => {
|
|
108
|
+
const sample = `
|
|
109
|
+
channelKey,timeStamp,value
|
|
110
|
+
test,123,5,extra
|
|
111
|
+
test2,124,6
|
|
112
|
+
`;
|
|
113
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
114
|
+
expect(decoded).toEqual({
|
|
115
|
+
channelKey: ["test", "test2"],
|
|
116
|
+
timeStamp: [123, 124],
|
|
117
|
+
value: [5, 6],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should handle CSV with different types of values", () => {
|
|
122
|
+
const sample = `
|
|
123
|
+
key,number,string
|
|
124
|
+
test,123,"hello"
|
|
125
|
+
test2,456,"world"
|
|
126
|
+
`;
|
|
127
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
128
|
+
expect(decoded).toEqual({
|
|
129
|
+
key: ["test", "test2"],
|
|
130
|
+
number: [123, 456],
|
|
131
|
+
string: ["hello", "world"],
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle CSV with spaces around values", () => {
|
|
136
|
+
const sample = `
|
|
137
|
+
key, number , string
|
|
138
|
+
test , 123 , "hello"
|
|
139
|
+
test2 , 456 , "world"
|
|
140
|
+
`;
|
|
141
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
142
|
+
expect(decoded).toEqual({
|
|
143
|
+
key: ["test", "test2"],
|
|
144
|
+
number: [123, 456],
|
|
145
|
+
string: ["hello", "world"],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should handle CSV with empty rows", () => {
|
|
150
|
+
const sample = `
|
|
151
|
+
key,number,string
|
|
152
|
+
test,123,"hello"
|
|
153
|
+
,
|
|
154
|
+
test2,456,"world"
|
|
155
|
+
`;
|
|
156
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
157
|
+
expect(decoded).toEqual({
|
|
158
|
+
key: ["test", "test2"],
|
|
159
|
+
number: [123, 456],
|
|
160
|
+
string: ["hello", "world"],
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should handle CSV with single column", () => {
|
|
165
|
+
const sample = `
|
|
166
|
+
key
|
|
167
|
+
test
|
|
168
|
+
test2
|
|
169
|
+
`;
|
|
170
|
+
const decoded = binary.CSV_CODEC.decodeString(sample);
|
|
171
|
+
expect(decoded).toEqual({
|
|
172
|
+
key: ["test", "test2"],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should correctly encode array of objects to CSV", () => {
|
|
177
|
+
const sampleData = [
|
|
178
|
+
{ name: "John", age: 30, city: "New York" },
|
|
179
|
+
{ name: "Alice", age: 25, city: "Boston" },
|
|
180
|
+
{ name: "Bob", age: 40, city: "Chicago" },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const encoded = binary.CSV_CODEC.encodeString(sampleData);
|
|
184
|
+
expect(encoded).toBe(
|
|
185
|
+
'name,age,city\n"John",30,"New York"\n"Alice",25,"Boston"\n"Bob",40,"Chicago"',
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should handle objects with missing values", () => {
|
|
190
|
+
const sampleData = [
|
|
191
|
+
{ name: "John", age: 30, city: "New York" },
|
|
192
|
+
{ name: "Alice", age: 25 },
|
|
193
|
+
{ name: "Bob", city: "Chicago" },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const encoded = binary.CSV_CODEC.encodeString(sampleData);
|
|
197
|
+
expect(encoded).toBe(
|
|
198
|
+
'name,age,city\n"John",30,"New York"\n"Alice",25,""\n"Bob","","Chicago"',
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should handle objects with null and undefined values", () => {
|
|
203
|
+
const sampleData = [
|
|
204
|
+
{ name: "John", age: null, city: undefined },
|
|
205
|
+
{ name: "Alice", age: 25, city: null },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const encoded = binary.CSV_CODEC.encodeString(sampleData);
|
|
209
|
+
expect(encoded).toBe('name,age,city\n"John","",""\n"Alice",25,""');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should handle different data types in objects", () => {
|
|
213
|
+
const sampleData = [
|
|
214
|
+
{ name: "John", active: true, score: 98.5 },
|
|
215
|
+
{ name: "Alice", active: false, score: 92.3 },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const encoded = binary.CSV_CODEC.encodeString(sampleData);
|
|
219
|
+
expect(encoded).toBe('name,active,score\n"John",true,98.5\n"Alice",false,92.3');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should throw error when encoding empty array", () => {
|
|
223
|
+
const sampleData: any[] = [];
|
|
224
|
+
|
|
225
|
+
expect(() => {
|
|
226
|
+
binary.CSV_CODEC.encodeString(sampleData);
|
|
227
|
+
}).toThrow("Payload must be an array of objects");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should throw error when encoding non-array data", () => {
|
|
231
|
+
const sampleData = { name: "John", age: 30 };
|
|
232
|
+
|
|
233
|
+
expect(() => {
|
|
234
|
+
binary.CSV_CODEC.encodeString(sampleData);
|
|
235
|
+
}).toThrow("Payload must be an array of objects");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should round-trip encode and decode CSV data", () => {
|
|
239
|
+
const sampleData = [
|
|
240
|
+
{ name: "John", age: 30, city: "New York" },
|
|
241
|
+
{ name: "Alice", age: 25, city: "Boston" },
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const encoded = binary.CSV_CODEC.encode(sampleData);
|
|
245
|
+
const decoded = binary.CSV_CODEC.decode(encoded);
|
|
246
|
+
|
|
247
|
+
expect(decoded).toEqual({
|
|
248
|
+
name: ["John", "Alice"],
|
|
249
|
+
age: [30, 25],
|
|
250
|
+
city: ["New York", "Boston"],
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("MsgPack", () => {
|
|
256
|
+
it("should correctly convert keys to snake case", () => {
|
|
257
|
+
const sample = {
|
|
258
|
+
channelKey: "test",
|
|
259
|
+
timeStamp: 123,
|
|
260
|
+
value: [1, 2, 3],
|
|
261
|
+
};
|
|
262
|
+
const encoded = binary.MSGPACK_CODEC.encode(sample);
|
|
263
|
+
const decoded = binary.MSGPACK_CODEC.decode(encoded);
|
|
264
|
+
expect(decoded).toEqual(sample);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should correctly validate with schema", () => {
|
|
268
|
+
const sample = {
|
|
269
|
+
channelKey: "test",
|
|
270
|
+
timeStamp: 123,
|
|
271
|
+
value: [1, 2, 3],
|
|
272
|
+
};
|
|
273
|
+
const encoded = binary.MSGPACK_CODEC.encode(sample);
|
|
274
|
+
const decoded = binary.MSGPACK_CODEC.decode(encoded, sampleSchema);
|
|
275
|
+
expect(decoded).toEqual(sample);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should handle complex nested objects", () => {
|
|
279
|
+
const sample = {
|
|
280
|
+
channelKey: "test",
|
|
281
|
+
timeStamp: 123,
|
|
282
|
+
nestedObject: {
|
|
283
|
+
innerKey: "value",
|
|
284
|
+
numberArray: [1, 2, 3],
|
|
285
|
+
deepNesting: {
|
|
286
|
+
anotherKey: true,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
const encoded = binary.MSGPACK_CODEC.encode(sample);
|
|
291
|
+
const decoded = binary.MSGPACK_CODEC.decode(encoded);
|
|
292
|
+
expect(decoded).toEqual(sample);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should handle binary data", () => {
|
|
296
|
+
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
297
|
+
const sample = {
|
|
298
|
+
channelKey: "binary-test",
|
|
299
|
+
timeStamp: 456,
|
|
300
|
+
value: binaryData,
|
|
301
|
+
};
|
|
302
|
+
const encoded = binary.MSGPACK_CODEC.encode(sample);
|
|
303
|
+
const decoded = binary.MSGPACK_CODEC.decode<typeof sampleSchema>(encoded);
|
|
304
|
+
|
|
305
|
+
// Check that the structure is preserved
|
|
306
|
+
expect(decoded.channelKey).toEqual(sample.channelKey);
|
|
307
|
+
expect(decoded.timeStamp).toEqual(sample.timeStamp);
|
|
308
|
+
|
|
309
|
+
// Verify that binary data is handled properly
|
|
310
|
+
// Note: The exact format might depend on msgpack implementation
|
|
311
|
+
expect(
|
|
312
|
+
Array.isArray(decoded.value) || ArrayBuffer.isView(decoded.value),
|
|
313
|
+
).toBeTruthy();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
class CustomValueEncoder {
|
|
317
|
+
readonly encodeValue = true;
|
|
318
|
+
|
|
319
|
+
value = "cat";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
it("should correctly encode and decode custom value", () => {
|
|
323
|
+
const sample = {
|
|
324
|
+
channelKey: "test",
|
|
325
|
+
timeStamp: 123,
|
|
326
|
+
value: new CustomValueEncoder(),
|
|
327
|
+
};
|
|
328
|
+
const encoded = binary.MSGPACK_CODEC.encode(sample);
|
|
329
|
+
const decoded = binary.MSGPACK_CODEC.decode(encoded, sampleSchema);
|
|
330
|
+
expect(decoded).toEqual({ ...sample, value: "cat" });
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("TextCodec", () => {
|
|
335
|
+
it("should correctly encode and decode text", () => {
|
|
336
|
+
const sampleText = "Hello, world!";
|
|
337
|
+
const encoded = binary.TEXT_CODEC.encode(sampleText);
|
|
338
|
+
const decoded = binary.TEXT_CODEC.decode(encoded);
|
|
339
|
+
expect(decoded).toEqual(sampleText);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should handle empty strings", () => {
|
|
343
|
+
const sampleText = "";
|
|
344
|
+
const encoded = binary.TEXT_CODEC.encode(sampleText);
|
|
345
|
+
const decoded = binary.TEXT_CODEC.decode(encoded);
|
|
346
|
+
expect(decoded).toEqual(sampleText);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should validate text with schema", () => {
|
|
350
|
+
const textSchema = z.string();
|
|
351
|
+
const sampleText = "Validation test";
|
|
352
|
+
const encoded = binary.TEXT_CODEC.encode(sampleText);
|
|
353
|
+
const decoded = binary.TEXT_CODEC.decode(encoded, textSchema);
|
|
354
|
+
expect(decoded).toEqual(sampleText);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should handle special characters", () => {
|
|
358
|
+
const sampleText = "Special characters: äöü!@#$%^&*()_+";
|
|
359
|
+
const encoded = binary.TEXT_CODEC.encode(sampleText);
|
|
360
|
+
const decoded = binary.TEXT_CODEC.decode(encoded);
|
|
361
|
+
expect(decoded).toEqual(sampleText);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should handle multi-line text", () => {
|
|
365
|
+
const sampleText = "Line 1\nLine 2\nLine 3";
|
|
366
|
+
const encoded = binary.TEXT_CODEC.encode(sampleText);
|
|
367
|
+
const decoded = binary.TEXT_CODEC.decode(encoded);
|
|
368
|
+
expect(decoded).toEqual(sampleText);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -7,7 +7,8 @@
|
|
|
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
|
-
import {
|
|
10
|
+
import { decode, encode, ExtensionCodec } from "@msgpack/msgpack";
|
|
11
|
+
import { type z } from "zod";
|
|
11
12
|
|
|
12
13
|
import { caseconv } from "@/caseconv";
|
|
13
14
|
import { isObject } from "@/identity";
|
|
@@ -26,7 +27,7 @@ export interface Codec {
|
|
|
26
27
|
* @param payload - The payload to encode.
|
|
27
28
|
* @returns An ArrayBuffer containing the encoded payload.
|
|
28
29
|
*/
|
|
29
|
-
encode: (payload: unknown) =>
|
|
30
|
+
encode: (payload: unknown) => Uint8Array;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Decodes the given binary representation into a type checked payload.
|
|
@@ -34,7 +35,10 @@ export interface Codec {
|
|
|
34
35
|
* @param data - The data to decode.
|
|
35
36
|
* @param schema - The schema to decode the data with.
|
|
36
37
|
*/
|
|
37
|
-
decode: <P
|
|
38
|
+
decode: <P extends z.ZodTypeAny>(
|
|
39
|
+
data: Uint8Array | ArrayBuffer,
|
|
40
|
+
schema?: P,
|
|
41
|
+
) => z.output<P>;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** JSONCodec is a JSON implementation of Codec. */
|
|
@@ -48,8 +52,8 @@ export class JSONCodec implements Codec {
|
|
|
48
52
|
this.encoder = new TextEncoder();
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
encode(payload: unknown):
|
|
52
|
-
return this.encoder.encode(this.encodeString(payload))
|
|
55
|
+
encode(payload: unknown): Uint8Array {
|
|
56
|
+
return this.encoder.encode(this.encodeString(payload));
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
decode<P extends z.ZodTypeAny>(
|
|
@@ -87,9 +91,9 @@ export class JSONCodec implements Codec {
|
|
|
87
91
|
export class CSVCodec implements Codec {
|
|
88
92
|
contentType = "text/csv";
|
|
89
93
|
|
|
90
|
-
encode(payload: unknown):
|
|
94
|
+
encode(payload: unknown): Uint8Array {
|
|
91
95
|
const csvString = this.encodeString(payload);
|
|
92
|
-
return new TextEncoder().encode(csvString)
|
|
96
|
+
return new TextEncoder().encode(csvString);
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
decode<P extends z.ZodTypeAny>(
|
|
@@ -155,8 +159,8 @@ export class CSVCodec implements Codec {
|
|
|
155
159
|
export class TextCodec implements Codec {
|
|
156
160
|
contentType = "text/plain";
|
|
157
161
|
|
|
158
|
-
encode(payload: unknown):
|
|
159
|
-
return new TextEncoder().encode(payload as string)
|
|
162
|
+
encode(payload: unknown): Uint8Array {
|
|
163
|
+
return new TextEncoder().encode(payload as string);
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
decode<P extends z.ZodTypeAny>(
|
|
@@ -168,8 +172,48 @@ export class TextCodec implements Codec {
|
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
const extensionCodec = new ExtensionCodec();
|
|
176
|
+
|
|
177
|
+
extensionCodec.register({
|
|
178
|
+
type: 0,
|
|
179
|
+
encode: (value: unknown): Uint8Array | null => {
|
|
180
|
+
if (ArrayBuffer.isView(value)) {
|
|
181
|
+
const array = Array.from(value as Uint8Array);
|
|
182
|
+
return encode(array, { extensionCodec });
|
|
183
|
+
}
|
|
184
|
+
if (isObject(value) && "encode_value" in value) {
|
|
185
|
+
if (typeof value.value === "bigint")
|
|
186
|
+
return encode(value.value.toString(), { extensionCodec });
|
|
187
|
+
return encode(value.value, { extensionCodec });
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === "bigint") return encode(value.toString(), { extensionCodec });
|
|
190
|
+
return null;
|
|
191
|
+
},
|
|
192
|
+
decode: (data: Uint8Array) => decode(data, { extensionCodec }),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export class MsgPackCodec implements Codec {
|
|
196
|
+
contentType = "application/msgpack";
|
|
197
|
+
|
|
198
|
+
encode(payload: unknown): Uint8Array {
|
|
199
|
+
const caseConverted = caseconv.camelToSnake(payload);
|
|
200
|
+
const d = encode(caseConverted, { extensionCodec });
|
|
201
|
+
return d.slice();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
decode<P extends z.ZodTypeAny>(
|
|
205
|
+
data: Uint8Array | ArrayBuffer,
|
|
206
|
+
schema?: P,
|
|
207
|
+
): z.output<P> {
|
|
208
|
+
const decoded = decode(data, { extensionCodec });
|
|
209
|
+
const unpacked = caseconv.snakeToCamel(decoded);
|
|
210
|
+
return schema != null ? schema.parse(unpacked) : (unpacked as z.output<P>);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static registerCustomType(): void {}
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
export const JSON_CODEC = new JSONCodec();
|
|
172
217
|
export const CSV_CODEC = new CSVCodec();
|
|
173
218
|
export const TEXT_CODEC = new TextCodec();
|
|
174
|
-
|
|
175
|
-
export const ENCODERS: Codec[] = [JSON_CODEC];
|
|
219
|
+
export const MSGPACK_CODEC = new MsgPackCodec();
|
package/src/binary/index.ts
CHANGED
|
@@ -15,8 +15,8 @@ import { TimeSpan } from "@/telem";
|
|
|
15
15
|
describe("breaker", () => {
|
|
16
16
|
it("should allow first attempt without sleeping", async () => {
|
|
17
17
|
const mockSleep = vi.fn();
|
|
18
|
-
const brk = breaker.
|
|
19
|
-
const canRetry = await brk();
|
|
18
|
+
const brk = new breaker.Breaker({ sleepFn: mockSleep });
|
|
19
|
+
const canRetry = await brk.wait();
|
|
20
20
|
|
|
21
21
|
expect(canRetry).toBe(true);
|
|
22
22
|
expect(mockSleep).toHaveBeenCalled();
|
|
@@ -24,58 +24,49 @@ describe("breaker", () => {
|
|
|
24
24
|
|
|
25
25
|
it("should retry specified number of times before failing", async () => {
|
|
26
26
|
const mockSleep = vi.fn();
|
|
27
|
-
const brk = breaker.
|
|
27
|
+
const brk = new breaker.Breaker({
|
|
28
28
|
maxRetries: 2,
|
|
29
|
-
|
|
29
|
+
baseInterval: TimeSpan.milliseconds(1),
|
|
30
30
|
sleepFn: mockSleep,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// First attempt
|
|
34
|
-
expect(await brk()).toBe(true);
|
|
34
|
+
expect(await brk.wait()).toBe(true);
|
|
35
35
|
// Second attempt
|
|
36
|
-
expect(await brk()).toBe(true);
|
|
36
|
+
expect(await brk.wait()).toBe(true);
|
|
37
37
|
// Third attempt (should fail)
|
|
38
|
-
expect(await brk()).toBe(false);
|
|
38
|
+
expect(await brk.wait()).toBe(false);
|
|
39
39
|
|
|
40
40
|
expect(mockSleep).toHaveBeenCalledTimes(2);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("should increase delay between retries according to scale", async () => {
|
|
44
44
|
const mockSleep = vi.fn();
|
|
45
|
-
const brk = breaker.
|
|
46
|
-
|
|
45
|
+
const brk = new breaker.Breaker({
|
|
46
|
+
baseInterval: TimeSpan.seconds(1),
|
|
47
47
|
maxRetries: 3,
|
|
48
48
|
scale: 2,
|
|
49
49
|
sleepFn: mockSleep,
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
await brk(); // First attempt - 1s
|
|
53
|
-
await brk(); // Second attempt - 1s * 2 = 2s;
|
|
54
|
-
await brk(); // Third attempt - 2s *2 = 4s;
|
|
52
|
+
await brk.wait(); // First attempt - 1s
|
|
53
|
+
await brk.wait(); // Second attempt - 1s * 2 = 2s;
|
|
54
|
+
await brk.wait(); // Third attempt - 2s *2 = 4s;
|
|
55
55
|
|
|
56
56
|
expect(mockSleep).toHaveBeenNthCalledWith(1, TimeSpan.seconds(1));
|
|
57
57
|
expect(mockSleep).toHaveBeenNthCalledWith(2, TimeSpan.seconds(2));
|
|
58
58
|
expect(mockSleep).toHaveBeenNthCalledWith(3, TimeSpan.seconds(4));
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it("should use default values when no options provided", async () => {
|
|
62
|
-
const brk = breaker.create();
|
|
63
|
-
let attempts = 0;
|
|
64
|
-
|
|
65
|
-
while (await brk()) attempts++;
|
|
66
|
-
|
|
67
|
-
expect(attempts).toBe(5); // Default maxRetries is 5
|
|
68
|
-
});
|
|
69
|
-
|
|
70
61
|
it("should use custom sleep function when provided", async () => {
|
|
71
62
|
const customSleep = vi.fn();
|
|
72
|
-
const brk = breaker.
|
|
73
|
-
|
|
63
|
+
const brk = new breaker.Breaker({
|
|
64
|
+
baseInterval: TimeSpan.milliseconds(100),
|
|
74
65
|
sleepFn: customSleep,
|
|
75
66
|
});
|
|
76
67
|
|
|
77
|
-
await brk();
|
|
78
|
-
await brk();
|
|
68
|
+
await brk.wait();
|
|
69
|
+
await brk.wait();
|
|
79
70
|
|
|
80
71
|
expect(customSleep).toHaveBeenCalledTimes(2);
|
|
81
72
|
});
|
package/src/breaker/breaker.ts
CHANGED
|
@@ -12,31 +12,48 @@ import { z } from "zod";
|
|
|
12
12
|
import { sleep } from "@/sleep";
|
|
13
13
|
import { type CrudeTimeSpan, TimeSpan } from "@/telem";
|
|
14
14
|
|
|
15
|
+
export class Breaker {
|
|
16
|
+
private readonly config: Omit<Required<Config>, "baseInterval"> & {
|
|
17
|
+
baseInterval: TimeSpan;
|
|
18
|
+
};
|
|
19
|
+
private retries: number;
|
|
20
|
+
private interval: TimeSpan;
|
|
21
|
+
|
|
22
|
+
constructor(cfg?: Config) {
|
|
23
|
+
this.config = {
|
|
24
|
+
baseInterval: new TimeSpan(cfg?.baseInterval ?? TimeSpan.seconds(1)),
|
|
25
|
+
maxRetries: cfg?.maxRetries ?? 5,
|
|
26
|
+
scale: cfg?.scale ?? 1,
|
|
27
|
+
sleepFn: cfg?.sleepFn ?? sleep.sleep,
|
|
28
|
+
};
|
|
29
|
+
this.retries = 0;
|
|
30
|
+
this.interval = new TimeSpan(this.config.baseInterval);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async wait(): Promise<boolean> {
|
|
34
|
+
const { maxRetries, scale, sleepFn } = this.config;
|
|
35
|
+
if (this.retries >= maxRetries) return false;
|
|
36
|
+
await sleepFn(this.interval);
|
|
37
|
+
this.interval = this.interval.mult(scale);
|
|
38
|
+
this.retries++;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reset() {
|
|
43
|
+
this.retries = 0;
|
|
44
|
+
this.interval = this.config.baseInterval;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
export const breakerConfig = z.object({
|
|
16
|
-
|
|
49
|
+
baseInterval: TimeSpan.z.optional(),
|
|
17
50
|
maxRetries: z.number().optional(),
|
|
18
51
|
scale: z.number().optional(),
|
|
19
52
|
});
|
|
20
53
|
|
|
21
|
-
export interface Config extends Omit<z.infer<typeof breakerConfig>, "
|
|
22
|
-
|
|
54
|
+
export interface Config extends Omit<z.infer<typeof breakerConfig>, "baseInterval"> {
|
|
55
|
+
baseInterval?: CrudeTimeSpan;
|
|
23
56
|
maxRetries?: number;
|
|
24
57
|
scale?: number;
|
|
25
58
|
sleepFn?: (duration: TimeSpan) => Promise<void>;
|
|
26
59
|
}
|
|
27
|
-
|
|
28
|
-
export const create = (options: Config = {}): (() => Promise<boolean>) => {
|
|
29
|
-
const sleepFn = options.sleepFn || sleep.sleep;
|
|
30
|
-
const maxRetries = options.maxRetries ?? 5;
|
|
31
|
-
const scale = options.scale ?? 1;
|
|
32
|
-
let retries = 0;
|
|
33
|
-
let interval = new TimeSpan(options.interval ?? TimeSpan.milliseconds(1));
|
|
34
|
-
return async () => {
|
|
35
|
-
// Change from arrow function to regular function to preserve 'this'
|
|
36
|
-
if (retries >= maxRetries) return false;
|
|
37
|
-
await sleepFn(interval);
|
|
38
|
-
interval = interval.mult(scale);
|
|
39
|
-
retries++;
|
|
40
|
-
return true;
|
|
41
|
-
};
|
|
42
|
-
};
|