@synnaxlabs/client 0.42.3 → 0.43.1

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 +28499 -9313
  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 +20 -21
  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
@@ -9,59 +9,129 @@
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";
12
+ import { z } from "zod/v4";
13
13
 
14
14
  import { type channel } from "@/channel";
15
15
  import { ReadAdapter } from "@/framer/adapter";
16
16
  import { WSStreamerCodec } from "@/framer/codec";
17
17
  import { Frame, frameZ } from "@/framer/frame";
18
18
  import { StreamProxy } from "@/framer/streamProxy";
19
+ import { payloadZ } from "@/ranger/payload";
19
20
 
20
- const reqZ = z.object({ keys: z.number().array(), downSampleFactor: z.number() });
21
+ const reqZ = z.object({ keys: z.number().array(), downsampleFactor: z.number() });
21
22
 
22
- export type StreamerRequest = z.infer<typeof reqZ>;
23
+ /**
24
+ * Request interface for streaming frames from a Synnax cluster.
25
+ * Contains the keys of channels to stream from and a downsample factor.
26
+ */
27
+ export interface StreamerRequest extends z.infer<typeof reqZ> {}
23
28
 
24
29
  const resZ = z.object({ frame: frameZ });
25
30
 
26
- export type StreamerResponse = z.infer<typeof resZ>;
31
+ /**
32
+ * Response interface for streaming frames from a Synnax cluster.
33
+ * Contains a frame of telemetry data.
34
+ */
35
+ export interface StreamerResponse extends z.infer<typeof resZ> {}
27
36
 
28
37
  const ENDPOINT = "/frame/stream";
29
38
 
39
+ /**
40
+ * Configuration options for creating a new streamer.
41
+ */
30
42
  export interface StreamerConfig {
43
+ /** The channels to stream data from. Can be channel keys, names, or payloads. */
31
44
  channels: channel.Params;
32
- downSampleFactor?: number;
45
+ /** 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. */
33
48
  useExperimentalCodec?: boolean;
34
49
  }
35
50
 
51
+ /**
52
+ * A streamer is used to stream frames of telemetry in real-time from a Synnax cluster.
53
+ * It should not be constructed directly, and should instead be created using the
54
+ * client's openStreamer method.
55
+ *
56
+ * To open a streamer, use the openStreamer method on the client and pass it in the list
57
+ * of channels you'd like to receive data from. Once the streamer has been opened, call
58
+ * the `read` method to read the next frame of telemetry, or use the streamer as an
59
+ * async iterator to iterate over the frames of telemetry as they are received.
60
+ *
61
+ * The list of channels being streamed can be updated at any time by using the `update`
62
+ * method.
63
+ *
64
+ * Once done, call the `close` method to close the streamer and free all associated
65
+ * resources. We recommend using the streamer within a try-finally block to ensure
66
+ * that it is closed properly in the event of an error.
67
+ *
68
+ * For details documentation, see https://docs.synnaxlabs.com/reference/typescript-client/stream-data
69
+ */
36
70
  export interface Streamer extends AsyncIterator<Frame>, AsyncIterable<Frame> {
71
+ /** The keys of the channels currently being streamed from. */
37
72
  keys: channel.Key[];
73
+ /**
74
+ * Update the list of channels being streamed from. This replaces the list of channels
75
+ * being streamed from with the new list of channels.
76
+ */
38
77
  update: (channels: channel.Params) => Promise<void>;
78
+ /** Close the streamer and free all associated resources. */
39
79
  close: () => void;
80
+ /** Read the next frame of telemetry. */
40
81
  read: () => Promise<Frame>;
41
82
  }
42
83
 
84
+ /**
85
+ * A function that opens a streamer.
86
+ */
43
87
  export interface StreamOpener {
44
88
  (config: StreamerConfig | channel.Params): Promise<Streamer>;
45
89
  }
