@synnaxlabs/client 0.43.1 → 0.44.2

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 (245) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/dist/access/payload.d.ts +1 -1
  3. package/dist/access/payload.d.ts.map +1 -1
  4. package/dist/access/policy/client.d.ts +263 -6
  5. package/dist/access/policy/client.d.ts.map +1 -1
  6. package/dist/access/policy/external.d.ts +0 -1
  7. package/dist/access/policy/external.d.ts.map +1 -1
  8. package/dist/access/policy/payload.d.ts +105 -93
  9. package/dist/access/policy/payload.d.ts.map +1 -1
  10. package/dist/auth/auth.d.ts +1 -1
  11. package/dist/auth/auth.d.ts.map +1 -1
  12. package/dist/channel/client.d.ts +23 -17
  13. package/dist/channel/client.d.ts.map +1 -1
  14. package/dist/channel/payload.d.ts +151 -21
  15. package/dist/channel/payload.d.ts.map +1 -1
  16. package/dist/channel/retriever.d.ts +9 -16
  17. package/dist/channel/retriever.d.ts.map +1 -1
  18. package/dist/channel/writer.d.ts +1 -1
  19. package/dist/channel/writer.d.ts.map +1 -1
  20. package/dist/client.cjs +27 -135
  21. package/dist/client.d.ts +3 -3
  22. package/dist/client.d.ts.map +1 -1
  23. package/dist/client.js +8657 -28963
  24. package/dist/connection/checker.d.ts +1 -1
  25. package/dist/connection/checker.d.ts.map +1 -1
  26. package/dist/control/client.d.ts +1 -0
  27. package/dist/control/client.d.ts.map +1 -1
  28. package/dist/control/state.d.ts +6 -6
  29. package/dist/control/state.d.ts.map +1 -1
  30. package/dist/errors.d.ts +18 -5
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/framer/adapter.d.ts +3 -3
  33. package/dist/framer/adapter.d.ts.map +1 -1
  34. package/dist/framer/client.d.ts +4 -13
  35. package/dist/framer/client.d.ts.map +1 -1
  36. package/dist/framer/codec.d.ts +1 -1
  37. package/dist/framer/codec.d.ts.map +1 -1
  38. package/dist/framer/deleter.d.ts +5 -5
  39. package/dist/framer/deleter.d.ts.map +1 -1
  40. package/dist/framer/frame.d.ts +5 -7
  41. package/dist/framer/frame.d.ts.map +1 -1
  42. package/dist/framer/streamProxy.d.ts +1 -1
  43. package/dist/framer/streamProxy.d.ts.map +1 -1
  44. package/dist/framer/streamer.d.ts +235 -20
  45. package/dist/framer/streamer.d.ts.map +1 -1
  46. package/dist/framer/writer.d.ts +302 -33
  47. package/dist/framer/writer.d.ts.map +1 -1
  48. package/dist/hardware/device/client.d.ts +49 -28
  49. package/dist/hardware/device/client.d.ts.map +1 -1
  50. package/dist/hardware/device/payload.d.ts +126 -46
  51. package/dist/hardware/device/payload.d.ts.map +1 -1
  52. package/dist/hardware/rack/client.d.ts +78 -22
  53. package/dist/hardware/rack/client.d.ts.map +1 -1
  54. package/dist/hardware/rack/payload.d.ts +99 -56
  55. package/dist/hardware/rack/payload.d.ts.map +1 -1
  56. package/dist/hardware/task/client.d.ts +100 -41
  57. package/dist/hardware/task/client.d.ts.map +1 -1
  58. package/dist/hardware/task/payload.d.ts +83 -61
  59. package/dist/hardware/task/payload.d.ts.map +1 -1
  60. package/dist/index.d.ts +2 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/label/client.d.ts +138 -20
  63. package/dist/label/client.d.ts.map +1 -1
  64. package/dist/label/external.d.ts +0 -2
  65. package/dist/label/external.d.ts.map +1 -1
  66. package/dist/label/payload.d.ts +4 -5
  67. package/dist/label/payload.d.ts.map +1 -1
  68. package/dist/ontology/client.d.ts +45 -135
  69. package/dist/ontology/client.d.ts.map +1 -1
  70. package/dist/ontology/group/group.d.ts +3 -3
  71. package/dist/ontology/group/group.d.ts.map +1 -1
  72. package/dist/ontology/group/payload.d.ts +3 -27
  73. package/dist/ontology/group/payload.d.ts.map +1 -1
  74. package/dist/ontology/payload.d.ts +114 -243
  75. package/dist/ontology/payload.d.ts.map +1 -1
  76. package/dist/ontology/writer.d.ts +4 -4
  77. package/dist/ontology/writer.d.ts.map +1 -1
  78. package/dist/ranger/alias.d.ts +15 -5
  79. package/dist/ranger/alias.d.ts.map +1 -1
  80. package/dist/ranger/client.d.ts +91 -30
  81. package/dist/ranger/client.d.ts.map +1 -1
  82. package/dist/ranger/external.d.ts +1 -1
  83. package/dist/ranger/external.d.ts.map +1 -1
  84. package/dist/ranger/kv.d.ts +11 -12
  85. package/dist/ranger/kv.d.ts.map +1 -1
  86. package/dist/ranger/payload.d.ts +19 -44
  87. package/dist/ranger/payload.d.ts.map +1 -1
  88. package/dist/ranger/writer.d.ts +22 -19
  89. package/dist/ranger/writer.d.ts.map +1 -1
  90. package/dist/testutil/client.d.ts +4 -0
  91. package/dist/testutil/client.d.ts.map +1 -0
  92. package/dist/user/client.d.ts +59 -6
  93. package/dist/user/client.d.ts.map +1 -1
  94. package/dist/user/payload.d.ts +4 -6
  95. package/dist/user/payload.d.ts.map +1 -1
  96. package/dist/user/retriever.d.ts +2 -2
  97. package/dist/user/retriever.d.ts.map +1 -1
  98. package/dist/util/decodeJSONString.d.ts +2 -2
  99. package/dist/util/decodeJSONString.d.ts.map +1 -1
  100. package/dist/util/parseWithoutKeyConversion.d.ts +2 -2
  101. package/dist/util/parseWithoutKeyConversion.d.ts.map +1 -1
  102. package/dist/util/retrieve.d.ts +1 -1
  103. package/dist/util/retrieve.d.ts.map +1 -1
  104. package/dist/util/zod.d.ts +1 -1
  105. package/dist/util/zod.d.ts.map +1 -1
  106. package/dist/workspace/client.d.ts +17 -6
  107. package/dist/workspace/client.d.ts.map +1 -1
  108. package/dist/workspace/lineplot/client.d.ts +2 -2
  109. package/dist/workspace/lineplot/client.d.ts.map +1 -1
  110. package/dist/workspace/lineplot/payload.d.ts +8 -9
  111. package/dist/workspace/lineplot/payload.d.ts.map +1 -1
  112. package/dist/workspace/log/client.d.ts +2 -2
  113. package/dist/workspace/log/client.d.ts.map +1 -1
  114. package/dist/workspace/log/payload.d.ts +8 -9
  115. package/dist/workspace/log/payload.d.ts.map +1 -1
  116. package/dist/workspace/payload.d.ts +10 -11
  117. package/dist/workspace/payload.d.ts.map +1 -1
  118. package/dist/workspace/schematic/client.d.ts +2 -2
  119. package/dist/workspace/schematic/client.d.ts.map +1 -1
  120. package/dist/workspace/schematic/payload.d.ts +10 -11
  121. package/dist/workspace/schematic/payload.d.ts.map +1 -1
  122. package/dist/workspace/table/client.d.ts +2 -2
  123. package/dist/workspace/table/client.d.ts.map +1 -1
  124. package/dist/workspace/table/payload.d.ts +10 -11
  125. package/dist/workspace/table/payload.d.ts.map +1 -1
  126. package/examples/node/package-lock.json +47 -39
  127. package/examples/node/package.json +2 -1
  128. package/examples/node/streamWrite.js +5 -11
  129. package/package.json +14 -13
  130. package/src/access/payload.ts +1 -1
  131. package/src/access/policy/client.ts +87 -32
  132. package/src/access/policy/external.ts +0 -1
  133. package/src/access/policy/payload.ts +4 -4
  134. package/src/access/policy/policy.spec.ts +86 -83
  135. package/src/auth/auth.spec.ts +29 -18
  136. package/src/auth/auth.ts +1 -1
  137. package/src/channel/batchRetriever.spec.ts +4 -9
  138. package/src/channel/channel.spec.ts +24 -6
  139. package/src/channel/client.ts +52 -51
  140. package/src/channel/payload.ts +15 -16
  141. package/src/channel/retriever.ts +26 -41
  142. package/src/channel/writer.ts +7 -4
  143. package/src/client.ts +4 -4
  144. package/src/connection/checker.ts +1 -1
  145. package/src/connection/connection.spec.ts +31 -23
  146. package/src/control/client.ts +2 -2
  147. package/src/control/state.spec.ts +3 -3
  148. package/src/control/state.ts +1 -1
  149. package/src/errors.spec.ts +9 -5
  150. package/src/errors.ts +28 -15
  151. package/src/framer/adapter.spec.ts +118 -9
  152. package/src/framer/adapter.ts +24 -11
  153. package/src/framer/client.spec.ts +125 -2
  154. package/src/framer/client.ts +41 -47
  155. package/src/framer/codec.ts +1 -1
  156. package/src/framer/deleter.spec.ts +2 -2
  157. package/src/framer/deleter.ts +1 -1
  158. package/src/framer/frame.ts +1 -4
  159. package/src/framer/iterator.spec.ts +8 -8
  160. package/src/framer/iterator.ts +1 -1
  161. package/src/framer/streamProxy.ts +1 -1
  162. package/src/framer/streamer.spec.ts +185 -36
  163. package/src/framer/streamer.ts +28 -36
  164. package/src/framer/writer.spec.ts +7 -6
  165. package/src/framer/writer.ts +97 -111
  166. package/src/hardware/device/client.ts +45 -131
  167. package/src/hardware/device/device.spec.ts +163 -52
  168. package/src/hardware/device/payload.ts +10 -21
  169. package/src/hardware/rack/client.ts +87 -105
  170. package/src/hardware/rack/payload.ts +4 -13
  171. package/src/hardware/rack/rack.spec.ts +28 -35
  172. package/src/hardware/task/client.ts +335 -291
  173. package/src/hardware/task/payload.ts +86 -62
  174. package/src/hardware/task/task.spec.ts +208 -32
  175. package/src/index.ts +2 -1
  176. package/src/label/client.ts +100 -95
  177. package/src/label/external.ts +0 -2
  178. package/src/label/label.spec.ts +8 -6
  179. package/src/label/payload.ts +3 -4
  180. package/src/ontology/client.ts +41 -324
  181. package/src/ontology/group/group.spec.ts +2 -2
  182. package/src/ontology/group/group.ts +4 -5
  183. package/src/ontology/group/payload.ts +2 -25
  184. package/src/ontology/group/writer.ts +1 -1
  185. package/src/ontology/ontology.spec.ts +355 -41
  186. package/src/ontology/payload.ts +77 -112
  187. package/src/ontology/writer.ts +8 -17
  188. package/src/ranger/alias.ts +45 -37
  189. package/src/ranger/client.ts +144 -149
  190. package/src/ranger/external.ts +1 -1
  191. package/src/ranger/kv.ts +9 -27
  192. package/src/ranger/payload.ts +23 -37
  193. package/src/ranger/ranger.spec.ts +37 -56
  194. package/src/ranger/writer.ts +1 -1
  195. package/src/{signals/index.ts → testutil/client.ts} +11 -1
  196. package/src/user/client.ts +122 -47
  197. package/src/user/payload.ts +2 -5
  198. package/src/user/retriever.ts +1 -1
  199. package/src/user/user.spec.ts +31 -31
  200. package/src/user/writer.ts +1 -1
  201. package/src/util/decodeJSONString.ts +3 -3
  202. package/src/util/parseWithoutKeyConversion.ts +2 -2
  203. package/src/util/retrieve.ts +1 -1
  204. package/src/util/zod.ts +1 -1
  205. package/src/workspace/client.ts +20 -36
  206. package/src/workspace/lineplot/client.ts +5 -7
  207. package/src/workspace/lineplot/lineplot.spec.ts +2 -2
  208. package/src/workspace/lineplot/payload.ts +4 -7
  209. package/src/workspace/log/client.ts +5 -7
  210. package/src/workspace/log/log.spec.ts +2 -2
  211. package/src/workspace/log/payload.ts +4 -7
  212. package/src/workspace/payload.ts +4 -7
  213. package/src/workspace/schematic/client.ts +5 -7
  214. package/src/workspace/schematic/payload.ts +4 -7
  215. package/src/workspace/schematic/schematic.spec.ts +2 -2
  216. package/src/workspace/table/client.ts +5 -7
  217. package/src/workspace/table/payload.ts +4 -7
  218. package/src/workspace/table/table.spec.ts +2 -2
  219. package/src/workspace/workspace.spec.ts +2 -2
  220. package/dist/access/policy/ontology.d.ts +0 -5
  221. package/dist/access/policy/ontology.d.ts.map +0 -1
  222. package/dist/access/policy/retriever.d.ts +0 -40
  223. package/dist/access/policy/retriever.d.ts.map +0 -1
  224. package/dist/access/policy/writer.d.ts +0 -9
  225. package/dist/access/policy/writer.d.ts.map +0 -1
  226. package/dist/label/retriever.d.ts +0 -14
  227. package/dist/label/retriever.d.ts.map +0 -1
  228. package/dist/label/writer.d.ts +0 -54
  229. package/dist/label/writer.d.ts.map +0 -1
  230. package/dist/setupspecs.d.ts +0 -5
  231. package/dist/setupspecs.d.ts.map +0 -1
  232. package/dist/signals/external.d.ts +0 -2
  233. package/dist/signals/external.d.ts.map +0 -1
  234. package/dist/signals/index.d.ts +0 -2
  235. package/dist/signals/index.d.ts.map +0 -1
  236. package/dist/signals/observable.d.ts +0 -12
  237. package/dist/signals/observable.d.ts.map +0 -1
  238. package/src/access/policy/ontology.ts +0 -17
  239. package/src/access/policy/retriever.ts +0 -44
  240. package/src/access/policy/writer.ts +0 -65
  241. package/src/label/retriever.ts +0 -63
  242. package/src/label/writer.ts +0 -95
  243. package/src/setupspecs.ts +0 -27
  244. package/src/signals/external.ts +0 -10
  245. package/src/signals/observable.ts +0 -42
