@synnaxlabs/client 0.54.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/dist/client.cjs +60 -42
  3. package/dist/client.js +8037 -6265
  4. package/dist/src/access/policy/client.d.ts +70 -80
  5. package/dist/src/access/policy/client.d.ts.map +1 -1
  6. package/dist/src/access/policy/types.gen.d.ts +18 -20
  7. package/dist/src/access/policy/types.gen.d.ts.map +1 -1
  8. package/dist/src/access/role/client.d.ts.map +1 -1
  9. package/dist/src/access/role/types.gen.d.ts +2 -2
  10. package/dist/src/actions/actions.d.ts +68 -0
  11. package/dist/src/actions/actions.d.ts.map +1 -0
  12. package/dist/src/actions/actions.spec.d.ts +2 -0
  13. package/dist/src/actions/actions.spec.d.ts.map +1 -0
  14. package/dist/src/actions/external.d.ts +2 -0
  15. package/dist/src/actions/external.d.ts.map +1 -0
  16. package/dist/src/actions/index.d.ts +2 -0
  17. package/dist/src/actions/index.d.ts.map +1 -0
  18. package/dist/src/arc/arc.spec.d.ts +2 -0
  19. package/dist/src/arc/arc.spec.d.ts.map +1 -0
  20. package/dist/src/arc/client.d.ts.map +1 -1
  21. package/dist/src/arc/compiler/types.gen.d.ts +1 -1
  22. package/dist/src/arc/compiler/types.gen.d.ts.map +1 -1
  23. package/dist/src/arc/graph/types.gen.d.ts +40 -40
  24. package/dist/src/arc/graph/types.gen.d.ts.map +1 -1
  25. package/dist/src/arc/ir/types.gen.d.ts +202 -233
  26. package/dist/src/arc/ir/types.gen.d.ts.map +1 -1
  27. package/dist/src/arc/module/types.gen.d.ts +63 -82
  28. package/dist/src/arc/module/types.gen.d.ts.map +1 -1
  29. package/dist/src/arc/program/types.gen.d.ts +63 -82
  30. package/dist/src/arc/program/types.gen.d.ts.map +1 -1
  31. package/dist/src/arc/types/types.gen.d.ts +11 -11
  32. package/dist/src/arc/types/types.gen.d.ts.map +1 -1
  33. package/dist/src/arc/types.gen.d.ts +139 -158
  34. package/dist/src/arc/types.gen.d.ts.map +1 -1
  35. package/dist/src/auth/auth.d.ts +3 -3
  36. package/dist/src/auth/auth.d.ts.map +1 -1
  37. package/dist/src/channel/client.d.ts +2 -2
  38. package/dist/src/channel/client.d.ts.map +1 -1
  39. package/dist/src/channel/retriever.d.ts +5 -8
  40. package/dist/src/channel/retriever.d.ts.map +1 -1
  41. package/dist/src/channel/types.gen.d.ts +3 -3
  42. package/dist/src/channel/types.gen.d.ts.map +1 -1
  43. package/dist/src/channel/writer.d.ts.map +1 -1
  44. package/dist/src/client.d.ts +5 -0
  45. package/dist/src/client.d.ts.map +1 -1
  46. package/dist/src/connection/checker.d.ts +17 -2
  47. package/dist/src/connection/checker.d.ts.map +1 -1
  48. package/dist/src/control/state.d.ts.map +1 -1
  49. package/dist/src/device/client.d.ts.map +1 -1
  50. package/dist/src/device/types.gen.d.ts +6 -8
  51. package/dist/src/device/types.gen.d.ts.map +1 -1
  52. package/dist/src/errors.d.ts +2 -0
  53. package/dist/src/errors.d.ts.map +1 -1
  54. package/dist/src/framer/adapter.d.ts.map +1 -1
  55. package/dist/src/framer/client.d.ts +2 -2
  56. package/dist/src/framer/client.d.ts.map +1 -1
  57. package/dist/src/framer/codec.d.ts +9 -1
  58. package/dist/src/framer/codec.d.ts.map +1 -1
  59. package/dist/src/framer/deleter.d.ts.map +1 -1
  60. package/dist/src/framer/frame.d.ts +1 -1
  61. package/dist/src/framer/iterator.d.ts +84 -3
  62. package/dist/src/framer/iterator.d.ts.map +1 -1
  63. package/dist/src/framer/streamProxy.d.ts.map +1 -1
  64. package/dist/src/framer/streamer.d.ts +1 -3
  65. package/dist/src/framer/streamer.d.ts.map +1 -1
  66. package/dist/src/framer/types.gen.d.ts +18 -0
  67. package/dist/src/framer/types.gen.d.ts.map +1 -1
  68. package/dist/src/framer/writer.d.ts +8 -8
  69. package/dist/src/framer/writer.d.ts.map +1 -1
  70. package/dist/src/group/client.d.ts +1 -2
  71. package/dist/src/group/client.d.ts.map +1 -1
  72. package/dist/src/group/types.gen.d.ts +2 -2
  73. package/dist/src/index.d.ts +2 -1
  74. package/dist/src/index.d.ts.map +1 -1
  75. package/dist/src/label/client.d.ts +5 -8
  76. package/dist/src/label/client.d.ts.map +1 -1
  77. package/dist/src/lineplot/client.d.ts.map +1 -1
  78. package/dist/src/lineplot/types.gen.d.ts +2 -2
  79. package/dist/src/log/client.d.ts.map +1 -1
  80. package/dist/src/log/types.gen.d.ts +2 -2
  81. package/dist/src/ontology/client.d.ts +1 -3
  82. package/dist/src/ontology/client.d.ts.map +1 -1
  83. package/dist/src/ontology/payload.d.ts +12 -16
  84. package/dist/src/ontology/payload.d.ts.map +1 -1
  85. package/dist/src/ontology/types.gen.d.ts +1 -2
  86. package/dist/src/ontology/types.gen.d.ts.map +1 -1
  87. package/dist/src/ontology/writer.d.ts +5 -10
  88. package/dist/src/ontology/writer.d.ts.map +1 -1
  89. package/dist/src/rack/client.d.ts.map +1 -1
  90. package/dist/src/rack/types.gen.d.ts +3 -3
  91. package/dist/src/ranger/alias/client.d.ts.map +1 -1
  92. package/dist/src/ranger/client.d.ts.map +1 -1
  93. package/dist/src/ranger/kv/client.d.ts.map +1 -1
  94. package/dist/src/ranger/types.gen.d.ts +6 -6
  95. package/dist/src/ranger/types.gen.d.ts.map +1 -1
  96. package/dist/src/ranger/writer.d.ts +2 -3
  97. package/dist/src/ranger/writer.d.ts.map +1 -1
  98. package/dist/src/schematic/actions.d.ts +147 -0
  99. package/dist/src/schematic/actions.d.ts.map +1 -0
  100. package/dist/src/schematic/actions.gen.d.ts +484 -0
  101. package/dist/src/schematic/actions.gen.d.ts.map +1 -0
  102. package/dist/src/schematic/actions.spec.d.ts +2 -0
  103. package/dist/src/schematic/actions.spec.d.ts.map +1 -0
  104. package/dist/src/schematic/client.d.ts +53 -2
  105. package/dist/src/schematic/client.d.ts.map +1 -1
  106. package/dist/src/schematic/external.d.ts +2 -0
  107. package/dist/src/schematic/external.d.ts.map +1 -1
  108. package/dist/src/schematic/symbol/client.d.ts.map +1 -1
  109. package/dist/src/schematic/symbol/types.gen.d.ts +48 -58
  110. package/dist/src/schematic/symbol/types.gen.d.ts.map +1 -1
  111. package/dist/src/schematic/types.gen.d.ts +131 -5
  112. package/dist/src/schematic/types.gen.d.ts.map +1 -1
  113. package/dist/src/status/client.d.ts.map +1 -1
  114. package/dist/src/status/payload.d.ts +3 -3
  115. package/dist/src/table/actions.d.ts +156 -0
  116. package/dist/src/table/actions.d.ts.map +1 -0
  117. package/dist/src/table/actions.gen.d.ts +587 -0
  118. package/dist/src/table/actions.gen.d.ts.map +1 -0
  119. package/dist/src/table/client.d.ts +28 -2
  120. package/dist/src/table/client.d.ts.map +1 -1
  121. package/dist/src/table/external.d.ts +2 -0
  122. package/dist/src/table/external.d.ts.map +1 -1
  123. package/dist/src/table/types.gen.d.ts +71 -4
  124. package/dist/src/table/types.gen.d.ts.map +1 -1
  125. package/dist/src/task/client.d.ts.map +1 -1
  126. package/dist/src/task/types.gen.d.ts +7 -7
  127. package/dist/src/task/types.gen.d.ts.map +1 -1
  128. package/dist/src/user/client.d.ts +2 -2
  129. package/dist/src/user/client.d.ts.map +1 -1
  130. package/dist/src/user/types.gen.d.ts +2 -2
  131. package/dist/src/view/client.d.ts.map +1 -1
  132. package/dist/src/view/types.gen.d.ts +2 -2
  133. package/dist/src/workspace/client.d.ts.map +1 -1
  134. package/dist/src/workspace/types.gen.d.ts +3 -3
  135. package/dist/src/workspace/types.gen.d.ts.map +1 -1
  136. package/package.json +12 -11
  137. package/src/access/policy/client.ts +4 -7
  138. package/src/access/role/client.ts +6 -26
  139. package/src/actions/actions.spec.ts +229 -0
  140. package/src/actions/actions.ts +104 -0
  141. package/src/actions/external.ts +10 -0
  142. package/src/actions/index.ts +10 -0
  143. package/src/arc/arc.spec.ts +44 -0
  144. package/src/arc/client.ts +3 -7
  145. package/src/arc/compiler/types.gen.ts +2 -1
  146. package/src/arc/ir/types.gen.ts +102 -48
  147. package/src/arc/lsp.spec.ts +3 -7
  148. package/src/arc/types/types.gen.ts +3 -3
  149. package/src/auth/auth.spec.ts +12 -13
  150. package/src/auth/auth.ts +48 -34
  151. package/src/channel/batchRetriever.spec.ts +13 -4
  152. package/src/channel/channel.spec.ts +13 -0
  153. package/src/channel/client.ts +8 -6
  154. package/src/channel/retriever.ts +7 -16
  155. package/src/channel/types.gen.ts +1 -2
  156. package/src/channel/writer.ts +4 -20
  157. package/src/client.ts +3 -0
  158. package/src/connection/checker.ts +48 -10
  159. package/src/connection/connection.spec.ts +64 -2
  160. package/src/control/state.ts +5 -4
  161. package/src/device/client.ts +5 -8
  162. package/src/device/device.spec.ts +7 -5
  163. package/src/device/types.gen.ts +4 -4
  164. package/src/errors.ts +8 -9
  165. package/src/framer/adapter.ts +2 -4
  166. package/src/framer/client.ts +12 -0
  167. package/src/framer/codec.spec.ts +53 -3
  168. package/src/framer/codec.ts +58 -25
  169. package/src/framer/deleter.ts +2 -8
  170. package/src/framer/iterator.ts +42 -39
  171. package/src/framer/streamProxy.ts +11 -12
  172. package/src/framer/streamer.spec.ts +1 -1
  173. package/src/framer/streamer.ts +2 -7
  174. package/src/framer/types.gen.ts +20 -0
  175. package/src/framer/writer.spec.ts +221 -1
  176. package/src/framer/writer.ts +53 -28
  177. package/src/group/client.ts +4 -7
  178. package/src/index.ts +3 -2
  179. package/src/label/client.ts +6 -16
  180. package/src/label/label.spec.ts +12 -0
  181. package/src/lineplot/client.ts +6 -21
  182. package/src/log/client.ts +6 -21
  183. package/src/ontology/client.ts +2 -3
  184. package/src/ontology/ontology.spec.ts +10 -0
  185. package/src/ontology/types.gen.ts +0 -1
  186. package/src/ontology/writer.ts +4 -7
  187. package/src/rack/client.ts +4 -7
  188. package/src/rack/rack.spec.ts +12 -1
  189. package/src/ranger/alias/client.ts +6 -11
  190. package/src/ranger/client.ts +2 -3
  191. package/src/ranger/kv/client.ts +4 -7
  192. package/src/ranger/ranger.spec.ts +12 -0
  193. package/src/ranger/writer.ts +4 -17
  194. package/src/schematic/access.spec.ts +6 -6
  195. package/src/schematic/actions.gen.ts +200 -0
  196. package/src/schematic/actions.spec.ts +699 -0
  197. package/src/schematic/actions.ts +168 -0
  198. package/src/schematic/client.ts +34 -30
  199. package/src/schematic/external.ts +2 -0
  200. package/src/schematic/schematic.spec.ts +233 -69
  201. package/src/schematic/symbol/client.spec.ts +33 -9
  202. package/src/schematic/symbol/client.ts +6 -11
  203. package/src/schematic/symbol/types.gen.ts +1 -10
  204. package/src/schematic/types.gen.ts +55 -6
  205. package/src/status/client.ts +4 -10
  206. package/src/status/status.spec.ts +7 -6
  207. package/src/table/access.spec.ts +0 -6
  208. package/src/table/actions.gen.ts +243 -0
  209. package/src/table/actions.ts +255 -0
  210. package/src/table/client.ts +21 -25
  211. package/src/table/external.ts +2 -0
  212. package/src/table/table.spec.ts +588 -43
  213. package/src/table/types.gen.ts +58 -5
  214. package/src/task/client.ts +14 -20
  215. package/src/task/task.spec.ts +15 -1
  216. package/src/task/types.gen.ts +8 -6
  217. package/src/user/client.ts +6 -11
  218. package/src/view/client.ts +4 -7
  219. package/src/view/view.spec.ts +9 -5
  220. package/src/workspace/client.ts +6 -16
  221. package/src/workspace/types.gen.ts +2 -1
  222. package/src/workspace/workspace.spec.ts +14 -1