46
90
 
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
+ /**
108
+ * Creates a function that opens streamers with the given retriever and client.
109
+ * @param retriever - The channel retriever to use for resolving channel information
110
+ * @param client - The WebSocket client to use for streaming
111
+ * @returns A function that opens streamers with the given configuration
112
+ */
47
113
  export const createStreamOpener =
48
114
  (retriever: channel.Retriever, client: WebSocketClient): StreamOpener =>
49
115
  async (config) => {
50
- let cfg: StreamerConfig;
51
- if (Array.isArray(config) || typeof config !== "object")
52
- cfg = { channels: config as channel.Params, downSampleFactor: 1 };
53
- else cfg = config as StreamerConfig;
116
+ const cfg = parseStreamerConfig(config);
54
117
  const adapter = await ReadAdapter.open(retriever, cfg.channels);
55
118
  if (cfg.useExperimentalCodec)
56
119
  client = client.withCodec(new WSStreamerCodec(adapter.codec));
57
120
  const stream = await client.stream(ENDPOINT, reqZ, resZ);
58
121
  const streamer = new CoreStreamer(stream, adapter);
59
- stream.send({ keys: adapter.keys, downSampleFactor: cfg.downSampleFactor ?? 1 });
122
+ stream.send({ keys: adapter.keys, downsampleFactor: cfg.downsampleFactor ?? 1 });
60
123
  const [, err] = await stream.receive();
61
124
  if (err != null) throw err;
62
125
  return streamer;
63
126
  };
64
127
 
128
+ /**
129
+ * Opens a new streamer with the given configuration.
130
+ * @param retriever - The channel retriever to use for resolving channel information
131
+ * @param client - The WebSocket client to use for streaming
132
+ * @param config - The configuration for the streamer
133
+ * @returns A promise that resolves to a new streamer
134
+ */
65
135
  export const openStreamer = async (
66
136
  retriever: channel.Retriever,
67
137
  client: WebSocketClient,
@@ -101,7 +171,7 @@ class CoreStreamer implements Streamer {
101
171
  await this.adapter.update(channels);
102
172
  this.stream.send({
103
173
  keys: this.adapter.keys,
104
- downSampleFactor: this.downsampleFactor,
174
+ downsampleFactor: this.downsampleFactor,
105
175
  });
106
176
  }
107
177
 
@@ -114,30 +184,46 @@ class CoreStreamer implements Streamer {
114
184
  }
115
185
  }
116
186
 
187
+ /**
188
+ * A hardened streamer that automatically reconnects on failure.
189
+ * This streamer wraps a regular streamer and adds automatic reconnection
190
+ * logic when the connection is lost or errors occur.
191
+ */
117
192
  export class HardenedStreamer implements Streamer {
118
193
  private wrapped_: Streamer | null = null;
119
194
  private readonly breaker: breaker.Breaker;
120
195
  private readonly opener: StreamOpener;
121
196
  private readonly config: StreamerConfig;
122
197
 
123
- private constructor(opener: StreamOpener, config: StreamerConfig | channel.Params) {
198
+ private constructor(
199
+ opener: StreamOpener,
200
+ config: StreamerConfig | channel.Params,
201
+ breakerConfig: breaker.Config = {},
202
+ ) {
124
203
  this.opener = opener;
125
- if (Array.isArray(config) || typeof config !== "object")
126
- this.config = { channels: config as channel.Params, downSampleFactor: 1 };
127
- else this.config = config as StreamerConfig;
128
- this.breaker = new breaker.Breaker({
129
- maxRetries: 5000,
130
- baseInterval: TimeSpan.seconds(1),
131
- scale: 1,
132
- });
204
+ this.config = parseStreamerConfig(config);
205
+ const {
206
+ maxRetries = 5000,
207
+ baseInterval = TimeSpan.seconds(1),
208
+ scale = 1,
209
+ } = breakerConfig ?? {};
210
+ this.breaker = new breaker.Breaker({ maxRetries, baseInterval, scale });
133
211
  }
134
212
 
213
+ /**
214
+ * Opens a new hardened streamer with the given configuration.
215
+ * @param opener - The function to use for opening streamers
216
+ * @param config - The configuration for the streamer
217
+ * @returns A promise that resolves to a new hardened streamer
218
+ */
135
219
  static async open(
136
220
  opener: StreamOpener,
137
221
  config: StreamerConfig | channel.Params,
222
+ breakerConfig?: breaker.Config,
138
223
  ): Promise<HardenedStreamer> {
139
- const h = new HardenedStreamer(opener, config);
224
+ const h = new HardenedStreamer(opener, config, breakerConfig);
140
225
  await h.runStreamer();
226
+
141
227
  return h;
142
228
  }
143
229
 
@@ -151,6 +237,7 @@ export class HardenedStreamer implements Streamer {
151
237
  } catch (e) {
152
238
  this.wrapped_ = null;
153
239
  if (!(await this.breaker.wait())) throw e;
240
+ console.error("failed to open streamer", e);
154
241
  continue;
155
242
  }
156
243
  }
@@ -203,6 +290,10 @@ export class HardenedStreamer implements Streamer {
203
290
  }
204
291
  }