@@ -11,12 +11,12 @@ import { TimeRange, TimeSpan, TimeStamp } from "@synnaxlabs/x/telem";
11
11
  import { describe, expect, test } from "vitest";
12
12
 
13
13
  import { AUTO_SPAN } from "@/framer/iterator";
14
- import { newClient } from "@/setupspecs";
15
14
  import { newIndexedPair } from "@/testutil/channels";
15
+ import { createTestClient } from "@/testutil/client";
16
16
  import { secondsLinspace } from "@/testutil/telem";
17
17
  import { randomSeries } from "@/util/telem";
18
18
 
19
- const client = newClient();
19
+ const client = createTestClient();
20
20
 
21
21
  describe("Iterator", () => {
22
22
  test("happy path", async () => {
@@ -50,7 +50,7 @@ describe("Iterator", () => {
50
50
  );
51
51
 
52
52
  try {
53
- expect(await iter.seekFirst()).toBeTruthy();
53
+ expect(await iter.seekFirst()).toBe(true);
54
54
  let c = 0;
55
55
  while (await iter.next(TimeSpan.seconds(1))) {
56
56
  c++;
@@ -78,24 +78,24 @@ describe("Iterator", () => {
78
78
  const iter = await client.openIterator(TimeRange.MAX, channels, { chunkSize: 4 });
79
79
 
80
80
  try {
81
- expect(await iter.seekFirst()).toBeTruthy();
81
+ expect(await iter.seekFirst()).toBe(true);
82
82
 
83
- expect(await iter.next(AUTO_SPAN)).toBeTruthy();
83
+ expect(await iter.next(AUTO_SPAN)).toBe(true);
84
84
  expect(iter.value.get(idx_ch.key).data).toEqual(
85
85
  new BigInt64Array(secondsLinspace(1, 4).map((v) => v.valueOf())),
86
86
  );
87
87
 
88
- expect(await iter.next(AUTO_SPAN)).toBeTruthy();
88
+ expect(await iter.next(AUTO_SPAN)).toBe(true);
89
89
  expect(iter.value.get(idx_ch.key).data).toEqual(
90
90
  new BigInt64Array(secondsLinspace(5, 4).map((v) => v.valueOf())),
91
91
  );
92
92
 
93
- expect(await iter.next(AUTO_SPAN)).toBeTruthy();
93
+ expect(await iter.next(AUTO_SPAN)).toBe(true);
94
94
  expect(iter.value.get(idx_ch.key).data).toEqual(
95
95
  new BigInt64Array(secondsLinspace(9, 2).map((v) => v.valueOf())),
96
96
  );
97
97
 
98
- expect(await iter.next(AUTO_SPAN)).toBeFalsy();
98
+ expect(await iter.next(AUTO_SPAN)).toBe(false);
99
99
  } finally {
100
100
  await iter.close();
101
101
  }
@@ -17,7 +17,7 @@ import {
17
17
  TimeSpan,
18
18
  TimeStamp,
19
19
  } from "@synnaxlabs/x/telem";
20
- import { z } from "zod/v4";
20
+ import { z } from "zod";
21
21
 
22
22
  import { channel } from "@/channel";
23
23
  import { ReadAdapter } from "@/framer/adapter";
@@ -8,7 +8,7 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { EOF, type Stream } from "@synnaxlabs/freighter";
11
- import { type z } from "zod/v4";
11
+ import { type z } from "zod";
12
12
 
13
13
  export class StreamProxy<RQ extends z.ZodType, RS extends z.ZodType> {
14
14
  readonly name: string;
@@ -15,13 +15,14 @@ import { type channel } from "@/channel";
15
15
  import { Frame } from "@/framer/frame";
16
16
  import {
17
17
  HardenedStreamer,
18
- parseStreamerConfig,
18
+ ObservableStreamer,
19
19
  type Streamer,
20
+ streamerConfigZ,
20
21
  } from "@/framer/streamer";
21
- import { newClient } from "@/setupspecs";
22
22
  import { newVirtualChannel } from "@/testutil/channels";
23
+ import { createTestClient } from "@/testutil/client";
23
24
 
24
- const client = newClient();
25
+ const client = createTestClient();
25
26
 
26
27
  describe("Streamer", () => {
27
28
  describe("standard", () => {
@@ -273,55 +274,60 @@ describe("Streamer", () => {
273
274
  });
274
275
  });
275
276
 
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();
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;
292
289
  }
290
+ this.updateMock(channels);
291
+ return Promise.resolve();
292
+ }
293
293
 
294
- close(): void {
295
- this.closeMock();
296
- }
294
+ close(): void {
295
+ this.closeMock();
296
+ }
297
297
 
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
- }
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
305
 
306
- async next(): Promise<IteratorResult<Frame, any>> {
306
+ async next(): Promise<IteratorResult<Frame, any>> {
307
+ try {
307
308
  const fr = await this.read();
308
309
  return { done: false, value: fr };
310
+ } catch (err) {
311
+ if (EOF.matches(err)) return { done: true, value: undefined };
312
+ throw err;
309
313
  }
314
+ }
310
315
 
311
- [Symbol.asyncIterator](): AsyncIterator<Frame, any, undefined> {
312
- return this;
313
- }
316
+ [Symbol.asyncIterator](): AsyncIterator<Frame, any, undefined> {
317
+ return this;
314
318
  }
319
+ }
315
320
 
321
+ describe("hardened", () => {
316
322
  it("should correctly call the underlying streamer methods", async () => {
317
323
  const streamer = new MockStreamer();
318
324
  const openMock = vi.fn();
319
- const config = { channels: [1, 2, 3] };
325
+ const config = { channels: [1, 2, 3], useExperimentalCodec: false };
320
326
  const fr = new Frame({ 1: new Series([1]) });
321
327
  const hardened = await HardenedStreamer.open(
322
328
  async (cfg) => {
323
329
  openMock(cfg);
324
- const cfg_ = parseStreamerConfig(cfg);
330
+ const cfg_ = streamerConfigZ.parse(cfg);
325
331
  streamer.responses = [[fr, null]];
326
332
  streamer.keys = cfg_.channels as channel.Key[];
327
333
  return streamer;
@@ -329,7 +335,10 @@ describe("Streamer", () => {
329
335
  { channels: [1, 2, 3] },
330
336
  );
331
337
  expect(hardened.keys).toEqual([1, 2, 3]);
332
- expect(openMock).toHaveBeenCalledWith(config);
338
+ expect(openMock).toHaveBeenCalledWith({
339
+ ...config,
340
+ downsampleFactor: 1,
341
+ });
333
342
  await hardened.update([1, 2, 3]);
334
343
  expect(streamer.updateMock).toHaveBeenCalledWith([1, 2, 3]);
335
344
  const fr2 = await hardened.read();
@@ -465,4 +474,144 @@ describe("Streamer", () => {
465
474
  expect(openerMock).toHaveBeenCalledTimes(2);
466
475
  });
467
476
  });
477
+
478
+ describe("observable", () => {
479
+ it("should notify observers when frames are received", async () => {
480
+ const mockStreamer = new MockStreamer();
481
+ const frame1 = new Frame({ 1: new Series([1, 2, 3]) });
482
+ const frame2 = new Frame({ 1: new Series([4, 5, 6]) });
483
+
484
+ mockStreamer.responses = [
485
+ [frame1, null],
486
+ [frame2, null],
487
+ ];
488
+ mockStreamer.keys = [1];
489
+
490
+ const observable = new ObservableStreamer(mockStreamer);
491
+
492
+ const receivedFrames: Frame[] = [];
493
+ observable.onChange((frame) => {
494
+ receivedFrames.push(frame);
495
+ });
496
+
497
+ await expect.poll(() => receivedFrames.length).toBe(2);
498
+ expect(receivedFrames[0]).toEqual(frame1);
499
+ expect(receivedFrames[1]).toEqual(frame2);
500
+
501
+ await observable.close();
502
+ expect(mockStreamer.closeMock).toHaveBeenCalled();
503
+ });
504
+
505
+ test("should apply transform function to frames", async () => {
506
+ const mockStreamer = new MockStreamer();
507
+ const frame1 = new Frame({ 1: new Series([1, 2, 3]) });
508
+ const frame2 = new Frame({ 1: new Series([4, 5, 6]) });
509
+
510
+ mockStreamer.responses = [
511
+ [frame1, null],
512
+ [frame2, null],
513
+ ];
514
+ mockStreamer.keys = [1];
515
+
516
+ const transform = (frame: Frame): [number, true] | [null, false] => {
517
+ try {
518
+ const data = Array.from(frame.get(1));
519
+ const firstValue = data[0] as number;
520
+ return [firstValue, true];
521
+ } catch {
522
+ return [null, false];
523
+ }
524
+ };
525
+
526
+ const observable = new ObservableStreamer(mockStreamer, transform);
527
+
528
+ const receivedValues: number[] = [];
529
+ observable.onChange((value) => {
530
+ if (value !== null) receivedValues.push(value);
531
+ });
532
+
533
+ await expect.poll(() => receivedValues.length).toBe(2);
534
+ expect(receivedValues[0]).toBe(1);
535
+ expect(receivedValues[1]).toBe(4);
536
+
537
+ await observable.close();
538
+ });
539
+
540
+ test("should handle multiple observers", async () => {
541
+ const mockStreamer = new MockStreamer();
542
+ const frame1 = new Frame({ 1: new Series([10, 20]) });
543
+
544
+ mockStreamer.responses = [[frame1, null]];
545
+ mockStreamer.keys = [1];
546
+
547
+ const observable = new ObservableStreamer(mockStreamer);
548
+
549
+ const observer1Results: Frame[] = [];
550
+ const observer2Results: Frame[] = [];
551
+
552
+ observable.onChange((frame) => {
553
+ observer1Results.push(frame);
554
+ });
555
+
556
+ observable.onChange((frame) => {
557
+ observer2Results.push(frame);
558
+ });
559
+
560
+ await expect.poll(() => observer1Results.length).toBe(1);
561
+ expect(observer2Results).toHaveLength(1);
562
+ expect(observer1Results[0]).toEqual(frame1);
563
+ expect(observer2Results[0]).toEqual(frame1);
564
+
565
+ await observable.close();
566
+ });
567
+
568
+ test("should update channels on underlying streamer", async () => {
569
+ const mockStreamer = new MockStreamer();
570
+ mockStreamer.keys = [1, 2];
571
+
572
+ const observable = new ObservableStreamer(mockStreamer);
573
+
574
+ await observable.update([3, 4, 5]);
575
+
576
+ expect(mockStreamer.updateMock).toHaveBeenCalledWith([3, 4, 5]);
577
+
578
+ await observable.close();
579
+ });
580
+
581
+ test("should handle empty frame stream gracefully", async () => {
582
+ const mockStreamer = new MockStreamer();
583
+ mockStreamer.keys = [1];
584
+
585
+ const observable = new ObservableStreamer(mockStreamer);
586
+
587
+ const receivedFrames: Frame[] = [];
588
+ observable.onChange((frame) => {
589
+ receivedFrames.push(frame);
590
+ });
591
+
592
+ await expect.poll(() => receivedFrames.length).toBe(0);
593
+ expect(receivedFrames).toHaveLength(0);
594
+
595
+ await observable.close();
596
+ });
597
+
598
+ test("should properly close and cleanup resources", async () => {
599
+ const mockStreamer = new MockStreamer();
600
+ const frame1 = new Frame({ 1: new Series([1]) });
601
+
602
+ mockStreamer.responses = [[frame1, null]];
603
+ mockStreamer.keys = [1];
604
+
605
+ const observable = new ObservableStreamer(mockStreamer);
606
+
607
+ const receivedFrames: Frame[] = [];
608
+ observable.onChange((frame) => {
609
+ receivedFrames.push(frame);
610
+ });
611
+
612
+ await observable.close();
613
+
614
+ expect(mockStreamer.closeMock).toHaveBeenCalled();
615
+ });
616
+ });
468
617
  });
@@ -9,14 +9,14 @@
9
9
 
10
10
  import { EOF, type Stream, type WebSocketClient } from "@synnaxlabs/freighter";
11
11
  import { breaker, observe, TimeSpan } from "@synnaxlabs/x";
12
- import { z } from "zod/v4";
12
+ import { z } from "zod";
13
13
 
14
14
  import { type channel } from "@/channel";
15
+ import { paramsZ } from "@/channel/payload";
15
16
  import { ReadAdapter } from "@/framer/adapter";
16
17
  import { WSStreamerCodec } from "@/framer/codec";
17
18
  import { Frame, frameZ } from "@/framer/frame";
18
19
  import { StreamProxy } from "@/framer/streamProxy";
19
- import { payloadZ } from "@/ranger/payload";
20
20
 
21
21
  const reqZ = z.object({ keys: z.number().array(), downsampleFactor: z.number() });
22
22
 
@@ -36,17 +36,21 @@ export interface StreamerResponse extends z.infer<typeof resZ> {}
36
36
 
37
37
  const ENDPOINT = "/frame/stream";
38
38
 
39
- /**
40
- * Configuration options for creating a new streamer.
41
- */
42
- export interface StreamerConfig {
39
+ const intermediateStreamerConfigZ = z.object({
43
40
  /** The channels to stream data from. Can be channel keys, names, or payloads. */
44
- channels: channel.Params;
41
+ channels: paramsZ,
45
42
  /** Optional factor to downsample the data by. Defaults to 1 (no downsampling). */
46
- downsampleFactor?: number;
47
- /** Whether to use the experimental codec for streaming. Defaults to false. */
48
- useExperimentalCodec?: boolean;
49
- }
43
+ downsampleFactor: z.number().optional().default(1),
44
+ /** Whether to use the experimental codec for streaming. Defaults to true. */
45
+ useExperimentalCodec: z.boolean().optional().default(false),
46
+ });
47
+
48
+ export const streamerConfigZ = intermediateStreamerConfigZ.or(
49
+ paramsZ.transform((channels) => intermediateStreamerConfigZ.parse({ channels })),
50
+ );
51
+
52
+ export type StreamerConfig = z.input<typeof streamerConfigZ>;
53
+ type ParsedStreamerConfig = z.output<typeof streamerConfigZ>;
50
54
 
51
55
  /**
52
56
  * A streamer is used to stream frames of telemetry in real-time from a Synnax cluster.
@@ -85,25 +89,9 @@ export interface Streamer extends AsyncIterator<Frame>, AsyncIterable<Frame> {
85
89
  * A function that opens a streamer.
86
90
  */
87
91
  export interface StreamOpener {
88
- (config: StreamerConfig | channel.Params): Promise<Streamer>;
92
+ (config: StreamerConfig): Promise<Streamer>;
89
93
  }
90
94
 
91
- export const parseStreamerConfig = (
92
- config: StreamerConfig | channel.Params,
93
- ): StreamerConfig => {
94
- if (Array.isArray(config)) {
95
- if (typeof config[0] === "object")
96
- return {
97
- channels: (config as channel.Payload[]).map((c) => c.key),
98
- downsampleFactor: 1,
99
- };
100
- return { channels: config, downsampleFactor: 1 };
101
- }
102
- const parsed = payloadZ.safeParse(config);
103
- if (parsed.success) return { channels: [parsed.data.key], downsampleFactor: 1 };
104
- return config as StreamerConfig;
105
- };
106
-
107
95
  /**
108
96
  * Creates a function that opens streamers with the given retriever and client.
109
97
  * @param retriever - The channel retriever to use for resolving channel information
@@ -113,7 +101,7 @@ export const parseStreamerConfig = (
113
101
  export const createStreamOpener =
114
102
  (retriever: channel.Retriever, client: WebSocketClient): StreamOpener =>
115
103
  async (config) => {
116
- const cfg = parseStreamerConfig(config);
104
+ const cfg = streamerConfigZ.parse(config);
117
105
  const adapter = await ReadAdapter.open(retriever, cfg.channels);
118
106
  if (cfg.useExperimentalCodec)
119
107
  client = client.withCodec(new WSStreamerCodec(adapter.codec));
@@ -168,7 +156,8 @@ class CoreStreamer implements Streamer {
168
156
  }
169
157
 
170
158
  async update(channels: channel.Params): Promise<void> {
171
- await this.adapter.update(channels);
159
+ const hasChanged = await this.adapter.update(channels);
160
+ if (!hasChanged) return;
172
161
  this.stream.send({
173
162
  keys: this.adapter.keys,
174
163
  downsampleFactor: this.downsampleFactor,
@@ -193,15 +182,15 @@ export class HardenedStreamer implements Streamer {
193
182
  private wrapped_: Streamer | null = null;
194
183
  private readonly breaker: breaker.Breaker;
195
184
  private readonly opener: StreamOpener;
196
- private readonly config: StreamerConfig;
185
+ private readonly config: ParsedStreamerConfig;
197
186
 
198
187
  private constructor(
199
188
  opener: StreamOpener,
200
- config: StreamerConfig | channel.Params,
189
+ config: StreamerConfig,
201
190
  breakerConfig: breaker.Config = {},
202
191
  ) {
203
192
  this.opener = opener;
204
- this.config = parseStreamerConfig(config);
193
+ this.config = streamerConfigZ.parse(config);
205
194
  const {
206
195
  maxRetries = 5000,
207
196
  baseInterval = TimeSpan.seconds(1),
@@ -218,12 +207,11 @@ export class HardenedStreamer implements Streamer {
218
207
  */
219
208
  static async open(
220
209
  opener: StreamOpener,
221
- config: StreamerConfig | channel.Params,
210
+ config: StreamerConfig,
222
211
  breakerConfig?: breaker.Config,
223
212
  ): Promise<HardenedStreamer> {
224
213
  const h = new HardenedStreamer(opener, config, breakerConfig);
225
214
  await h.runStreamer();
226
-
227
215
  return h;
228
216
  }
229
217
 
@@ -248,7 +236,7 @@ export class HardenedStreamer implements Streamer {
248
236
  }
249
237
 
250
238
  async update(channels: channel.Params): Promise<void> {
251
- this.config.channels = channels;
239
+ this.config.channels = paramsZ.parse(channels);
252
240
  try {
253
241
  await this.wrapped.update(channels);
254
242
  } catch {
@@ -314,6 +302,10 @@ export class ObservableStreamer<V = Frame>
314
302
  this.closePromise = this.stream();
315
303
  }
316
304
 
305
+ async update(channels: channel.Params): Promise<void> {
306
+ await this.streamer.update(channels);
307
+ }
308
+
317
309
  async close(): Promise<void> {
318
310
  this.streamer.close();
319
311
  await this.closePromise;
@@ -12,12 +12,13 @@ import { describe, expect, it, test } from "vitest";
12
12
 
13
13
  import { UnauthorizedError, ValidationError } from "@/errors";
14
14
  import { ALWAYS_INDEX_PERSIST_ON_AUTO_COMMIT, WriterMode } from "@/framer/writer";
15
- import { newClient } from "@/setupspecs";
16
15
  import { newIndexedPair } from "@/testutil/channels";
16
+ import { createTestClient } from "@/testutil/client";
17
17
  import { secondsLinspace } from "@/testutil/telem";
18
18
  import { randomSeries } from "@/util/telem";
19
19
 
20
- const client = newClient();
20
+ const client = createTestClient();
21
+
21
22
  describe("Writer", () => {
22
23
  describe("Writer", () => {
23
24
  test("basic write", async () => {
@@ -34,7 +35,7 @@ describe("Writer", () => {
34
35
  } finally {
35
36
  await writer.close();
36
37
  }
37
- expect(true).toBeTruthy();
38
+ expect(true).toBe(true);
38
39
  });
39
40
 
40
41
  test("write to unknown channel key", async () => {
@@ -87,7 +88,7 @@ describe("Writer", () => {
87
88
  } finally {
88
89
  await writer.close();
89
90
  }
90
- expect(true).toBeTruthy();
91
+ expect(true).toBe(true);
91
92
 
92
93
  const f = await client.read(
93
94
  new TimeRange(TimeStamp.seconds(1), TimeStamp.seconds(11)),
@@ -113,7 +114,7 @@ describe("Writer", () => {
113
114
  } finally {
114
115
  await writer.close();
115
116
  }
116
- expect(true).toBeTruthy();
117
+ expect(true).toBe(true);
117
118
  });
118
119
 
119
120
  test("write with auto commit and a set interval", async () => {
@@ -133,7 +134,7 @@ describe("Writer", () => {
133
134
  } finally {
134
135
  await writer.close();
135
136
  }
136
- expect(true).toBeTruthy();
137
+ expect(true).toBe(true);
137
138
  });
138
139
 
139
140
  test("write with auto-commit off and incorrect data length validation error", async () => {