@@ -7,8 +7,9 @@
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 { URL } from "@synnaxlabs/x";
11
- import { describe, expect, it } from "vitest";
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { TimeSpan, TimeStamp, URL } from "@synnaxlabs/x";
12
+ import { describe, expect, it, vi } from "vitest";
12
13
  import { z } from "zod";
13
14
 
14
15
  import { auth } from "@/auth";
@@ -87,4 +88,65 @@ describe("connectivity", () => {
87
88
  expect(state.clientVersion).toBe("0.0.0");
88
89
  });
89
90
  });
91
+ describe("clock skew", () => {
92
+ const createMockClient = (nodeTime: TimeStamp): UnaryClient => ({
93
+ send: vi.fn().mockResolvedValue({
94
+ clusterKey: "test-cluster",
95
+ nodeVersion: __VERSION__,
96
+ nodeTime,
97
+ }),
98
+ use: vi.fn(),
99
+ });
100
+
101
+ it("should detect clock skew exceeding threshold", async () => {
102
+ const farFuture = TimeStamp.now().add(TimeSpan.hours(1));
103
+ const checker = new connection.Checker(
104
+ createMockClient(farFuture),
105
+ TimeSpan.seconds(30),
106
+ __VERSION__,
107
+ undefined,
108
+ TimeSpan.seconds(1),
109
+ );
110
+ const state = await checker.check();
111
+ expect(state.clockSkewExceeded).toBe(true);
112
+ expect(state.clockSkew.valueOf()).not.toBe(0n);
113
+ checker.stop();
114
+ });
115
+
116
+ it("should not flag skew within threshold", async () => {
117
+ const now = TimeStamp.now();
118
+ const checker = new connection.Checker(
119
+ createMockClient(now),
120
+ TimeSpan.seconds(30),
121
+ __VERSION__,
122
+ undefined,
123
+ TimeSpan.seconds(1),
124
+ );
125
+ const state = await checker.check();
126
+ expect(state.clockSkewExceeded).toBe(false);
127
+ checker.stop();
128
+ });
129
+
130
+ it("should fire onChange when clockSkewExceeded changes", async () => {
131
+ let callCount = 0;
132
+ const farFuture = TimeStamp.now().add(TimeSpan.hours(1));
133
+ const checker = new connection.Checker(
134
+ createMockClient(farFuture),
135
+ TimeSpan.seconds(30),
136
+ __VERSION__,
137
+ undefined,
138
+ TimeSpan.seconds(1),
139
+ );
140
+ // Wait for the constructor's initial check to complete
141
+ await checker.check();
142
+ checker.onChange(() => {
143
+ callCount++;
144
+ });
145
+ // Trigger another check - skewExceeded stays true, status stays connected,
146
+ // so onChange should not fire
147
+ await checker.check();
148
+ expect(callCount).toBe(0);
149
+ checker.stop();
150
+ });
151
+ });
90
152
  });