205
292
 
293
+ /**
294
+ * Wraps a standard streamer to implement an observable interface for handling changes
295
+ * to channel values through an onChange handler.
296
+ */
206
297
  export class ObservableStreamer<V = Frame>
207
298
  extends observe.Observer<Frame, V>
208
299
  implements observe.ObservableAsyncCloseable<V>
@@ -210,6 +301,13 @@ export class ObservableStreamer<V = Frame>
210
301
  private readonly streamer: Streamer;
211
302
  private readonly closePromise: Promise<void>;
212
303
 
304
+ /**
305
+ * Creates a new observable streamer.
306
+ * @param streamer - The streamer to wrap
307
+ * @param transform - An optional transform function to apply to each frame
308
+ * @template V - The type of the transformed value. Only relevant if transform is
309
+ * provided. Defaults to Frame.
310
+ */
213
311
  constructor(streamer: Streamer, transform?: observe.Transform<Frame, V>) {
214
312
  super(transform);
215
313
  this.streamer = streamer;
@@ -13,7 +13,7 @@ import { describe, expect, it, test } from "vitest";
13
13
  import { UnauthorizedError, ValidationError } from "@/errors";
14
14
  import { ALWAYS_INDEX_PERSIST_ON_AUTO_COMMIT, WriterMode } from "@/framer/writer";
15
15
  import { newClient } from "@/setupspecs";
16
- import { newIndexedPair } from "@/testutil/indexedPair";
16
+ import { newIndexedPair } from "@/testutil/channels";
17
17
  import { secondsLinspace } from "@/testutil/telem";
18
18
  import { randomSeries } from "@/util/telem";
19
19
 
@@ -8,21 +8,20 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { EOF, type Stream, type WebSocketClient } from "@synnaxlabs/freighter";
11
- import { control, errors } from "@synnaxlabs/x";
11
+ import { array, control, errors } from "@synnaxlabs/x";
12
12
  import {
13
13
  type CrudeSeries,
14
14
  type CrudeTimeStamp,
15
15
  TimeSpan,
16
16
  TimeStamp,
17
17
  } from "@synnaxlabs/x/telem";
18
- import { toArray } from "@synnaxlabs/x/toArray";
19
- import { z } from "zod";
18
+ import { z } from "zod/v4";
20
19
 
21
20
  import { channel } from "@/channel";
22
21
  import { SynnaxError } from "@/errors";
23
22
  import { WriteAdapter } from "@/framer/adapter";
24
23
  import { WSWriterCodec } from "@/framer/codec";
25
- import { type Crude, frameZ } from "@/framer/frame";
24
+ import { type CrudeFrame, frameZ } from "@/framer/frame";
26
25
 
27
26
  export enum WriterCommand {
28
27
  Open = 0,
@@ -194,7 +193,7 @@ export class Writer {
194
193
  start: new TimeStamp(start),
195
194
  keys: adapter.keys,
196
195
  controlSubject: subject,
197
- authorities: toArray(authorities),
196
+ authorities: array.toArray(authorities),
198
197
  mode: constructWriterMode(mode),
199
198
  errOnUnauthorized,
200
199
  enableAutoCommit,
@@ -206,9 +205,14 @@ export class Writer {
206
205
 
207
206
  async write(channel: channel.KeyOrName, data: CrudeSeries): Promise<void>;
208
207
  async write(channel: channel.KeysOrNames, data: CrudeSeries[]): Promise<void>;
209
- async write(frame: Crude | Record<channel.KeyOrName, CrudeSeries>): Promise<void>;
210
208
  async write(
211
- channelsOrData: channel.Params | Record<channel.KeyOrName, CrudeSeries> | Crude,
209
+ frame: CrudeFrame | Record<channel.KeyOrName, CrudeSeries>,
210
+ ): Promise<void>;
211
+ async write(
212
+ channelsOrData:
213
+ | channel.Params
214
+ | Record<channel.KeyOrName, CrudeSeries>
215
+ | CrudeFrame,
212
216
  series?: CrudeSeries | CrudeSeries[],
213
217
  ): Promise<void>;
214
218
 
@@ -227,7 +231,10 @@ export class Writer {
227
231
  * should acknowledge the error by calling the error method or closing the writer.
228
232
  */
229
233
  async write(
230
- channelsOrData: channel.Params | Record<channel.KeyOrName, CrudeSeries> | Crude,
234
+ channelsOrData:
235
+ | channel.Params
236
+ | Record<channel.KeyOrName, CrudeSeries>
237
+ | CrudeFrame,
231
238
  series?: CrudeSeries | CrudeSeries[],
232
239
  ): Promise<void> {
233
240
  if (this.closeErr != null) throw this.closeErr;
@@ -8,9 +8,9 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
- import { toArray, type UnknownRecord } from "@synnaxlabs/x";
11
+ import { array, type UnknownRecord } from "@synnaxlabs/x";
12
12
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
13
- import { z } from "zod";
13
+ import { z } from "zod/v4";
14
14
 
15
15
  import { framer } from "@/framer";
16
16
  import {
@@ -118,7 +118,7 @@ export class Client implements AsyncTermSearcher<string, Key, Device> {
118
118
  const res = await sendRequired(
119
119
  this.client,
120
120
  RETRIEVE_ENDPOINT,
121
- { keys: toArray(keys), ...options },
121
+ { keys: array.toArray(keys), ...options },
122
122
  retrieveReqZ,
123
123
  retrieveResZ,
124
124
  );
@@ -173,7 +173,7 @@ export class Client implements AsyncTermSearcher<string, Key, Device> {
173
173
  const res = await sendRequired(
174
174
  this.client,
175
175
  CREATE_ENDPOINT,
176
- { devices: toArray(devices) },
176
+ { devices: array.toArray(devices) },
177
177
  createReqZ,
178
178
  createResZ,
179
179
  );
@@ -186,7 +186,7 @@ export class Client implements AsyncTermSearcher<string, Key, Device> {
186
186
  await sendRequired(
187
187
  this.client,
188
188
  DELETE_ENDPOINT,
189
- { keys: toArray(keys) },
189
+ { keys: array.toArray(keys) },
190
190
  deleteReqZ,
191
191
  deleteResZ,
192
192
  );
@@ -19,16 +19,17 @@ describe("Device", async () => {
19
19
  const testRack = await client.hardware.racks.create({ name: "test" });
20
20
  describe("create", () => {
21
21
  it("should create a device on a rack", async () => {
22
+ const key = id.create();
22
23
  const d = await client.hardware.devices.create({
23
24
  rack: testRack.key,
24
25
  location: "Dev1",
25
- key: "SN222",
26
+ key,
26
27
  name: "test",
27
28
  make: "ni",
28
29
  model: "dog",
29
30
  properties: { cat: "dog" },
30
31
  });
31
- expect(d.key).toEqual("SN222");
32
+ expect(d.key).toEqual(key);
32
33
  expect(d.name).toBe("test");
33
34
  expect(d.make).toBe("ni");
34
35
  });
@@ -41,7 +42,7 @@ describe("Device", async () => {
41
42
  outputChannels: [{ port2: 232 }],
42
43
  };
43
44
  const d = await client.hardware.devices.create({
44
- key: "SN222",
45
+ key: id.create(),
45
46
  rack: testRack.key,
46
47
  location: "Dev1",
47
48
  name: "test",
@@ -55,7 +56,7 @@ describe("Device", async () => {
55
56
  describe("retrieve", () => {
56
57
  it("should retrieve a device by its key", async () => {
57
58
  const d = await client.hardware.devices.create({
58
- key: "SN222",
59
+ key: id.create(),
59
60
  rack: testRack.key,
60
61
  location: "Dev1",
61
62
  name: "test",
@@ -112,7 +113,7 @@ describe("Device", async () => {
112
113
  describe("state", () => {
113
114
  it("should not include state by default", async () => {
114
115
  const d = await client.hardware.devices.create({
115
- key: "SN_STATE_TEST1",
116
+ key: id.create(),
116
117
  rack: testRack.key,
117
118
  location: "Dev1",
118
119
  name: "state_test1",
@@ -122,12 +123,12 @@ describe("Device", async () => {
122
123
  });
123
124
 
124
125
  const retrieved = await client.hardware.devices.retrieve(d.key);
125
- expect(retrieved.state?.key).toHaveLength(0);
126
+ expect(retrieved.state).toBeUndefined();
126
127
  });
127
128
 
128
129
  it("should include state when includeState is true", async () => {
129
130
  const d = await client.hardware.devices.create({
130
- key: "SN_STATE_TEST2",
131
+ key: id.create(),
131
132
  rack: testRack.key,
132
133
  location: "Dev1",
133
134
  name: "state_test2",
@@ -136,20 +137,19 @@ describe("Device", async () => {
136
137
  properties: { cat: "dog" },
137
138
  });
138
139
 
139
- const retrieved = await client.hardware.devices.retrieve(d.key, {
140
- includeState: true,
141
- });
142
- expect(retrieved.state).toBeDefined();
143
- if (retrieved.state) {
144
- expect(retrieved.state.variant).toBeDefined();
145
- expect(retrieved.state.key).toBeDefined();
146
- expect(retrieved.state.details).toBeDefined();
147
- }
140
+ await expect
141
+ .poll(async () => {
142
+ const { state } = await client.hardware.devices.retrieve(d.key, {
143
+ includeState: true,
144
+ });
145
+ return state !== undefined;
146
+ })
147
+ .toBeTruthy();
148
148
  });
149
149
 
150
150
  it("should include state for multiple devices", async () => {
151
151
  const d1 = await client.hardware.devices.create({
152
- key: "SN_STATE_TEST3",
152
+ key: id.create(),
153
153
  rack: testRack.key,
154
154
  location: "Dev1",
155
155
  name: "state_test3",
@@ -159,7 +159,7 @@ describe("Device", async () => {
159
159
  });
160
160
 
161
161
  const d2 = await client.hardware.devices.create({
162
- key: "SN_STATE_TEST4",
162
+ key: id.create(),
163
163
  rack: testRack.key,
164
164
  location: "Dev2",
165
165
  name: "state_test4",
@@ -168,18 +168,16 @@ describe("Device", async () => {
168
168
  properties: { cat: "dog" },
169
169
  });
170
170
 
171
- const retrieved = await client.hardware.devices.retrieve([d1.key, d2.key], {
172
- includeState: true,
173
- });
174
- expect(retrieved).toHaveLength(2);
175
- retrieved.forEach((device) => {
176
- expect(device.state).toBeDefined();
177
- if (device.state) {
178
- expect(device.state.variant).toBeDefined();
179
- expect(device.state.key).toBeDefined();
180
- expect(device.state.details).toBeDefined();
181
- }
182
- });
171
+ await expect
172
+ .poll(async () => {
173
+ const retrievedDevices = await client.hardware.devices.retrieve(
174
+ [d1.key, d2.key],
175
+ { includeState: true },
176
+ );
177
+ if (retrievedDevices.length !== 2) return false;
178
+ return retrievedDevices.every(({ state }) => state !== undefined);
179
+ })
180
+ .toBeTruthy();
183
181
  });
184
182
 
185
183
  it("should handle state with type-safe details", async () => {
@@ -7,8 +7,8 @@
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 { binary, type UnknownRecord, unknownRecordZ } from "@synnaxlabs/x";
11
- import { z } from "zod";
10
+ import { binary, status, type UnknownRecord, unknownRecordZ, zod } from "@synnaxlabs/x";
11
+ import { z } from "zod/v4";
12
12
 
13
13
  import { keyZ as rackKeyZ } from "@/hardware/rack/payload";
14
14
  import { decodeJSONString } from "@/util/decodeJSONString";
@@ -18,7 +18,7 @@ export type Key = z.infer<typeof keyZ>;
18
18
 
19
19
  export const stateZ = z.object({
20
20
  key: keyZ,
21
- variant: z.string(),
21
+ variant: status.variantZ.or(z.literal("").transform<status.Variant>(() => "info")),
22
22
  details: unknownRecordZ.or(z.string().transform(decodeJSONString)),
23
23
  });
24
24
 
@@ -36,7 +36,7 @@ export const deviceZ = z.object({
36
36
  location: z.string(),
37
37
  configured: z.boolean().optional(),
38
38
  properties: unknownRecordZ.or(z.string().transform(decodeJSONString)),
39
- state: stateZ.optional(),
39
+ state: zod.nullToUndefined(stateZ),
40
40
  });
41
41
 
42
42
  export interface Device<
@@ -44,7 +44,7 @@ export interface Device<
44
44
  Make extends string = string,
45
45
  Model extends string = string,
46
46
  StateDetails extends {} = UnknownRecord,
47
- > extends Omit<z.output<typeof deviceZ>, "properties" | "state"> {
47
+ > extends Omit<z.infer<typeof deviceZ>, "properties" | "state"> {
48
48
  properties: Properties;
49
49
  make: Make;
50
50
  model: Model;
@@ -9,9 +9,9 @@
9
9
 
10
10
  import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { type UnknownRecord } from "@synnaxlabs/x";
12
+ import { array } from "@synnaxlabs/x/array";
12
13
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
13
- import { toArray } from "@synnaxlabs/x/toArray";
14
- import { z } from "zod";
14
+ import { z } from "zod/v4";
15
15
 
16
16
  import { framer } from "@/framer";
17
17
  import {
@@ -81,7 +81,7 @@ export class Client implements AsyncTermSearcher<string, Key, Payload> {
81
81
  await sendRequired<typeof deleteReqZ, typeof deleteResZ>(
82
82
  this.client,
83
83
  DELETE_ENDPOINT,
84
- { keys: toArray(keys) },
84
+ { keys: array.toArray(keys) },
85
85
  deleteReqZ,
86
86
  deleteResZ,
87
87
  );
@@ -94,7 +94,7 @@ export class Client implements AsyncTermSearcher<string, Key, Payload> {
94
94
  const res = await sendRequired<typeof createReqZ, typeof createResZ>(
95
95
  this.client,
96
96
  CREATE_ENDPOINT,
97
- { racks: toArray(rack) },
97
+ { racks: array.toArray(rack) },
98
98
  createReqZ,
99
99
  createResZ,
100
100
  );
@@ -7,18 +7,18 @@
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 { zod } from "@synnaxlabs/x";
10
+ import { status, zod } from "@synnaxlabs/x";
11
11
  import { TimeStamp } from "@synnaxlabs/x/telem";
12
- import { z } from "zod";
12
+ import { z } from "zod/v4";
13
13
 
14
- export const keyZ = zod.uint32;
14
+ export const keyZ = z.uint32();
15
15
  export type Key = z.infer<typeof keyZ>;
16
16
 
17
17
  export const stateZ = z.object({
18
18
  key: keyZ,
19
- variant: z.string(),
19
+ variant: status.variantZ.or(z.literal("").transform<status.Variant>(() => "info")),
20
20
  message: z.string(),
21
- lastReceived: TimeStamp.z.optional(),
21
+ lastReceived: TimeStamp.z,
22
22
  });
23
23
 
24
24
  export interface State extends z.infer<typeof stateZ> {}
@@ -26,7 +26,7 @@ export interface State extends z.infer<typeof stateZ> {}
26
26
  export const rackZ = z.object({
27
27
  key: keyZ,
28
28
  name: z.string(),
29
- state: stateZ.optional(),
29
+ state: zod.nullToUndefined(stateZ),
30
30
  });
31
31
 
32
32
  export interface Payload extends z.infer<typeof rackZ> {}
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { TimeStamp } from "@synnaxlabs/x";
11
11
  import { describe, expect, it } from "vitest";
12
- import { ZodError } from "zod";
12
+ import { ZodError } from "zod/v4";
13
13
 
14
14
  import { NotFoundError } from "@/errors";
15
15
  import { newClient } from "@/setupspecs";
@@ -9,11 +9,11 @@
9
9
 
10
10
  import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { id } from "@synnaxlabs/x";
12
+ import { array } from "@synnaxlabs/x/array";
12
13
  import { type UnknownRecord } from "@synnaxlabs/x/record";
13
14
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
14
15
  import { type CrudeTimeSpan, TimeSpan } from "@synnaxlabs/x/telem";
15
- import { toArray } from "@synnaxlabs/x/toArray";
16
- import { z } from "zod";
16
+ import { z } from "zod/v4";
17
17
 
18
18
  import { framer } from "@/framer";
19
19
  import { keyZ as rackKeyZ } from "@/hardware/rack/payload";
@@ -264,7 +264,7 @@ export class Client implements AsyncTermSearcher<string, Key, Payload> {
264
264
  const res = await sendRequired<typeof createReqZ, typeof createResZ>(
265
265
  this.client,
266
266
  CREATE_ENDPOINT,
267
- { tasks: toArray(task) },
267
+ { tasks: array.toArray(task) },
268
268
  createReqZ,
269
269
  createResZ,
270
270
  );
@@ -276,7 +276,7 @@ export class Client implements AsyncTermSearcher<string, Key, Payload> {
276
276
  await sendRequired<typeof deleteReqZ, typeof deleteResZ>(
277
277
  this.client,
278
278
  DELETE_ENDPOINT,
279
- { keys: toArray(keys) },
279
+ { keys: array.toArray(keys) },
280
280
  deleteReqZ,
281
281
  deleteResZ,
282
282
  );
@@ -393,21 +393,23 @@ export class Client implements AsyncTermSearcher<string, Key, Payload> {
393
393
 
394
394
  sugar(payloads: Payload | Payload[]): Task | Task[] {
395
395
  const isSingle = !Array.isArray(payloads);
396
- const res = toArray(payloads).map(
397
- ({ key, name, type, config, state, internal, snapshot }) =>
398
- new Task(
399
- key,
400
- name,
401
- type,
402
- config,
403
- internal,
404
- snapshot,
405
- state,
406
- this.frameClient,
407
- this.ontologyClient,
408
- this.rangeClient,
409
- ),
410
- );
396
+ const res = array
397
+ .toArray(payloads)
398
+ .map(
399
+ ({ key, name, type, config, state, internal, snapshot }) =>
400
+ new Task(
401
+ key,
402
+ name,
403
+ type,
404
+ config,
405
+ internal,
406
+ snapshot,
407
+ state,
408
+ this.frameClient,
409
+ this.ontologyClient,
410
+ this.rangeClient,
411
+ ),
412
+ );
411
413
  return isSingle ? res[0] : res;
412
414
  }
413
415