@synnaxlabs/client 0.43.0 → 0.44.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 (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 +12 -13
  13. package/dist/channel/client.d.ts.map +1 -1
  14. package/dist/channel/payload.d.ts +77 -19
  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 +8619 -28938
  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 +139 -20
  45. package/dist/framer/streamer.d.ts.map +1 -1
  46. package/dist/framer/writer.d.ts +222 -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 +113 -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 +11 -5
  79. package/dist/ranger/alias.d.ts.map +1 -1
  80. package/dist/ranger/client.d.ts +87 -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 +10 -12
  85. package/dist/ranger/kv.d.ts.map +1 -1
  86. package/dist/ranger/payload.d.ts +23 -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 +31 -46
  140. package/src/channel/payload.ts +13 -14
  141. package/src/channel/retriever.ts +26 -41
  142. package/src/channel/writer.ts +3 -3
  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 +6 -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 +74 -112
  187. package/src/ontology/writer.ts +8 -17
  188. package/src/ranger/alias.ts +19 -37
  189. package/src/ranger/client.ts +118 -150
  190. package/src/ranger/external.ts +9 -1
  191. package/src/ranger/kv.ts +6 -27
  192. package/src/ranger/payload.ts +21 -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
package/src/errors.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { type Middleware, Unreachable } from "@synnaxlabs/freighter";
11
- import { errors } from "@synnaxlabs/x";
11
+ import { array, errors } from "@synnaxlabs/x";
12
+ import { z } from "zod";
12
13
 
13
14
  export class SynnaxError extends errors.createTyped("sy") {}
14
15
 
@@ -17,14 +18,24 @@ export class SynnaxError extends errors.createTyped("sy") {}
17
18
  */
18
19
  export class ValidationError extends SynnaxError.sub("validation") {}
19
20
 
20
- export class FieldError extends ValidationError.sub("field") {
21
- readonly field: string;
22
- readonly message: string;
21
+ export class PathError extends ValidationError.sub("path") {
22
+ readonly path: string[];
23
+ readonly error: Error;
24
+ static readonly encodedSchema = z.object({
25
+ path: z.string().array(),
26
+ error: errors.payloadZ,
27
+ });
28
+
29
+ constructor(path: string | string[], error: Error) {
30
+ const arrPath = array.toArray(path);
31
+ super(`${arrPath.join(".")}: ${error.message}`);
32
+ this.path = arrPath.flatMap((p) => p.split("."));
33
+ this.error = error;
34
+ }
23
35
 
24
- constructor(field: string, message: string) {
25
- super(`${field}: ${message}`);
26
- this.field = field;
27
- this.message = message;
36
+ static decode(payload: errors.Payload): PathError {
37
+ const decoded = PathError.encodedSchema.parse(JSON.parse(payload.data));
38
+ return new PathError(decoded.path, errors.decode(decoded.error) as Error);
28
39
  }
29
40
  }
30
41
 
@@ -80,6 +91,12 @@ export class ControlError extends SynnaxError.sub("control") {}
80
91
 
81
92
  export class UnauthorizedError extends ControlError.sub("unauthorized") {}
82
93
 
94
+ export class DisconnectedError extends SynnaxError.sub("disconnected") {
95
+ constructor(message: string = "Operation failed because no cluster is connected.") {
96
+ super(message);
97
+ }
98
+ }
99
+
83
100
  /**
84
101
  * Raised when time-series data is not contiguous.
85
102
  */
@@ -88,11 +105,7 @@ export class ContiguityError extends SynnaxError.sub("contiguity") {}
88
105
  const decode = (payload: errors.Payload): Error | null => {
89
106
  if (!payload.type.startsWith(SynnaxError.TYPE)) return null;
90
107
  if (payload.type.startsWith(ValidationError.TYPE)) {
91
- if (payload.type === FieldError.TYPE) {
92
- const values = payload.data.split(": ");
93
- if (values.length < 2) return new ValidationError(payload.data);
94
- return new FieldError(values[0], values[1]);
95
- }
108
+ if (payload.type === PathError.TYPE) return PathError.decode(payload);
96
109
  return new ValidationError(payload.data);
97
110
  }
98
111
 
@@ -128,7 +141,7 @@ const decode = (payload: errors.Payload): Error | null => {
128
141
  };
129
142
 
130
143
  const encode = (): errors.Payload => {
131
- throw new Error("Not implemented");
144
+ throw new errors.NotImplemented();
132
145
  };
133
146
 
134
147
  errors.register({ encode, decode });
@@ -138,7 +151,7 @@ export const validateFieldNotNull = (
138
151
  value: unknown,
139
152
  message: string = "must be provided",
140
153
  ): void => {
141
- if (value == null) throw new FieldError(key, message);
154
+ if (value == null) throw new PathError(key, new ValidationError(message));
142
155
  };
143
156
 
144
157
  export const errorsMiddleware: Middleware = async (ctx, next) => {
@@ -11,11 +11,11 @@ import { DataType, Series, TimeStamp } from "@synnaxlabs/x/telem";
11
11
  import { beforeAll, describe, expect, it } from "vitest";
12
12
 
13
13
  import { type channel } from "@/channel";
14
- import { WriteAdapter } from "@/framer/adapter";
14
+ import { ReadAdapter, WriteAdapter } from "@/framer/adapter";
15
15
  import { Frame } from "@/index";
16
- import { newClient } from "@/setupspecs";
16
+ import { newTestClient } from "@/testutil/client";
17
17
 
18
- const client = newClient();
18
+ const client = newTestClient();
19
19
 
20
20
  describe("WriteFrameAdapter", () => {
21
21
  let timeCh: channel.Channel;
@@ -160,14 +160,123 @@ describe("WriteFrameAdapter", () => {
160
160
  expect(res.get(bigIntCh.key).at(0)).toEqual(12n);
161
161
  });
162
162
 
163
- describe("adaptObjectKeys", () => {
163
+ describe("adaptParams", () => {
164
164
  it("should correctly adapt generic object keys", async () => {
165
- const res = await adapter.adaptObjectKeys({
166
- [timeCh.name]: 532,
167
- [dataCh.name]: 123,
165
+ const res = await adapter.adaptParams([timeCh.name, dataCh.name]);
166
+ expect(res).toContain(timeCh.key);
167
+ expect(res).toContain(dataCh.key);
168
+ });
169
+ });
170
+
171
+ describe("update", () => {
172
+ it("should return false when updating with the same channels", async () => {
173
+ const hasChanged = await adapter.update([timeCh.key, dataCh.key]);
174
+ expect(hasChanged).toBe(false);
175
+ });
176
+
177
+ it("should return true when adding a new channel", async () => {
178
+ const newCh = await client.channels.create({
179
+ name: `new-${Math.random()}-${TimeStamp.now().toString()}`,
180
+ dataType: DataType.FLOAT32,
181
+ index: timeCh.key,
182
+ });
183
+ const hasChanged = await adapter.update([timeCh.key, dataCh.key, newCh.key]);
184
+ expect(hasChanged).toBe(true);
185
+ });
186
+
187
+ it("should return true when removing a channel", async () => {
188
+ const hasChanged = await adapter.update([timeCh.key]);
189
+ expect(hasChanged).toBe(true);
190
+ });
191
+
192
+ it("should return true when replacing channels", async () => {
193
+ const newCh = await client.channels.create({
194
+ name: `replacement-${Math.random()}-${TimeStamp.now().toString()}`,
195
+ dataType: DataType.FLOAT32,
196
+ index: timeCh.key,
197
+ });
198
+ const hasChanged = await adapter.update([timeCh.key, newCh.key]);
199
+ expect(hasChanged).toBe(true);
200
+ });
201
+
202
+ it("should return false when updating with same channels in different order", async () => {
203
+ await adapter.update([timeCh.key, dataCh.key]);
204
+ const hasChanged = await adapter.update([dataCh.key, timeCh.key]);
205
+ expect(hasChanged).toBe(false);
206
+ });
207
+
208
+ it("should return false when updating with channel names that resolve to same keys", async () => {
209
+ await adapter.update([timeCh.key, dataCh.key]);
210
+ const hasChanged = await adapter.update([timeCh.name, dataCh.name]);
211
+ expect(hasChanged).toBe(false);
212
+ });
213
+ });
214
+ });
215
+
216
+ describe("ReadAdapter", () => {
217
+ let timeCh: channel.Channel;
218
+ let dataCh: channel.Channel;
219
+ let adapter: ReadAdapter;
220
+
221
+ beforeAll(async () => {
222
+ timeCh = await client.channels.create({
223
+ name: `read-time-${Math.random()}-${TimeStamp.now().toString()}`,
224
+ dataType: DataType.TIMESTAMP,
225
+ isIndex: true,
226
+ });
227
+ dataCh = await client.channels.create({
228
+ name: `read-data-${Math.random()}-${TimeStamp.now().toString()}`,
229
+ dataType: DataType.FLOAT32,
230
+ index: timeCh.key,
231
+ });
232
+
233
+ adapter = await ReadAdapter.open(client.channels.retriever, [
234
+ timeCh.key,
235
+ dataCh.key,
236
+ ]);
237
+ });
238
+
239
+ describe("update", () => {
240
+ it("should return false when updating with the same channels", async () => {
241
+ const hasChanged = await adapter.update([timeCh.key, dataCh.key]);
242
+ expect(hasChanged).toBe(false);
243
+ });
244
+
245
+ it("should return true when adding a new channel", async () => {
246
+ const newCh = await client.channels.create({
247
+ name: `read-new-${Math.random()}-${TimeStamp.now().toString()}`,
248
+ dataType: DataType.FLOAT32,
249
+ index: timeCh.key,
250
+ });
251
+ const hasChanged = await adapter.update([timeCh.key, dataCh.key, newCh.key]);
252
+ expect(hasChanged).toBe(true);
253
+ });
254
+
255
+ it("should return true when removing a channel", async () => {
256
+ const hasChanged = await adapter.update([timeCh.key]);
257
+ expect(hasChanged).toBe(true);
258
+ });
259
+
260
+ it("should return true when replacing channels", async () => {
261
+ const newCh = await client.channels.create({
262
+ name: `read-replacement-${Math.random()}-${TimeStamp.now().toString()}`,
263
+ dataType: DataType.FLOAT32,
264
+ index: timeCh.key,
168
265
  });
169
- expect(res).toHaveProperty(timeCh.key.toString());
170
- expect(res).toHaveProperty(dataCh.key.toString());
266
+ const hasChanged = await adapter.update([timeCh.key, newCh.key]);
267
+ expect(hasChanged).toBe(true);
268
+ });
269
+
270
+ it("should return false when updating with same channels in different order", async () => {
271
+ await adapter.update([timeCh.key, dataCh.key]);
272
+ const hasChanged = await adapter.update([dataCh.key, timeCh.key]);
273
+ expect(hasChanged).toBe(false);
274
+ });
275
+
276
+ it("should return false when updating with channel names that resolve to same keys", async () => {
277
+ await adapter.update([timeCh.key, dataCh.key]);
278
+ const hasChanged = await adapter.update([timeCh.name, dataCh.name]);
279
+ expect(hasChanged).toBe(false);
171
280
  });
172
281
  });
173
282
  });
@@ -7,6 +7,7 @@
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 { compare } from "@synnaxlabs/x";
10
11
  import { type CrudeSeries, Series } from "@synnaxlabs/x/telem";
11
12
 
12
13
  import { channel } from "@/channel";
@@ -36,17 +37,20 @@ export class ReadAdapter {
36
37
  return adapter;
37
38
  }
38
39
 
39
- async update(channels: channel.Params): Promise<void> {
40
+ async update(channels: channel.Params): Promise<boolean> {
40
41
  const { variant, normalized } = channel.analyzeParams(channels);
41
42
  const fetched = await this.retriever.retrieve(normalized);
43
+ const newKeys = fetched.map((c) => c.key);
44
+ if (compare.uniqueUnorderedPrimitiveArrays(this.keys, newKeys) === compare.EQUAL)
45
+ return false;
42
46
  this.codec.update(
43
- fetched.map((c) => c.key),
47
+ newKeys,
44
48
  fetched.map((c) => c.dataType),
45
49
  );
46
50
  if (variant === "keys") {
47
51
  this.adapter = null;
48
52
  this.keys = normalized as channel.Key[];
49
- return;
53
+ return true;
50
54
  }
51
55
  const a = new Map<channel.Key, channel.Name>();
52
56
  this.adapter = a;
@@ -56,6 +60,7 @@ export class ReadAdapter {
56
60
  a.set(channel.key, channel.name);
57
61
  });
58
62
  this.keys = Array.from(this.adapter.keys());
63
+ return true;
59
64
  }
60
65
 
61
66
  adapt(columnsOrData: Frame): Frame {
@@ -94,24 +99,32 @@ export class WriteAdapter {
94
99
  return adapter;
95
100
  }
96
101
 
97
- async adaptObjectKeys<V>(
98
- data: Record<channel.KeyOrName, V>,
99
- ): Promise<Record<channel.Key, V>> {
100
- const out: Record<channel.Key, V> = {};
101
- for (const [k, v] of Object.entries(data)) out[await this.adaptToKey(k)] = v;
102
- return out;
102
+ async adaptParams(data: channel.Params): Promise<channel.Keys> {
103
+ const arrParams = channel.paramsZ.parse(data);
104
+ const keys = await Promise.all(
105
+ arrParams.map(async (p) => await this.adaptToKey(p)),
106
+ );
107
+ return keys;
103
108
  }
104
109
 
105
- async update(channels: channel.Params): Promise<void> {
110
+ async update(channels: channel.Params): Promise<boolean> {
106
111
  const results = await channel.retrieveRequired(this.retriever, channels);
112
+ const newKeys = results.map((c) => c.key);
113
+ const previousKeySet = new Set(this.keys);
114
+ const newKeySet = new Set(newKeys);
115
+ const hasAddedKeys = !newKeySet.isSubsetOf(previousKeySet);
116
+ const hasRemovedKeys = !previousKeySet.isSubsetOf(newKeySet);
117
+ const hasChanged = hasAddedKeys || hasRemovedKeys;
118
+ if (!hasChanged) return false;
107
119
  this.adapter = new Map<channel.Name, channel.Key>(
108
120
  results.map((c) => [c.name, c.key]),
109
121
  );
110
- this.keys = results.map((c) => c.key);
122
+ this.keys = newKeys;
111
123
  this.codec.update(
112
124
  this.keys,
113
125
  results.map((c) => c.dataType),
114
126
  );
127
+ return true;
115
128
  }
116
129
 
117
130
  private async fetchChannel(
@@ -10,9 +10,9 @@
10
10
  import { TimeSpan, TimeStamp } from "@synnaxlabs/x";
11
11
  import { describe, expect, it } from "vitest";
12
12
 
13
- import { newClient } from "@/setupspecs";
13
+ import { newTestClient } from "@/testutil/client";
14
14
 
15
- const client = newClient();
15
+ const client = newTestClient();
16
16
 
17
17
  describe("Client", () => {
18
18
  describe("read + write", () => {
@@ -60,4 +60,127 @@ describe("Client", () => {
60
60
  expect(group.name).toEqual("Channels");
61
61
  });
62
62
  });
63
+ describe("readLatestN", () => {
64
+ it("should correctly read the latest N samples from a single channel", async () => {
65
+ const rand = `${TimeStamp.now().toString()}${Math.random()}`;
66
+ const time = await client.channels.create({
67
+ name: `time-${rand}`,
68
+ dataType: "timestamp",
69
+ isIndex: true,
70
+ });
71
+ const data = await client.channels.create({
72
+ name: `data-${rand}`,
73
+ dataType: "float32",
74
+ index: time.key,
75
+ });
76
+ const start = TimeStamp.now();
77
+ const timeData = [
78
+ start,
79
+ start.add(TimeSpan.seconds(1)),
80
+ start.add(TimeSpan.seconds(2)),
81
+ start.add(TimeSpan.seconds(3)),
82
+ start.add(TimeSpan.seconds(4)),
83
+ start.add(TimeSpan.seconds(5)),
84
+ start.add(TimeSpan.seconds(6)),
85
+ start.add(TimeSpan.seconds(7)),
86
+ start.add(TimeSpan.seconds(8)),
87
+ start.add(TimeSpan.seconds(9)),
88
+ ];
89
+ const dataValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
90
+ await client.write(start, { [time.key]: timeData, [data.key]: dataValues });
91
+
92
+ const result = await client.readLatest(data.key, 3);
93
+ expect(Array.from(result)).toEqual([8, 9, 10]);
94
+ });
95
+
96
+ it("should correctly read the latest N samples from multiple channels", async () => {
97
+ const rand = `${TimeStamp.now().toString()}${Math.random()}`;
98
+ const time = await client.channels.create({
99
+ name: `time-${rand}`,
100
+ dataType: "timestamp",
101
+ isIndex: true,
102
+ });
103
+ const data1 = await client.channels.create({
104
+ name: `data1-${rand}`,
105
+ dataType: "float32",
106
+ index: time.key,
107
+ });
108
+ const data2 = await client.channels.create({
109
+ name: `data2-${rand}`,
110
+ dataType: "float32",
111
+ index: time.key,
112
+ });
113
+ const start = TimeStamp.now();
114
+ const timeData = [
115
+ start,
116
+ start.add(TimeSpan.seconds(1)),
117
+ start.add(TimeSpan.seconds(2)),
118
+ start.add(TimeSpan.seconds(3)),
119
+ start.add(TimeSpan.seconds(4)),
120
+ start.add(TimeSpan.seconds(5)),
121
+ start.add(TimeSpan.seconds(6)),
122
+ start.add(TimeSpan.seconds(7)),
123
+ start.add(TimeSpan.seconds(8)),
124
+ start.add(TimeSpan.seconds(9)),
125
+ ];
126
+ const data1Values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
127
+ const data2Values = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
128
+ await client.write(start, {
129
+ [time.key]: timeData,
130
+ [data1.key]: data1Values,
131
+ [data2.key]: data2Values,
132
+ });
133
+
134
+ const frame = await client.readLatest([time.key, data1.key, data2.key], 3);
135
+ expect(Array.from(frame.get(data1.key))).toEqual([8, 9, 10]);
136
+ expect(Array.from(frame.get(data2.key))).toEqual([80, 90, 100]);
137
+ expect(Array.from(frame.get(time.key))).toEqual([
138
+ timeData[7].valueOf(),
139
+ timeData[8].valueOf(),
140
+ timeData[9].valueOf(),
141
+ ]);
142
+ });
143
+
144
+ it("should return empty series when no data exists", async () => {
145
+ const rand = `${TimeStamp.now().toString()}${Math.random()}`;
146
+ const time = await client.channels.create({
147
+ name: `time-${rand}`,
148
+ dataType: "timestamp",
149
+ isIndex: true,
150
+ });
151
+ const data = await client.channels.create({
152
+ name: `data-${rand}`,
153
+ dataType: "float32",
154
+ index: time.key,
155
+ });
156
+
157
+ const result = await client.readLatest(data.key, 5);
158
+ expect(Array.from(result)).toEqual([]);
159
+ });
160
+
161
+ it("should correctly handle N larger than available data", async () => {
162
+ const rand = `${TimeStamp.now().toString()}${Math.random()}`;
163
+ const time = await client.channels.create({
164
+ name: `time-${rand}`,
165
+ dataType: "timestamp",
166
+ isIndex: true,
167
+ });
168
+ const data = await client.channels.create({
169
+ name: `data-${rand}`,
170
+ dataType: "float32",
171
+ index: time.key,
172
+ });
173
+ const start = TimeStamp.now();
174
+ const timeData = [
175
+ start,
176
+ start.add(TimeSpan.seconds(1)),
177
+ start.add(TimeSpan.seconds(2)),
178
+ ];
179
+ const dataValues = [1, 2, 3];
180
+ await client.write(start, { [time.key]: timeData, [data.key]: dataValues });
181
+
182
+ const result = await client.readLatest(data.key, 10);
183
+ expect(Array.from(result)).toEqual([1, 2, 3]);
184
+ });
185
+ });
63
186
  });
@@ -13,32 +13,22 @@ import {
13
13
  type CrudeTimeRange,
14
14
  type CrudeTimeStamp,
15
15
  type MultiSeries,
16
- type TimeRange,
16
+ TimeRange,
17
17
  TimeSpan,
18
18
  } from "@synnaxlabs/x";
19
19
 
20
20
  import { channel } from "@/channel";
21
21
  import { Deleter } from "@/framer/deleter";
22
- import { Frame, ONTOLOGY_TYPE } from "@/framer/frame";
23
- import { Iterator, type IteratorConfig } from "@/framer/iterator";
22
+ import { Frame } from "@/framer/frame";
23
+ import { AUTO_SPAN, Iterator, type IteratorConfig } from "@/framer/iterator";
24
24
  import { openStreamer, type Streamer, type StreamerConfig } from "@/framer/streamer";
25
25
  import { Writer, type WriterConfig, WriterMode } from "@/framer/writer";
26
- import { ontology } from "@/ontology";
27
-
28
- export const ontologyID = (key: channel.Key): ontology.ID =>
29
- new ontology.ID({ type: ONTOLOGY_TYPE, key: key.toString() });
30
-
31
- const normalizeConfig = <T extends { channels: channel.Params }>(
32
- config: T | channel.Params,
33
- ): T => {
34
- if (
35
- Array.isArray(config) ||
36
- typeof config !== "object" ||
37
- (typeof config === "object" && "key" in config)
38
- )
39
- return { channels: config } as T;
40
- return config;
41
- };
26
+ import { type ontology } from "@/ontology";
27
+
28
+ export const ontologyID = (key: channel.Key): ontology.ID => ({
29
+ type: "framer",
30
+ key: key.toString(),
31
+ });
42
32
 
43
33
  export class Client {
44
34
  private readonly streamClient: WebSocketClient;
@@ -78,25 +68,10 @@ export class Client {
78
68
  * writerConfig for more detail.
79
69
  * @returns a new {@link Writer}.
80
70
  */
81
- async openWriter(config: WriterConfig | channel.Params): Promise<Writer> {
82
- return await Writer._open(
83
- this.retriever,
84
- this.streamClient,
85
- normalizeConfig<WriterConfig>(config),
86
- );
71
+ async openWriter(config: WriterConfig): Promise<Writer> {
72
+ return await Writer._open(this.retriever, this.streamClient, config);
87
73
  }
88
74
 
89
- /***
90
- * Opens a new streamer on the given channels.
91
- *
92
- * @param channels - A key, name, list of keys, or list of names of the channels to
93
- * stream values from.
94
- * @throws a QueryError if any of the given channels do not exist.
95
- * @returns a new {@link Streamer} that must be closed when done streaming, otherwise
96
- * a network socket will remain open.
97
- */
98
- async openStreamer(channels: channel.Params): Promise<Streamer>;
99
-
100
75
  /**
101
76
  * Opens a new streamer with the provided configuration.
102
77
  *
@@ -108,17 +83,8 @@ export class Client {
108
83
  * and then will start reading new values.
109
84
  *
110
85
  */
111
- async openStreamer(config: StreamerConfig): Promise<Streamer>;
112
-
113
- /** Overload to provide interface compatibility with @see StreamOpener */
114
- async openStreamer(config: StreamerConfig | channel.Params): Promise<Streamer>;
115
-
116
- async openStreamer(config: StreamerConfig | channel.Params): Promise<Streamer> {
117
- return await openStreamer(
118
- this.retriever,
119
- this.streamClient,
120
- normalizeConfig<StreamerConfig>(config),
121
- );
86
+ async openStreamer(config: StreamerConfig): Promise<Streamer> {
87
+ return await openStreamer(this.retriever, this.streamClient, config);
122
88
  }
123
89
 
124
90
  async write(
@@ -205,6 +171,34 @@ export class Client {
205
171
  return frame;
206
172
  }
207
173
 
174
+ async readLatest(channel: channel.KeyOrName, n: number): Promise<MultiSeries>;
175
+
176
+ async readLatest(channels: channel.Params, n: number): Promise<Frame>;
177
+
178
+ async readLatest(
179
+ channels: channel.Params,
180
+ n: number = 1,
181
+ ): Promise<MultiSeries | Frame> {
182
+ const { single } = channel.analyzeParams(channels);
183
+ const fr = await this.readLatestNFrame(channels, n);
184
+ if (single) return fr.get(channels as channel.KeyOrName);
185
+ return fr;
186
+ }
187
+
188
+ private async readLatestNFrame(channels: channel.Params, n: number): Promise<Frame> {
189
+ const i = await this.openIterator(TimeRange.MAX, channels, { chunkSize: n });
190
+ const frame = new Frame();
191
+ if (n > 0)
192
+ try {
193
+ await i.seekLast();
194
+ await i.prev(AUTO_SPAN);
195
+ frame.push(i.value);
196
+ } finally {
197
+ await i.close();
198
+ }
199
+ return frame;
200
+ }
201
+
208
202
  async delete(channels: channel.Params, timeRange: TimeRange): Promise<void> {
209
203
  const { normalized, variant } = channel.analyzeParams(channels);
210
204
  if (variant === "keys")
@@ -15,7 +15,7 @@ import {
15
15
  TimeRange,
16
16
  TimeStamp,
17
17
  } from "@synnaxlabs/x";
18
- import { type z } from "zod/v4";
18
+ import { type z } from "zod";
19
19
 
20
20
  import { type channel } from "@/channel";
21
21
  import { ValidationError } from "@/errors";
@@ -11,12 +11,12 @@ import { TimeRange, TimeStamp } from "@synnaxlabs/x/telem";
11
11
  import { describe, expect, test } from "vitest";
12
12
 
13
13
  import { NotFoundError, UnauthorizedError } from "@/errors";
14
- import { newClient } from "@/setupspecs";
15
14
  import { newIndexedPair } from "@/testutil/channels";
15
+ import { newTestClient } 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 = newTestClient();
20
20
 
21
21
  describe("Deleter", () => {
22
22
  test("Client - basic delete", async () => {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { TimeRange } from "@synnaxlabs/x";
12
- import { z } from "zod/v4";
12
+ import { z } from "zod";
13
13
 
14
14
  import { channel } from "@/channel";
15
15
 
@@ -18,14 +18,11 @@ import {
18
18
  TimeRange,
19
19
  TimeStamp,
20
20
  } from "@synnaxlabs/x/telem";
21
- import { z } from "zod/v4";
21
+ import { z } from "zod";
22
22
 
23
23
  import { type channel } from "@/channel";
24
24
  import { UnexpectedError, ValidationError } from "@/errors";
25
25
 
26
- export const ONTOLOGY_TYPE = "framer";
27
- export type OntologyType = typeof ONTOLOGY_TYPE;
28
-
29
26
  type ColumnType = "key" | "name" | null;
30
27
 
31
28
  export interface Digest extends Record<channel.KeyOrName, SeriesDigest[]> {}
@@ -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 { newTestClient } 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 = newTestClient();
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
  }