@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.
Files changed (180) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/.vscode/settings.json +2 -2
  3. package/CONTRIBUTING.md +6 -5
  4. package/README.md +7 -8
  5. package/dist/access/payload.d.ts +1 -1
  6. package/dist/access/payload.d.ts.map +1 -1
  7. package/dist/access/policy/payload.d.ts +9 -9
  8. package/dist/access/policy/payload.d.ts.map +1 -1
  9. package/dist/access/policy/retriever.d.ts +3 -3
  10. package/dist/access/policy/retriever.d.ts.map +1 -1
  11. package/dist/auth/auth.d.ts +2 -2
  12. package/dist/auth/auth.d.ts.map +1 -1
  13. package/dist/channel/client.d.ts +1 -0
  14. package/dist/channel/client.d.ts.map +1 -1
  15. package/dist/channel/payload.d.ts +21 -8
  16. package/dist/channel/payload.d.ts.map +1 -1
  17. package/dist/channel/retriever.d.ts +5 -5
  18. package/dist/channel/retriever.d.ts.map +1 -1
  19. package/dist/channel/writer.d.ts +3 -3
  20. package/dist/channel/writer.d.ts.map +1 -1
  21. package/dist/client.cjs +135 -39
  22. package/dist/client.d.ts +8 -8
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +28505 -9345
  25. package/dist/connection/checker.d.ts +5 -5
  26. package/dist/connection/checker.d.ts.map +1 -1
  27. package/dist/control/state.d.ts +46 -3
  28. package/dist/control/state.d.ts.map +1 -1
  29. package/dist/framer/adapter.d.ts +2 -2
  30. package/dist/framer/adapter.d.ts.map +1 -1
  31. package/dist/framer/client.d.ts +2 -0
  32. package/dist/framer/client.d.ts.map +1 -1
  33. package/dist/framer/codec.d.ts +3 -3
  34. package/dist/framer/codec.d.ts.map +1 -1
  35. package/dist/framer/deleter.d.ts +8 -8
  36. package/dist/framer/deleter.d.ts.map +1 -1
  37. package/dist/framer/frame.d.ts +17 -17
  38. package/dist/framer/frame.d.ts.map +1 -1
  39. package/dist/framer/streamProxy.d.ts +3 -3
  40. package/dist/framer/streamProxy.d.ts.map +1 -1
  41. package/dist/framer/streamer.d.ts +103 -22
  42. package/dist/framer/streamer.d.ts.map +1 -1
  43. package/dist/framer/writer.d.ts +25 -25
  44. package/dist/framer/writer.d.ts.map +1 -1
  45. package/dist/hardware/device/client.d.ts +3 -3
  46. package/dist/hardware/device/client.d.ts.map +1 -1
  47. package/dist/hardware/device/payload.d.ts +65 -18
  48. package/dist/hardware/device/payload.d.ts.map +1 -1
  49. package/dist/hardware/rack/client.d.ts.map +1 -1
  50. package/dist/hardware/rack/payload.d.ts +87 -30
  51. package/dist/hardware/rack/payload.d.ts.map +1 -1
  52. package/dist/hardware/task/client.d.ts +3 -3
  53. package/dist/hardware/task/client.d.ts.map +1 -1
  54. package/dist/hardware/task/payload.d.ts +14 -15
  55. package/dist/hardware/task/payload.d.ts.map +1 -1
  56. package/dist/label/payload.d.ts +2 -2
  57. package/dist/label/payload.d.ts.map +1 -1
  58. package/dist/label/writer.d.ts +4 -4
  59. package/dist/label/writer.d.ts.map +1 -1
  60. package/dist/ontology/client.d.ts +3 -3
  61. package/dist/ontology/client.d.ts.map +1 -1
  62. package/dist/ontology/group/payload.d.ts +2 -2
  63. package/dist/ontology/group/payload.d.ts.map +1 -1
  64. package/dist/ontology/payload.d.ts +25 -25
  65. package/dist/ontology/payload.d.ts.map +1 -1
  66. package/dist/ranger/client.d.ts +8 -8
  67. package/dist/ranger/client.d.ts.map +1 -1
  68. package/dist/ranger/kv.d.ts +6 -6
  69. package/dist/ranger/kv.d.ts.map +1 -1
  70. package/dist/ranger/payload.d.ts +15 -15
  71. package/dist/ranger/payload.d.ts.map +1 -1
  72. package/dist/ranger/writer.d.ts +10 -10
  73. package/dist/ranger/writer.d.ts.map +1 -1
  74. package/dist/testutil/{indexedPair.d.ts → channels.d.ts} +1 -1
  75. package/dist/testutil/channels.d.ts.map +1 -0
  76. package/dist/user/payload.d.ts +3 -3
  77. package/dist/user/payload.d.ts.map +1 -1
  78. package/dist/user/retriever.d.ts +2 -2
  79. package/dist/user/retriever.d.ts.map +1 -1
  80. package/dist/util/retrieve.d.ts +6 -6
  81. package/dist/util/retrieve.d.ts.map +1 -1
  82. package/dist/util/zod.d.ts +2 -2
  83. package/dist/util/zod.d.ts.map +1 -1
  84. package/dist/workspace/client.d.ts.map +1 -1
  85. package/dist/workspace/lineplot/client.d.ts.map +1 -1
  86. package/dist/workspace/lineplot/lineplot.spec.d.ts +2 -0
  87. package/dist/workspace/lineplot/lineplot.spec.d.ts.map +1 -0
  88. package/dist/workspace/lineplot/payload.d.ts +5 -5
  89. package/dist/workspace/lineplot/payload.d.ts.map +1 -1
  90. package/dist/workspace/log/client.d.ts.map +1 -1
  91. package/dist/workspace/log/payload.d.ts +5 -5
  92. package/dist/workspace/log/payload.d.ts.map +1 -1
  93. package/dist/workspace/payload.d.ts +6 -6
  94. package/dist/workspace/payload.d.ts.map +1 -1
  95. package/dist/workspace/schematic/client.d.ts.map +1 -1
  96. package/dist/workspace/schematic/payload.d.ts +7 -7
  97. package/dist/workspace/schematic/payload.d.ts.map +1 -1
  98. package/dist/workspace/table/client.d.ts.map +1 -1
  99. package/dist/workspace/table/payload.d.ts +6 -6
  100. package/dist/workspace/table/payload.d.ts.map +1 -1
  101. package/package.json +11 -12
  102. package/src/access/payload.ts +1 -1
  103. package/src/access/policy/client.ts +3 -3
  104. package/src/access/policy/payload.ts +1 -1
  105. package/src/access/policy/retriever.ts +1 -1
  106. package/src/access/policy/writer.ts +7 -7
  107. package/src/auth/auth.ts +1 -1
  108. package/src/channel/client.ts +6 -4
  109. package/src/channel/payload.ts +10 -18
  110. package/src/channel/retriever.ts +2 -2
  111. package/src/channel/writer.ts +11 -2
  112. package/src/client.ts +3 -3
  113. package/src/connection/checker.ts +1 -1
  114. package/src/connection/connection.spec.ts +1 -1
  115. package/src/control/client.ts +1 -1
  116. package/src/control/state.ts +4 -5
  117. package/src/errors.spec.ts +2 -3
  118. package/src/errors.ts +2 -2
  119. package/src/framer/adapter.ts +2 -2
  120. package/src/framer/client.ts +4 -3
  121. package/src/framer/codec.spec.ts +2 -2
  122. package/src/framer/codec.ts +5 -9
  123. package/src/framer/deleter.spec.ts +1 -1
  124. package/src/framer/deleter.ts +1 -1
  125. package/src/framer/frame.ts +15 -15
  126. package/src/framer/iterator.spec.ts +1 -1
  127. package/src/framer/iterator.ts +1 -1
  128. package/src/framer/streamProxy.ts +4 -4
  129. package/src/framer/streamer.spec.ts +420 -215
  130. package/src/framer/streamer.ts +119 -21
  131. package/src/framer/writer.spec.ts +1 -1
  132. package/src/framer/writer.ts +15 -8
  133. package/src/hardware/device/client.ts +5 -5
  134. package/src/hardware/device/device.spec.ts +28 -30
  135. package/src/hardware/device/payload.ts +5 -5
  136. package/src/hardware/rack/client.ts +4 -4
  137. package/src/hardware/rack/payload.ts +6 -6
  138. package/src/hardware/rack/rack.spec.ts +1 -1
  139. package/src/hardware/task/client.ts +21 -19
  140. package/src/hardware/task/payload.ts +8 -6
  141. package/src/label/payload.ts +1 -1
  142. package/src/label/retriever.ts +3 -3
  143. package/src/label/writer.ts +4 -4
  144. package/src/ontology/client.ts +4 -4
  145. package/src/ontology/group/payload.ts +3 -3
  146. package/src/ontology/group/writer.ts +1 -1
  147. package/src/ontology/payload.ts +2 -2
  148. package/src/ontology/writer.ts +1 -1
  149. package/src/ranger/alias.ts +1 -1
  150. package/src/ranger/client.ts +6 -4
  151. package/src/ranger/kv.ts +4 -4
  152. package/src/ranger/payload.ts +3 -3
  153. package/src/ranger/writer.ts +1 -1
  154. package/src/user/client.ts +3 -3
  155. package/src/user/payload.ts +1 -1
  156. package/src/user/retriever.ts +1 -1
  157. package/src/user/writer.ts +4 -4
  158. package/src/util/retrieve.spec.ts +7 -4
  159. package/src/util/retrieve.ts +10 -10
  160. package/src/util/zod.ts +3 -3
  161. package/src/workspace/client.ts +5 -5
  162. package/src/workspace/lineplot/client.ts +5 -5
  163. package/src/workspace/lineplot/{linePlot.spec.ts → lineplot.spec.ts} +2 -2
  164. package/src/workspace/lineplot/payload.ts +1 -1
  165. package/src/workspace/log/client.ts +5 -5
  166. package/src/workspace/log/log.spec.ts +2 -2
  167. package/src/workspace/log/payload.ts +1 -1
  168. package/src/workspace/payload.ts +1 -1
  169. package/src/workspace/schematic/client.ts +5 -5
  170. package/src/workspace/schematic/payload.ts +1 -1
  171. package/src/workspace/schematic/schematic.spec.ts +3 -3
  172. package/src/workspace/table/client.ts +5 -5
  173. package/src/workspace/table/payload.ts +1 -1
  174. package/src/workspace/table/table.spec.ts +2 -2
  175. package/src/workspace/workspace.spec.ts +2 -2
  176. package/tsconfig.json +3 -5
  177. package/dist/testutil/indexedPair.d.ts.map +0 -1
  178. package/dist/workspace/lineplot/linePlot.spec.d.ts +0 -2
  179. package/dist/workspace/lineplot/linePlot.spec.d.ts.map +0 -1
  180. /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 { DataType, TimeStamp } from "@synnaxlabs/x/telem";
