@synnaxlabs/client 0.37.0 → 0.38.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 (53) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/dist/access/policy/payload.d.ts +10 -10
  3. package/dist/channel/client.d.ts +13 -1
  4. package/dist/channel/client.d.ts.map +1 -1
  5. package/dist/channel/payload.d.ts +14 -0
  6. package/dist/channel/payload.d.ts.map +1 -1
  7. package/dist/client.cjs +30 -30
  8. package/dist/client.js +2149 -2092
  9. package/dist/framer/frame.d.ts.map +1 -1
  10. package/dist/hardware/device/payload.d.ts +2 -2
  11. package/dist/hardware/device/payload.d.ts.map +1 -1
  12. package/dist/hardware/task/client.d.ts.map +1 -1
  13. package/dist/hardware/task/payload.d.ts +4 -4
  14. package/dist/hardware/task/payload.d.ts.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/label/client.d.ts.map +1 -1
  18. package/dist/ontology/group/group.d.ts.map +1 -1
  19. package/dist/ontology/payload.d.ts.map +1 -1
  20. package/dist/ranger/client.d.ts.map +1 -1
  21. package/dist/ranger/payload.d.ts +2 -2
  22. package/dist/ranger/payload.d.ts.map +1 -1
  23. package/dist/util/retrieve.d.ts.map +1 -1
  24. package/dist/util/zod.d.ts +1 -1
  25. package/dist/util/zod.d.ts.map +1 -1
  26. package/examples/node/package-lock.json +56 -5509
  27. package/examples/node/package.json +1 -1
  28. package/package.json +10 -14
  29. package/src/access/policy/policy.spec.ts +1 -4
  30. package/src/channel/batchRetriever.spec.ts +4 -0
  31. package/src/channel/channel.spec.ts +108 -37
  32. package/src/channel/client.ts +45 -2
  33. package/src/channel/payload.ts +5 -0
  34. package/src/framer/frame.ts +3 -4
  35. package/src/framer/streamer.spec.ts +178 -0
  36. package/src/hardware/task/client.ts +3 -2
  37. package/src/index.ts +1 -1
  38. package/src/label/client.ts +2 -2
  39. package/src/ontology/group/group.ts +3 -5
  40. package/src/ontology/payload.ts +1 -1
  41. package/src/ranger/client.ts +6 -11
  42. package/src/ranger/payload.ts +2 -2
  43. package/src/util/retrieve.ts +2 -2
  44. package/src/util/zod.ts +4 -1
  45. package/vite.config.ts +5 -12
  46. package/api/client.api.md +0 -3473
  47. package/api-extractor.json +0 -7
  48. package/dist/hardware/task/ni/types.d.ts +0 -14495
  49. package/dist/hardware/task/ni/types.d.ts.map +0 -1
  50. package/dist/workspace/lineplot/payload.d.ts +0 -23
  51. package/dist/workspace/lineplot/payload.d.ts.map +0 -1
  52. package/src/hardware/task/ni/types.ts +0 -1716
  53. package/src/workspace/lineplot/payload.ts +0 -30
@@ -11,6 +11,6 @@
11
11
  "author": "",
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
- "@synnaxlabs/client": "^0.31.0"
14
+ "@synnaxlabs/client": "^0.38.0"
15
15
  }
16
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synnaxlabs/client",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "The Synnax Client Library",
5
5
  "keywords": [
6
6
  "synnax",
@@ -25,21 +25,20 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "async-mutex": "^0.5.0",
28
- "uuid": "^11.0.3",
28
+ "uuid": "^11.0.5",
29
29
  "zod": "^3.24.1",
30
- "@synnaxlabs/x": "0.37.0",
31
- "@synnaxlabs/freighter": "0.37.0"
30
+ "@synnaxlabs/freighter": "0.38.0",
31
+ "@synnaxlabs/x": "0.38.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^22.10.2",
34
+ "@types/node": "^22.10.6",
35
35
  "@types/uuid": "^10.0.0",
36
- "@vitest/coverage-v8": "^2.1.8",
37
- "eslint": "^9.17.0",
38
- "typescript": "^5.7.2",
39
- "vite": "^6.0.3",
36
+ "eslint": "^9.18.0",
37
+ "typescript": "^5.7.3",
38
+ "vite": "^6.0.7",
40
39
  "vitest": "^2.1.8",
41
- "@synnaxlabs/tsconfig": "0.0.2",
42
40
  "@synnaxlabs/vite-plugin": "0.0.1",
41
+ "@synnaxlabs/tsconfig": "0.0.2",
43
42
  "eslint-config-synnaxlabs": "0.0.1"
44
43
  },
