@synnaxlabs/client 0.41.0 → 0.42.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 +6 -6
- package/dist/access/payload.d.ts +7 -1
- package/dist/access/payload.d.ts.map +1 -1
- package/dist/access/policy/payload.d.ts +182 -142
- package/dist/access/policy/payload.d.ts.map +1 -1
- package/dist/access/policy/retriever.d.ts +25 -22
- package/dist/access/policy/retriever.d.ts.map +1 -1
- package/dist/auth/auth.d.ts +1 -7
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/channel/client.d.ts +2 -7
- package/dist/channel/client.d.ts.map +1 -1
- package/dist/channel/payload.d.ts +13 -74
- package/dist/channel/payload.d.ts.map +1 -1
- package/dist/channel/retriever.d.ts +5 -31
- package/dist/channel/retriever.d.ts.map +1 -1
- package/dist/channel/writer.d.ts +6 -18
- package/dist/channel/writer.d.ts.map +1 -1
- package/dist/client.cjs +36 -31
- package/dist/client.d.ts +8 -56
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6486 -3979
- package/dist/connection/checker.d.ts +22 -39
- package/dist/connection/checker.d.ts.map +1 -1
- package/dist/control/client.d.ts.map +1 -1
- package/dist/control/state.d.ts +6 -26
- package/dist/control/state.d.ts.map +1 -1
- package/dist/errors.d.ts +31 -56
- package/dist/errors.d.ts.map +1 -1
- package/dist/framer/adapter.d.ts +4 -0
- package/dist/framer/adapter.d.ts.map +1 -1
- package/dist/framer/client.d.ts +2 -2
- package/dist/framer/client.d.ts.map +1 -1
- package/dist/framer/codec.d.ts +34 -0
- package/dist/framer/codec.d.ts.map +1 -0
- package/dist/framer/codec.spec.d.ts +2 -0
- package/dist/framer/codec.spec.d.ts.map +1 -0
- package/dist/framer/deleter.d.ts +12 -49
- package/dist/framer/deleter.d.ts.map +1 -1
- package/dist/framer/frame.d.ts +26 -88
- package/dist/framer/frame.d.ts.map +1 -1
- package/dist/framer/iterator.d.ts.map +1 -1
- package/dist/framer/streamer.d.ts +69 -11
- package/dist/framer/streamer.d.ts.map +1 -1
- package/dist/framer/writer.d.ts +60 -257
- package/dist/framer/writer.d.ts.map +1 -1
- package/dist/hardware/device/client.d.ts +7 -31
- package/dist/hardware/device/client.d.ts.map +1 -1
- package/dist/hardware/device/payload.d.ts +11 -93
- package/dist/hardware/device/payload.d.ts.map +1 -1
- package/dist/hardware/rack/payload.d.ts +15 -103
- package/dist/hardware/rack/payload.d.ts.map +1 -1
- package/dist/hardware/task/client.d.ts +4 -20
- package/dist/hardware/task/client.d.ts.map +1 -1
- package/dist/hardware/task/payload.d.ts +41 -116
- package/dist/hardware/task/payload.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/label/payload.d.ts +1 -9
- package/dist/label/payload.d.ts.map +1 -1
- package/dist/label/writer.d.ts +27 -36
- package/dist/label/writer.d.ts.map +1 -1
- package/dist/ontology/client.d.ts +46 -36
- package/dist/ontology/client.d.ts.map +1 -1
- package/dist/ontology/group/payload.d.ts +1 -7
- package/dist/ontology/group/payload.d.ts.map +1 -1
- package/dist/ontology/payload.d.ts +239 -146
- package/dist/ontology/payload.d.ts.map +1 -1
- package/dist/ranger/client.d.ts +13 -58
- package/dist/ranger/client.d.ts.map +1 -1
- package/dist/ranger/kv.d.ts +7 -49
- package/dist/ranger/kv.d.ts.map +1 -1
- package/dist/ranger/payload.d.ts +21 -99
- package/dist/ranger/payload.d.ts.map +1 -1
- package/dist/ranger/writer.d.ts +35 -88
- package/dist/ranger/writer.d.ts.map +1 -1
- package/dist/testutil/indexedPair.d.ts +5 -0
- package/dist/testutil/indexedPair.d.ts.map +1 -0
- package/dist/testutil/telem.d.ts +3 -0
- package/dist/testutil/telem.d.ts.map +1 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.d.ts.map +1 -1
- package/dist/user/payload.d.ts +3 -29
- package/dist/user/payload.d.ts.map +1 -1
- package/dist/user/retriever.d.ts +3 -9
- package/dist/user/retriever.d.ts.map +1 -1
- package/dist/util/decodeJSONString.d.ts.map +1 -1
- package/dist/util/zod.d.ts +1 -1
- package/dist/util/zod.d.ts.map +1 -1
- package/dist/workspace/lineplot/payload.d.ts +10 -26
- package/dist/workspace/lineplot/payload.d.ts.map +1 -1
- package/dist/workspace/log/payload.d.ts +10 -26
- package/dist/workspace/log/payload.d.ts.map +1 -1
- package/dist/workspace/payload.d.ts +14 -40
- package/dist/workspace/payload.d.ts.map +1 -1
- package/dist/workspace/schematic/payload.d.ts +13 -45
- package/dist/workspace/schematic/payload.d.ts.map +1 -1
- package/dist/workspace/table/payload.d.ts +13 -39
- package/dist/workspace/table/payload.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/channel/channel.spec.ts +26 -27
- package/src/channel/client.ts +0 -9
- package/src/channel/payload.ts +22 -5
- package/src/channel/retriever.ts +12 -6
- package/src/client.ts +3 -3
- package/src/control/client.ts +5 -2
- package/src/control/state.ts +8 -3
- package/src/errors.spec.ts +5 -4
- package/src/errors.ts +21 -82
- package/src/framer/adapter.ts +22 -3
- package/src/framer/client.ts +38 -21
- package/src/framer/codec.spec.ts +303 -0
- package/src/framer/codec.ts +396 -0
- package/src/framer/deleter.spec.ts +51 -63
- package/src/framer/frame.ts +16 -5
- package/src/framer/iterator.spec.ts +45 -28
- package/src/framer/iterator.ts +6 -5
- package/src/framer/streamProxy.ts +1 -1
- package/src/framer/streamer.spec.ts +10 -18
- package/src/framer/streamer.ts +138 -22
- package/src/framer/writer.spec.ts +125 -150
- package/src/framer/writer.ts +74 -68
- package/src/hardware/device/payload.ts +3 -3
- package/src/hardware/task/payload.ts +9 -6
- package/src/hardware/task/task.spec.ts +1 -1
- package/src/index.ts +0 -2
- package/src/ontology/group/group.spec.ts +2 -2
- package/src/ontology/payload.ts +3 -3
- package/src/ranger/ranger.spec.ts +9 -7
- package/src/testutil/indexedPair.ts +40 -0
- package/src/testutil/telem.ts +13 -0
- package/src/transport.ts +1 -2
- package/src/util/decodeJSONString.ts +2 -2
- package/src/util/retrieve.spec.ts +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { type WebsocketMessage } from "@synnaxlabs/freighter";
|
|
2
|
+
import { DataType, Series, TimeStamp } from "@synnaxlabs/x";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { type channel } from "@/channel";
|
|
6
|
+
import { ValidationError } from "@/errors";
|
|
7
|
+
import { framer } from "@/framer";
|
|
8
|
+
import {
|
|
9
|
+
Codec,
|
|
10
|
+
HIGH_PERF_SPECIAL_CHAR,
|
|
11
|
+
LOW_PER_SPECIAL_CHAR,
|
|
12
|
+
WSWriterCodec,
|
|
13
|
+
} from "@/framer/codec";
|
|
14
|
+
import { Frame } from "@/framer/frame";
|
|
15
|
+
import { WriterCommand, type WriteRequest } from "@/framer/writer";
|
|
16
|
+
|
|
17
|
+
describe("encoder", () => {
|
|
18
|
+
describe("base codec", () => {
|
|
19
|
+
interface Spec {
|
|
20
|
+
name: string;
|
|
21
|
+
channels: channel.Keys;
|
|
22
|
+
dataTypes: DataType[];
|
|
23
|
+
frame: framer.Frame;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SPECS: Spec[] = [
|
|
27
|
+
{
|
|
28
|
+
name: "All Channels Present, In Order",
|
|
29
|
+
channels: [1, 2, 3],
|
|
30
|
+
dataTypes: [DataType.INT64, DataType.FLOAT32, DataType.FLOAT64],
|
|
31
|
+
frame: new framer.Frame(
|
|
32
|
+
[1, 2, 3],
|
|
33
|
+
[
|
|
34
|
+
new Series(new BigInt64Array([1n, 2n, 3n])),
|
|
35
|
+
new Series(new Float32Array([4, 5, 6])),
|
|
36
|
+
new Series(new Float64Array([7, 8, 9])),
|
|
37
|
+
],
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "All Channels Present, Out of Order",
|
|
42
|
+
channels: [3, 1, 2],
|
|
43
|
+
dataTypes: [DataType.FLOAT64, DataType.INT64, DataType.FLOAT32],
|
|
44
|
+
frame: new framer.Frame(
|
|
45
|
+
[2, 3, 1],
|
|
46
|
+
[
|
|
47
|
+
new Series(new Float32Array([4, 5, 6])),
|
|
48
|
+
new Series(new Float64Array([7, 8, 9])),
|
|
49
|
+
new Series(new BigInt64Array([1n, 2n, 3n])),
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "Some Channels Present, In Order",
|
|
55
|
+
channels: [1, 2, 3],
|
|
56
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32, DataType.FLOAT64],
|
|
57
|
+
frame: new framer.Frame(
|
|
58
|
+
[1, 3],
|
|
59
|
+
[
|
|
60
|
+
new Series(new Uint8Array([1, 2, 3])),
|
|
61
|
+
new Series(new Float64Array([7, 8, 9])),
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Some Channels Present, Out of Order",
|
|
67
|
+
channels: [1, 2, 3],
|
|
68
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32, DataType.FLOAT64],
|
|
69
|
+
frame: new framer.Frame(
|
|
70
|
+
[3, 1],
|
|
71
|
+
[
|
|
72
|
+
new Series(new Float64Array([7, 8, 9])),
|
|
73
|
+
new Series(new Uint8Array([1, 2, 3])),
|
|
74
|
+
],
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "Only One Channel Present",
|
|
79
|
+
channels: [1, 2, 3, 4, 5],
|
|
80
|
+
dataTypes: [
|
|
81
|
+
DataType.UINT8,
|
|
82
|
+
DataType.UINT8,
|
|
83
|
+
DataType.UINT8,
|
|
84
|
+
DataType.UINT8,
|
|
85
|
+
DataType.UINT8,
|
|
86
|
+
],
|
|
87
|
+
frame: new framer.Frame([3], [new Series(new Uint8Array([1, 2, 3, 4, 5]))]),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "All Same Time Range",
|
|
91
|
+
channels: [1, 2],
|
|
92
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32],
|
|
93
|
+
frame: new framer.Frame(
|
|
94
|
+
[1, 2],
|
|
95
|
+
[
|
|
96
|
+
new Series({
|
|
97
|
+
dataType: DataType.UINT8,
|
|
98
|
+
data: new Uint8Array([1]),
|
|
99
|
+
timeRange: new TimeStamp(0).spanRange(5),
|
|
100
|
+
}),
|
|
101
|
+
new Series({
|
|
102
|
+
dataType: DataType.FLOAT32,
|
|
103
|
+
data: new Float32Array([1, 2, 3, 4]),
|
|
104
|
+
timeRange: new TimeStamp(0).spanRange(5),
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "Different Time Ranges",
|
|
111
|
+
channels: [1, 2],
|
|
112
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32],
|
|
113
|
+
frame: new framer.Frame(
|
|
114
|
+
[1, 2],
|
|
115
|
+
[
|
|
116
|
+
new Series({
|
|
117
|
+
dataType: DataType.UINT8,
|
|
118
|
+
data: new Uint8Array([1]),
|
|
119
|
+
timeRange: new TimeStamp(0).spanRange(5),
|
|
120
|
+
}),
|
|
121
|
+
new Series({
|
|
122
|
+
dataType: DataType.FLOAT32,
|
|
123
|
+
data: new Float32Array([1, 2, 3, 4]),
|
|
124
|
+
timeRange: new TimeStamp(0).spanRange(5),
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "Partial Present, Different Lengths",
|
|
131
|
+
channels: [1, 2, 3],
|
|
132
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32, DataType.FLOAT64],
|
|
133
|
+
frame: new framer.Frame(
|
|
134
|
+
[1, 3],
|
|
135
|
+
[new Series(new Uint8Array([1])), new Series(new Float64Array([1, 2, 3, 4]))],
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "Same Alignments",
|
|
140
|
+
channels: [1, 2],
|
|
141
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32],
|
|
142
|
+
frame: new framer.Frame(
|
|
143
|
+
[1, 2],
|
|
144
|
+
[
|
|
145
|
+
new Series({
|
|
146
|
+
dataType: DataType.UINT8,
|
|
147
|
+
data: new Uint8Array([1]),
|
|
148
|
+
alignment: 5n,
|
|
149
|
+
}),
|
|
150
|
+
new Series({
|
|
151
|
+
dataType: DataType.FLOAT32,
|
|
152
|
+
data: new Uint8Array([1, 2, 3, 4]),
|
|
153
|
+
alignment: 5n,
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "Different Alignments",
|
|
160
|
+
channels: [1, 2],
|
|
161
|
+
dataTypes: [DataType.UINT8, DataType.FLOAT32],
|
|
162
|
+
frame: new Frame(
|
|
163
|
+
[1, 2],
|
|
164
|
+
[
|
|
165
|
+
new Series({
|
|
166
|
+
dataType: DataType.UINT8,
|
|
167
|
+
data: new Uint8Array([1]),
|
|
168
|
+
alignment: 5n,
|
|
169
|
+
}),
|
|
170
|
+
new Series({
|
|
171
|
+
dataType: DataType.FLOAT32,
|
|
172
|
+
data: new Uint8Array([1, 2, 3, 4]),
|
|
173
|
+
alignment: 10n,
|
|
174
|
+
}),
|
|
175
|
+
],
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "Variable Data Types",
|
|
180
|
+
channels: [1, 2, 3],
|
|
181
|
+
dataTypes: [DataType.UINT8, DataType.STRING, DataType.JSON],
|
|
182
|
+
frame: new framer.Frame(
|
|
183
|
+
[1, 2, 3],
|
|
184
|
+
[
|
|
185
|
+
new Series(new Uint8Array([1])),
|
|
186
|
+
new Series({
|
|
187
|
+
data: ["one", "two", "three"],
|
|
188
|
+
dataType: DataType.STRING,
|
|
189
|
+
}),
|
|
190
|
+
new Series({
|
|
191
|
+
data: [{ a: 1 }, { b: 2 }, { c: 3 }],
|
|
192
|
+
dataType: DataType.JSON,
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
SPECS.forEach((spec) => {
|
|
200
|
+
it(`should encode & decode ${spec.name}`, () => {
|
|
201
|
+
const codec = new Codec(spec.channels, spec.dataTypes);
|
|
202
|
+
const encoded = codec.encode(spec.frame.toPayload());
|
|
203
|
+
const decoded = codec.decode(encoded);
|
|
204
|
+
decoded.keys.forEach((k, i) => {
|
|
205
|
+
const dcs = decoded.series[i];
|
|
206
|
+
const ser = spec.frame.get(k);
|
|
207
|
+
expect(ser.series.length).toBeGreaterThan(0);
|
|
208
|
+
const os = ser.series[0];
|
|
209
|
+
if (dcs.timeRange != null && !dcs.timeRange.isZero)
|
|
210
|
+
expect(dcs.timeRange.toString()).toEqual(os.timeRange?.toString());
|
|
211
|
+
expect(new Series(dcs).toString()).toEqual(os.toString());
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("dynamic codec", () => {
|
|
218
|
+
it("should allow the caller to update the codec", () => {
|
|
219
|
+
const codec = new Codec();
|
|
220
|
+
codec.update([1], [DataType.INT32]);
|
|
221
|
+
const encoded = codec.encode(
|
|
222
|
+
new framer.Frame([1], [new Series(new Int32Array([1, 2, 3]))]),
|
|
223
|
+
);
|
|
224
|
+
const decoded1 = new Frame(codec.decode(encoded));
|
|
225
|
+
expect(Array.from(decoded1.series[0])).toEqual([1, 2, 3]);
|
|
226
|
+
expect(decoded1.keys[0]).toEqual(1);
|
|
227
|
+
codec.update([2], [DataType.INT64]);
|
|
228
|
+
const encoded2 = codec.encode(
|
|
229
|
+
new framer.Frame([2], [new Series(new BigInt64Array([1n, 2n, 3n]))]),
|
|
230
|
+
);
|
|
231
|
+
const decoded2 = new Frame(codec.decode(encoded2));
|
|
232
|
+
expect(Array.from(decoded2.series[0])).toEqual([1n, 2n, 3n]);
|
|
233
|
+
expect(decoded2.keys[0]).toEqual(2);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should throw an error if the codec is not initialized", () => {
|
|
237
|
+
const codec = new Codec();
|
|
238
|
+
expect(() =>
|
|
239
|
+
codec.encode(new framer.Frame([1], [new Series(new Int32Array([1, 2, 3]))])),
|
|
240
|
+
).toThrow(ValidationError);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should use the correct encode/decode state even if the codecs are out of sync", () => {
|
|
244
|
+
const encoder = new Codec();
|
|
245
|
+
const decoder = new Codec();
|
|
246
|
+
encoder.update([1], [DataType.INT32]);
|
|
247
|
+
decoder.update([1], [DataType.INT32]);
|
|
248
|
+
|
|
249
|
+
const fr = new framer.Frame([1], [new Series(new Int32Array([1, 2, 3]))]);
|
|
250
|
+
let encoded = encoder.encode(fr);
|
|
251
|
+
let decoded = new Frame(decoder.decode(encoded));
|
|
252
|
+
expect(decoded.keys[0]).toEqual(1);
|
|
253
|
+
expect(decoded.series[0].data).toEqual(fr.series[0].data);
|
|
254
|
+
|
|
255
|
+
decoder.update([2], [DataType.INT64]);
|
|
256
|
+
encoded = encoder.encode(fr);
|
|
257
|
+
decoded = new Frame(decoder.decode(encoded));
|
|
258
|
+
expect(decoded.keys[0]).toEqual(1);
|
|
259
|
+
expect(decoded.series[0].data).toEqual(fr.series[0].data);
|
|
260
|
+
|
|
261
|
+
encoder.update([2], [DataType.INT64]);
|
|
262
|
+
expect(() => encoder.encode(fr)).toThrow(ValidationError);
|
|
263
|
+
const fr2 = new framer.Frame([2], [new Series(new BigInt64Array([1n, 2n, 3n]))]);
|
|
264
|
+
encoded = encoder.encode(fr2);
|
|
265
|
+
decoded = new Frame(decoder.decode(encoded));
|
|
266
|
+
expect(decoded.keys[0]).toEqual(2);
|
|
267
|
+
expect(decoded.series[0].data).toEqual(fr2.series[0].data);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("websocket writer codec", () => {
|
|
272
|
+
it("should correctly e + d a websocket write request", () => {
|
|
273
|
+
const baseCodec = new Codec([1], [DataType.INT32]);
|
|
274
|
+
const fr = new framer.Frame([1], [new Series(new Int32Array([1, 2, 3]))]);
|
|
275
|
+
const writeReq: WriteRequest = {
|
|
276
|
+
command: WriterCommand.Write,
|
|
277
|
+
frame: fr.toPayload(),
|
|
278
|
+
};
|
|
279
|
+
const msg: WebsocketMessage<WriteRequest> = { type: "data", payload: writeReq };
|
|
280
|
+
const codec = new WSWriterCodec(baseCodec);
|
|
281
|
+
const encoded = codec.encode(msg);
|
|
282
|
+
expect(encoded[0]).toEqual(HIGH_PERF_SPECIAL_CHAR);
|
|
283
|
+
const decoded = codec.decode<WebsocketMessage<WriteRequest>>(encoded);
|
|
284
|
+
expect(decoded.payload?.command).toEqual(WriterCommand.Write);
|
|
285
|
+
const decodedFr = new Frame(decoded.payload?.frame);
|
|
286
|
+
expect(decodedFr.series[0].data).toEqual(fr.series[0].data);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should correctly e +d a WS write set authority request", () => {
|
|
290
|
+
const baseCodec = new Codec([1], [DataType.INT32]);
|
|
291
|
+
const writeReq: WriteRequest = {
|
|
292
|
+
command: WriterCommand.SetAuthority,
|
|
293
|
+
config: { authorities: [123] },
|
|
294
|
+
};
|
|
295
|
+
const codec = new WSWriterCodec(baseCodec);
|
|
296
|
+
const msg: WebsocketMessage<WriteRequest> = { type: "data", payload: writeReq };
|
|
297
|
+
const encoded = codec.encode(msg);
|
|
298
|
+
expect(encoded[0]).toEqual(LOW_PER_SPECIAL_CHAR);
|
|
299
|
+
const decoded = codec.decode(encoded);
|
|
300
|
+
expect(decoded).toEqual(msg);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Copyright 2024 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 WebsocketMessage } from "@synnaxlabs/freighter";
|
|
11
|
+
import {
|
|
12
|
+
binary,
|
|
13
|
+
DataType,
|
|
14
|
+
type SeriesPayload,
|
|
15
|
+
TimeRange,
|
|
16
|
+
TimeStamp,
|
|
17
|
+
} from "@synnaxlabs/x";
|
|
18
|
+
import { type ZodSchema } from "zod";
|
|
19
|
+
|
|
20
|
+
import { type channel } from "@/channel";
|
|
21
|
+
import { ValidationError } from "@/errors";
|
|
22
|
+
import { type Frame, type Payload } from "@/framer/frame";
|
|
23
|
+
import { type StreamerResponse } from "@/framer/streamer";
|
|
24
|
+
import { WriterCommand, type WriteRequest } from "@/framer/writer";
|
|
25
|
+
|
|
26
|
+
// For detailed information about the specifications,
|
|
27
|
+
// please refer to the official RFC 0016 document.
|
|
28
|
+
// Document here: docs/tech/rfc/0016-231001-frame-flight-protocol.md
|
|
29
|
+
|
|
30
|
+
const seriesPldLength = (series: SeriesPayload): number =>
|
|
31
|
+
series.data.byteLength / series.dataType.density.valueOf();
|
|
32
|
+
|
|
33
|
+
interface KeyedSeries extends SeriesPayload {
|
|
34
|
+
key: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sortFramePayloadByKey = (framePayload: Payload): void => {
|
|
38
|
+
const { keys, series } = framePayload;
|
|
39
|
+
keys.forEach((key, index) => {
|
|
40
|
+
(series[index] as KeyedSeries).key = key;
|
|
41
|
+
});
|
|
42
|
+
series.sort((a, b) => (a as KeyedSeries).key - (b as KeyedSeries).key);
|
|
43
|
+
keys.sort((a, b) => a - b);
|
|
44
|
+
// @ts-expect-error - deleting static property keys.
|
|
45
|
+
series.forEach((ser) => delete (ser as KeyedSeries).key);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ZERO_ALIGNMENTS_FLAG_POS = 5;
|
|
49
|
+
const EQUAL_ALIGNMENTS_FLAG_POS = 4;
|
|
50
|
+
const EQUAL_LENGTHS_FLAG_POS = 3;
|
|
51
|
+
const EQUAL_TIME_RANGES_FLAG_POS = 2;
|
|
52
|
+
const TIME_RANGES_ZERO_FLAG_POS = 1;
|
|
53
|
+
const ALL_CHANNELS_PRESENT_FLAG_POS = 0;
|
|
54
|
+
|
|
55
|
+
const TIMESTAMP_SIZE = DataType.TIMESTAMP.density.valueOf();
|
|
56
|
+
const ALIGNMENT_SIZE = 8;
|
|
57
|
+
const DATA_LENGTH_SIZE = 4;
|
|
58
|
+
const KEY_SIZE = 4;
|
|
59
|
+
const SEQ_NUM_SIZE = 4;
|
|
60
|
+
const FLAGS_SIZE = 1;
|
|
61
|
+
|
|
62
|
+
interface CodecState {
|
|
63
|
+
keys: channel.Keys;
|
|
64
|
+
keyDataTypes: Map<channel.Key, DataType>;
|
|
65
|
+
hasVariableDataTypes: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class Codec {
|
|
69
|
+
contentType: string = "application/sy-framer";
|
|
70
|
+
private states: Map<number, CodecState> = new Map();
|
|
71
|
+
private currState: CodecState | undefined;
|
|
72
|
+
private seqNum: number = 0;
|
|
73
|
+
|
|
74
|
+
constructor(keys: channel.Keys = [], dataTypes: DataType[] = []) {
|
|
75
|
+
if (keys.length > 0 || dataTypes.length > 0) this.update(keys, dataTypes);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
update(keys: channel.Keys, dataTypes: DataType[]): void {
|
|
79
|
+
this.seqNum++;
|
|
80
|
+
const state = {
|
|
81
|
+
keys,
|
|
82
|
+
keyDataTypes: new Map(),
|
|
83
|
+
hasVariableDataTypes: false,
|
|
84
|
+
};
|
|
85
|
+
keys.forEach((k, i) => {
|
|
86
|
+
const dt = dataTypes[i];
|
|
87
|
+
state.keyDataTypes.set(k, dt);
|
|
88
|
+
if (dt.isVariable) state.hasVariableDataTypes = true;
|
|
89
|
+
});
|
|
90
|
+
state.keys.sort();
|
|
91
|
+
this.states.set(this.seqNum, state);
|
|
92
|
+
this.currState = state;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private throwIfNotUpdated(op: string): void {
|
|
96
|
+
if (this.seqNum < 1)
|
|
97
|
+
throw new ValidationError(`
|
|
98
|
+
The codec has not been updated with a list of channels and data types.
|
|
99
|
+
Please call the update method before calling ${op}.
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
encode(payload: unknown, startOffset: number = 0): Uint8Array {
|
|
104
|
+
this.throwIfNotUpdated("encode");
|
|
105
|
+
let src = payload as Payload;
|
|
106
|
+
if (payload != null && typeof payload === "object" && "toPayload" in payload)
|
|
107
|
+
src = (payload as Frame).toPayload();
|
|
108
|
+
sortFramePayloadByKey(src);
|
|
109
|
+
let currDataSize = -1;
|
|
110
|
+
let startTime: TimeStamp | undefined;
|
|
111
|
+
let endTime: TimeStamp | undefined;
|
|
112
|
+
let currAlignment: bigint | undefined;
|
|
113
|
+
let byteArraySize = startOffset + FLAGS_SIZE + SEQ_NUM_SIZE;
|
|
114
|
+
let equalLengthsFlag = !this.currState?.hasVariableDataTypes;
|
|
115
|
+
let equalTimeRangesFlag = true;
|
|
116
|
+
let timeRangesZeroFlag = true;
|
|
117
|
+
let channelFlag = true;
|
|
118
|
+
let equalAlignmentsFlag = true;
|
|
119
|
+
let zeroAlignmentsFlag = true;
|
|
120
|
+
|
|
121
|
+
if (src.keys.length !== this.currState?.keys.length) {
|
|
122
|
+
channelFlag = false;
|
|
123
|
+
byteArraySize += src.keys.length * KEY_SIZE;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
src.series.forEach((series, i) => {
|
|
127
|
+
const pldLength = seriesPldLength(series);
|
|
128
|
+
const key = src.keys[i];
|
|
129
|
+
const dt = this.currState?.keyDataTypes.get(key);
|
|
130
|
+
if (dt == null)
|
|
131
|
+
throw new ValidationError(
|
|
132
|
+
`Channel ${key} was not provided in the list of channels when opening the writer`,
|
|
133
|
+
);
|
|
134
|
+
if (!dt.equals(series.dataType))
|
|
135
|
+
throw new ValidationError(
|
|
136
|
+
`Series data type of ${series.dataType.toString()} does not match the data type of ${dt.toString()} for channel ${key}`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
byteArraySize += series.data.byteLength;
|
|
140
|
+
if (currDataSize === -1) {
|
|
141
|
+
currDataSize = pldLength;
|
|
142
|
+
startTime = series.timeRange?.start;
|
|
143
|
+
endTime = series.timeRange?.end;
|
|
144
|
+
currAlignment = BigInt(series.alignment ?? 0n);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (currDataSize !== pldLength) equalLengthsFlag = false;
|
|
148
|
+
if (
|
|
149
|
+
startTime?.valueOf() !== series.timeRange?.start.valueOf() ||
|
|
150
|
+
endTime?.valueOf() !== series.timeRange?.end.valueOf()
|
|
151
|
+
)
|
|
152
|
+
equalTimeRangesFlag = false;
|
|
153
|
+
if (currAlignment !== BigInt(series.alignment ?? 0)) equalAlignmentsFlag = false;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
timeRangesZeroFlag = equalTimeRangesFlag && startTime == null && endTime == null;
|
|
157
|
+
|
|
158
|
+
zeroAlignmentsFlag =
|
|
159
|
+
equalAlignmentsFlag && (currAlignment === undefined || currAlignment === 0n);
|
|
160
|
+
|
|
161
|
+
if (equalLengthsFlag) byteArraySize += DATA_LENGTH_SIZE;
|
|
162
|
+
else byteArraySize += src.keys.length * DATA_LENGTH_SIZE;
|
|
163
|
+
|
|
164
|
+
if (!timeRangesZeroFlag)
|
|
165
|
+
if (equalTimeRangesFlag) byteArraySize += TIMESTAMP_SIZE * 2;
|
|
166
|
+
else byteArraySize += src.keys.length * TIMESTAMP_SIZE * 2;
|
|
167
|
+
|
|
168
|
+
if (!zeroAlignmentsFlag)
|
|
169
|
+
if (equalAlignmentsFlag) byteArraySize += ALIGNMENT_SIZE;
|
|
170
|
+
else byteArraySize += src.keys.length * ALIGNMENT_SIZE;
|
|
171
|
+
|
|
172
|
+
const buffer = new Uint8Array(byteArraySize);
|
|
173
|
+
const view = new DataView(buffer.buffer);
|
|
174
|
+
let offset = startOffset;
|
|
175
|
+
buffer[offset] =
|
|
176
|
+
(Number(zeroAlignmentsFlag) << ZERO_ALIGNMENTS_FLAG_POS) |
|
|
177
|
+
(Number(equalAlignmentsFlag) << EQUAL_ALIGNMENTS_FLAG_POS) |
|
|
178
|
+
(Number(equalLengthsFlag) << EQUAL_LENGTHS_FLAG_POS) |
|
|
179
|
+
(Number(equalTimeRangesFlag) << EQUAL_TIME_RANGES_FLAG_POS) |
|
|
180
|
+
(Number(timeRangesZeroFlag) << TIME_RANGES_ZERO_FLAG_POS) |
|
|
181
|
+
(Number(channelFlag) << ALL_CHANNELS_PRESENT_FLAG_POS);
|
|
182
|
+
offset++;
|
|
183
|
+
view.setUint32(offset, this.seqNum, true);
|
|
184
|
+
offset += SEQ_NUM_SIZE;
|
|
185
|
+
|
|
186
|
+
if (equalLengthsFlag) {
|
|
187
|
+
view.setUint32(offset, currDataSize, true);
|
|
188
|
+
offset += DATA_LENGTH_SIZE;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (equalTimeRangesFlag && !timeRangesZeroFlag) {
|
|
192
|
+
view.setBigUint64(offset, startTime?.valueOf() ?? 0n, true);
|
|
193
|
+
view.setBigUint64(offset, endTime?.valueOf() ?? 0n, true);
|
|
194
|
+
offset += TIMESTAMP_SIZE * 2;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (equalAlignmentsFlag && !zeroAlignmentsFlag) {
|
|
198
|
+
view.setBigUint64(offset, currAlignment ?? 0n, true);
|
|
199
|
+
offset += ALIGNMENT_SIZE;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
src.series.forEach((series, i) => {
|
|
203
|
+
if (!channelFlag) {
|
|
204
|
+
view.setUint32(offset, src.keys[i], true);
|
|
205
|
+
offset += KEY_SIZE;
|
|
206
|
+
}
|
|
207
|
+
if (!equalLengthsFlag) {
|
|
208
|
+
let seriesLengthOrSize = series.data.byteLength;
|
|
209
|
+
if (!series.dataType.isVariable) seriesLengthOrSize = seriesPldLength(series);
|
|
210
|
+
view.setUint32(offset, seriesLengthOrSize, true);
|
|
211
|
+
offset += DATA_LENGTH_SIZE;
|
|
212
|
+
}
|
|
213
|
+
buffer.set(new Uint8Array(series.data), offset);
|
|
214
|
+
offset += series.data.byteLength;
|
|
215
|
+
if (!equalTimeRangesFlag && !timeRangesZeroFlag) {
|
|
216
|
+
view.setBigUint64(offset, series.timeRange?.start.valueOf() ?? 0n, true);
|
|
217
|
+
view.setBigUint64(offset, series.timeRange?.end.valueOf() ?? 0n, true);
|
|
218
|
+
offset += TIMESTAMP_SIZE * 2;
|
|
219
|
+
}
|
|
220
|
+
if (!equalAlignmentsFlag && !zeroAlignmentsFlag) {
|
|
221
|
+
view.setBigUint64(offset, series.alignment ?? 0n, true);
|
|
222
|
+
offset += ALIGNMENT_SIZE;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return buffer;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
decode(data: Uint8Array | ArrayBuffer, offset: number = 0): Payload {
|
|
229
|
+
this.throwIfNotUpdated("decode");
|
|
230
|
+
const src = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
231
|
+
const returnFrame: Payload = { keys: [], series: [] };
|
|
232
|
+
let index = offset;
|
|
233
|
+
let sizeRepresentation = 0;
|
|
234
|
+
let currSize = 0;
|
|
235
|
+
let startTime: TimeStamp | undefined;
|
|
236
|
+
let endTime: TimeStamp | undefined;
|
|
237
|
+
let currAlignment: bigint | undefined;
|
|
238
|
+
|
|
239
|
+
const view = new DataView(src.buffer, src.byteOffset, src.byteLength);
|
|
240
|
+
const zeroAlignmentsFlag = Boolean((src[index] >> ZERO_ALIGNMENTS_FLAG_POS) & 1);
|
|
241
|
+
const equalAlignmentsFlag = Boolean((src[index] >> EQUAL_ALIGNMENTS_FLAG_POS) & 1);
|
|
242
|
+
const sizeFlag = Boolean((src[index] >> EQUAL_LENGTHS_FLAG_POS) & 1);
|
|
243
|
+
const equalTimeRangesFlag = Boolean((src[index] >> EQUAL_TIME_RANGES_FLAG_POS) & 1);
|
|
244
|
+
const timeRangesZeroFlag = Boolean((src[index] >> TIME_RANGES_ZERO_FLAG_POS) & 1);
|
|
245
|
+
const channelFlag = Boolean((src[index] >> ALL_CHANNELS_PRESENT_FLAG_POS) & 1);
|
|
246
|
+
index++;
|
|
247
|
+
|
|
248
|
+
const seqNum = view.getUint32(index, true);
|
|
249
|
+
index += SEQ_NUM_SIZE;
|
|
250
|
+
const state = this.states.get(seqNum);
|
|
251
|
+
if (state == null) return returnFrame;
|
|
252
|
+
|
|
253
|
+
if (sizeFlag) {
|
|
254
|
+
if (index + DATA_LENGTH_SIZE > view.byteLength) return returnFrame;
|
|
255
|
+
sizeRepresentation = view.getUint32(index, true);
|
|
256
|
+
index += DATA_LENGTH_SIZE;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (equalTimeRangesFlag && !timeRangesZeroFlag) {
|
|
260
|
+
if (index + TIMESTAMP_SIZE > view.byteLength) return returnFrame;
|
|
261
|
+
startTime = new TimeStamp(view.getBigUint64(index, true));
|
|
262
|
+
index += TIMESTAMP_SIZE;
|
|
263
|
+
endTime = new TimeStamp(view.getBigUint64(index, true));
|
|
264
|
+
index += TIMESTAMP_SIZE;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (equalAlignmentsFlag && !zeroAlignmentsFlag) {
|
|
268
|
+
if (index + ALIGNMENT_SIZE > view.byteLength) return returnFrame;
|
|
269
|
+
currAlignment = view.getBigUint64(index, true);
|
|
270
|
+
index += ALIGNMENT_SIZE;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (channelFlag) returnFrame.keys = [...state.keys];
|
|
274
|
+
state.keys.forEach((k, i) => {
|
|
275
|
+
if (!channelFlag) {
|
|
276
|
+
if (index >= view.byteLength) return;
|
|
277
|
+
const frameKey = view.getUint32(index, true);
|
|
278
|
+
if (frameKey !== k) return;
|
|
279
|
+
index += KEY_SIZE;
|
|
280
|
+
returnFrame.keys.push(k);
|
|
281
|
+
}
|
|
282
|
+
const dataType = state.keyDataTypes.get(k) as DataType;
|
|
283
|
+
currSize = 0;
|
|
284
|
+
if (!sizeFlag) {
|
|
285
|
+
if (index + DATA_LENGTH_SIZE > view.byteLength) return;
|
|
286
|
+
currSize = view.getUint32(index, true);
|
|
287
|
+
index += DATA_LENGTH_SIZE;
|
|
288
|
+
} else currSize = sizeRepresentation;
|
|
289
|
+
|
|
290
|
+
let dataByteLength = currSize;
|
|
291
|
+
if (!dataType.isVariable) dataByteLength *= dataType.density.valueOf();
|
|
292
|
+
if (index + dataByteLength > view.byteLength) {
|
|
293
|
+
returnFrame.keys.splice(i, 1);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const currSeries: SeriesPayload = {
|
|
297
|
+
dataType,
|
|
298
|
+
data: src.slice(index, index + dataByteLength).buffer,
|
|
299
|
+
};
|
|
300
|
+
index += dataByteLength;
|
|
301
|
+
if (!equalTimeRangesFlag && !timeRangesZeroFlag) {
|
|
302
|
+
if (index + TIMESTAMP_SIZE * 2 > view.byteLength) return;
|
|
303
|
+
const start = view.getBigUint64(index, true);
|
|
304
|
+
index += TIMESTAMP_SIZE;
|
|
305
|
+
const end = view.getBigUint64(index, true);
|
|
306
|
+
index += TIMESTAMP_SIZE;
|
|
307
|
+
currSeries.timeRange = new TimeRange({ start, end });
|
|
308
|
+
} else if (!timeRangesZeroFlag)
|
|
309
|
+
currSeries.timeRange = new TimeRange({
|
|
310
|
+
start: startTime?.valueOf() ?? 0n,
|
|
311
|
+
end: endTime?.valueOf() ?? 0n,
|
|
312
|
+
});
|
|
313
|
+
else currSeries.timeRange = new TimeRange({ start: 0n, end: 0n });
|
|
314
|
+
|
|
315
|
+
if (!equalAlignmentsFlag && !zeroAlignmentsFlag) {
|
|
316
|
+
if (index + ALIGNMENT_SIZE > view.byteLength) return;
|
|
317
|
+
currAlignment = view.getBigUint64(index, true);
|
|
318
|
+
index += ALIGNMENT_SIZE;
|
|
319
|
+
currSeries.alignment = currAlignment;
|
|
320
|
+
} else if (!zeroAlignmentsFlag) currSeries.alignment = currAlignment;
|
|
321
|
+
else currSeries.alignment = 0n;
|
|
322
|
+
|
|
323
|
+
returnFrame.series.push(currSeries);
|
|
324
|
+
});
|
|
325
|
+
return returnFrame;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const LOW_PER_SPECIAL_CHAR = 254;
|
|
330
|
+
const LOW_PERF_SPECIAL_CHAR_BUF = new Uint8Array([LOW_PER_SPECIAL_CHAR]);
|
|
331
|
+
export const HIGH_PERF_SPECIAL_CHAR = 255;
|
|
332
|
+
const HIGH_PERF_SPECIAL_CHAR_BUF = new Uint8Array([HIGH_PERF_SPECIAL_CHAR]);
|
|
333
|
+
const CONTENT_TYPE = "application/sy-framer";
|
|
334
|
+
|
|
335
|
+
export class WSWriterCodec implements binary.Codec {
|
|
336
|
+
contentType = CONTENT_TYPE;
|
|
337
|
+
private base: Codec;
|
|
338
|
+
private lowPerfCodec: binary.Codec;
|
|
339
|
+
|
|
340
|
+
constructor(base: Codec) {
|
|
341
|
+
this.base = base;
|
|
342
|
+
this.lowPerfCodec = binary.JSON_CODEC;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
encode(payload: unknown): Uint8Array {
|
|
346
|
+
const pld = payload as WebsocketMessage<WriteRequest>;
|
|
347
|
+
if (pld.type == "close" || pld.payload?.command != WriterCommand.Write) {
|
|
348
|
+
const data = this.lowPerfCodec.encode(pld);
|
|
349
|
+
const b = new Uint8Array({ length: data.byteLength + 1 });
|
|
350
|
+
b.set(LOW_PERF_SPECIAL_CHAR_BUF, 0);
|
|
351
|
+
b.set(new Uint8Array(data), 1);
|
|
352
|
+
return b;
|
|
353
|
+
}
|
|
354
|
+
const data = this.base.encode(pld.payload?.frame, 1);
|
|
355
|
+
data.set(HIGH_PERF_SPECIAL_CHAR_BUF, 0);
|
|
356
|
+
return data;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
decode<P>(data: Uint8Array | ArrayBuffer, schema?: ZodSchema<P>): P {
|
|
360
|
+
const dv = new DataView(data instanceof Uint8Array ? data.buffer : data);
|
|
361
|
+
const codec = dv.getUint8(0);
|
|
362
|
+
if (codec === LOW_PER_SPECIAL_CHAR)
|
|
363
|
+
return this.lowPerfCodec.decode(data.slice(1), schema);
|
|
364
|
+
const v: WebsocketMessage<WriteRequest> = { type: "data" };
|
|
365
|
+
const frame = this.base.decode(data, 1);
|
|
366
|
+
v.payload = { command: WriterCommand.Write, frame };
|
|
367
|
+
return v as P;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export class WSStreamerCodec implements binary.Codec {
|
|
372
|
+
contentType = CONTENT_TYPE;
|
|
373
|
+
private base: Codec;
|
|
374
|
+
private lowPerfCodec: binary.Codec;
|
|
375
|
+
|
|
376
|
+
constructor(base: Codec) {
|
|
377
|
+
this.base = base;
|
|
378
|
+
this.lowPerfCodec = binary.JSON_CODEC;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
encode(payload: unknown): Uint8Array {
|
|
382
|
+
return this.lowPerfCodec.encode(payload);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
decode<P>(data: Uint8Array | ArrayBuffer, schema?: ZodSchema<P>): P {
|
|
386
|
+
const dv = new DataView(data instanceof Uint8Array ? data.buffer : data);
|
|
387
|
+
const codec = dv.getUint8(0);
|
|
388
|
+
if (codec === LOW_PER_SPECIAL_CHAR)
|
|
389
|
+
return this.lowPerfCodec.decode(data.slice(1), schema);
|
|
390
|
+
const v: WebsocketMessage<StreamerResponse> = {
|
|
391
|
+
type: "data",
|
|
392
|
+
payload: { frame: this.base.decode(data, 1) },
|
|
393
|
+
};
|
|
394
|
+
return v as P;
|
|
395
|
+
}
|
|
396
|
+
}
|