@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.
- package/.turbo/turbo-build.log +7 -7
- package/dist/access/policy/payload.d.ts +10 -10
- package/dist/channel/client.d.ts +13 -1
- package/dist/channel/client.d.ts.map +1 -1
- package/dist/channel/payload.d.ts +14 -0
- package/dist/channel/payload.d.ts.map +1 -1
- package/dist/client.cjs +30 -30
- package/dist/client.js +2149 -2092
- package/dist/framer/frame.d.ts.map +1 -1
- package/dist/hardware/device/payload.d.ts +2 -2
- package/dist/hardware/device/payload.d.ts.map +1 -1
- package/dist/hardware/task/client.d.ts.map +1 -1
- package/dist/hardware/task/payload.d.ts +4 -4
- package/dist/hardware/task/payload.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/label/client.d.ts.map +1 -1
- package/dist/ontology/group/group.d.ts.map +1 -1
- package/dist/ontology/payload.d.ts.map +1 -1
- package/dist/ranger/client.d.ts.map +1 -1
- package/dist/ranger/payload.d.ts +2 -2
- package/dist/ranger/payload.d.ts.map +1 -1
- package/dist/util/retrieve.d.ts.map +1 -1
- package/dist/util/zod.d.ts +1 -1
- package/dist/util/zod.d.ts.map +1 -1
- package/examples/node/package-lock.json +56 -5509
- package/examples/node/package.json +1 -1
- package/package.json +10 -14
- package/src/access/policy/policy.spec.ts +1 -4
- package/src/channel/batchRetriever.spec.ts +4 -0
- package/src/channel/channel.spec.ts +108 -37
- package/src/channel/client.ts +45 -2
- package/src/channel/payload.ts +5 -0
- package/src/framer/frame.ts +3 -4
- package/src/framer/streamer.spec.ts +178 -0
- package/src/hardware/task/client.ts +3 -2
- package/src/index.ts +1 -1
- package/src/label/client.ts +2 -2
- package/src/ontology/group/group.ts +3 -5
- package/src/ontology/payload.ts +1 -1
- package/src/ranger/client.ts +6 -11
- package/src/ranger/payload.ts +2 -2
- package/src/util/retrieve.ts +2 -2
- package/src/util/zod.ts +4 -1
- package/vite.config.ts +5 -12
- package/api/client.api.md +0 -3473
- package/api-extractor.json +0 -7
- package/dist/hardware/task/ni/types.d.ts +0 -14495
- package/dist/hardware/task/ni/types.d.ts.map +0 -1
- package/dist/workspace/lineplot/payload.d.ts +0 -23
- package/dist/workspace/lineplot/payload.d.ts.map +0 -1
- package/src/hardware/task/ni/types.ts +0 -1716
- package/src/workspace/lineplot/payload.ts +0 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
|
-
"version": "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.
|
|
28
|
+
"uuid": "^11.0.5",
|
|
29
29
|
"zod": "^3.24.1",
|
|
30
|
-
"@synnaxlabs/
|
|
31
|
-
"@synnaxlabs/
|
|
30
|
+
"@synnaxlabs/freighter": "0.38.0",
|
|
31
|
+
"@synnaxlabs/x": "0.38.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^22.10.
|
|
34
|
+
"@types/node": "^22.10.6",
|
|
35
35
|
"@types/uuid": "^10.0.0",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/channel/client.ts
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/src/channel/payload.ts
CHANGED
|
@@ -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>;
|
package/src/framer/frame.ts
CHANGED
|
@@ -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
|
|
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";
|
package/src/label/client.ts
CHANGED
|
@@ -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:
|
|
108
|
+
id: ontologyID(l.key),
|
|
109
109
|
key: l.key,
|
|
110
110
|
name: l.name,
|
|
111
111
|
data: l,
|