45
44
  "type": "module",
@@ -49,10 +48,7 @@
49
48
  "build": "tsc --noEmit && vite build",
50
49
  "watch": "tsc --noEmit && vite build --watch",
51
50
  "test": "vitest",
52
- "cov": "vitest --coverage",
53
51
  "lint": "eslint --cache",
54
- "fix": "eslint --cache --fix",
55
- "genApi": "tsc --noEmit && vite build && npx api-extractor run --local",
56
- "checkApi": "tsc --noEmit && vite build && npx api-extractor run"
52
+ "fix": "eslint --cache --fix"
57
53
  }
58
54
  }
@@ -298,10 +298,7 @@ describe("privilege", async () => {
298
298
  password: "pwd1",
299
299
  });
300
300
  await expect(
301
- client2.user.create({
302
- username: id.id(),
303
- password: id.id(),
304
- }),
301
+ client2.user.create({ username: id.id(), password: id.id() }),
305
302
  ).rejects.toThrow(AuthError);
306
303
 
307
304
  const policy = await client.access.policy.create({
@@ -56,6 +56,8 @@ describe("channelRetriever", () => {
56
56
  leaseholder: 1,
57
57
  index: 0,
58
58
  virtual: false,
59
+ expression: "",
60
+ requires: [],
59
61
  }));
60
62
  });
61
63
  const retriever = new DebouncedBatchRetriever(base, 10);
@@ -83,6 +85,8 @@ describe("channelRetriever", () => {
83
85
  leaseholder: 1,
84
86
  index: 0,
85
87
  virtual: false,
88
+ expression: "",
89
+ requires: [],
86
90
  }));
87
91
  });
88
92
  const retriever = new DebouncedBatchRetriever(base, 10);
@@ -8,7 +8,7 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { DataType, Rate, TimeStamp } from "@synnaxlabs/x/telem";
11
- import { describe, expect, it, test } from "vitest";
11
+ import { beforeAll, describe, expect, it, test } from "vitest";
12
12
 
13
13
  import { Channel } from "@/channel/client";
14
14
  import { NotFoundError, QueryError } from "@/errors";
@@ -31,6 +31,27 @@ describe("Channel", () => {
31
31
  expect(channel.dataType).toEqual(DataType.FLOAT32);
32
32
  });
33
33
 
