@synnaxlabs/client 0.27.0 → 0.29.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/CONTRIBUTING.md +47 -0
- package/README.md +17 -32
- package/api/client.api.md +57 -15
- package/dist/channel/client.d.ts +10 -8
- package/dist/channel/client.d.ts.map +1 -1
- package/dist/channel/retriever.d.ts +1 -1
- package/dist/channel/retriever.d.ts.map +1 -1
- package/dist/client.cjs +20 -16
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1880 -1722
- package/dist/connection/checker.d.ts +21 -1
- package/dist/connection/checker.d.ts.map +1 -1
- package/dist/control/state.d.ts.map +1 -1
- package/dist/framer/client.d.ts.map +1 -1
- package/dist/framer/deleter.d.ts.map +1 -1
- package/dist/framer/frame.d.ts.map +1 -1
- package/dist/framer/streamer.d.ts +2 -4
- package/dist/framer/streamer.d.ts.map +1 -1
- package/dist/framer/writer.d.ts.map +1 -1
- package/dist/hardware/task/client.d.ts +12 -2
- package/dist/hardware/task/client.d.ts.map +1 -1
- package/dist/hardware/task/ni/types.d.ts +14495 -0
- package/dist/hardware/task/ni/types.d.ts.map +1 -0
- package/dist/hardware/task/payload.d.ts +6 -0
- package/dist/hardware/task/payload.d.ts.map +1 -1
- package/dist/label/retriever.d.ts +1 -1
- package/dist/ontology/client.d.ts +16 -5
- package/dist/ontology/client.d.ts.map +1 -1
- package/dist/ontology/group/payload.d.ts +1 -1
- package/dist/ontology/group/writer.d.ts.map +1 -1
- package/dist/ontology/payload.d.ts +1 -2
- package/dist/ontology/payload.d.ts.map +1 -1
- package/dist/ranger/client.d.ts +3 -1
- package/dist/ranger/client.d.ts.map +1 -1
- package/dist/ranger/payload.d.ts +25 -25
- package/dist/ranger/payload.d.ts.map +1 -1
- package/dist/ranger/writer.d.ts.map +1 -1
- package/dist/workspace/lineplot/client.d.ts +1 -1
- package/dist/workspace/lineplot/client.d.ts.map +1 -1
- package/dist/workspace/lineplot/retriever.d.ts +1 -1
- package/dist/workspace/lineplot/retriever.d.ts.map +1 -1
- package/dist/workspace/lineplot/writer.d.ts +3 -3
- package/dist/workspace/lineplot/writer.d.ts.map +1 -1
- package/dist/workspace/retriever.d.ts +1 -1
- package/dist/workspace/retriever.d.ts.map +1 -1
- package/dist/workspace/schematic/retriever.d.ts +1 -1
- package/dist/workspace/schematic/retriever.d.ts.map +1 -1
- package/dist/workspace/schematic/writer.d.ts +1 -1
- package/dist/workspace/schematic/writer.d.ts.map +1 -1
- package/package.json +24 -16
- package/src/channel/client.ts +10 -8
- package/src/channel/retriever.ts +2 -2
- package/src/client.ts +13 -5
- package/src/connection/checker.ts +58 -1
- package/src/connection/connection.spec.ts +43 -3
- package/src/framer/client.ts +2 -3
- package/src/framer/streamer.spec.ts +2 -7
- package/src/framer/streamer.ts +5 -10
- package/src/hardware/rack/client.ts +3 -4
- package/src/hardware/task/client.ts +82 -6
- package/src/hardware/task/ni/types.ts +1716 -0
- package/src/hardware/task/payload.ts +1 -0
- package/src/hardware/task/task.spec.ts +45 -30
- package/src/label/client.ts +5 -5
- package/src/label/retriever.ts +2 -2
- package/src/ontology/client.ts +43 -31
- package/src/ontology/group/payload.ts +4 -4
- package/src/ontology/group/writer.ts +10 -12
- package/src/ontology/ontology.spec.ts +3 -5
- package/src/ontology/payload.ts +1 -1
- package/src/ranger/client.ts +55 -49
- package/src/ranger/payload.ts +4 -4
- package/src/vite-env.d.ts +1 -1
- package/src/workspace/lineplot/client.ts +2 -2
- package/src/workspace/lineplot/linePlot.spec.ts +12 -12
- package/src/workspace/lineplot/retriever.ts +2 -2
- package/src/workspace/lineplot/writer.ts +8 -8
- package/src/workspace/retriever.ts +2 -2
- package/src/workspace/schematic/retriever.ts +2 -2
- package/src/workspace/schematic/writer.ts +8 -6
- package/vite.config.ts +5 -0
package/package.json
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "The Client Library for Synnax",
|
|
7
|
-
"repository": "https://github.com/synnaxlabs/synnax/tree/main/client/ts",
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "The Synnax Client Library",
|
|
8
5
|
"keywords": [
|
|
9
6
|
"synnax",
|
|
10
7
|
"grpc",
|
|
@@ -15,29 +12,40 @@
|
|
|
15
12
|
"telemetry",
|
|
16
13
|
"control systems"
|
|
17
14
|
],
|
|
15
|
+
"homepage": "https://github.com/synnaxlabs/synnax/tree/main/client/ts",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/synnaxlabs/synnax/issues"
|
|
18
|
+
},
|
|
19
|
+
"license": "BUSL-1.1",
|
|
20
|
+
"main": "dist/client.cjs",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/synnaxlabs/synnax.git",
|
|
24
|
+
"directory": "client/ts"
|
|
25
|
+
},
|
|
18
26
|
"dependencies": {
|
|
19
27
|
"async-mutex": "^0.5.0",
|
|
20
28
|
"zod": "3.23.8",
|
|
21
|
-
"@synnaxlabs/freighter": "0.
|
|
22
|
-
"@synnaxlabs/x": "0.
|
|
29
|
+
"@synnaxlabs/freighter": "0.29.0",
|
|
30
|
+
"@synnaxlabs/x": "0.29.0"
|
|
23
31
|
},
|
|
24
32
|
"devDependencies": {
|
|
25
|
-
"@types/node": "^22.
|
|
26
|
-
"@vitest/coverage-v8": "^2.0
|
|
27
|
-
"eslint": "^9.
|
|
28
|
-
"typescript": "^5.
|
|
29
|
-
"vite": "5.4.
|
|
30
|
-
"vitest": "^2.0
|
|
33
|
+
"@types/node": "^22.5.4",
|
|
34
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
35
|
+
"eslint": "^9.10.0",
|
|
36
|
+
"typescript": "^5.6.2",
|
|
37
|
+
"vite": "5.4.4",
|
|
38
|
+
"vitest": "^2.1.0",
|
|
31
39
|
"@synnaxlabs/tsconfig": "0.0.2",
|
|
32
40
|
"@synnaxlabs/vite-plugin": "0.0.1",
|
|
33
41
|
"eslint-config-synnaxlabs": "0.0.1"
|
|
34
42
|
},
|
|
35
43
|
"peerDependencies": {
|
|
36
|
-
"zod": "
|
|
44
|
+
"zod": "3.23.8"
|
|
37
45
|
},
|
|
38
|
-
"
|
|
39
|
-
"module": "dist/client.js",
|
|
46
|
+
"type": "module",
|
|
40
47
|
"types": "dist/index.d.ts",
|
|
48
|
+
"module": "dist/client.js",
|
|
41
49
|
"scripts": {
|
|
42
50
|
"build": "tsc --noEmit && vite build",
|
|
43
51
|
"watch": "tsc --noEmit && vite build --watch",
|
package/src/channel/client.ts
CHANGED
|
@@ -53,7 +53,8 @@ interface CreateOptions {
|
|
|
53
53
|
* instantiated directly, but instead created via the `.channels.create` or retrieved
|
|
54
54
|
* via the `.channels.retrieve` method on a Synnax client.
|
|
55
55
|
*
|
|
56
|
-
* Please refer to the [Synnax
|
|
56
|
+
* Please refer to the [Synnax
|
|
57
|
+
* documentation](https://docs.synnaxlabs.com/reference/concepts/channels) for detailed
|
|
57
58
|
* information on what channels are and how to use them.
|
|
58
59
|
*/
|
|
59
60
|
export class Channel {
|
|
@@ -228,13 +229,14 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
|
|
|
228
229
|
* @param rate - The rate of the channel. This only applies to fixed rate channels.
|
|
229
230
|
* @param dataType - The data type for the samples stored in the channel.
|
|
230
231
|
* @param index - The key of the index channel that this channel should be associated
|
|
231
|
-
* with. An 'index' channel is a channel that stores timestamps for other channels.
|
|
232
|
-
* to the Synnax documentation
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
232
|
+
* with. An 'index' channel is a channel that stores timestamps for other channels.
|
|
233
|
+
* Refer to the Synnax documentation
|
|
234
|
+
* (https://docs.synnaxlabs.com/reference/concepts/channels) for more information. The
|
|
235
|
+
* index channel must have already been created. This field does not need to be
|
|
236
|
+
* specified if the channel is an index channel, or the channel is a fixed rate
|
|
237
|
+
* channel. If this value is specified, the 'rate' parameter will be ignored.
|
|
238
|
+
* @param isIndex - Set to true if the channel is an index channel, and false
|
|
239
|
+
* otherwise. Index channels must have a data type of `DataType.TIMESTAMP`.
|
|
238
240
|
* @returns the created channel. {@see Channel}
|
|
239
241
|
* @throws {ValidationError} if any of the parameters for creating the channel are
|
|
240
242
|
* invalid.
|
package/src/channel/retriever.ts
CHANGED
|
@@ -267,9 +267,9 @@ export class DebouncedBatchRetriever implements Retriever {
|
|
|
267
267
|
|
|
268
268
|
export const retrieveRequired = async (
|
|
269
269
|
r: Retriever,
|
|
270
|
-
|
|
270
|
+
channels: Params,
|
|
271
271
|
): Promise<Payload[]> => {
|
|
272
|
-
const { normalized } = analyzeChannelParams(
|
|
272
|
+
const { normalized } = analyzeChannelParams(channels);
|
|
273
273
|
const results = await r.retrieve(normalized);
|
|
274
274
|
const notFound: KeyOrName[] = [];
|
|
275
275
|
normalized.forEach((v) => {
|
package/src/client.ts
CHANGED
|
@@ -79,6 +79,11 @@ export default class Synnax extends framer.Client {
|
|
|
79
79
|
static readonly connectivity = connection.Checker;
|
|
80
80
|
private readonly transport: Transport;
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The version of the client.
|
|
84
|
+
*/
|
|
85
|
+
readonly clientVersion: string = __VERSION__;
|
|
86
|
+
|
|
82
87
|
/**
|
|
83
88
|
* @param props.host - Hostname of a node in the cluster.
|
|
84
89
|
* @param props.port - Port of the node in the cluster.
|
|
@@ -101,10 +106,7 @@ export default class Synnax extends framer.Client {
|
|
|
101
106
|
transport.use(errorsMiddleware);
|
|
102
107
|
let auth_: auth.Client | undefined;
|
|
103
108
|
if (username != null && password != null) {
|
|
104
|
-
auth_ = new auth.Client(transport.unary, {
|
|
105
|
-
username,
|
|
106
|
-
password,
|
|
107
|
-
});
|
|
109
|
+
auth_ = new auth.Client(transport.unary, { username, password });
|
|
108
110
|
transport.use(auth_.middleware());
|
|
109
111
|
}
|
|
110
112
|
const chRetriever = new channel.CacheRetriever(
|
|
@@ -120,6 +122,7 @@ export default class Synnax extends framer.Client {
|
|
|
120
122
|
this.connectivity = new connection.Checker(
|
|
121
123
|
transport.unary,
|
|
122
124
|
connectivityPollFrequency,
|
|
125
|
+
this.clientVersion,
|
|
123
126
|
props.name,
|
|
124
127
|
);
|
|
125
128
|
this.control = new control.Client(this);
|
|
@@ -138,7 +141,12 @@ export default class Synnax extends framer.Client {
|
|
|
138
141
|
this.user = new user.Client(this.transport.unary);
|
|
139
142
|
this.workspaces = new workspace.Client(this.transport.unary);
|
|
140
143
|
const devices = new device.Client(this.transport.unary, this);
|
|
141
|
-
const tasks = new task.Client(
|
|
144
|
+
const tasks = new task.Client(
|
|
145
|
+
this.transport.unary,
|
|
146
|
+
this,
|
|
147
|
+
this.ontology,
|
|
148
|
+
this.ranges,
|
|
149
|
+
);
|
|
142
150
|
const racks = new rack.Client(this.transport.unary, this, tasks);
|
|
143
151
|
this.hardware = new hardware.Client(tasks, racks, devices);
|
|
144
152
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// included in the file licenses/APL.txt.
|
|
9
9
|
|
|
10
10
|
import type { UnaryClient } from "@synnaxlabs/freighter";
|
|
11
|
+
import { migrate } from "@synnaxlabs/x";
|
|
11
12
|
import { TimeSpan } from "@synnaxlabs/x/telem";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
|
|
@@ -20,12 +21,16 @@ export const state = z.object({
|
|
|
20
21
|
error: z.instanceof(Error).optional(),
|
|
21
22
|
message: z.string().optional(),
|
|
22
23
|
clusterKey: z.string(),
|
|
24
|
+
clientVersion: z.string(),
|
|
25
|
+
clientServerCompatible: z.boolean(),
|
|
26
|
+
nodeVersion: z.string().optional(),
|
|
23
27
|
});
|
|
24
28
|
|
|
25
29
|
export type State = z.infer<typeof state>;
|
|
26
30
|
|
|
27
31
|
const responseZ = z.object({
|
|
28
32
|
clusterKey: z.string(),
|
|
33
|
+
nodeVersion: z.string().optional(),
|
|
29
34
|
});
|
|
30
35
|
|
|
31
36
|
const DEFAULT: State = {
|
|
@@ -33,6 +38,19 @@ const DEFAULT: State = {
|
|
|
33
38
|
status: "disconnected",
|
|
34
39
|
error: undefined,
|
|
35
40
|
message: "Disconnected",
|
|
41
|
+
clientServerCompatible: false,
|
|
42
|
+
clientVersion: __VERSION__,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const generateWarning = (
|
|
46
|
+
nodeVersion: string | null,
|
|
47
|
+
clientVersion: string,
|
|
48
|
+
clientIsNewer: boolean,
|
|
49
|
+
): string => {
|
|
50
|
+
const toUpgrade = clientIsNewer ? "cluster" : "client";
|
|
51
|
+
return `Synnax cluster node version ${nodeVersion != null ? nodeVersion + " " : ""}is too ${clientIsNewer ? "old" : "new"} for client version ${clientVersion}.
|
|
52
|
+
This may cause compatibility issues. We recommend updating the ${toUpgrade}. For more information, see
|
|
53
|
+
https://docs.synnaxlabs.com/reference/typescript-client/troubleshooting#old-${toUpgrade}-version`;
|
|
36
54
|
};
|
|
37
55
|
|
|
38
56
|
/** Polls a synnax cluster for connectivity information. */
|
|
@@ -44,8 +62,10 @@ export class Checker {
|
|
|
44
62
|
private readonly client: UnaryClient;
|
|
45
63
|
private readonly name?: string;
|
|
46
64
|
private interval?: NodeJS.Timeout;
|
|
65
|
+
private readonly clientVersion: string;
|
|
47
66
|
private readonly onChangeHandlers: Array<(state: State) => void> = [];
|
|
48
67
|
static readonly connectionStateZ = state;
|
|
68
|
+
private versionWarned = false;
|
|
49
69
|
|
|
50
70
|
/**
|
|
51
71
|
* @param client - The transport client to use for connectivity checks.
|
|
@@ -55,11 +75,13 @@ export class Checker {
|
|
|
55
75
|
constructor(
|
|
56
76
|
client: UnaryClient,
|
|
57
77
|
pollFreq: TimeSpan = TimeSpan.seconds(30),
|
|
78
|
+
clientVersion: string,
|
|
58
79
|
name?: string,
|
|
59
80
|
) {
|
|
60
81
|
this._state = { ...DEFAULT };
|
|
61
82
|
this.client = client;
|
|
62
83
|
this.pollFrequency = pollFreq;
|
|
84
|
+
this.clientVersion = clientVersion;
|
|
63
85
|
this.name = name;
|
|
64
86
|
void this.check();
|
|
65
87
|
this.startChecking();
|
|
@@ -77,11 +99,46 @@ export class Checker {
|
|
|
77
99
|
async check(): Promise<State> {
|
|
78
100
|
const prevStatus = this._state.status;
|
|
79
101
|
try {
|
|
80
|
-
const [res, err] = await this.client.send(
|
|
102
|
+
const [res, err] = await this.client.send(
|
|
103
|
+
Checker.ENDPOINT,
|
|
104
|
+
{},
|
|
105
|
+
z.object({}),
|
|
106
|
+
responseZ,
|
|
107
|
+
);
|
|
81
108
|
if (err != null) throw err;
|
|
109
|
+
const nodeVersion = res.nodeVersion;
|
|
110
|
+
const clientVersion = this.clientVersion;
|
|
111
|
+
const warned = this.versionWarned;
|
|
112
|
+
if (nodeVersion == null) {
|
|
113
|
+
this._state.clientServerCompatible = false;
|
|
114
|
+
if (!warned) {
|
|
115
|
+
console.warn(generateWarning(null, clientVersion, true));
|
|
116
|
+
this.versionWarned = true;
|
|
117
|
+
}
|
|
118
|
+
} else if (
|
|
119
|
+
!migrate.versionsEqual(clientVersion, nodeVersion, {
|
|
120
|
+
checkMajor: true,
|
|
121
|
+
checkMinor: true,
|
|
122
|
+
checkPatch: false,
|
|
123
|
+
})
|
|
124
|
+
) {
|
|
125
|
+
this._state.clientServerCompatible = false;
|
|
126
|
+
if (!warned) {
|
|
127
|
+
console.warn(
|
|
128
|
+
generateWarning(
|
|
129
|
+
nodeVersion,
|
|
130
|
+
clientVersion,
|
|
131
|
+
migrate.semVerNewer(clientVersion, nodeVersion),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
this.versionWarned = true;
|
|
135
|
+
}
|
|
136
|
+
} else this._state.clientServerCompatible = true;
|
|
82
137
|
this._state.status = "connected";
|
|
83
138
|
this._state.message = `Connected to ${this.name ?? "cluster"}`;
|
|
84
139
|
this._state.clusterKey = res.clusterKey;
|
|
140
|
+
this._state.nodeVersion = res.nodeVersion;
|
|
141
|
+
this._state.clientVersion = this.clientVersion;
|
|
85
142
|
} catch (err) {
|
|
86
143
|
this._state.status = "failed";
|
|
87
144
|
this._state.error = err as Error;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { URL } from "@synnaxlabs/x/url";
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { z } from "zod";
|
|
12
13
|
|
|
13
14
|
import { auth } from "@/auth";
|
|
14
15
|
import { Checker } from "@/connection/checker";
|
|
@@ -23,8 +24,47 @@ describe("connectivity", () => {
|
|
|
23
24
|
password: "seldon",
|
|
24
25
|
});
|
|
25
26
|
transport.use(client.middleware());
|
|
26
|
-
const connectivity = new Checker(transport.unary);
|
|
27
|
-
await connectivity.check();
|
|
28
|
-
expect(
|
|
27
|
+
const connectivity = new Checker(transport.unary, undefined, __VERSION__);
|
|
28
|
+
const state = await connectivity.check();
|
|
29
|
+
expect(state.status).toEqual("connected");
|
|
30
|
+
expect(z.string().uuid().safeParse(state.clusterKey).success).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
describe("version compatibility", () => {
|
|
33
|
+
it("should pull the server and client versions", async () => {
|
|
34
|
+
const transport = new Transport(new URL({ host: HOST, port: PORT }));
|
|
35
|
+
const client = new auth.Client(transport.unary, {
|
|
36
|
+
username: "synnax",
|
|
37
|
+
password: "seldon",
|
|
38
|
+
});
|
|
39
|
+
transport.use(client.middleware());
|
|
40
|
+
const connectivity = new Checker(transport.unary, undefined, __VERSION__);
|
|
41
|
+
const state = await connectivity.check();
|
|
42
|
+
expect(state.clientServerCompatible).toBe(true);
|
|
43
|
+
expect(state.clientVersion).toBe(__VERSION__);
|
|
44
|
+
});
|
|
45
|
+
it("should adjust state if the server is too old", async () => {
|
|
46
|
+
const transport = new Transport(new URL({ host: HOST, port: PORT }));
|
|
47
|
+
const client = new auth.Client(transport.unary, {
|
|
48
|
+
username: "synnax",
|
|
49
|
+
password: "seldon",
|
|
50
|
+
});
|
|
51
|
+
transport.use(client.middleware());
|
|
52
|
+
const connectivity = new Checker(transport.unary, undefined, "50000.0.0");
|
|
53
|
+
const state = await connectivity.check();
|
|
54
|
+
expect(state.clientServerCompatible).toBe(false);
|
|
55
|
+
expect(state.clientVersion).toBe("50000.0.0");
|
|
56
|
+
});
|
|
57
|
+
it("should adjust state if the server is too new", async () => {
|
|
58
|
+
const transport = new Transport(new URL({ host: HOST, port: PORT }));
|
|
59
|
+
const client = new auth.Client(transport.unary, {
|
|
60
|
+
username: "synnax",
|
|
61
|
+
password: "seldon",
|
|
62
|
+
});
|
|
63
|
+
transport.use(client.middleware());
|
|
64
|
+
const connectivity = new Checker(transport.unary, undefined, "0.0.0");
|
|
65
|
+
const state = await connectivity.check();
|
|
66
|
+
expect(state.clientServerCompatible).toBe(false);
|
|
67
|
+
expect(state.clientVersion).toBe("0.0.0");
|
|
68
|
+
});
|
|
29
69
|
});
|
|
30
70
|
});
|
package/src/framer/client.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
type MultiSeries,
|
|
16
16
|
TimeRange,
|
|
17
17
|
TimeSpan,
|
|
18
|
-
toArray,
|
|
19
18
|
} from "@synnaxlabs/x";
|
|
20
19
|
|
|
21
20
|
import { type Key, type KeyOrName, KeysOrNames, type Params } from "@/channel/payload";
|
|
@@ -170,8 +169,8 @@ export class Client {
|
|
|
170
169
|
return fr;
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
private async readFrame(tr: CrudeTimeRange,
|
|
174
|
-
const i = await this.openIterator(tr,
|
|
172
|
+
private async readFrame(tr: CrudeTimeRange, channels: Params): Promise<Frame> {
|
|
173
|
+
const i = await this.openIterator(tr, channels);
|
|
175
174
|
const frame = new Frame();
|
|
176
175
|
try {
|
|
177
176
|
for await (const f of i) frame.push(f);
|
|
@@ -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 { describe, expect, it, test } from "vitest";
|
|
12
12
|
|
|
13
13
|
import { type channel } from "@/channel";
|
|
14
14
|
import { newClient } from "@/setupspecs";
|
|
@@ -42,12 +42,7 @@ describe("Streamer", () => {
|
|
|
42
42
|
});
|
|
43
43
|
test("open with config", async () => {
|
|
44
44
|
const ch = await newChannel();
|
|
45
|
-
await expect(
|
|
46
|
-
client.openStreamer({
|
|
47
|
-
channels: ch.key,
|
|
48
|
-
from: TimeStamp.now(),
|
|
49
|
-
}),
|
|
50
|
-
).resolves.not.toThrow();
|
|
45
|
+
await expect(client.openStreamer({ channels: ch.key })).resolves.not.toThrow();
|
|
51
46
|
});
|
|
52
47
|
it("should not throw an error when the streamer is opened with zero channels", async () => {
|
|
53
48
|
await expect(client.openStreamer([])).resolves.not.toThrow();
|
package/src/framer/streamer.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import { EOF, errorZ, type Stream, type StreamClient } from "@synnaxlabs/freighter";
|
|
11
11
|
import { observe } from "@synnaxlabs/x";
|
|
12
|
-
import { type CrudeTimeStamp, TimeStamp } from "@synnaxlabs/x/telem";
|
|
13
12
|
import { z } from "zod";
|
|
14
13
|
|
|
15
14
|
import { type Key, type Params } from "@/channel/payload";
|
|
@@ -18,10 +17,7 @@ import { ReadFrameAdapter } from "@/framer/adapter";
|
|
|
18
17
|
import { Frame, frameZ } from "@/framer/frame";
|
|
19
18
|
import { StreamProxy } from "@/framer/streamProxy";
|
|
20
19
|
|
|
21
|
-
const reqZ = z.object({
|
|
22
|
-
start: TimeStamp.z.optional(),
|
|
23
|
-
keys: z.number().array(),
|
|
24
|
-
});
|
|
20
|
+
const reqZ = z.object({ keys: z.number().array() });
|
|
25
21
|
|
|
26
22
|
const resZ = z.object({
|
|
27
23
|
frame: frameZ,
|
|
@@ -32,7 +28,6 @@ const ENDPOINT = "/frame/stream";
|
|
|
32
28
|
|
|
33
29
|
export interface StreamerConfig {
|
|
34
30
|
channels: Params;
|
|
35
|
-
from?: CrudeTimeStamp;
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
|
|
@@ -54,12 +49,12 @@ export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
|
|
|
54
49
|
static async _open(
|
|
55
50
|
retriever: Retriever,
|
|
56
51
|
client: StreamClient,
|
|
57
|
-
{ channels
|
|
52
|
+
{ channels }: StreamerConfig,
|
|
58
53
|
): Promise<Streamer> {
|
|
59
54
|
const adapter = await ReadFrameAdapter.open(retriever, channels);
|
|
60
55
|
const stream = await client.stream(ENDPOINT, reqZ, resZ);
|
|
61
56
|
const streamer = new Streamer(stream, adapter);
|
|
62
|
-
stream.send({
|
|
57
|
+
stream.send({ keys: adapter.keys });
|
|
63
58
|
return streamer;
|
|
64
59
|
}
|
|
65
60
|
|
|
@@ -77,8 +72,8 @@ export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
|
|
|
77
72
|
return this.adapter.adapt(new Frame((await this.stream.receive()).frame));
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
async update(
|
|
81
|
-
await this.adapter.update(
|
|
75
|
+
async update(channels: Params): Promise<void> {
|
|
76
|
+
await this.adapter.update(channels);
|
|
82
77
|
this.stream.send({ keys: this.adapter.keys });
|
|
83
78
|
}
|
|
84
79
|
|
|
@@ -127,9 +127,9 @@ export class Client implements AsyncTermSearcher<string, RackKey, Rack> {
|
|
|
127
127
|
async retrieve(keys: number[] | RackKey[]): Promise<Rack[]>;
|
|
128
128
|
|
|
129
129
|
async retrieve(
|
|
130
|
-
|
|
130
|
+
racks: string | RackKey | string[] | RackKey[],
|
|
131
131
|
): Promise<Rack | Rack[]> {
|
|
132
|
-
const { variant, normalized, single } = analyzeParams(
|
|
132
|
+
const { variant, normalized, single } = analyzeParams(racks, {
|
|
133
133
|
string: "names",
|
|
134
134
|
number: "keys",
|
|
135
135
|
});
|
|
@@ -141,7 +141,7 @@ export class Client implements AsyncTermSearcher<string, RackKey, Rack> {
|
|
|
141
141
|
retrieveRackResZ,
|
|
142
142
|
);
|
|
143
143
|
const sugared = this.sugar(res.racks);
|
|
144
|
-
checkForMultipleOrNoResults("Rack",
|
|
144
|
+
checkForMultipleOrNoResults("Rack", racks, sugared, single);
|
|
145
145
|
return single ? sugared[0] : sugared;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -186,4 +186,3 @@ export class Rack {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
export { rackKeyZ };
|
|
189
|
-
|
|
@@ -18,7 +18,19 @@ import { z } from "zod";
|
|
|
18
18
|
import { framer } from "@/framer";
|
|
19
19
|
import { type Frame } from "@/framer/frame";
|
|
20
20
|
import { rack } from "@/hardware/rack";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
NewTask,
|
|
23
|
+
newTaskZ,
|
|
24
|
+
Payload,
|
|
25
|
+
State,
|
|
26
|
+
StateObservable,
|
|
27
|
+
stateZ,
|
|
28
|
+
TaskKey,
|
|
29
|
+
taskKeyZ,
|
|
30
|
+
taskZ,
|
|
31
|
+
} from "@/hardware/task/payload";
|
|
32
|
+
import { ontology } from "@/ontology";
|
|
33
|
+
import { ranger } from "@/ranger";
|
|
22
34
|
import { signals } from "@/signals";
|
|
23
35
|
import { analyzeParams, checkForMultipleOrNoResults } from "@/util/retrieve";
|
|
24
36
|
import { nullableArrayZ } from "@/util/zod";
|
|
@@ -26,6 +38,8 @@ import { nullableArrayZ } from "@/util/zod";
|
|
|
26
38
|
const TASK_STATE_CHANNEL = "sy_task_state";
|
|
27
39
|
const TASK_CMD_CHANNEL = "sy_task_cmd";
|
|
28
40
|
|
|
41
|
+
const TASK_NOT_CREATED = new Error("Task not created");
|
|
42
|
+
|
|
29
43
|
export class Task<
|
|
30
44
|
C extends UnknownRecord = UnknownRecord,
|
|
31
45
|
D extends {} = UnknownRecord,
|
|
@@ -36,25 +50,34 @@ export class Task<
|
|
|
36
50
|
readonly internal: boolean;
|
|
37
51
|
readonly type: T;
|
|
38
52
|
readonly config: C;
|
|
53
|
+
readonly snapshot: boolean;
|
|
39
54
|
state?: State<D>;
|
|
40
|
-
private readonly frameClient: framer.Client;
|
|
55
|
+
private readonly frameClient: framer.Client | null;
|
|
56
|
+
private readonly ontologyClient: ontology.Client | null;
|
|
57
|
+
private readonly rangeClient: ranger.Client | null;
|
|
41
58
|
|
|
42
59
|
constructor(
|
|
43
60
|
key: TaskKey,
|
|
44
61
|
name: string,
|
|
45
62
|
type: T,
|
|
46
63
|
config: C,
|
|
47
|
-
frameClient: framer.Client,
|
|
48
64
|
internal: boolean = false,
|
|
65
|
+
snapshot: boolean = false,
|
|
49
66
|
state?: State<D> | null,
|
|
67
|
+
frameClient: framer.Client | null = null,
|
|
68
|
+
ontologyClient: ontology.Client | null = null,
|
|
69
|
+
rangeClient: ranger.Client | null = null,
|
|
50
70
|
) {
|
|
51
71
|
this.key = key;
|
|
52
72
|
this.name = name;
|
|
53
73
|
this.type = type;
|
|
54
74
|
this.config = config;
|
|
55
75
|
this.internal = internal;
|
|
76
|
+
this.snapshot = snapshot;
|
|
56
77
|
if (state !== null) this.state = state;
|
|
57
78
|
this.frameClient = frameClient;
|
|
79
|
+
this.ontologyClient = ontologyClient;
|
|
80
|
+
this.rangeClient = rangeClient;
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
get payload(): Payload<C, D> {
|
|
@@ -68,7 +91,12 @@ export class Task<
|
|
|
68
91
|
};
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
get ontologyID(): ontology.ID {
|
|
95
|
+
return new ontology.ID({ type: "task", key: this.key });
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
async executeCommand(type: string, args?: UnknownRecord): Promise<string> {
|
|
99
|
+
if (this.frameClient == null) throw TASK_NOT_CREATED;
|
|
72
100
|
const writer = await this.frameClient.openWriter(TASK_CMD_CHANNEL);
|
|
73
101
|
const key = id.id();
|
|
74
102
|
await writer.write(TASK_CMD_CHANNEL, [{ task: this.key, type, key, args }]);
|
|
@@ -81,6 +109,7 @@ export class Task<
|
|
|
81
109
|
args: UnknownRecord,
|
|
82
110
|
timeout: CrudeTimeSpan,
|
|
83
111
|
): Promise<State<D>> {
|
|
112
|
+
if (this.frameClient == null) throw TASK_NOT_CREATED;
|
|
84
113
|
const streamer = await this.frameClient.openStreamer(TASK_STATE_CHANNEL);
|
|
85
114
|
const cmdKey = await this.executeCommand(type, args);
|
|
86
115
|
let res: State<D>;
|
|
@@ -105,6 +134,7 @@ export class Task<
|
|
|
105
134
|
async openStateObserver<D extends UnknownRecord = UnknownRecord>(): Promise<
|
|
106
135
|
StateObservable<D>
|
|
107
136
|
> {
|
|
137
|
+
if (this.frameClient == null) throw TASK_NOT_CREATED;
|
|
108
138
|
return new framer.ObservableStreamer<State<D>>(
|
|
109
139
|
await this.frameClient.openStreamer(TASK_STATE_CHANNEL),
|
|
110
140
|
(frame) => {
|
|
@@ -118,6 +148,14 @@ export class Task<
|
|
|
118
148
|
},
|
|
119
149
|
);
|
|
120
150
|
}
|
|
151
|
+
|
|
152
|
+
async snapshottedTo(): Promise<ontology.Resource | null> {
|
|
153
|
+
if (this.ontologyClient == null || this.rangeClient == null) throw TASK_NOT_CREATED;
|
|
154
|
+
if (!this.snapshot) return null;
|
|
155
|
+
const parents = await this.ontologyClient.retrieveParents(this.ontologyID);
|
|
156
|
+
if (parents.length == 0) return null;
|
|
157
|
+
return parents[0];
|
|
158
|
+
}
|
|
121
159
|
}
|
|
122
160
|
|
|
123
161
|
const retrieveReqZ = z.object({
|
|
@@ -143,20 +181,36 @@ export type RetrieveOptions = Pick<
|
|
|
143
181
|
const RETRIEVE_ENDPOINT = "/hardware/task/retrieve";
|
|
144
182
|
const CREATE_ENDPOINT = "/hardware/task/create";
|
|
145
183
|
const DELETE_ENDPOINT = "/hardware/task/delete";
|
|
184
|
+
const COPY_ENDPOINT = "/hardware/task/copy";
|
|
146
185
|
|
|
147
186
|
const createReqZ = z.object({ tasks: newTaskZ.array() });
|
|
148
187
|
const createResZ = z.object({ tasks: taskZ.array() });
|
|
149
188
|
const deleteReqZ = z.object({ keys: taskKeyZ.array() });
|
|
150
189
|
const deleteResZ = z.object({});
|
|
190
|
+
const copyReqZ = z.object({
|
|
191
|
+
key: taskKeyZ,
|
|
192
|
+
name: z.string(),
|
|
193
|
+
snapshot: z.boolean(),
|
|
194
|
+
});
|
|
195
|
+
const copyResZ = z.object({ task: taskZ });
|
|
151
196
|
|
|
152
197
|
export class Client implements AsyncTermSearcher<string, TaskKey, Payload> {
|
|
153
198
|
readonly type: string = "task";
|
|
154
199
|
private readonly client: UnaryClient;
|
|
155
200
|
private readonly frameClient: framer.Client;
|
|
201
|
+
private readonly ontologyClient: ontology.Client;
|
|
202
|
+
private readonly rangeClient: ranger.Client;
|
|
156
203
|
|
|
157
|
-
constructor(
|
|
204
|
+
constructor(
|
|
205
|
+
client: UnaryClient,
|
|
206
|
+
frameClient: framer.Client,
|
|
207
|
+
ontologyClient: ontology.Client,
|
|
208
|
+
rangeClient: ranger.Client,
|
|
209
|
+
) {
|
|
158
210
|
this.client = client;
|
|
159
211
|
this.frameClient = frameClient;
|
|
212
|
+
this.ontologyClient = ontologyClient;
|
|
213
|
+
this.rangeClient = rangeClient;
|
|
160
214
|
}
|
|
161
215
|
|
|
162
216
|
async create<
|
|
@@ -251,6 +305,17 @@ export class Client implements AsyncTermSearcher<string, TaskKey, Payload> {
|
|
|
251
305
|
return single && variant !== "rack" ? sugared[0] : sugared;
|
|
252
306
|
}
|
|
253
307
|
|
|
308
|
+
async copy(key: string, name: string, snapshot: boolean): Promise<Task> {
|
|
309
|
+
const res = await sendRequired(
|
|
310
|
+
this.client,
|
|
311
|
+
COPY_ENDPOINT,
|
|
312
|
+
{ key, name, snapshot },
|
|
313
|
+
copyReqZ,
|
|
314
|
+
copyResZ,
|
|
315
|
+
);
|
|
316
|
+
return this.sugar([res.task])[0];
|
|
317
|
+
}
|
|
318
|
+
|
|
254
319
|
async retrieveByName(name: string, rack?: number): Promise<Task> {
|
|
255
320
|
const tasks = await this.execRetrieve({ names: [name], rack });
|
|
256
321
|
checkForMultipleOrNoResults("Task", name, tasks, true);
|
|
@@ -270,8 +335,19 @@ export class Client implements AsyncTermSearcher<string, TaskKey, Payload> {
|
|
|
270
335
|
|
|
271
336
|
private sugar(payloads: Payload[]): Task[] {
|
|
272
337
|
return payloads.map(
|
|
273
|
-
({ key, name, type, config, state, internal }) =>
|
|
274
|
-
new Task(
|
|
338
|
+
({ key, name, type, config, state, internal, snapshot }) =>
|
|
339
|
+
new Task(
|
|
340
|
+
key,
|
|
341
|
+
name,
|
|
342
|
+
type,
|
|
343
|
+
config,
|
|
344
|
+
internal,
|
|
345
|
+
snapshot,
|
|
346
|
+
state,
|
|
347
|
+
this.frameClient,
|
|
348
|
+
this.ontologyClient,
|
|
349
|
+
this.rangeClient,
|
|
350
|
+
),
|
|
275
351
|
);
|
|
276
352
|
}
|
|
277
353
|
|