@@ -46,13 +46,14 @@ export class StateTracker
46
46
  implements observe.ObservableAsyncCloseable<Transfer[]>
47
47
  {
48
48
  readonly states: Map<channel.Key, State>;
49
- private readonly codec: binary.Codec;
49
+ private readonly codec: binary.JSONCodec;
50
50
 
51
51
  constructor(streamer: framer.Streamer) {
52
52
  super(streamer, (frame) => {
53
- const update: Update = this.codec.decode(frame.series[0].buffer, updateZ);
54
- this.merge(update);
55
- return [update.transfers, true];
53
+ const raw = Array.from(frame.series[0].as("string"));
54
+ const updates: Update[] = raw.map((r) => this.codec.decodeString(r, updateZ));
55
+ updates.forEach((u) => this.merge(u));
56
+ return [updates.flatMap((u) => u.transfers), true];
56
57
  });
57
58
  this.states = new Map();
58
59
  this.codec = new binary.JSONCodec();
@@ -7,7 +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 { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { array, type record, zod } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
@@ -122,10 +122,9 @@ export class Client {
122
122
  async retrieve(
123
123
  args: RetrieveArgs & { schemas?: DeviceSchemas },
124
124
  ): Promise<Device | Array<Device>> {
125
- const { schemas, ...rest } = args as RetrieveArgs & { schemas?: DeviceSchemas };
125
+ const { schemas, ...rest } = args;
126
126
  const isSingle = typeof rest === "object" && "key" in rest;
127
- const res = await sendRequired(
128
- this.client,
127
+ const res = await this.client.send(
129
128
  "/device/retrieve",
130
129
  rest,
131
130
  retrieveArgsZ,
@@ -166,8 +165,7 @@ export class Client {
166
165
  schemas?: DeviceSchemas,
167
166
  ): Promise<Device | Device[]> {
168
167
  const isSingle = !Array.isArray(devices);
169
- const res = await sendRequired(
170
- this.client,
168
+ const res = await this.client.send(
171
169
  "/device/create",
172
170
  { devices: array.toArray(devices) },
173
171
  createReqZ(schemas),
@@ -177,8 +175,7 @@ export class Client {
177
175
  }
178
176
 
179
177
  async delete(keys: Key | Key[]): Promise<void> {
180
- await sendRequired(
181
- this.client,
178
+ await this.client.send(
182
179
  "/device/delete",
183
180
  { keys: array.toArray(keys) },
184
181
  deleteReqZ,
@@ -312,11 +312,13 @@ describe("Device", async () => {
312
312
  });
313
313
 
314
314
  it("should retrieve devices by search term", async () => {
315
- const result = await client.devices.retrieve({
316
- searchTerm: "sensor1",
317
- });
318
- expect(result.length).toBeGreaterThanOrEqual(2);
319
- expect(result.every((d) => d.name.includes("sensor"))).toBe(true);
315
+ const sensor1 = testDevices.find((d) => d.name === "sensor1")!;
316
+ await expect
317
+ .poll(async () => {
318
+ const result = await client.devices.retrieve({ searchTerm: "sensor1" });
319
+ return result.find((d) => d.key === sensor1.key);
320
+ })
321
+ .toMatchObject({ name: "sensor1", key: sensor1.key });
320
322
  });
321
323
 
322
324
  it("should support pagination with limit and offset", async () => {
@@ -15,18 +15,18 @@ import { z } from "zod";
15
15
  import { ontology } from "@/ontology";
16
16
  import { rack } from "@/rack";
17
17
 
18
+ export const keyZ = z.string();
19
+ export type Key = z.infer<typeof keyZ>;
20
+
18
21
  /** StatusDetails contains device-specific status details identifying the device and its associated rack. */
19
22
  export const statusDetailsZ = z.object({
20
23
  /** rack is the key of the rack this device belongs to. */
21
24
  rack: rack.keyZ,
22
25
  /** device is the device identifier. */
23
- device: z.string(),
26
+ device: keyZ,
24
27
  });
25
28
  export interface StatusDetails extends z.infer<typeof statusDetailsZ> {}
26
29
 
27
- export const keyZ = z.string();
28
- export type Key = z.infer<typeof keyZ>;
29
-
30
30
  export const statusZ = status.statusZ({ details: statusDetailsZ });
31
31
  export type Status = z.infer<typeof statusZ>;
32
32
 
package/src/errors.ts CHANGED
@@ -157,15 +157,14 @@ export const validateFieldNotNull = (
157
157
  };
158
158
 
159
159
  export const errorsMiddleware: Middleware = async (ctx, next) => {
160
- const [res, err] = await next(ctx);
161
- if (err == null) return [res, err];
162
- if (err instanceof Unreachable)
163
- return [
164
- res,
165
- new Unreachable({
160
+ try {
161
+ return await next(ctx);
162
+ } catch (err) {
163
+ if (err instanceof Unreachable)
164
+ throw new Unreachable({
166
165
  message: `Cannot reach Core at ${err.url.host}:${err.url.port}`,
167
166
  url: err.url,
168
- }),
169
- ];
170
- return [res, err];
167
+ });
168
+ throw err;
169
+ }
171
170
  };
@@ -164,16 +164,14 @@ export class WriteAdapter {
164
164
  throw new ValidationError(`
165
165
  Received a single channel name or key but no series.
166
166
  `);
167
- if (Array.isArray(series)) {
167
+ if (Array.isArray(series))
168
168
  if (series.some((s) => s instanceof Series || Array.isArray(s)))
169
169
  throw new ValidationError(`
170
170
  Received a single channel name or key but multiple series.
171
171
  `);
172
172
 
173
- series = series as CrudeSeries;
174
- }
175
173
  const pld = await this.fetchChannel(columnsOrData);
176
- const s = new Series({ data: series as CrudeSeries, dataType: pld.dataType });
174
+ const s = new Series({ data: series, dataType: pld.dataType });
177
175
  return new Frame(pld.key, s);
178
176
  }
179
177
 
@@ -180,6 +180,18 @@ export class Client {
180
180
 
181
181
  async readLatest(channels: channel.Params, n: number): Promise<Frame>;
182
182
 
183
+ /**
184
+ * Reads the latest n samples from the given channel(s).
185
+ *
186
+ * If fewer than n samples are available, returns only the samples that
187
+ * exist.
188
+ *
189
+ * @param channels - A single channel key/name or an array of channel
190
+ * keys/names.
191
+ * @param n - The maximum number of samples to read. Defaults to 1.
192
+ * @returns A MultiSeries when a single channel is provided, or a Frame when
193
+ * multiple channels are provided.
194
+ */
183
195
  async readLatest(
184
196
  channels: channel.Params,
185
197
  n: number = 1,
@@ -17,11 +17,13 @@ import { framer } from "@/framer";
17
17
  import {
18
18
  Codec,
19
19
  HIGH_PERF_SPECIAL_CHAR,
20
- LOW_PER_SPECIAL_CHAR,
20
+ LOW_PERF_SPECIAL_CHAR,
21
+ WSIteratorCodec,
21
22
  WSWriterCodec,
22
23
  } from "@/framer/codec";
23
24
  import { Frame } from "@/framer/frame";
24
- import { WriterCommand } from "@/framer/types.gen";
25
+ import { type IteratorResponse } from "@/framer/iterator";
26
+ import { IteratorResponseVariant, WriterCommand } from "@/framer/types.gen";
25
27
  import { type WriteRequest } from "@/framer/writer";
26
28
 
27
29
  describe("encoder", () => {
@@ -325,9 +327,57 @@ describe("encoder", () => {
325
327
  const codec = new WSWriterCodec(baseCodec);
326
328
  const msg: WebsocketMessage<WriteRequest> = { type: "data", payload: writeReq };
327
329
  const encoded = codec.encode(msg);
328
- expect(encoded[0]).toEqual(LOW_PER_SPECIAL_CHAR);
330
+ expect(encoded[0]).toEqual(LOW_PERF_SPECIAL_CHAR);
329
331
  const decoded = codec.decode(encoded);
330
332
  expect(decoded).toEqual(msg);
331
333
  });
332
334
  });
335
+
336
+ describe("websocket iterator codec", () => {
337
+ it("should JSON-encode an iterator request without a special char prefix", () => {
338
+ const baseCodec = new Codec([1], [DataType.INT32]);
339
+ const codec = new WSIteratorCodec(baseCodec);
340
+ const msg: WebsocketMessage<unknown> = {
341
+ type: "data",
342
+ payload: { command: 1, span: 0 },
343
+ };
344
+ const encoded = codec.encode(msg);
345
+ // Iterator requests are sent as raw JSON; no special-char prefix.
346
+ // Confirm the first byte is JSON ('{' or '[').
347
+ expect([0x7b, 0x5b]).toContain(encoded[0]);
348
+ });
349
+
350
+ it("should binary-decode a data variant response and synthesize variant=Data", () => {
351
+ const baseCodec = new Codec([1], [DataType.INT32]);
352
+ const fr = new framer.Frame([1], [new Series(new Int32Array([1, 2, 3]))]);
353
+ const codec = new WSIteratorCodec(baseCodec);
354
+ const encoded = new Uint8Array(baseCodec.encode(fr.toPayload(), 1));
355
+ encoded[0] = HIGH_PERF_SPECIAL_CHAR;
356
+ const decoded = codec.decode(encoded) as WebsocketMessage<IteratorResponse>;
357
+ expect(decoded.payload?.variant).toEqual(IteratorResponseVariant.Data);
358
+ const decodedFr = new Frame(decoded.payload?.frame);
359
+ expect(decodedFr.series[0].data).toEqual(fr.series[0].data);
360
+ });
361
+
362
+ it("should JSON-decode an ack variant response prefixed with the low-perf char", () => {
363
+ const baseCodec = new Codec([1], [DataType.INT32]);
364
+ const codec = new WSIteratorCodec(baseCodec);
365
+ const ackMsg: WebsocketMessage<IteratorResponse> = {
366
+ type: "data",
367
+ payload: {
368
+ variant: IteratorResponseVariant.Ack,
369
+ ack: true,
370
+ command: 1,
371
+ },
372
+ };
373
+ // Simulate what the server sends: LOW_PERF_SPECIAL_CHAR + JSON-encoded message.
374
+ const json = new TextEncoder().encode(JSON.stringify(ackMsg));
375
+ const encoded = new Uint8Array(json.byteLength + 1);
376
+ encoded[0] = LOW_PERF_SPECIAL_CHAR;
377
+ encoded.set(json, 1);
378
+ const decoded = codec.decode(encoded) as WebsocketMessage<IteratorResponse>;
379
+ expect(decoded.payload?.variant).toEqual(IteratorResponseVariant.Ack);
380
+ expect(decoded.payload?.ack).toEqual(true);
381
+ });
382
+ });
333
383
  });
@@ -21,7 +21,7 @@ import { type channel } from "@/channel";
21
21
  import { ValidationError } from "@/errors";
22
22
  import { type Frame, type Payload } from "@/framer/frame";
23
23
  import { type StreamerResponse } from "@/framer/streamer";
24
- import { WriterCommand } from "@/framer/types.gen";
24
+ import { IteratorResponseVariant, WriterCommand } from "@/framer/types.gen";
25
25
  import { type WriteRequest } from "@/framer/writer";
26
26
 
27
27
  const seriesPldLength = (series: SeriesPayload): number =>
@@ -63,7 +63,7 @@ interface CodecState {
63
63
  }
64
64
 
65
65
  export class Codec {
66
- contentType: string = "application/sy-framer";
66
+ contentType: string = CONTENT_TYPE;
67
67
  private states: Map<number, CodecState> = new Map();
68
68
  private currState: CodecState | undefined;
69
69
  private seqNum: number = 0;
@@ -269,36 +269,26 @@ export class Codec {
269
269
  index += ALIGNMENT_SIZE;
270
270
  }
271
271
 
272
- if (channelFlag) returnFrame.keys = [...state.keys];
273
- state.keys.forEach((k, i) => {
274
- if (!channelFlag) {
275
- if (index >= view.byteLength) return;
276
- const frameKey = view.getUint32(index, true);
277
- if (frameKey !== k) return;
278
- index += KEY_SIZE;
279
- returnFrame.keys.push(k);
280
- }
281
- const dataType = state.keyDataTypes.get(k) as DataType;
272
+ const decodeSeries = (k: channel.Key): boolean => {
273
+ const dataType = state.keyDataTypes.get(k);
274
+ if (dataType == null) return false;
282
275
  currSize = 0;
283
276
  if (!sizeFlag) {
284
- if (index + DATA_LENGTH_SIZE > view.byteLength) return;
277
+ if (index + DATA_LENGTH_SIZE > view.byteLength) return false;
285
278
  currSize = view.getUint32(index, true);
286
279
  index += DATA_LENGTH_SIZE;
287
280
  } else currSize = sizeRepresentation;
288
281
 
289
282
  let dataByteLength = currSize;
290
283
  if (!dataType.isVariable) dataByteLength *= dataType.density.valueOf();
291
- if (index + dataByteLength > view.byteLength) {
292
- returnFrame.keys.splice(i, 1);
293
- return;
294
- }
284
+ if (index + dataByteLength > view.byteLength) return false;
295
285
  const currSeries: SeriesPayload = {
296
286
  dataType,
297
287
  data: src.slice(index, index + dataByteLength).buffer,
298
288
  };
299
289
  index += dataByteLength;
300
290
  if (!equalTimeRangesFlag && !timeRangesZeroFlag) {
301
- if (index + TIMESTAMP_SIZE * 2 > view.byteLength) return;
291
+ if (index + TIMESTAMP_SIZE * 2 > view.byteLength) return false;
302
292
  const start = view.getBigUint64(index, true);
303
293
  index += TIMESTAMP_SIZE;
304
294
  const end = view.getBigUint64(index, true);
@@ -312,7 +302,7 @@ export class Codec {
312
302
  else currSeries.timeRange = new TimeRange({ start: 0n, end: 0n });
313
303
 
314
304
  if (!equalAlignmentsFlag && !zeroAlignmentsFlag) {
315
- if (index + ALIGNMENT_SIZE > view.byteLength) return;
305
+ if (index + ALIGNMENT_SIZE > view.byteLength) return false;
316
306
  currAlignment = view.getBigUint64(index, true);
317
307
  index += ALIGNMENT_SIZE;
318
308
  currSeries.alignment = currAlignment;
@@ -320,16 +310,27 @@ export class Codec {
320
310
  else currSeries.alignment = 0n;
321
311
 
322
312
  returnFrame.series.push(currSeries);
323
- });
313
+ returnFrame.keys.push(k);
314
+ return true;
315
+ };
316
+
317
+ if (channelFlag) state.keys.forEach((k) => decodeSeries(k));
318
+ else
319
+ while (index < view.byteLength) {
320
+ if (index + KEY_SIZE > view.byteLength) break;
321
+ const frameKey = view.getUint32(index, true);
322
+ index += KEY_SIZE;
323
+ if (!decodeSeries(frameKey)) break;
324
+ }
324
325
  return returnFrame;
325
326
  }
326
327
  }
327
328
 
328
- export const LOW_PER_SPECIAL_CHAR = 254;
329
- const LOW_PERF_SPECIAL_CHAR_BUF = new Uint8Array([LOW_PER_SPECIAL_CHAR]);
329
+ export const LOW_PERF_SPECIAL_CHAR = 254;
330
+ const LOW_PERF_SPECIAL_CHAR_BUF = new Uint8Array([LOW_PERF_SPECIAL_CHAR]);
330
331
  export const HIGH_PERF_SPECIAL_CHAR = 255;
331
332
  const HIGH_PERF_SPECIAL_CHAR_BUF = new Uint8Array([HIGH_PERF_SPECIAL_CHAR]);
332
- const CONTENT_TYPE = "application/sy-framer";
333
+ const CONTENT_TYPE = "application/vnd.synnax.frame";
333
334
 
334
335
  export class WSWriterCodec implements binary.Codec {
335
336
  contentType = CONTENT_TYPE;
@@ -358,7 +359,7 @@ export class WSWriterCodec implements binary.Codec {
358
359
  decode<P extends z.ZodType>(data: Uint8Array | ArrayBuffer, schema?: P): z.infer<P> {
359
360
  const dv = new DataView(data instanceof Uint8Array ? data.buffer : data);
360
361
  const codec = dv.getUint8(0);
361
- if (codec === LOW_PER_SPECIAL_CHAR)
362
+ if (codec === LOW_PERF_SPECIAL_CHAR)
362
363
  return this.lowPerfCodec.decode(data.slice(1), schema);
363
364
  const v: WebsocketMessage<WriteRequest> = { type: "data" };
364
365
  const frame = this.base.decode(data, 1);
@@ -384,7 +385,7 @@ export class WSStreamerCodec implements binary.Codec {
384
385
  decode<P extends z.ZodType>(data: Uint8Array | ArrayBuffer, schema?: P): z.infer<P> {
385
386
  const dv = new DataView(data instanceof Uint8Array ? data.buffer : data);
386
387
  const codec = dv.getUint8(0);
387
- if (codec === LOW_PER_SPECIAL_CHAR)
388
+ if (codec === LOW_PERF_SPECIAL_CHAR)
388
389
  return this.lowPerfCodec.decode(data.slice(1), schema);
389
390
  const v: WebsocketMessage<StreamerResponse> = {
390
391
  type: "data",
@@ -393,3 +394,35 @@ export class WSStreamerCodec implements binary.Codec {
393
394
  return v as z.infer<P>;
394
395
  }
395
396
  }
397
+
398
+ export class WSIteratorCodec implements binary.Codec {
399
+ contentType = CONTENT_TYPE;
400
+ private base: Codec;
401
+ private lowPerfCodec: binary.Codec;
402
+
403
+ constructor(base: Codec) {
404
+ this.base = base;
405
+ this.lowPerfCodec = binary.JSON_CODEC;
406
+ }
407
+
408
+ encode(payload: unknown): Uint8Array<ArrayBuffer> {
409
+ return this.lowPerfCodec.encode(payload);
410
+ }
411
+
412
+ decode<P extends z.ZodType>(data: Uint8Array | ArrayBuffer, schema?: P): z.infer<P> {
413
+ const dv = new DataView(data instanceof Uint8Array ? data.buffer : data);
414
+ const codec = dv.getUint8(0);
415
+ if (codec === LOW_PERF_SPECIAL_CHAR)
416
+ return this.lowPerfCodec.decode(data.slice(1), schema);
417
+ const v: WebsocketMessage<unknown> = {
418
+ type: "data",
419
+ payload: {
420
+ variant: IteratorResponseVariant.Data,
421
+ ack: false,
422
+ command: 0,
423
+ frame: this.base.decode(data, 1),
424
+ },
425
+ };
426
+ return v as z.infer<P>;
427
+ }
428
+ }
@@ -7,7 +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 { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
10
+ import { type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { TimeRange } from "@synnaxlabs/x";
12
12
  import { z } from "zod";
13
13
 
@@ -33,12 +33,6 @@ export class Deleter {
33
33
  }
34
34
 
35
35
  async delete(props: Request): Promise<void> {
36
- await sendRequired<typeof reqZ, typeof resZ>(
37
- this.client,
38
- "/frame/delete",
39
- props,
40
- reqZ,
41
- resZ,
42
- );
36
+ await this.client.send("/frame/delete", props, reqZ, resZ);
43
37
  }
44
38
  }