@synnaxlabs/client 0.42.3 → 0.43.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/.vscode/settings.json +2 -2
- package/CONTRIBUTING.md +6 -5
- package/README.md +7 -8
- package/dist/access/payload.d.ts +1 -1
- package/dist/access/payload.d.ts.map +1 -1
- package/dist/access/policy/payload.d.ts +9 -9
- package/dist/access/policy/payload.d.ts.map +1 -1
- package/dist/access/policy/retriever.d.ts +3 -3
- package/dist/access/policy/retriever.d.ts.map +1 -1
- package/dist/auth/auth.d.ts +2 -2
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/channel/client.d.ts +1 -0
- package/dist/channel/client.d.ts.map +1 -1
- package/dist/channel/payload.d.ts +21 -8
- package/dist/channel/payload.d.ts.map +1 -1
- package/dist/channel/retriever.d.ts +5 -5
- package/dist/channel/retriever.d.ts.map +1 -1
- package/dist/channel/writer.d.ts +3 -3
- package/dist/channel/writer.d.ts.map +1 -1
- package/dist/client.cjs +135 -39
- package/dist/client.d.ts +8 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28505 -9345
- package/dist/connection/checker.d.ts +5 -5
- package/dist/connection/checker.d.ts.map +1 -1
- package/dist/control/state.d.ts +46 -3
- package/dist/control/state.d.ts.map +1 -1
- package/dist/framer/adapter.d.ts +2 -2
- package/dist/framer/adapter.d.ts.map +1 -1
- package/dist/framer/client.d.ts +2 -0
- package/dist/framer/client.d.ts.map +1 -1
- package/dist/framer/codec.d.ts +3 -3
- package/dist/framer/codec.d.ts.map +1 -1
- package/dist/framer/deleter.d.ts +8 -8
- package/dist/framer/deleter.d.ts.map +1 -1
- package/dist/framer/frame.d.ts +17 -17
- package/dist/framer/frame.d.ts.map +1 -1
- package/dist/framer/streamProxy.d.ts +3 -3
- package/dist/framer/streamProxy.d.ts.map +1 -1
- package/dist/framer/streamer.d.ts +103 -22
- package/dist/framer/streamer.d.ts.map +1 -1
- package/dist/framer/writer.d.ts +25 -25
- package/dist/framer/writer.d.ts.map +1 -1
- package/dist/hardware/device/client.d.ts +3 -3
- package/dist/hardware/device/client.d.ts.map +1 -1
- package/dist/hardware/device/payload.d.ts +65 -18
- package/dist/hardware/device/payload.d.ts.map +1 -1
- package/dist/hardware/rack/client.d.ts.map +1 -1
- package/dist/hardware/rack/payload.d.ts +87 -30
- package/dist/hardware/rack/payload.d.ts.map +1 -1
- package/dist/hardware/task/client.d.ts +3 -3
- package/dist/hardware/task/client.d.ts.map +1 -1
- package/dist/hardware/task/payload.d.ts +14 -15
- package/dist/hardware/task/payload.d.ts.map +1 -1
- package/dist/label/payload.d.ts +2 -2
- package/dist/label/payload.d.ts.map +1 -1
- package/dist/label/writer.d.ts +4 -4
- package/dist/label/writer.d.ts.map +1 -1
- package/dist/ontology/client.d.ts +3 -3
- package/dist/ontology/client.d.ts.map +1 -1
- package/dist/ontology/group/payload.d.ts +2 -2
- package/dist/ontology/group/payload.d.ts.map +1 -1
- package/dist/ontology/payload.d.ts +25 -25
- package/dist/ontology/payload.d.ts.map +1 -1
- package/dist/ranger/client.d.ts +8 -8
- package/dist/ranger/client.d.ts.map +1 -1
- package/dist/ranger/kv.d.ts +6 -6
- package/dist/ranger/kv.d.ts.map +1 -1
- package/dist/ranger/payload.d.ts +15 -15
- package/dist/ranger/payload.d.ts.map +1 -1
- package/dist/ranger/writer.d.ts +10 -10
- package/dist/ranger/writer.d.ts.map +1 -1
- package/dist/testutil/{indexedPair.d.ts → channels.d.ts} +1 -1
- package/dist/testutil/channels.d.ts.map +1 -0
- package/dist/user/payload.d.ts +3 -3
- package/dist/user/payload.d.ts.map +1 -1
- package/dist/user/retriever.d.ts +2 -2
- package/dist/user/retriever.d.ts.map +1 -1
- package/dist/util/retrieve.d.ts +6 -6
- package/dist/util/retrieve.d.ts.map +1 -1
- package/dist/util/zod.d.ts +2 -2
- package/dist/util/zod.d.ts.map +1 -1
- package/dist/workspace/client.d.ts.map +1 -1
- package/dist/workspace/lineplot/client.d.ts.map +1 -1
- package/dist/workspace/lineplot/lineplot.spec.d.ts +2 -0
- package/dist/workspace/lineplot/lineplot.spec.d.ts.map +1 -0
- package/dist/workspace/lineplot/payload.d.ts +5 -5
- package/dist/workspace/lineplot/payload.d.ts.map +1 -1
- package/dist/workspace/log/client.d.ts.map +1 -1
- package/dist/workspace/log/payload.d.ts +5 -5
- package/dist/workspace/log/payload.d.ts.map +1 -1
- package/dist/workspace/payload.d.ts +6 -6
- package/dist/workspace/payload.d.ts.map +1 -1
- package/dist/workspace/schematic/client.d.ts.map +1 -1
- package/dist/workspace/schematic/payload.d.ts +7 -7
- package/dist/workspace/schematic/payload.d.ts.map +1 -1
- package/dist/workspace/table/client.d.ts.map +1 -1
- package/dist/workspace/table/payload.d.ts +6 -6
- package/dist/workspace/table/payload.d.ts.map +1 -1
- package/package.json +11 -12
- package/src/access/payload.ts +1 -1
- package/src/access/policy/client.ts +3 -3
- package/src/access/policy/payload.ts +1 -1
- package/src/access/policy/retriever.ts +1 -1
- package/src/access/policy/writer.ts +7 -7
- package/src/auth/auth.ts +1 -1
- package/src/channel/client.ts +6 -4
- package/src/channel/payload.ts +10 -18
- package/src/channel/retriever.ts +2 -2
- package/src/channel/writer.ts +11 -2
- package/src/client.ts +3 -3
- package/src/connection/checker.ts +1 -1
- package/src/connection/connection.spec.ts +1 -1
- package/src/control/client.ts +1 -1
- package/src/control/state.ts +4 -5
- package/src/errors.spec.ts +2 -3
- package/src/errors.ts +2 -2
- package/src/framer/adapter.ts +2 -2
- package/src/framer/client.ts +4 -3
- package/src/framer/codec.spec.ts +2 -2
- package/src/framer/codec.ts +5 -9
- package/src/framer/deleter.spec.ts +1 -1
- package/src/framer/deleter.ts +1 -1
- package/src/framer/frame.ts +15 -15
- package/src/framer/iterator.spec.ts +1 -1
- package/src/framer/iterator.ts +1 -1
- package/src/framer/streamProxy.ts +4 -4
- package/src/framer/streamer.spec.ts +420 -215
- package/src/framer/streamer.ts +119 -21
- package/src/framer/writer.spec.ts +1 -1
- package/src/framer/writer.ts +15 -8
- package/src/hardware/device/client.ts +5 -5
- package/src/hardware/device/device.spec.ts +28 -30
- package/src/hardware/device/payload.ts +5 -5
- package/src/hardware/rack/client.ts +4 -4
- package/src/hardware/rack/payload.ts +6 -6
- package/src/hardware/rack/rack.spec.ts +1 -1
- package/src/hardware/task/client.ts +21 -19
- package/src/hardware/task/payload.ts +8 -6
- package/src/label/payload.ts +1 -1
- package/src/label/retriever.ts +3 -3
- package/src/label/writer.ts +4 -4
- package/src/ontology/client.ts +4 -4
- package/src/ontology/group/payload.ts +3 -3
- package/src/ontology/group/writer.ts +1 -1
- package/src/ontology/payload.ts +2 -2
- package/src/ontology/writer.ts +1 -1
- package/src/ranger/alias.ts +1 -1
- package/src/ranger/client.ts +6 -4
- package/src/ranger/kv.ts +4 -4
- package/src/ranger/payload.ts +3 -3
- package/src/ranger/writer.ts +1 -1
- package/src/user/client.ts +3 -3
- package/src/user/payload.ts +1 -1
- package/src/user/retriever.ts +1 -1
- package/src/user/writer.ts +4 -4
- package/src/util/retrieve.spec.ts +7 -4
- package/src/util/retrieve.ts +10 -10
- package/src/util/zod.ts +3 -3
- package/src/workspace/client.ts +5 -5
- package/src/workspace/lineplot/client.ts +5 -5
- package/src/workspace/lineplot/{linePlot.spec.ts → lineplot.spec.ts} +2 -2
- package/src/workspace/lineplot/payload.ts +1 -1
- package/src/workspace/log/client.ts +5 -5
- package/src/workspace/log/log.spec.ts +2 -2
- package/src/workspace/log/payload.ts +1 -1
- package/src/workspace/payload.ts +1 -1
- package/src/workspace/schematic/client.ts +5 -5
- package/src/workspace/schematic/payload.ts +1 -1
- package/src/workspace/schematic/schematic.spec.ts +3 -3
- package/src/workspace/table/client.ts +5 -5
- package/src/workspace/table/payload.ts +1 -1
- package/src/workspace/table/table.spec.ts +2 -2
- package/src/workspace/workspace.spec.ts +2 -2
- package/tsconfig.json +3 -5
- package/dist/testutil/indexedPair.d.ts.map +0 -1
- package/dist/workspace/lineplot/linePlot.spec.d.ts +0 -2
- package/dist/workspace/lineplot/linePlot.spec.d.ts.map +0 -1
- /package/src/testutil/{indexedPair.ts → channels.ts} +0 -0
|
@@ -7,257 +7,462 @@
|
|
|
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 {
|
|
11
|
-
import {
|
|
10
|
+
import { EOF, Unreachable } from "@synnaxlabs/freighter";
|
|
11
|
+
import { DataType, Series, TimeSpan, TimeStamp } from "@synnaxlabs/x/telem";
|
|
12
|
+
import { describe, expect, it, test, vi } from "vitest";
|
|
12
13
|
|
|
14
|
+
import { type channel } from "@/channel";
|
|
15
|
+
import { Frame } from "@/framer/frame";
|
|
16
|
+
import {
|
|
17
|
+
HardenedStreamer,
|
|
18
|
+
parseStreamerConfig,
|
|
19
|
+
type Streamer,
|
|
20
|
+
} from "@/framer/streamer";
|
|
13
21
|
import { newClient } from "@/setupspecs";
|
|
14
|
-
import { newVirtualChannel } from "@/testutil/
|
|
22
|
+
import { newVirtualChannel } from "@/testutil/channels";
|
|
15
23
|
|
|
16
24
|
const client = newClient();
|
|
17
25
|
|
|
18
26
|
describe("Streamer", () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
describe("standard", () => {
|
|
28
|
+
test("happy path", async () => {
|
|
29
|
+
const ch = await newVirtualChannel(client);
|
|
30
|
+
const streamer = await client.openStreamer(ch.key);
|
|
31
|
+
const writer = await client.openWriter({
|
|
32
|
+
start: TimeStamp.now(),
|
|
33
|
+
channels: ch.key,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
await writer.write(ch.key, new Float64Array([1, 2, 3]));
|
|
37
|
+
} finally {
|
|
38
|
+
await writer.close();
|
|
39
|
+
}
|
|
40
|
+
const d = await streamer.read();
|
|
41
|
+
expect(Array.from(d.get(ch.key))).toEqual([1, 2, 3]);
|
|
25
42
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await writer.close();
|
|
30
|
-
}
|
|
31
|
-
const d = await streamer.read();
|
|
32
|
-
expect(Array.from(d.get(ch.key))).toEqual([1, 2, 3]);
|
|
33
|
-
});
|
|
34
|
-
test("open with config", async () => {
|
|
35
|
-
const ch = await newVirtualChannel(client);
|
|
36
|
-
await expect(client.openStreamer({ channels: ch.key })).resolves.not.toThrow();
|
|
37
|
-
});
|
|
38
|
-
it("should not throw an error when the streamer is opened with zero channels", async () => {
|
|
39
|
-
await expect(client.openStreamer([])).resolves.not.toThrow();
|
|
40
|
-
});
|
|
41
|
-
it("should throw an error when the streamer is opened with a channel that does not exist", async () => {
|
|
42
|
-
await expect(client.openStreamer([5678])).rejects.toThrow("not found");
|
|
43
|
-
});
|
|
44
|
-
test("downsample factor of 1", async () => {
|
|
45
|
-
const ch = await newVirtualChannel(client);
|
|
46
|
-
const streamer = await client.openStreamer({
|
|
47
|
-
channels: ch.key,
|
|
48
|
-
downSampleFactor: 1,
|
|
43
|
+
test("open with config", async () => {
|
|
44
|
+
const ch = await newVirtualChannel(client);
|
|
45
|
+
await expect(client.openStreamer({ channels: ch.key })).resolves.not.toThrow();
|
|
49
46
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
channels: ch.key,
|
|
47
|
+
it("should not throw an error when the streamer is opened with zero channels", async () => {
|
|
48
|
+
await expect(client.openStreamer([])).resolves.not.toThrow();
|
|
53
49
|
});
|
|
54
|
-
|
|
55
|
-
await
|
|
56
|
-
} finally {
|
|
57
|
-
await writer.close();
|
|
58
|
-
}
|
|
59
|
-
const d = await streamer.read();
|
|
60
|
-
expect(Array.from(d.get(ch.key))).toEqual([1, 2, 3, 4, 5]);
|
|
61
|
-
});
|
|
62
|
-
test("downsample factor of 2", async () => {
|
|
63
|
-
const ch = await newVirtualChannel(client);
|
|
64
|
-
const streamer = await client.openStreamer({
|
|
65
|
-
channels: ch.key,
|
|
66
|
-
downSampleFactor: 2,
|
|
50
|
+
it("should throw an error when the streamer is opened with a channel that does not exist", async () => {
|
|
51
|
+
await expect(client.openStreamer([5678])).rejects.toThrow("not found");
|
|
67
52
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
53
|
+
describe("downsampling", () => {
|
|
54
|
+
test("downsample factor of 1", async () => {
|
|
55
|
+
const ch = await newVirtualChannel(client);
|
|
56
|
+
const streamer = await client.openStreamer({
|
|
57
|
+
channels: ch.key,
|
|
58
|
+
downsampleFactor: 1,
|
|
59
|
+
});
|
|
60
|
+
const writer = await client.openWriter({
|
|
61
|
+
start: TimeStamp.now(),
|
|
62
|
+
channels: ch.key,
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5]));
|
|
66
|
+
} finally {
|
|
67
|
+
await writer.close();
|
|
68
|
+
}
|
|
69
|
+
const d = await streamer.read();
|
|
70
|
+
expect(Array.from(d.get(ch.key))).toEqual([1, 2, 3, 4, 5]);
|
|
71
|
+
});
|
|
72
|
+
test("downsample factor of 2", async () => {
|
|
73
|
+
const ch = await newVirtualChannel(client);
|
|
74
|
+
const streamer = await client.openStreamer({
|
|
75
|
+
channels: ch.key,
|
|
76
|
+
downsampleFactor: 2,
|
|
77
|
+
});
|
|
78
|
+
const writer = await client.openWriter({
|
|
79
|
+
start: TimeStamp.now(),
|
|
80
|
+
channels: ch.key,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
|
|
84
|
+
} finally {
|
|
85
|
+
await writer.close();
|
|
86
|
+
}
|
|
87
|
+
const d = await streamer.read();
|
|
88
|
+
expect(Array.from(d.get(ch.key))).toEqual([1, 3, 5, 7, 9]);
|
|
89
|
+
});
|
|
90
|
+
test("downsample factor of 10", async () => {
|
|
91
|
+
const ch = await newVirtualChannel(client);
|
|
92
|
+
const streamer = await client.openStreamer({
|
|
93
|
+
channels: ch.key,
|
|
94
|
+
downsampleFactor: 10,
|
|
95
|
+
});
|
|
96
|
+
const writer = await client.openWriter({
|
|
97
|
+
start: TimeStamp.now(),
|
|
98
|
+
channels: ch.key,
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
|
|
102
|
+
} finally {
|
|
103
|
+
await writer.close();
|
|
104
|
+
}
|
|
105
|
+
const d = await streamer.read();
|
|
106
|
+
expect(Array.from(d.get(ch.key))).toEqual([1]);
|
|
107
|
+
});
|
|
89
108
|
});
|
|
90
|
-
try {
|
|
91
|
-
await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
|
|
92
|
-
} finally {
|
|
93
|
-
await writer.close();
|
|
94
|
-
}
|
|
95
|
-
const d = await streamer.read();
|
|
96
|
-
expect(Array.from(d.get(ch.key))).toEqual([1]);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
109
|
|
|
100
|
-
describe("
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
describe("calculations", () => {
|
|
111
|
+
test("basic calculated channel streaming", async () => {
|
|
112
|
+
// Create a timestamp index channel
|
|
113
|
+
const timeChannel = await client.channels.create({
|
|
114
|
+
name: "calc_test_time",
|
|
115
|
+
isIndex: true,
|
|
116
|
+
dataType: DataType.TIMESTAMP,
|
|
117
|
+
});
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Create calculated channel that adds the two source channels
|
|
124
|
-
const calcChannel = await client.channels.create({
|
|
125
|
-
name: "test_calc",
|
|
126
|
-
dataType: DataType.FLOAT64,
|
|
127
|
-
index: timeChannel.key,
|
|
128
|
-
virtual: true,
|
|
129
|
-
expression: "return test_a + test_b",
|
|
130
|
-
requires: [channelA.key, channelB.key],
|
|
131
|
-
});
|
|
119
|
+
// Create source channels with the timestamp index
|
|
120
|
+
const [channelA, channelB] = await client.channels.create([
|
|
121
|
+
{
|
|
122
|
+
name: "test_a",
|
|
123
|
+
dataType: DataType.FLOAT64,
|
|
124
|
+
index: timeChannel.key,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "test_b",
|
|
128
|
+
dataType: DataType.FLOAT64,
|
|
129
|
+
index: timeChannel.key,
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// Create calculated channel that adds the two source channels
|
|
134
|
+
const calcChannel = await client.channels.create({
|
|
135
|
+
name: "test_calc",
|
|
136
|
+
dataType: DataType.FLOAT64,
|
|
137
|
+
index: timeChannel.key,
|
|
138
|
+
virtual: true,
|
|
139
|
+
expression: "return test_a + test_b",
|
|
140
|
+
requires: [channelA.key, channelB.key],
|
|
141
|
+
});
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
// Set up streamer to listen for calculated results
|
|
144
|
+
const streamer = await client.openStreamer(calcChannel.key);
|
|
145
|
+
|
|
146
|
+
// Write test data
|
|
147
|
+
const startTime = TimeStamp.now();
|
|
148
|
+
const writer = await client.openWriter({
|
|
149
|
+
start: startTime,
|
|
150
|
+
channels: [timeChannel.key, channelA.key, channelB.key],
|
|
151
|
+
});
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
try {
|
|
154
|
+
// Write test values - each source gets 2.5 so sum should be 5.0
|
|
155
|
+
await writer.write({
|
|
156
|
+
[timeChannel.key]: [startTime],
|
|
157
|
+
[channelA.key]: new Float64Array([2.5]),
|
|
158
|
+
[channelB.key]: new Float64Array([2.5]),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Read from streamer
|
|
162
|
+
const frame = await streamer.read();
|
|
163
|
+
|
|
164
|
+
// Verify calculated results
|
|
165
|
+
const calcData = Array.from(frame.get(calcChannel.key));
|
|
166
|
+
expect(calcData).toEqual([5.0]);
|
|
167
|
+
} finally {
|
|
168
|
+
await writer.close();
|
|
169
|
+
streamer.close();
|
|
170
|
+
}
|
|
149
171
|
});
|
|
172
|
+
test("calculated channel with constant", async () => {
|
|
173
|
+
// Create an index channel for timestamps
|
|
174
|
+
const timeChannel = await client.channels.create({
|
|
175
|
+
name: "calc_const_time",
|
|
176
|
+
isIndex: true,
|
|
177
|
+
dataType: DataType.TIMESTAMP,
|
|
178
|
+
});
|
|
150
179
|
|
|
151
|
-
|
|
152
|
-
|
|
180
|
+
// Create base channel with index
|
|
181
|
+
const baseChannel = await client.channels.create({
|
|
182
|
+
name: "base_channel",
|
|
183
|
+
dataType: DataType.FLOAT64,
|
|
184
|
+
index: timeChannel.key,
|
|
185
|
+
});
|
|
153
186
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Create an index channel for timestamps
|
|
164
|
-
const timeChannel = await client.channels.create({
|
|
165
|
-
name: "calc_const_time",
|
|
166
|
-
isIndex: true,
|
|
167
|
-
dataType: DataType.TIMESTAMP,
|
|
168
|
-
});
|
|
187
|
+
// Create calculated channel that adds 5
|
|
188
|
+
const calcChannel = await client.channels.create({
|
|
189
|
+
name: "calc_const_channel",
|
|
190
|
+
dataType: DataType.FLOAT64,
|
|
191
|
+
index: timeChannel.key,
|
|
192
|
+
virtual: true,
|
|
193
|
+
expression: `return ${baseChannel.name} + 5`,
|
|
194
|
+
requires: [baseChannel.key],
|
|
195
|
+
});
|
|
169
196
|
|
|
170
|
-
|
|
171
|
-
const baseChannel = await client.channels.create({
|
|
172
|
-
name: "base_channel",
|
|
173
|
-
dataType: DataType.FLOAT64,
|
|
174
|
-
index: timeChannel.key,
|
|
175
|
-
});
|
|
197
|
+
const streamer = await client.openStreamer(calcChannel.key);
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
virtual: true,
|
|
183
|
-
expression: `return ${baseChannel.name} + 5`,
|
|
184
|
-
requires: [baseChannel.key],
|
|
185
|
-
});
|
|
199
|
+
const startTime = TimeStamp.now();
|
|
200
|
+
const writer = await client.openWriter({
|
|
201
|
+
start: startTime,
|
|
202
|
+
channels: [timeChannel.key, baseChannel.key],
|
|
203
|
+
});
|
|
186
204
|
|
|
187
|
-
|
|
205
|
+
try {
|
|
206
|
+
const timestamps = [
|
|
207
|
+
startTime,
|
|
208
|
+
new TimeStamp(startTime.valueOf() + BigInt(1000000000)),
|
|
209
|
+
new TimeStamp(startTime.valueOf() + BigInt(2000000000)),
|
|
210
|
+
];
|
|
188
211
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
212
|
+
await writer.write({
|
|
213
|
+
[timeChannel.key]: timestamps,
|
|
214
|
+
[baseChannel.key]: new Float64Array([1, 2, 3]),
|
|
215
|
+
});
|
|
194
216
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
217
|
+
const frame = await streamer.read();
|
|
218
|
+
const calcData = Array.from(frame.get(calcChannel.key));
|
|
219
|
+
expect(calcData).toEqual([6, 7, 8]); // Original values + 5
|
|
220
|
+
} finally {
|
|
221
|
+
await writer.close();
|
|
222
|
+
streamer.close();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("calculated channel with multiple operations", async () => {
|
|
227
|
+
// Create timestamp channel
|
|
228
|
+
const timeChannel = await client.channels.create({
|
|
229
|
+
name: "calc_multi_time",
|
|
230
|
+
isIndex: true,
|
|
231
|
+
dataType: DataType.TIMESTAMP,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Create source channels
|
|
235
|
+
const [channelA, channelB] = await client.channels.create([
|
|
236
|
+
{ name: "multi_test_a", dataType: DataType.FLOAT64, index: timeChannel.key },
|
|
237
|
+
{ name: "multi_test_b", dataType: DataType.FLOAT64, index: timeChannel.key },
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
// Create calculated channel with multiple operations
|
|
241
|
+
const calcChannel = await client.channels.create({
|
|
242
|
+
name: "multi_calc",
|
|
243
|
+
dataType: DataType.FLOAT64,
|
|
244
|
+
index: timeChannel.key,
|
|
245
|
+
virtual: true,
|
|
246
|
+
expression: "return (multi_test_a * 2) + (multi_test_b / 2)",
|
|
247
|
+
requires: [channelA.key, channelB.key],
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const streamer = await client.openStreamer(calcChannel.key);
|
|
201
251
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
252
|
+
const startTime = TimeStamp.now();
|
|
253
|
+
const writer = await client.openWriter({
|
|
254
|
+
start: startTime,
|
|
255
|
+
channels: [timeChannel.key, channelA.key, channelB.key],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await writer.write({
|
|
260
|
+
[timeChannel.key]: [startTime],
|
|
261
|
+
[channelA.key]: new Float64Array([2.0]), // Will be multiplied by 2 = 4.0
|
|
262
|
+
[channelB.key]: new Float64Array([4.0]), // Will be divided by 2 = 2.0
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const frame = await streamer.read();
|
|
266
|
+
const calcData = Array.from(frame.get(calcChannel.key));
|
|
267
|
+
expect(calcData).toEqual([6.0]); // (2.0 * 2) + (4.0 / 2) = 4.0 + 2.0 = 6.0
|
|
268
|
+
} finally {
|
|
269
|
+
await writer.close();
|
|
270
|
+
streamer.close();
|
|
271
|
+
}
|
|
205
272
|
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("hardened", () => {
|
|
277
|
+
class MockStreamer implements Streamer {
|
|
278
|
+
keys: channel.Key[] = [];
|
|
279
|
+
updateMock = vi.fn();
|
|
280
|
+
readMock = vi.fn();
|
|
281
|
+
closeMock = vi.fn();
|
|
282
|
+
responses: [Frame, Error | null][] = [];
|
|
283
|
+
updateErrors: (Error | null)[] = [];
|
|
284
|
+
|
|
285
|
+
update(channels: channel.Params): Promise<void> {
|
|
286
|
+
if (this.updateErrors.length > 0) {
|
|
287
|
+
const err = this.updateErrors.shift()!;
|
|
288
|
+
if (err) throw err;
|
|
289
|
+
}
|
|
290
|
+
this.updateMock(channels);
|
|
291
|
+
return Promise.resolve();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
close(): void {
|
|
295
|
+
this.closeMock();
|
|
296
|
+
}
|
|
206
297
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
298
|
+
async read(): Promise<Frame> {
|
|
299
|
+
this.readMock();
|
|
300
|
+
if (this.responses.length === 0) throw new EOF();
|
|
301
|
+
const [frame, err] = this.responses.shift()!;
|
|
302
|
+
if (err) throw err;
|
|
303
|
+
return frame;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async next(): Promise<IteratorResult<Frame, any>> {
|
|
307
|
+
const fr = await this.read();
|
|
308
|
+
return { done: false, value: fr };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
[Symbol.asyncIterator](): AsyncIterator<Frame, any, undefined> {
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
213
314
|
}
|
|
214
|
-
});
|
|
215
315
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
316
|
+
it("should correctly call the underlying streamer methods", async () => {
|
|
317
|
+
const streamer = new MockStreamer();
|
|
318
|
+
const openMock = vi.fn();
|
|
319
|
+
const config = { channels: [1, 2, 3] };
|
|
320
|
+
const fr = new Frame({ 1: new Series([1]) });
|
|
321
|
+
const hardened = await HardenedStreamer.open(
|
|
322
|
+
async (cfg) => {
|
|
323
|
+
openMock(cfg);
|
|
324
|
+
const cfg_ = parseStreamerConfig(cfg);
|
|
325
|
+
streamer.responses = [[fr, null]];
|
|
326
|
+
streamer.keys = cfg_.channels as channel.Key[];
|
|
327
|
+
return streamer;
|
|
328
|
+
},
|
|
329
|
+
{ channels: [1, 2, 3] },
|
|
330
|
+
);
|
|
331
|
+
expect(hardened.keys).toEqual([1, 2, 3]);
|
|
332
|
+
expect(openMock).toHaveBeenCalledWith(config);
|
|
333
|
+
await hardened.update([1, 2, 3]);
|
|
334
|
+
expect(streamer.updateMock).toHaveBeenCalledWith([1, 2, 3]);
|
|
335
|
+
const fr2 = await hardened.read();
|
|
336
|
+
expect(streamer.readMock).toHaveBeenCalled();
|
|
337
|
+
expect(fr2).toEqual(fr);
|
|
338
|
+
hardened.close();
|
|
339
|
+
expect(streamer.closeMock).toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should correctly iterate over the streamer", async () => {
|
|
343
|
+
const streamer = new MockStreamer();
|
|
344
|
+
const fr = new Frame({ 1: new Series([1]) });
|
|
345
|
+
const fr2 = new Frame({ 1: new Series([2]) });
|
|
346
|
+
streamer.responses = [
|
|
347
|
+
[fr, null],
|
|
348
|
+
[fr2, null],
|
|
349
|
+
];
|
|
350
|
+
const hardened = await HardenedStreamer.open(async () => streamer, {
|
|
351
|
+
channels: [1],
|
|
352
|
+
});
|
|
353
|
+
const first = await hardened.next();
|
|
354
|
+
expect(first.value).toEqual(fr);
|
|
355
|
+
const second = await hardened.next();
|
|
356
|
+
expect(second.value).toEqual(fr2);
|
|
357
|
+
const third = await hardened.next();
|
|
358
|
+
expect(third.done).toBe(true);
|
|
359
|
+
expect(streamer.readMock).toHaveBeenCalledTimes(3);
|
|
222
360
|
});
|
|
223
361
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
362
|
+
it("should try to re-open the streamer when read fails", async () => {
|
|
363
|
+
const streamer1 = new MockStreamer();
|
|
364
|
+
const streamer2 = new MockStreamer();
|
|
365
|
+
const fr1 = new Frame({ 1: new Series([1]) });
|
|
366
|
+
const fr2 = new Frame({ 1: new Series([2]) });
|
|
367
|
+
streamer1.responses = [
|
|
368
|
+
[fr1, null],
|
|
369
|
+
[fr2, new Unreachable({ message: "cat" })],
|
|
370
|
+
];
|
|
371
|
+
streamer2.responses = [[fr2, null]];
|
|
372
|
+
let count = 0;
|
|
373
|
+
const openerMock = vi.fn();
|
|
374
|
+
const hardened = await HardenedStreamer.open(
|
|
375
|
+
async () => {
|
|
376
|
+
count++;
|
|
377
|
+
openerMock();
|
|
378
|
+
if (count === 1) return streamer1;
|
|
379
|
+
return streamer2;
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
channels: [1],
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
const fr = await hardened.read();
|
|
386
|
+
expect(streamer1.readMock).toHaveBeenCalledTimes(1);
|
|
387
|
+
expect(fr).toEqual(fr1);
|
|
388
|
+
const fr3 = await hardened.read();
|
|
389
|
+
expect(fr3).toEqual(fr2);
|
|
390
|
+
expect(streamer2.readMock).toHaveBeenCalledTimes(1);
|
|
391
|
+
expect(openerMock).toHaveBeenCalledTimes(2);
|
|
238
392
|
});
|
|
239
393
|
|
|
240
|
-
|
|
394
|
+
it("should repeatedly try re-opening the streamer when read fails", async () => {
|
|
395
|
+
const streamer1 = new MockStreamer();
|
|
396
|
+
const streamer5 = new MockStreamer();
|
|
397
|
+
const fr1 = new Frame({ 1: new Series([1]) });
|
|
398
|
+
const fr5 = new Frame({ 1: new Series([4]) });
|
|
399
|
+
streamer1.responses = [
|
|
400
|
+
[fr1, null],
|
|
401
|
+
[fr5, new Unreachable({ message: "cat" })],
|
|
402
|
+
];
|
|
403
|
+
streamer5.responses = [[fr5, null]];
|
|
404
|
+
const openerMock = vi.fn();
|
|
405
|
+
let count = 0;
|
|
406
|
+
const hardened = await HardenedStreamer.open(
|
|
407
|
+
async () => {
|
|
408
|
+
count++;
|
|
409
|
+
openerMock();
|
|
410
|
+
if (count === 1) return streamer1;
|
|
411
|
+
if (count < 5) throw new Unreachable({ message: "very unreachable" });
|
|
412
|
+
return streamer5;
|
|
413
|
+
},
|
|
414
|
+
{ channels: [1] },
|
|
415
|
+
{ baseInterval: TimeSpan.milliseconds(1) },
|
|
416
|
+
);
|
|
417
|
+
const fr = await hardened.read();
|
|
418
|
+
expect(fr).toEqual(fr1);
|
|
419
|
+
const fr2 = await hardened.read();
|
|
420
|
+
expect(fr2).toEqual(fr5);
|
|
421
|
+
expect(openerMock).toHaveBeenCalledTimes(5);
|
|
422
|
+
});
|
|
241
423
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
424
|
+
it("should rethrow the error when the breaker exceeds the max retries", async () => {
|
|
425
|
+
const streamer = new MockStreamer();
|
|
426
|
+
const fr = new Frame({ 1: new Series([1]) });
|
|
427
|
+
streamer.responses = [[fr, null]];
|
|
428
|
+
const openerMock = vi.fn();
|
|
429
|
+
await expect(
|
|
430
|
+
HardenedStreamer.open(
|
|
431
|
+
async () => {
|
|
432
|
+
openerMock();
|
|
433
|
+
throw new Unreachable({ message: "very unreachable" });
|
|
434
|
+
},
|
|
435
|
+
{ channels: [1] },
|
|
436
|
+
{ maxRetries: 3, baseInterval: TimeSpan.milliseconds(1) },
|
|
437
|
+
),
|
|
438
|
+
).rejects.toThrow("very unreachable");
|
|
246
439
|
});
|
|
247
440
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
});
|
|
441
|
+
it("should retry update when the underlying streamer fails", async () => {
|
|
442
|
+
const streamer1 = new MockStreamer();
|
|
443
|
+
streamer1.updateErrors = [null, new Unreachable({ message: "cat" })];
|
|
444
|
+
const streamer2 = new MockStreamer();
|
|
445
|
+
const fr1 = new Frame({ 1: new Series([1]) });
|
|
446
|
+
const fr2 = new Frame({ 1: new Series([2]) });
|
|
447
|
+
streamer1.responses = [[fr1, null]];
|
|
448
|
+
streamer2.responses = [[fr2, null]];
|
|
449
|
+
let count = 0;
|
|
450
|
+
const openerMock = vi.fn();
|
|
451
|
+
const hardened = await HardenedStreamer.open(
|
|
452
|
+
async () => {
|
|
453
|
+
count++;
|
|
454
|
+
openerMock();
|
|
455
|
+
if (count === 1) return streamer1;
|
|
456
|
+
return streamer2;
|
|
457
|
+
},
|
|
458
|
+
{ channels: [1] },
|
|
459
|
+
);
|
|
254
460
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
461
|
+
await hardened.update([1, 2]);
|
|
462
|
+
expect(streamer1.updateMock).toHaveBeenCalledWith([1, 2]);
|
|
463
|
+
|
|
464
|
+
await hardened.update([2, 3]);
|
|
465
|
+
expect(openerMock).toHaveBeenCalledTimes(2);
|
|
466
|
+
});
|
|
262
467
|
});
|
|
263
468
|
});
|