11
- import { describe, expect, it, test } from "vitest";
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/indexedPair";
22
+ import { newVirtualChannel } from "@/testutil/channels";
15
23
 
16
24
  const client = newClient();
17
25
 
18
26
  describe("Streamer", () => {
19
- test("happy path", async () => {
20
- const ch = await newVirtualChannel(client);
21
- const streamer = await client.openStreamer(ch.key);
22
- const writer = await client.openWriter({
23
- start: TimeStamp.now(),
24
- channels: ch.key,
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
- try {
27
- await writer.write(ch.key, new Float64Array([1, 2, 3]));
28
- } finally {
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
- const writer = await client.openWriter({
51
- start: TimeStamp.now(),
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
- try {
55
- await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5]));
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
- const writer = await client.openWriter({
69
- start: TimeStamp.now(),
70
- channels: ch.key,
71
- });
72
- try {
73
- await writer.write(ch.key, new Float64Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
74
- } finally {
75
- await writer.close();
76
- }
77
- const d = await streamer.read();
78
- expect(Array.from(d.get(ch.key))).toEqual([1, 3, 5, 7, 9]);
79
- });
80
- test("downsample factor of 10", async () => {
81
- const ch = await newVirtualChannel(client);
82
- const streamer = await client.openStreamer({
83
- channels: ch.key,
84
- downSampleFactor: 10,
85
- });
86
- const writer = await client.openWriter({
87
- start: TimeStamp.now(),
88
- channels: ch.key,
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("Streamer - Calculated Channels", () => {
101
- test("basic calculated channel streaming", async () => {
102
- // Create a timestamp index channel
103
- const timeChannel = await client.channels.create({
104
- name: "calc_test_time",
105
- isIndex: true,
106
- dataType: DataType.TIMESTAMP,
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
- // Create source channels with the timestamp index
110
- const [channelA, channelB] = await client.channels.create([
111
- {
112
- name: "test_a",
113
- dataType: DataType.FLOAT64,
114
- index: timeChannel.key,
115
- },
116
- {
117
- name: "test_b",
118
- dataType: DataType.FLOAT64,
119
- index: timeChannel.key,
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
- // Set up streamer to listen for calculated results
134
- const streamer = await client.openStreamer(calcChannel.key);
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
- // Write test data
137
- const startTime = TimeStamp.now();
138
- const writer = await client.openWriter({
139
- start: startTime,
140
- channels: [timeChannel.key, channelA.key, channelB.key],
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
- try {
144
- // Write test values - each source gets 2.5 so sum should be 5.0
145
- await writer.write({
146
- [timeChannel.key]: [startTime],
147
- [channelA.key]: new Float64Array([2.5]),
148
- [channelB.key]: new Float64Array([2.5]),
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
- // Read from streamer
152
- const frame = await streamer.read();
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
- // Verify calculated results
155
- const calcData = Array.from(frame.get(calcChannel.key));
156
- expect(calcData).toEqual([5.0]);
157
- } finally {
158
- await writer.close();
159
- streamer.close();
160
- }
161
- });
162
- test("calculated channel with constant", async () => {
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
- // Create base channel with index
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
- // Create calculated channel that adds 5
178
- const calcChannel = await client.channels.create({
179
- name: "calc_const_channel",
180
- dataType: DataType.FLOAT64,
181
- index: timeChannel.key,
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
- const streamer = await client.openStreamer(calcChannel.key);
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
- const startTime = TimeStamp.now();
190
- const writer = await client.openWriter({
191
- start: startTime,
192
- channels: [timeChannel.key, baseChannel.key],
193
- });
212
+ await writer.write({
213
+ [timeChannel.key]: timestamps,
214
+ [baseChannel.key]: new Float64Array([1, 2, 3]),
215
+ });
194
216
 
195
- try {
196
- const timestamps = [
197
- startTime,
198
- new TimeStamp(startTime.valueOf() + BigInt(1000000000)),
199
- new TimeStamp(startTime.valueOf() + BigInt(2000000000)),
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
- await writer.write({
203
- [timeChannel.key]: timestamps,
204
- [baseChannel.key]: new Float64Array([1, 2, 3]),
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
- const frame = await streamer.read();
208
- const calcData = Array.from(frame.get(calcChannel.key));
209
- expect(calcData).toEqual([6, 7, 8]); // Original values + 5
210
- } finally {
211
- await writer.close();
212
- streamer.close();
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
- test("calculated channel with multiple operations", async () => {
217
- // Create timestamp channel
218
- const timeChannel = await client.channels.create({
219
- name: "calc_multi_time",
220
- isIndex: true,
221
- dataType: DataType.TIMESTAMP,
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
- // Create source channels
225
- const [channelA, channelB] = await client.channels.create([
226
- { name: "multi_test_a", dataType: DataType.FLOAT64, index: timeChannel.key },
227
- { name: "multi_test_b", dataType: DataType.FLOAT64, index: timeChannel.key },
228
- ]);
229
-
230
- // Create calculated channel with multiple operations
231
- const calcChannel = await client.channels.create({
232
- name: "multi_calc",
233
- dataType: DataType.FLOAT64,
234
- index: timeChannel.key,
235
- virtual: true,
236
- expression: "return (multi_test_a * 2) + (multi_test_b / 2)",
237
- requires: [channelA.key, channelB.key],
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
- const streamer = await client.openStreamer(calcChannel.key);
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
- const startTime = TimeStamp.now();
243
- const writer = await client.openWriter({
244
- start: startTime,
245
- channels: [timeChannel.key, channelA.key, channelB.key],
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
- try {
249
- await writer.write({
250
- [timeChannel.key]: [startTime],
251
- [channelA.key]: new Float64Array([2.0]), // Will be multiplied by 2 = 4.0
252
- [channelB.key]: new Float64Array([4.0]), // Will be divided by 2 = 2.0
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
- const frame = await streamer.read();
256
- const calcData = Array.from(frame.get(calcChannel.key));
257
- expect(calcData).toEqual([6.0]); // (2.0 * 2) + (4.0 / 2) = 4.0 + 2.0 = 6.0
258
- } finally {
259
- await writer.close();
260
- streamer.close();
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
  });