@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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/CONTRIBUTING.md +47 -0
  3. package/README.md +17 -32
  4. package/api/client.api.md +57 -15
  5. package/dist/channel/client.d.ts +10 -8
  6. package/dist/channel/client.d.ts.map +1 -1
  7. package/dist/channel/retriever.d.ts +1 -1
  8. package/dist/channel/retriever.d.ts.map +1 -1
  9. package/dist/client.cjs +20 -16
  10. package/dist/client.d.ts +4 -0
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +1880 -1722
  13. package/dist/connection/checker.d.ts +21 -1
  14. package/dist/connection/checker.d.ts.map +1 -1
  15. package/dist/control/state.d.ts.map +1 -1
  16. package/dist/framer/client.d.ts.map +1 -1
  17. package/dist/framer/deleter.d.ts.map +1 -1
  18. package/dist/framer/frame.d.ts.map +1 -1
  19. package/dist/framer/streamer.d.ts +2 -4
  20. package/dist/framer/streamer.d.ts.map +1 -1
  21. package/dist/framer/writer.d.ts.map +1 -1
  22. package/dist/hardware/task/client.d.ts +12 -2
  23. package/dist/hardware/task/client.d.ts.map +1 -1
  24. package/dist/hardware/task/ni/types.d.ts +14495 -0
  25. package/dist/hardware/task/ni/types.d.ts.map +1 -0
  26. package/dist/hardware/task/payload.d.ts +6 -0
  27. package/dist/hardware/task/payload.d.ts.map +1 -1
  28. package/dist/label/retriever.d.ts +1 -1
  29. package/dist/ontology/client.d.ts +16 -5
  30. package/dist/ontology/client.d.ts.map +1 -1
  31. package/dist/ontology/group/payload.d.ts +1 -1
  32. package/dist/ontology/group/writer.d.ts.map +1 -1
  33. package/dist/ontology/payload.d.ts +1 -2
  34. package/dist/ontology/payload.d.ts.map +1 -1
  35. package/dist/ranger/client.d.ts +3 -1
  36. package/dist/ranger/client.d.ts.map +1 -1
  37. package/dist/ranger/payload.d.ts +25 -25
  38. package/dist/ranger/payload.d.ts.map +1 -1
  39. package/dist/ranger/writer.d.ts.map +1 -1
  40. package/dist/workspace/lineplot/client.d.ts +1 -1
  41. package/dist/workspace/lineplot/client.d.ts.map +1 -1
  42. package/dist/workspace/lineplot/retriever.d.ts +1 -1
  43. package/dist/workspace/lineplot/retriever.d.ts.map +1 -1
  44. package/dist/workspace/lineplot/writer.d.ts +3 -3
  45. package/dist/workspace/lineplot/writer.d.ts.map +1 -1
  46. package/dist/workspace/retriever.d.ts +1 -1
  47. package/dist/workspace/retriever.d.ts.map +1 -1
  48. package/dist/workspace/schematic/retriever.d.ts +1 -1
  49. package/dist/workspace/schematic/retriever.d.ts.map +1 -1
  50. package/dist/workspace/schematic/writer.d.ts +1 -1
  51. package/dist/workspace/schematic/writer.d.ts.map +1 -1
  52. package/package.json +24 -16
  53. package/src/channel/client.ts +10 -8
  54. package/src/channel/retriever.ts +2 -2
  55. package/src/client.ts +13 -5
  56. package/src/connection/checker.ts +58 -1
  57. package/src/connection/connection.spec.ts +43 -3
  58. package/src/framer/client.ts +2 -3
  59. package/src/framer/streamer.spec.ts +2 -7
  60. package/src/framer/streamer.ts +5 -10
  61. package/src/hardware/rack/client.ts +3 -4
  62. package/src/hardware/task/client.ts +82 -6
  63. package/src/hardware/task/ni/types.ts +1716 -0
  64. package/src/hardware/task/payload.ts +1 -0
  65. package/src/hardware/task/task.spec.ts +45 -30
  66. package/src/label/client.ts +5 -5
  67. package/src/label/retriever.ts +2 -2
  68. package/src/ontology/client.ts +43 -31
  69. package/src/ontology/group/payload.ts +4 -4
  70. package/src/ontology/group/writer.ts +10 -12
  71. package/src/ontology/ontology.spec.ts +3 -5
  72. package/src/ontology/payload.ts +1 -1
  73. package/src/ranger/client.ts +55 -49
  74. package/src/ranger/payload.ts +4 -4
  75. package/src/vite-env.d.ts +1 -1
  76. package/src/workspace/lineplot/client.ts +2 -2
  77. package/src/workspace/lineplot/linePlot.spec.ts +12 -12
  78. package/src/workspace/lineplot/retriever.ts +2 -2
  79. package/src/workspace/lineplot/writer.ts +8 -8
  80. package/src/workspace/retriever.ts +2 -2
  81. package/src/workspace/schematic/retriever.ts +2 -2
  82. package/src/workspace/schematic/writer.ts +8 -6
  83. package/vite.config.ts +5 -0