34
+ test("create calculated", async () => {
35
+ let chOne = new Channel({
36
+ name: "test",
37
+ isIndex: true,
38
+ dataType: DataType.TIMESTAMP,
39
+ });
40
+ chOne = await client.channels.create(chOne);
41
+ let calculatedCH = new Channel({
42
+ name: "test2",
43
+ virtual: true,
44
+ dataType: DataType.FLOAT32,
45
+ expression: "test * 2",
46
+ requires: [chOne.key],
47
+ });
48
+ calculatedCH = await client.channels.create(calculatedCH);
49
+ expect(calculatedCH.key).not.toEqual(0);
50
+ expect(calculatedCH.virtual).toEqual(true);
51
+ expect(calculatedCH.expression).toEqual("test * 2");
52
+ expect(calculatedCH.requires).toEqual([chOne.key]);
53
+ });
54
+
34
55
  test("create index and indexed pair", async () => {
35
56
  const one = await client.channels.create({
36
57
  name: "Time",
@@ -48,18 +69,8 @@ describe("Channel", () => {
48
69
 
49
70
  test("create many", async () => {
50
71
  const channels = await client.channels.create([
51
- {
52
- name: "test1",
53
- leaseholder: 1,
54
- rate: Rate.hz(1),
55
- dataType: DataType.FLOAT32,
56
- },
57
- {
58
- name: "test2",
59
- leaseholder: 1,
60
- rate: Rate.hz(1),
61
- dataType: DataType.FLOAT32,
62
- },
72
+ { name: "test1", leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
73
+ { name: "test2", leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
63
74
  ]);
64
75
  expect(channels.length).toEqual(2);
65
76
  expect(channels[0].name).toEqual("test1");
@@ -116,12 +127,7 @@ describe("Channel", () => {
116
127
  dataType: DataType.FLOAT32,
117
128
  });
118
129
  const channelTwo = await client.channels.create(
119
- {
120
- name,
121
- leaseholder: 1,
122
- rate: Rate.hz(1),
123
- dataType: DataType.FLOAT32,
124
- },
130
+ { name, leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
125
131
  { retrieveIfNameExists: true },
126
132
  );
127
133
  expect(channelTwo.key).toEqual(channel.key);
@@ -155,12 +161,7 @@ describe("Channel", () => {
155
161
  });
156
162
  const channelTwo = await client.channels.create(
157
163
  [
158
- {
159
- name,
160
- leaseholder: 1,
161
- rate: Rate.hz(1),
162
- dataType: DataType.FLOAT32,
163
- },
164
+ { name, leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
164
165
  {
165
166
  name: `${name}-2`,
166
167
  leaseholder: 1,
@@ -253,18 +254,8 @@ describe("Channel", () => {
253
254
  });
254
255
  test("multiple rename", async () => {
255
256
  const channels = await client.channels.create([
256
- {
257
- name: "test1",
258
- leaseholder: 1,
259
- rate: Rate.hz(1),
260
- dataType: DataType.FLOAT32,
261
- },
262
- {
263
- name: "test2",
264
- leaseholder: 1,
265
- rate: Rate.hz(1),
266
- dataType: DataType.FLOAT32,
267
- },
257
+ { name: "test1", leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
258
+ { name: "test2", leaseholder: 1, rate: Rate.hz(1), dataType: DataType.FLOAT32 },
268
259
  ]);
269
260
  // Retrieve channels here to ensure we check for cache invalidation
270
261
  const initial = await client.channels.retrieve(channels.map((c) => c.key));
@@ -279,4 +270,84 @@ describe("Channel", () => {
279
270
  expect(renamed[1].name).toEqual("test4");
280
271
  });
281
272
  });
273
+
274
+ describe("update", () => {
275
+ let idxCH: Channel;
276
+ beforeAll(async () => {
277
+ idxCH = await client.channels.create({
278
+ name: "idx",
279
+ dataType: DataType.TIMESTAMP,
280
+ isIndex: true,
281
+ });
282
+ });
283
+ test("update virtual channel expression", async () => {
284
+ const channel = await client.channels.create({
285
+ name: "virtual-calc",
286
+ dataType: DataType.FLOAT32,
287
+ virtual: true,
288
+ expression: "return 1",
289
+ requires: [idxCH.key],
290
+ });
291
+
292
+ const updated = await client.channels.create({
293
+ key: channel.key,
294
+ name: channel.name,
295
+ dataType: channel.dataType,
296
+ virtual: true,
297
+ expression: "return 2",
298
+ requires: [idxCH.key],
299
+ });
300
+
301
+ const channelsWithName = await client.channels.retrieve(["virtual-calc"]);
302
+ expect(channelsWithName.length).toEqual(1);
303
+
304
+ expect(updated.expression).toEqual("return 2");
305
+
306
+ const retrieved = await client.channels.retrieve(channel.key);
307
+ expect(retrieved.expression).toEqual("return 2");
308
+ });
309
+
310
+ test("update calculated channel name", async () => {
311
+ const channel = await client.channels.create({
312
+ name: "virtual-calc",
313
+ dataType: DataType.FLOAT32,
314
+ virtual: true,
315
+ expression: "return 1",
316
+ requires: [idxCH.key],
317
+ });
318
+
319
+ const updated = await client.channels.create({
320
+ key: channel.key,
321
+ name: "new-name",
322
+ dataType: channel.dataType,
323
+ virtual: true,
324
+ expression: channel.expression,
325
+ requires: [idxCH.key],
326
+ });
327
+ expect(updated.name).toEqual("new-name");
328
+
329
+ const retrieved = await client.channels.retrieve(channel.key);
330
+ expect(retrieved.name).toEqual("new-name");
331
+ });
332
+
333
+ test("should not allow updates to non-virtual channels", async () => {
334
+ const channel = await client.channels.create({
335
+ name: "regular-channel",
336
+ leaseholder: 1,
337
+ rate: Rate.hz(1),
338
+ dataType: DataType.FLOAT32,
339
+ });
340
+
341
+ const _updated = await client.channels.create({
342
+ key: channel.key,
343
+ name: "new-name",
344
+ leaseholder: channel.leaseholder,
345
+ rate: channel.rate,
346
+ dataType: channel.dataType,
347
+ });
348
+
349
+ const retrieved = await client.channels.retrieve(channel.key);
350
+ expect(retrieved.name).toEqual("regular-channel");
351
+ });
352
+ });
282
353
  });
@@ -25,6 +25,7 @@ import {
25
25
  type Key,
26
26
  type KeyOrName,
27
27
  type NewPayload,
28
+ ontologyID as payloadOntologyID,
28
29
  type Params,
29
30
  type Payload,
30
31
  payload,
@@ -40,7 +41,7 @@ import {
40
41
  import { type Writer } from "@/channel/writer";
41
42
  import { ValidationError } from "@/errors";
42
43
  import { type framer } from "@/framer";
43
- import { ontology } from "@/ontology";
44
+ import { type ontology } from "@/ontology";
44
45
  import { group } from "@/ontology/group";
45
46
  import { checkForMultipleOrNoResults } from "@/util/retrieve";
46
47
 
@@ -107,6 +108,15 @@ export class Channel {
107
108
  * database, but can still be used for streaming purposes.
108
109
  */
109
110
  readonly virtual: boolean;
111
+ /**
112
+ * Only used for calculated channels. Specifies the python expression to evaluate
113
+ * the calculated value
114
+ */
115
+ readonly expression: string;
116
+ /**
117
+ * Only used for calculated channels. Specifies the channels required for calculation
118
+ */
119
+ readonly requires: Key[];
110
120
 
111
121
  constructor({
112
122
  dataType,
@@ -120,6 +130,8 @@ export class Channel {
120
130
  virtual = false,
121
131
  frameClient,
122
132
  alias,
133
+ expression = "",
134
+ requires = [],
123
135
  }: NewPayload & {
124
136
  frameClient?: framer.Client;
125
137
  density?: CrudeDensity;
@@ -134,6 +146,8 @@ export class Channel {
134
146
  this.internal = internal;
135
147
  this.alias = alias;
136
148
  this.virtual = virtual;
149
+ this.expression = expression;
150
+ this.requires = requires ?? [];
137
151
  this._frameClient = frameClient ?? null;
138
152
  }
139
153
 
@@ -158,14 +172,21 @@ export class Channel {
158
172
  index: this.index,
159
173
  isIndex: this.isIndex,
160
174
  internal: this.internal,
175
+ virtual: this.virtual,
176
+ expression: this.expression,
177
+ requires: this.requires,
161
178
  });
162
179
  }
163
180
 
181
+ get isCalculated(): boolean {
182
+ return isCalculated(this.payload);
183
+ }
184
+
164
185
  /***
165
186
  * @returns the ontology ID of the channel
166
187
  */
167
188
  get ontologyID(): ontology.ID {
168
- return new ontology.ID({ type: "channel", key: this.key.toString() });
189
+ return payloadOntologyID(this.key);
169
190
  }
170
191
 
171
192
  /**
@@ -421,3 +442,25 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
421
442
  return new group.Group(res.group.name, res.group.key);
422
443
  }
423
444
  }
445
+
446
+ export const isCalculated = ({ virtual, expression }: Payload): boolean =>
447
+ virtual && expression !== "";
448
+
449
+ export const resolveCalculatedIndex = async (
450
+ retrieve: (key: Key) => Promise<Payload>,
451
+ channel: Payload,
452
+ ): Promise<Key | null> => {
453
+ if (!isCalculated(channel)) return channel.index;
454
+ for (const required of channel.requires) {
455
+ const requiredChannel = await retrieve(required);
456
+ if (!requiredChannel.virtual) return requiredChannel.index;
457
+ }
458
+ for (const required of channel.requires) {
459
+ const requiredChannel = await retrieve(required);
460
+ if (isCalculated(requiredChannel)) {
461
+ const index = await resolveCalculatedIndex(retrieve, requiredChannel);
462
+ if (index != null) return index;
463
+ }
464
+ }
465
+ return null;
466
+ };
@@ -11,6 +11,7 @@ import { DataType, Rate } from "@synnaxlabs/x/telem";
11
11
  import { z } from "zod";
12
12
 
13
13
  import { ontology } from "@/ontology";
14
+ import { nullableArrayZ } from "@/util/zod";
14
15
 
15
16
  export const keyZ = z.number();
16
17
  export type Key = number;
@@ -32,6 +33,8 @@ export const payload = z.object({
32
33
  internal: z.boolean(),
33
34
  virtual: z.boolean(),
34
35
  alias: z.string().optional(),
36
+ expression: z.string().default(""),
37
+ requires: nullableArrayZ(keyZ),
35
38
  });
36
39
 
37
40
  export type Payload = z.infer<typeof payload>;
@@ -44,6 +47,8 @@ export const newPayload = payload.extend({
44
47
  isIndex: z.boolean().optional(),
45
48
  internal: z.boolean().optional().default(false),
46
49
  virtual: z.boolean().optional().default(false),
50
+ expression: z.string().optional().default(""),
51
+ requires: nullableArrayZ(keyZ).optional().default([]),
47
52
  });
48
53
 
49
54
  export type NewPayload = z.input<typeof newPayload>;
@@ -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 { toArray, unique } from "@synnaxlabs/x";
10
11
  import {
11
12
  MultiSeries,
12
13
  Series,
@@ -17,8 +18,6 @@ import {
17
18
  TimeRange,
18
19
  TimeStamp,
19
20
  } from "@synnaxlabs/x/telem";
20
- import { toArray } from "@synnaxlabs/x/toArray";
21
- import { unique } from "@synnaxlabs/x/unique";
22
21
  import { z } from "zod";
23
22
 
24
23
  import {
@@ -177,7 +176,7 @@ export class Frame {
177
176
  * error otherwise.
178
177
  */
179
178
  get uniqueKeys(): Keys {
180
- return unique(this.keys);
179
+ return unique.unique(this.keys);
181
180
  }
182
181
 
183
182
  /**
@@ -194,7 +193,7 @@ export class Frame {
194
193
  * otherwise.
195
194
  */
196
195
  get uniqueNames(): Names {
197
- return unique(this.names);
196
+ return unique.unique(this.names);
198
197
  }
199
198
 
200
199
  /**
@@ -108,3 +108,181 @@ describe("Streamer", () => {
108
108
  expect(Array.from(d.get(ch.key))).toEqual([1]);
109
109
  });
110
110
  });
111
+
112
+ describe("Streamer - Calculated Channels", () => {
113
+ test("basic calculated channel streaming", async () => {
114
+ // Create a timestamp index channel
115
+ const timeChannel = await client.channels.create({
116
+ name: "calc_test_time",
117
+ isIndex: true,
118
+ dataType: DataType.TIMESTAMP,
119
+ });
120
+
121
+ // Create source channels with the timestamp index
122
+ const [channelA, channelB] = await client.channels.create([
123
+ {
124
+ name: "test_a",
125
+ dataType: DataType.FLOAT64,
126
+ index: timeChannel.key,
127
+ },
128
+ {
129
+ name: "test_b",
130
+ dataType: DataType.FLOAT64,
131
+ index: timeChannel.key,
132
+ },
133
+ ]);
134
+
135
+ // Create calculated channel that adds the two source channels
136
+ const calcChannel = await client.channels.create({
137
+ name: "test_calc",
138
+ dataType: DataType.FLOAT64,
139
+ index: timeChannel.key,
140
+ virtual: true,
141
+ expression: "return test_a + test_b",
142
+ requires: [channelA.key, channelB.key],
143
+ });
144
+
145
+ // Set up streamer to listen for calculated results
146
+ const streamer = await client.openStreamer(calcChannel.key);
147
+
148
+ // Give streamer time to initialize
149
+ await new Promise((resolve) => setTimeout(resolve, 100));
150
+
151
+ // Write test data
152
+ const startTime = TimeStamp.now();
153
+ const writer = await client.openWriter({
154
+ start: startTime,
155
+ channels: [timeChannel.key, channelA.key, channelB.key],
156
+ });
157
+
158
+ try {
159
+ // Write test values - each source gets 2.5 so sum should be 5.0
160
+ await writer.write({
161
+ [timeChannel.key]: [startTime],
162
+ [channelA.key]: new Float64Array([2.5]),
163
+ [channelB.key]: new Float64Array([2.5]),
164
+ });
165
+
166
+ // Read from streamer
167
+ const frame = await streamer.read();
168
+
169
+ // Verify calculated results
170
+ const calcData = Array.from(frame.get(calcChannel.key));
171
+ expect(calcData).toEqual([5.0]);
172
+ } finally {
173
+ await writer.close();
174
+ streamer.close();
175
+ }
176
+ });
177
+ test("calculated channel with constant", async () => {
178
+ // Create an index channel for timestamps
179
+ const timeChannel = await client.channels.create({
180
+ name: "calc_const_time",
181
+ isIndex: true,
182
+ dataType: DataType.TIMESTAMP,
183
+ });
184
+
185
+ // Create base channel with index
186
+ const baseChannel = await client.channels.create({
187
+ name: "base_channel",
188
+ dataType: DataType.FLOAT64,
189
+ index: timeChannel.key,
190
+ });
191
+
192
+ // Create calculated channel that adds 5
193
+ const calcChannel = await client.channels.create({
194
+ name: "calc_const_channel",
195
+ dataType: DataType.FLOAT64,
196
+ index: timeChannel.key,
197
+ virtual: true,
198
+ expression: `return ${baseChannel.name} + 5`,
199
+ requires: [baseChannel.key],
200
+ });
201
+
202
+ const streamer = await client.openStreamer(calcChannel.key);
203
+ await new Promise((resolve) => setTimeout(resolve, 100));
204
+
205
+ const startTime = TimeStamp.now();
206
+ const writer = await client.openWriter({
207
+ start: startTime,
208
+ channels: [timeChannel.key, baseChannel.key],
209
+ });
210
+
211
+ try {
212
+ const timestamps = [
213
+ startTime,
214
+ new TimeStamp(startTime.valueOf() + BigInt(1000000000)),
215
+ new TimeStamp(startTime.valueOf() + BigInt(2000000000)),
216
+ ];
217
+
218
+ await writer.write({
219
+ [timeChannel.key]: timestamps,
220
+ [baseChannel.key]: new Float64Array([1, 2, 3]),
221
+ });
222
+
223
+ const frame = await streamer.read();
224
+ const calcData = Array.from(frame.get(calcChannel.key));
225
+ expect(calcData).toEqual([6, 7, 8]); // Original values + 5
226
+ } finally {
227
+ await writer.close();
228
+ streamer.close();
229
+ }
230
+ });
231
+
232
+ test("calculated channel with multiple operations", async () => {
233
+ // Create timestamp channel
234
+ const timeChannel = await client.channels.create({
235
+ name: "calc_multi_time",
236
+ isIndex: true,
237
+ dataType: DataType.TIMESTAMP,
238
+ });
239
+
240
+ // Create source channels
241
+ const [channelA, channelB] = await client.channels.create([
242
+ {
243
+ name: "multi_test_a",
244
+ dataType: DataType.FLOAT64,
245
+ index: timeChannel.key,
246
+ },
247
+ {
248
+ name: "multi_test_b",
249
+ dataType: DataType.FLOAT64,
250
+ index: timeChannel.key,
251
+ },
252
+ ]);
253
+
254
+ // Create calculated channel with multiple operations
255
+ const calcChannel = await client.channels.create({
256
+ name: "multi_calc",
257
+ dataType: DataType.FLOAT64,
258
+ index: timeChannel.key,
259
+ virtual: true,
260
+ expression: "return (multi_test_a * 2) + (multi_test_b / 2)",
261
+ requires: [channelA.key, channelB.key],
262
+ });
263
+
264
+ const streamer = await client.openStreamer(calcChannel.key);
265
+ await new Promise((resolve) => setTimeout(resolve, 100));
266
+
267
+ const startTime = TimeStamp.now();
268
+ const writer = await client.openWriter({
269
+ start: startTime,
270
+ channels: [timeChannel.key, channelA.key, channelB.key],
271
+ });
272
+
273
+ try {
274
+ await writer.write({
275
+ [timeChannel.key]: [startTime],
276
+ [channelA.key]: new Float64Array([2.0]), // Will be multiplied by 2 = 4.0
277
+ [channelB.key]: new Float64Array([4.0]), // Will be divided by 2 = 2.0
278
+ });
279
+
280
+ const frame = await streamer.read();
281
+ const calcData = Array.from(frame.get(calcChannel.key));
282
+ expect(calcData).toEqual([6.0]); // (2.0 * 2) + (4.0 / 2) = 4.0 + 2.0 = 6.0
283
+ } finally {
284
+ await writer.close();
285
+ streamer.close();
286
+ }
287
+ });
288
+ });
@@ -24,6 +24,7 @@ import {
24
24
  commandZ,
25
25
  type NewTask,
26
26
  newTaskZ,
27
+ ontologyID as payloadOntologyID,
27
28
  type Payload,
28
29
  type State,
29
30
  type StateObservable,
@@ -32,7 +33,7 @@ import {
32
33
  taskKeyZ,
33
34
  taskZ,
34
35
  } from "@/hardware/task/payload";
35
- import { ontology } from "@/ontology";
36
+ import { type ontology } from "@/ontology";
36
37
  import { type ranger } from "@/ranger";
37
38
  import { signals } from "@/signals";
38
39
  import { analyzeParams, checkForMultipleOrNoResults } from "@/util/retrieve";
@@ -95,7 +96,7 @@ export class Task<
95
96
  }
96
97
 
97
98
  get ontologyID(): ontology.ID {
98
- return new ontology.ID({ type: "task", key: this.key });
99
+ return payloadOntologyID(this.key);
99
100
  }
100
101
 
101
102
  async executeCommand(type: string, args?: UnknownRecord): Promise<string> {
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  export { access } from "@/access";
11
11
  export { policy } from "@/access/policy";
12
12
  export { channel } from "@/channel";
13
- export { Channel } from "@/channel/client";
13
+ export { Channel, isCalculated } from "@/channel/client";
14
14
  export { default as Synnax, type SynnaxProps, synnaxPropsZ } from "@/client";
15
15
  export * from "@/connection";
16
16
  export { control } from "@/control";
@@ -12,7 +12,7 @@ import { observe } from "@synnaxlabs/x";
12
12
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
13
13
 
14
14
  import { type framer } from "@/framer";
15
- import { type Key, type Label, labelZ } from "@/label/payload";
15
+ import { type Key, type Label, labelZ, ontologyID } from "@/label/payload";
16
16
  import { Retriever } from "@/label/retriever";
17
17
  import { type NewLabelPayload, type SetOptions, Writer } from "@/label/writer";
18
18
  import { ontology } from "@/ontology";
@@ -105,7 +105,7 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
105
105
  ): Promise<observe.ObservableAsyncCloseable<Label[]>> {
106
106
  const wrapper = new observe.Observer<Label[]>();
107
107
  const initial = (await this.retrieveFor(id)).map((l) => ({
108
- id: new ontology.ID({ key: l.key, type: "label" }),
108
+ id: ontologyID(l.key),
109
109
  key: l.key,
110
110
  name: l.name,
111
111
  data: l,