@synnaxlabs/client 0.47.0 → 0.48.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 +7 -7
- package/dist/client.cjs +35 -35
- package/dist/client.js +4567 -4479
- package/dist/eslint.config.d.ts +3 -2
- package/dist/eslint.config.d.ts.map +1 -1
- package/dist/src/channel/payload.d.ts +1 -0
- package/dist/src/channel/payload.d.ts.map +1 -1
- package/dist/src/client.d.ts +9 -5
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/connection/checker.d.ts +2 -3
- package/dist/src/connection/checker.d.ts.map +1 -1
- package/dist/src/connection.spec.d.ts +2 -0
- package/dist/src/connection.spec.d.ts.map +1 -0
- package/dist/src/framer/adapter.d.ts +2 -2
- package/dist/src/framer/adapter.d.ts.map +1 -1
- package/dist/src/framer/frame.d.ts +1 -0
- package/dist/src/framer/frame.d.ts.map +1 -1
- package/dist/src/framer/streamer.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/testutil/client.d.ts +3 -3
- package/dist/src/testutil/client.d.ts.map +1 -1
- package/eslint.config.ts +3 -1
- package/package.json +8 -8
- package/src/auth/auth.spec.ts +13 -13
- package/src/channel/payload.ts +2 -1
- package/src/client.ts +24 -10
- package/src/connection/checker.ts +10 -10
- package/src/connection/connection.spec.ts +13 -13
- package/src/connection.spec.ts +145 -0
- package/src/framer/adapter.spec.ts +339 -1
- package/src/framer/adapter.ts +23 -13
- package/src/framer/frame.spec.ts +296 -0
- package/src/framer/frame.ts +20 -1
- package/src/framer/iterator.ts +1 -1
- package/src/framer/streamer.spec.ts +57 -0
- package/src/framer/streamer.ts +6 -3
- package/src/index.ts +9 -2
- package/src/testutil/client.ts +4 -4
|
@@ -0,0 +1,145 @@
|
|
|
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 { checkConnection, newConnectionChecker } from "@/client";
|
|
14
|
+
import { TEST_CLIENT_PARAMS } from "@/testutil/client";
|
|
15
|
+
|
|
16
|
+
describe("checkConnection", () => {
|
|
17
|
+
it("should check connection to the server", async () => {
|
|
18
|
+
const state = await checkConnection({
|
|
19
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
20
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
21
|
+
secure: false,
|
|
22
|
+
});
|
|
23
|
+
expect(state.status).toEqual("connected");
|
|
24
|
+
expect(z.uuid().safeParse(state.clusterKey).success).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should include client version in the connection check", async () => {
|
|
28
|
+
const state = await checkConnection({
|
|
29
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
30
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
31
|
+
secure: false,
|
|
32
|
+
});
|
|
33
|
+
expect(state.clientVersion).toBeDefined();
|
|
34
|
+
expect(state.clientServerCompatible).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should support custom name parameter", async () => {
|
|
38
|
+
const state = await checkConnection({
|
|
39
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
40
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
41
|
+
secure: false,
|
|
42
|
+
name: "test-client",
|
|
43
|
+
});
|
|
44
|
+
expect(state.status).toEqual("connected");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should handle connection failure to invalid host", async () => {
|
|
48
|
+
const state = await checkConnection({
|
|
49
|
+
host: "invalid-host-that-does-not-exist",
|
|
50
|
+
port: 9999,
|
|
51
|
+
secure: false,
|
|
52
|
+
retry: {
|
|
53
|
+
maxRetries: 0, // Disable retries for faster test
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
expect(state.status).toEqual("failed");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle connection failure to invalid port", async () => {
|
|
60
|
+
const state = await checkConnection({
|
|
61
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
62
|
+
port: 9999, // Wrong port
|
|
63
|
+
secure: false,
|
|
64
|
+
retry: {
|
|
65
|
+
maxRetries: 0, // Disable retries for faster test
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
expect(state.status).toEqual("failed");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("newConnectionChecker", () => {
|
|
73
|
+
it("should create a connection checker", () => {
|
|
74
|
+
const checker = newConnectionChecker({
|
|
75
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
76
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
77
|
+
secure: false,
|
|
78
|
+
});
|
|
79
|
+
expect(checker).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should create a checker that can check connection", async () => {
|
|
83
|
+
const checker = newConnectionChecker({
|
|
84
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
85
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
86
|
+
secure: false,
|
|
87
|
+
});
|
|
88
|
+
const state = await checker.check();
|
|
89
|
+
expect(state.status).toEqual("connected");
|
|
90
|
+
expect(z.uuid().safeParse(state.clusterKey).success).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should support custom name parameter", async () => {
|
|
94
|
+
const checker = newConnectionChecker({
|
|
95
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
96
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
97
|
+
secure: false,
|
|
98
|
+
name: "custom-checker-name",
|
|
99
|
+
});
|
|
100
|
+
const state = await checker.check();
|
|
101
|
+
expect(state.status).toEqual("connected");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should support secure connection parameter", () => {
|
|
105
|
+
const checker = newConnectionChecker({
|
|
106
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
107
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
108
|
+
secure: true,
|
|
109
|
+
});
|
|
110
|
+
expect(checker).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should create multiple independent checkers", async () => {
|
|
114
|
+
const checker1 = newConnectionChecker({
|
|
115
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
116
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
117
|
+
secure: false,
|
|
118
|
+
name: "checker-1",
|
|
119
|
+
});
|
|
120
|
+
const checker2 = newConnectionChecker({
|
|
121
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
122
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
123
|
+
secure: false,
|
|
124
|
+
name: "checker-2",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const state1 = await checker1.check();
|
|
128
|
+
const state2 = await checker2.check();
|
|
129
|
+
|
|
130
|
+
expect(state1.status).toEqual("connected");
|
|
131
|
+
expect(state2.status).toEqual("connected");
|
|
132
|
+
expect(checker1).not.toBe(checker2); // Different instances
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle version compatibility checking", async () => {
|
|
136
|
+
const checker = newConnectionChecker({
|
|
137
|
+
host: TEST_CLIENT_PARAMS.host,
|
|
138
|
+
port: TEST_CLIENT_PARAMS.port,
|
|
139
|
+
secure: false,
|
|
140
|
+
});
|
|
141
|
+
const state = await checker.check();
|
|
142
|
+
expect(state.clientVersion).toBeDefined();
|
|
143
|
+
expect(state.clientServerCompatible).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -213,9 +213,10 @@ describe("WriteFrameAdapter", () => {
|
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
describe("
|
|
216
|
+
describe("ReadFrameAdapter", () => {
|
|
217
217
|
let timeCh: channel.Channel;
|
|
218
218
|
let dataCh: channel.Channel;
|
|
219
|
+
let extraCh: channel.Channel;
|
|
219
220
|
let adapter: ReadAdapter;
|
|
220
221
|
|
|
221
222
|
beforeAll(async () => {
|
|
@@ -229,6 +230,11 @@ describe("ReadAdapter", () => {
|
|
|
229
230
|
dataType: DataType.FLOAT32,
|
|
230
231
|
index: timeCh.key,
|
|
231
232
|
});
|
|
233
|
+
extraCh = await client.channels.create({
|
|
234
|
+
name: `read-extra-${Math.random()}-${TimeStamp.now().toString()}`,
|
|
235
|
+
dataType: DataType.FLOAT64,
|
|
236
|
+
index: timeCh.key,
|
|
237
|
+
});
|
|
232
238
|
|
|
233
239
|
adapter = await ReadAdapter.open(client.channels.retriever, [
|
|
234
240
|
timeCh.key,
|
|
@@ -236,6 +242,338 @@ describe("ReadAdapter", () => {
|
|
|
236
242
|
]);
|
|
237
243
|
});
|
|
238
244
|
|
|
245
|
+
describe("adapt", () => {
|
|
246
|
+
describe("with keys (no conversion)", () => {
|
|
247
|
+
describe("hot path - exact channel match", () => {
|
|
248
|
+
it("should return frame unchanged when all channels match", () => {
|
|
249
|
+
// HOT PATH: Frame has exactly the channels registered with adapter
|
|
250
|
+
const ts = TimeStamp.now().valueOf();
|
|
251
|
+
const inputFrame = new Frame({
|
|
252
|
+
[timeCh.key]: new Series([ts]),
|
|
253
|
+
[dataCh.key]: new Series([1.5]),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = adapter.adapt(inputFrame);
|
|
257
|
+
|
|
258
|
+
// Frame should be returned unchanged (zero allocations)
|
|
259
|
+
expect(result).toBe(inputFrame); // Same object reference
|
|
260
|
+
expect(result.columns).toHaveLength(2);
|
|
261
|
+
expect(result.has(timeCh.key)).toBe(true);
|
|
262
|
+
expect(result.has(dataCh.key)).toBe(true);
|
|
263
|
+
expect(result.get(timeCh.key).at(0)).toEqual(ts);
|
|
264
|
+
expect(result.get(dataCh.key).at(0)).toEqual(1.5);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should preserve series data types in hot path", () => {
|
|
268
|
+
const ts = TimeStamp.now().valueOf();
|
|
269
|
+
const inputFrame = new Frame({
|
|
270
|
+
[timeCh.key]: new Series({ data: [ts], dataType: DataType.TIMESTAMP }),
|
|
271
|
+
[dataCh.key]: new Series({ data: [1.5], dataType: DataType.FLOAT32 }),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = adapter.adapt(inputFrame);
|
|
275
|
+
|
|
276
|
+
// Data types should be preserved
|
|
277
|
+
expect(result.get(timeCh.key).dataType).toEqual(DataType.TIMESTAMP);
|
|
278
|
+
expect(result.get(dataCh.key).dataType).toEqual(DataType.FLOAT32);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("cold path - filtering needed", () => {
|
|
283
|
+
it("should filter out extra channels in key mode", () => {
|
|
284
|
+
// COLD PATH: Frame has extra channels not in adapter
|
|
285
|
+
const ts = TimeStamp.now().valueOf();
|
|
286
|
+
const inputFrame = new Frame({
|
|
287
|
+
[timeCh.key]: new Series([ts]),
|
|
288
|
+
[dataCh.key]: new Series([1.5]),
|
|
289
|
+
[extraCh.key]: new Series([999.0]), // Extra channel
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const result = adapter.adapt(inputFrame);
|
|
293
|
+
|
|
294
|
+
// Should filter out extraCh
|
|
295
|
+
expect(result).not.toBe(inputFrame); // Different object (filtered)
|
|
296
|
+
expect(result.columns).toHaveLength(2);
|
|
297
|
+
expect(result.has(timeCh.key)).toBe(true);
|
|
298
|
+
expect(result.has(dataCh.key)).toBe(true);
|
|
299
|
+
expect(result.has(extraCh.key)).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should handle partial matches in key mode", () => {
|
|
303
|
+
// Frame has some matching and some extra channels
|
|
304
|
+
const ts = TimeStamp.now().valueOf();
|
|
305
|
+
const inputFrame = new Frame({
|
|
306
|
+
[timeCh.key]: new Series([ts]),
|
|
307
|
+
[extraCh.key]: new Series([999.0]),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const result = adapter.adapt(inputFrame);
|
|
311
|
+
|
|
312
|
+
expect(result.columns).toHaveLength(1);
|
|
313
|
+
expect(result.has(timeCh.key)).toBe(true);
|
|
314
|
+
expect(result.has(extraCh.key)).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should return empty frame when no channels match in key mode", () => {
|
|
318
|
+
const inputFrame = new Frame({
|
|
319
|
+
[extraCh.key]: new Series([999.0]),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const result = adapter.adapt(inputFrame);
|
|
323
|
+
|
|
324
|
+
expect(result.columns).toHaveLength(0);
|
|
325
|
+
expect(result.series).toHaveLength(0);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("with names (conversion)", () => {
|
|
331
|
+
let nameAdapter: ReadAdapter;
|
|
332
|
+
|
|
333
|
+
beforeAll(async () => {
|
|
334
|
+
// Create adapter with channel names (triggers key-to-name mapping)
|
|
335
|
+
nameAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
336
|
+
timeCh.name,
|
|
337
|
+
dataCh.name,
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("hot path - exact match, only convert", () => {
|
|
342
|
+
it("should convert channel keys to names when all channels match", () => {
|
|
343
|
+
// HOT PATH: Frame has exactly the channels in adapter
|
|
344
|
+
const ts = TimeStamp.now().valueOf();
|
|
345
|
+
const inputFrame = new Frame({
|
|
346
|
+
[timeCh.key]: new Series([ts]),
|
|
347
|
+
[dataCh.key]: new Series([2.5]),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const result = nameAdapter.adapt(inputFrame);
|
|
351
|
+
|
|
352
|
+
// Output should have names instead of keys (one allocation for conversion)
|
|
353
|
+
expect(result.columns).toHaveLength(2);
|
|
354
|
+
expect(result.has(timeCh.name)).toBe(true);
|
|
355
|
+
expect(result.has(dataCh.name)).toBe(true);
|
|
356
|
+
expect(result.get(timeCh.name).at(0)).toEqual(ts);
|
|
357
|
+
expect(result.get(dataCh.name).at(0)).toEqual(2.5);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should handle multiple values in hot path", () => {
|
|
361
|
+
const ts = TimeStamp.now().valueOf();
|
|
362
|
+
const inputFrame = new Frame({
|
|
363
|
+
[timeCh.key]: new Series([ts, ts + 1000n]),
|
|
364
|
+
[dataCh.key]: new Series([1.0, 2.0]),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const result = nameAdapter.adapt(inputFrame);
|
|
368
|
+
|
|
369
|
+
expect(result.columns).toHaveLength(2);
|
|
370
|
+
expect(result.get(timeCh.name)).toHaveLength(2);
|
|
371
|
+
expect(result.get(dataCh.name)).toHaveLength(2);
|
|
372
|
+
expect(result.get(timeCh.name).at(0)).toEqual(ts);
|
|
373
|
+
expect(result.get(timeCh.name).at(1)).toEqual(ts + 1000n);
|
|
374
|
+
expect(result.get(dataCh.name).at(0)).toEqual(1.0);
|
|
375
|
+
expect(result.get(dataCh.name).at(1)).toEqual(2.0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should preserve data types during name conversion", () => {
|
|
379
|
+
const ts = TimeStamp.now().valueOf();
|
|
380
|
+
const inputFrame = new Frame({
|
|
381
|
+
[timeCh.key]: new Series({ data: [ts], dataType: DataType.TIMESTAMP }),
|
|
382
|
+
[dataCh.key]: new Series({ data: [3.5], dataType: DataType.FLOAT32 }),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const result = nameAdapter.adapt(inputFrame);
|
|
386
|
+
|
|
387
|
+
expect(result.get(timeCh.name).dataType).toEqual(DataType.TIMESTAMP);
|
|
388
|
+
expect(result.get(dataCh.name).dataType).toEqual(DataType.FLOAT32);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("cold path - filter and convert", () => {
|
|
393
|
+
it("should filter out extra channels while converting", async () => {
|
|
394
|
+
// COLD PATH: Frame has extra channels that need filtering
|
|
395
|
+
const ts = TimeStamp.now().valueOf();
|
|
396
|
+
const inputFrame = new Frame({
|
|
397
|
+
[timeCh.key]: new Series([ts]),
|
|
398
|
+
[dataCh.key]: new Series([1.5]),
|
|
399
|
+
[extraCh.key]: new Series([999.0]), // Extra channel
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const result = nameAdapter.adapt(inputFrame);
|
|
403
|
+
|
|
404
|
+
// Should filter extraCh and convert remaining keys to names
|
|
405
|
+
expect(result.columns).toHaveLength(2);
|
|
406
|
+
expect(result.has(timeCh.name)).toBe(true);
|
|
407
|
+
expect(result.has(dataCh.name)).toBe(true);
|
|
408
|
+
expect(result.has(extraCh.key)).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should handle partial matches while converting", async () => {
|
|
412
|
+
const filterAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
413
|
+
timeCh.name,
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
const ts = TimeStamp.now().valueOf();
|
|
417
|
+
const inputFrame = new Frame({
|
|
418
|
+
[timeCh.key]: new Series([ts]),
|
|
419
|
+
[extraCh.key]: new Series([999.0]),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const result = filterAdapter.adapt(inputFrame);
|
|
423
|
+
|
|
424
|
+
expect(result.columns).toHaveLength(1);
|
|
425
|
+
expect(result.has(timeCh.name)).toBe(true);
|
|
426
|
+
expect(result.has(extraCh.key)).toBe(false);
|
|
427
|
+
expect(result.get(timeCh.name).at(0)).toEqual(ts);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should return empty frame when no channels match", async () => {
|
|
431
|
+
const filterAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
432
|
+
timeCh.name,
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
const inputFrame = new Frame({
|
|
436
|
+
[extraCh.key]: new Series([999.0]),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const result = filterAdapter.adapt(inputFrame);
|
|
440
|
+
|
|
441
|
+
expect(result.columns).toHaveLength(0);
|
|
442
|
+
expect(result.series).toHaveLength(0);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("edge cases", () => {
|
|
448
|
+
it("should handle empty frames", () => {
|
|
449
|
+
const inputFrame = new Frame({});
|
|
450
|
+
|
|
451
|
+
const result = adapter.adapt(inputFrame);
|
|
452
|
+
|
|
453
|
+
expect(result.columns).toHaveLength(0);
|
|
454
|
+
expect(result.series).toHaveLength(0);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should handle frames with empty series", () => {
|
|
458
|
+
const inputFrame = new Frame({
|
|
459
|
+
[timeCh.key]: new Series({ data: [], dataType: DataType.TIMESTAMP }),
|
|
460
|
+
[dataCh.key]: new Series({ data: [], dataType: DataType.FLOAT32 }),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const result = adapter.adapt(inputFrame);
|
|
464
|
+
|
|
465
|
+
expect(result.columns).toHaveLength(2);
|
|
466
|
+
expect(result.get(timeCh.key)).toHaveLength(0);
|
|
467
|
+
expect(result.get(dataCh.key)).toHaveLength(0);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe("data integrity", () => {
|
|
472
|
+
it("should preserve series values across multiple data types", async () => {
|
|
473
|
+
const int64Ch = await client.channels.create({
|
|
474
|
+
name: `read-int64-${Math.random()}-${TimeStamp.now().toString()}`,
|
|
475
|
+
dataType: DataType.INT64,
|
|
476
|
+
index: timeCh.key,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const testAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
480
|
+
timeCh.key,
|
|
481
|
+
dataCh.key,
|
|
482
|
+
int64Ch.key,
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
const ts = TimeStamp.now().valueOf();
|
|
486
|
+
const inputFrame = new Frame({
|
|
487
|
+
[timeCh.key]: new Series([ts, ts + 1000n]),
|
|
488
|
+
[dataCh.key]: new Series([1.5, 2.5]),
|
|
489
|
+
[int64Ch.key]: new Series([100n, 200n]),
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = testAdapter.adapt(inputFrame);
|
|
493
|
+
|
|
494
|
+
// Verify all values preserved
|
|
495
|
+
expect(result.get(timeCh.key).at(0)).toEqual(ts);
|
|
496
|
+
expect(result.get(timeCh.key).at(1)).toEqual(ts + 1000n);
|
|
497
|
+
expect(result.get(dataCh.key).at(0)).toEqual(1.5);
|
|
498
|
+
expect(result.get(dataCh.key).at(1)).toEqual(2.5);
|
|
499
|
+
expect(result.get(int64Ch.key).at(0)).toEqual(100n);
|
|
500
|
+
expect(result.get(int64Ch.key).at(1)).toEqual(200n);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("should preserve series lengths after filtering", () => {
|
|
504
|
+
const ts = TimeStamp.now().valueOf();
|
|
505
|
+
const inputFrame = new Frame({
|
|
506
|
+
[timeCh.key]: new Series([ts, ts + 1000n, ts + 2000n]),
|
|
507
|
+
[dataCh.key]: new Series([1.0, 2.0, 3.0]),
|
|
508
|
+
[extraCh.key]: new Series([999.0, 888.0, 777.0]),
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const result = adapter.adapt(inputFrame);
|
|
512
|
+
|
|
513
|
+
// Lengths should be preserved for included channels
|
|
514
|
+
expect(result.get(timeCh.key)).toHaveLength(3);
|
|
515
|
+
expect(result.get(dataCh.key)).toHaveLength(3);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("should preserve series order", () => {
|
|
519
|
+
const ts = TimeStamp.now().valueOf();
|
|
520
|
+
// Create frame with explicit column order
|
|
521
|
+
const inputFrame = new Frame(
|
|
522
|
+
[dataCh.key, timeCh.key],
|
|
523
|
+
[new Series([1.0, 2.0, 3.0]), new Series([ts, ts + 1000n, ts + 2000n])],
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const result = adapter.adapt(inputFrame);
|
|
527
|
+
|
|
528
|
+
// Order should be preserved (dataCh first, then timeCh)
|
|
529
|
+
expect(result.columns[0]).toEqual(dataCh.key);
|
|
530
|
+
expect(result.columns[1]).toEqual(timeCh.key);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe("state management", () => {
|
|
535
|
+
it("should handle multiple sequential updates correctly", async () => {
|
|
536
|
+
// Start with NAME mode to enable filtering
|
|
537
|
+
const newAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
538
|
+
timeCh.name,
|
|
539
|
+
]);
|
|
540
|
+
|
|
541
|
+
// Initial state: only timeCh registered
|
|
542
|
+
const ts = TimeStamp.now().valueOf();
|
|
543
|
+
const inputFrame = new Frame({
|
|
544
|
+
[timeCh.key]: new Series([ts]),
|
|
545
|
+
[dataCh.key]: new Series([1.5]),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Should filter out dataCh and convert timeCh key to name
|
|
549
|
+
let result = newAdapter.adapt(inputFrame);
|
|
550
|
+
expect(result.columns).toHaveLength(1);
|
|
551
|
+
expect(result.has(timeCh.name)).toBe(true);
|
|
552
|
+
expect(result.has(dataCh.name)).toBe(false);
|
|
553
|
+
|
|
554
|
+
// Update to include dataCh
|
|
555
|
+
await newAdapter.update([timeCh.name, dataCh.name]);
|
|
556
|
+
|
|
557
|
+
// Should now include both channels (converted to names)
|
|
558
|
+
result = newAdapter.adapt(inputFrame);
|
|
559
|
+
expect(result.columns).toHaveLength(2);
|
|
560
|
+
expect(result.has(timeCh.name)).toBe(true);
|
|
561
|
+
expect(result.has(dataCh.name)).toBe(true);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe("codec integration", () => {
|
|
566
|
+
it("should update codec when channels change", async () => {
|
|
567
|
+
const codecAdapter = await ReadAdapter.open(client.channels.retriever, [
|
|
568
|
+
timeCh.key,
|
|
569
|
+
]);
|
|
570
|
+
expect(codecAdapter.keys).toHaveLength(1);
|
|
571
|
+
await codecAdapter.update([timeCh.key, dataCh.key]);
|
|
572
|
+
expect(codecAdapter.keys).toHaveLength(2);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
239
577
|
describe("update", () => {
|
|
240
578
|
it("should return false when updating with the same channels", async () => {
|
|
241
579
|
const hasChanged = await adapter.update([timeCh.key, dataCh.key]);
|
package/src/framer/adapter.ts
CHANGED
|
@@ -17,13 +17,13 @@ import { type CrudeFrame, Frame } from "@/framer/frame";
|
|
|
17
17
|
export class ReadAdapter {
|
|
18
18
|
private adapter: Map<channel.Key, channel.Name> | null;
|
|
19
19
|
retriever: channel.Retriever;
|
|
20
|
-
keys: channel.Key
|
|
20
|
+
keys: Set<channel.Key>;
|
|
21
21
|
codec: Codec;
|
|
22
22
|
|
|
23
23
|
private constructor(retriever: channel.Retriever) {
|
|
24
24
|
this.retriever = retriever;
|
|
25
25
|
this.adapter = null;
|
|
26
|
-
this.keys =
|
|
26
|
+
this.keys = new Set();
|
|
27
27
|
this.codec = new Codec();
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -40,7 +40,10 @@ export class ReadAdapter {
|
|
|
40
40
|
const { variant, normalized } = channel.analyzeParams(channels);
|
|
41
41
|
const fetched = await this.retriever.retrieve(normalized);
|
|
42
42
|
const newKeys = fetched.map((c) => c.key);
|
|
43
|
-
if (
|
|
43
|
+
if (
|
|
44
|
+
compare.uniqueUnorderedPrimitiveArrays(Array.from(this.keys), newKeys) ===
|
|
45
|
+
compare.EQUAL
|
|
46
|
+
)
|
|
44
47
|
return false;
|
|
45
48
|
this.codec.update(
|
|
46
49
|
newKeys,
|
|
@@ -48,7 +51,7 @@ export class ReadAdapter {
|
|
|
48
51
|
);
|
|
49
52
|
if (variant === "keys") {
|
|
50
53
|
this.adapter = null;
|
|
51
|
-
this.keys = normalized as channel.Key[];
|
|
54
|
+
this.keys = new Set(normalized as channel.Key[]);
|
|
52
55
|
return true;
|
|
53
56
|
}
|
|
54
57
|
const a = new Map<channel.Key, channel.Name>();
|
|
@@ -58,20 +61,27 @@ export class ReadAdapter {
|
|
|
58
61
|
if (channel == null) throw new Error(`Channel ${name} not found`);
|
|
59
62
|
a.set(channel.key, channel.name);
|
|
60
63
|
});
|
|
61
|
-
this.keys =
|
|
64
|
+
this.keys = new Set(this.adapter.keys());
|
|
62
65
|
return true;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
adapt(
|
|
66
|
-
if (this.adapter == null)
|
|
68
|
+
adapt(frm: Frame): Frame {
|
|
69
|
+
if (this.adapter == null) {
|
|
70
|
+
let shouldFilter = false;
|
|
71
|
+
frm.forEach((k) => {
|
|
72
|
+
if (!this.keys.has(k as channel.Key)) shouldFilter = true;
|
|
73
|
+
});
|
|
74
|
+
if (shouldFilter) return frm.filter((k) => this.keys.has(k as channel.Key));
|
|
75
|
+
return frm;
|
|
76
|
+
}
|
|
67
77
|
const a = this.adapter;
|
|
68
|
-
return
|
|
69
|
-
if (typeof
|
|
70
|
-
const name = a.get(
|
|
71
|
-
if (name == null)
|
|
72
|
-
return [name, arr];
|
|
78
|
+
return frm.mapFilter((col, arr) => {
|
|
79
|
+
if (typeof col === "number") {
|
|
80
|
+
const name = a.get(col);
|
|
81
|
+
if (name == null) return [col, arr, false];
|
|
82
|
+
return [name, arr, true];
|
|
73
83
|
}
|
|
74
|
-
return [
|
|
84
|
+
return [col, arr, true];
|
|
75
85
|
});
|
|
76
86
|
}
|
|
77
87
|
}
|