package/package.json CHANGED
@@ -1,10 +1,7 @@
1
1
  {
2
2
  "name": "@synnaxlabs/client",
3
- "private": false,
4
- "version": "0.27.0",
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.27.0",
22
- "@synnaxlabs/x": "0.27.0"
29
+ "@synnaxlabs/freighter": "0.29.0",
30
+ "@synnaxlabs/x": "0.29.0"
23
31
  },
24
32
  "devDependencies": {
25
- "@types/node": "^22.2.0",
26
- "@vitest/coverage-v8": "^2.0.5",
27
- "eslint": "^9.9.0",
28
- "typescript": "^5.5.4",
29
- "vite": "5.4.0",
30
- "vitest": "^2.0.5",
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": "^3.23.8"
44
+ "zod": "3.23.8"
37
45
  },
38
- "main": "dist/client.cjs",
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",
@@ -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 documentation](https://docs.synnaxlabs.com) for detailed
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. Refer
232
- * to the Synnax documentation (https://docs.synnaxlabs.com) for more information. The
233
- * index channel must have already been created. This field does not need to be specified
234
- * if the channel is an index channel, or the channel is a fixed rate channel. If this
235
- * value is specified, the 'rate' parameter will be ignored.
236
- * @param isIndex - Set to true if the channel is an index channel, and false otherwise.
237
- * Index channels must have a data type of `DataType.TIMESTAMP`.
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.
@@ -267,9 +267,9 @@ export class DebouncedBatchRetriever implements Retriever {
267
267
 
268
268
  export const retrieveRequired = async (
269
269
  r: Retriever,
270
- params: Params,
270
+ channels: Params,
271
271
  ): Promise<Payload[]> => {
272
- const { normalized } = analyzeChannelParams(params);
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(this.transport.unary, this);
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(Checker.ENDPOINT, {}, z.object({}), responseZ);
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(connectivity.state.status).toEqual("connected");
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
  });
@@ -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, params: Params): Promise<Frame> {
174
- const i = await this.openIterator(tr, params);
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();
@@ -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, from }: StreamerConfig,
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({ start: new TimeStamp(from), keys: adapter.keys });
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(params: Params): Promise<void> {
81
- await this.adapter.update(params);
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
- params: string | RackKey | string[] | RackKey[],
130
+ racks: string | RackKey | string[] | RackKey[],
131
131
  ): Promise<Rack | Rack[]> {
132
- const { variant, normalized, single } = analyzeParams(params, {
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", params, sugared, single);
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 { NewTask, newTaskZ, Payload, State, StateObservable, stateZ, TaskKey, taskKeyZ, taskZ } from "@/hardware/task/payload";
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(client: UnaryClient, frameClient: framer.Client) {
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(key, name, type, config, this.frameClient, internal, state),
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