@synnaxlabs/client 0.23.0 → 0.24.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 (58) hide show
  1. package/.turbo/turbo-build.log +7 -6
  2. package/dist/auth/auth.d.ts +2 -2
  3. package/dist/channel/client.d.ts +7 -0
  4. package/dist/channel/client.d.ts.map +1 -1
  5. package/dist/channel/retriever.d.ts +5 -3
  6. package/dist/channel/retriever.d.ts.map +1 -1
  7. package/dist/channel/writer.d.ts +3 -1
  8. package/dist/channel/writer.d.ts.map +1 -1
  9. package/dist/client.cjs +22 -18
  10. package/dist/client.d.ts +1 -1
  11. package/dist/client.js +4469 -4218
  12. package/dist/errors.d.ts +33 -8
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.spec.d.ts +2 -0
  15. package/dist/errors.spec.d.ts.map +1 -0
  16. package/dist/framer/client.d.ts +6 -5
  17. package/dist/framer/client.d.ts.map +1 -1
  18. package/dist/framer/iterator.d.ts +12 -2
  19. package/dist/framer/iterator.d.ts.map +1 -1
  20. package/dist/hardware/device/client.d.ts.map +1 -1
  21. package/dist/hardware/task/client.d.ts +13 -2
  22. package/dist/hardware/task/client.d.ts.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/ontology/client.d.ts +4 -4
  26. package/dist/ontology/group/external.d.ts +1 -0
  27. package/dist/ontology/group/external.d.ts.map +1 -1
  28. package/dist/ontology/group/payload.d.ts +2 -2
  29. package/dist/ontology/group/payload.d.ts.map +1 -1
  30. package/dist/ontology/group/writer.d.ts.map +1 -1
  31. package/dist/ontology/payload.d.ts +34 -34
  32. package/dist/ranger/active.d.ts.map +1 -1
  33. package/examples/node/basicReadWrite.js +4 -4
  34. package/examples/node/package-lock.json +8 -8
  35. package/package.json +5 -5
  36. package/src/auth/auth.ts +1 -1
  37. package/src/channel/channel.spec.ts +6 -5
  38. package/src/channel/client.ts +30 -1
  39. package/src/channel/retriever.ts +60 -10
  40. package/src/channel/writer.ts +13 -10
  41. package/src/client.ts +2 -2
  42. package/src/errors.spec.ts +40 -0
  43. package/src/errors.ts +35 -7
  44. package/src/framer/client.spec.ts +6 -0
  45. package/src/framer/client.ts +25 -18
  46. package/src/framer/deleter.spec.ts +39 -38
  47. package/src/framer/iterator.spec.ts +26 -1
  48. package/src/framer/iterator.ts +15 -1
  49. package/src/framer/streamProxy.ts +1 -1
  50. package/src/framer/streamer.ts +1 -1
  51. package/src/hardware/device/client.ts +2 -2
  52. package/src/hardware/task/client.ts +46 -7
  53. package/src/hardware/task/task.spec.ts +12 -0
  54. package/src/index.ts +2 -0
  55. package/src/ontology/group/external.ts +1 -0
  56. package/src/ontology/group/payload.ts +2 -2
  57. package/src/ontology/group/writer.ts +2 -2
  58. package/src/ranger/active.ts +2 -2
package/src/client.ts CHANGED
@@ -83,7 +83,7 @@ export default class Synnax extends framer.Client {
83
83
  * @param props.password - Password for authentication. Not required if the
84
84
  * cluster is insecure.
85
85
  * @param props.connectivityPollFrequency - Frequency at which to poll the
86
- * cluster for connectivity information. Defaults to 5 seconds.
86
+ * cluster for connectivity information. Defaults to 30 seconds.
87
87
  * @param props.secure - Whether to connect to the cluster using TLS. The cluster
88
88
  * must be configured to support TLS. Defaults to false.
89
89
  *
@@ -106,7 +106,7 @@ export default class Synnax extends framer.Client {
106
106
  const chRetriever = new channel.CacheRetriever(
107
107
  new channel.ClusterRetriever(transport.unary),
108
108
  );
109
- const chCreator = new channel.Writer(transport.unary);
109
+ const chCreator = new channel.Writer(transport.unary, chRetriever);
110
110
  super(transport.stream, transport.unary, chRetriever);
111
111
  this.createdAt = TimeStamp.now();
112
112
  this.props = props;
@@ -0,0 +1,40 @@
1
+ import { BaseTypedError, TypedError } from "@synnaxlabs/freighter";
2
+ import { MatchableErrorType } from "@synnaxlabs/freighter/src/errors";
3
+ import { describe, expect, test } from "vitest";
4
+
5
+ import {
6
+ AuthError,
7
+ ContiguityError,
8
+ ControlError,
9
+ FieldError,
10
+ InvalidTokenError,
11
+ MultipleFoundError,
12
+ NotFoundError,
13
+ QueryError,
14
+ RouteError,
15
+ UnauthorizedError,
16
+ UnexpectedError,
17
+ ValidationError,
18
+ } from "@/errors";
19
+
20
+ describe("error", () => {
21
+ describe("type matching", () => {
22
+ const ERRORS: [string, Error, MatchableErrorType][] = [
23
+ [ValidationError.TYPE, new ValidationError(), ValidationError],
24
+ [FieldError.TYPE, new FieldError("field", "message"), FieldError],
25
+ [AuthError.TYPE, new AuthError(), AuthError],
26
+ [InvalidTokenError.TYPE, new InvalidTokenError(), InvalidTokenError],
27
+ [UnexpectedError.TYPE, new UnexpectedError("message"), UnexpectedError],
28
+ [QueryError.TYPE, new QueryError("message"), QueryError],
29
+ [NotFoundError.TYPE, new NotFoundError("message"), NotFoundError],
30
+ [MultipleFoundError.TYPE, new MultipleFoundError("message"), MultipleFoundError],
31
+ [RouteError.TYPE, new RouteError("message", ""), RouteError],
32
+ [ControlError.TYPE, new ControlError("message"), ControlError],
33
+ [UnauthorizedError.TYPE, new UnauthorizedError("message"), UnauthorizedError],
34
+ [ContiguityError.TYPE, new ContiguityError("message"), ContiguityError],
35
+ ];
36
+ ERRORS.forEach(([typeName, error, type]) =>
37
+ test(`matches ${typeName}`, () => expect(type.matches(error)).toBeTruthy()),
38
+ );
39
+ });
40
+ });
package/src/errors.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import {
11
+ BaseTypedError,
12
+ errorMatcher,
11
13
  type ErrorPayload,
12
14
  type Middleware,
13
15
  registerError,
@@ -24,12 +26,16 @@ export interface Field {
24
26
  /**
25
27
  * Raised when a validation error occurs.
26
28
  */
27
- export class ValidationError extends Error {
29
+ export class ValidationError extends BaseTypedError {
28
30
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "validation";
31
+ type = ValidationError.TYPE;
32
+ static readonly matches = errorMatcher(ValidationError.TYPE);
29
33
  }
30
34
 
31
35
  export class FieldError extends ValidationError {
32
36
  static readonly TYPE = ValidationError.TYPE + ".field";
37
+ type = FieldError.TYPE;
38
+ static readonly matches = errorMatcher(FieldError.TYPE);
33
39
  readonly field: string;
34
40
  readonly message: string;
35
41
 
@@ -43,8 +49,10 @@ export class FieldError extends ValidationError {
43
49
  /**
44
50
  * AuthError is raised when an authentication error occurs.
45
51
  */
46
- export class AuthError extends Error {
52
+ export class AuthError extends BaseTypedError {
47
53
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "auth";
54
+ type = AuthError.TYPE;
55
+ static readonly matches = errorMatcher(AuthError.TYPE);
48
56
  }
49
57
 
50
58
  /**
@@ -52,13 +60,17 @@ export class AuthError extends Error {
52
60
  */
53
61
  export class InvalidTokenError extends AuthError {
54
62
  static readonly TYPE = AuthError.TYPE + ".invalid-token";
63
+ type = InvalidTokenError.TYPE;
64
+ static readonly matches = errorMatcher(InvalidTokenError.TYPE);
55
65
  }
56
66
 
57
67
  /**
58
68
  * UnexpectedError is raised when an unexpected error occurs.
59
69
  */
60
- export class UnexpectedError extends Error {
70
+ export class UnexpectedError extends BaseTypedError {
61
71
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "unexpected";
72
+ type = UnexpectedError.TYPE;
73
+ static readonly matches = errorMatcher(UnexpectedError.TYPE);
62
74
 
63
75
  constructor(message: string) {
64
76
  super(`
@@ -74,23 +86,31 @@ export class UnexpectedError extends Error {
74
86
  /**
75
87
  * QueryError is raised when a query error occurs.
76
88
  */
77
- export class QueryError extends Error {
89
+ export class QueryError extends BaseTypedError {
78
90
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "query";
91
+ type = QueryError.TYPE;
92
+ static readonly matches = errorMatcher(QueryError.TYPE);
79
93
  }
80
94
 
81
95
  export class NotFoundError extends QueryError {
82
96
  static readonly TYPE = QueryError.TYPE + ".not_found";
97
+ type = NotFoundError.TYPE;
98
+ static readonly matches = errorMatcher(NotFoundError.TYPE);
83
99
  }
84
100
 
85
101
  export class MultipleFoundError extends QueryError {
86
102
  static readonly TYPE = QueryError.TYPE + ".multiple_results";
103
+ type = MultipleFoundError.TYPE;
104
+ static readonly matches = errorMatcher(MultipleFoundError.TYPE);
87
105
  }
88
106
 
89
107
  /**
90
108
  * RouteError is raised when a routing error occurs.
91
109
  */
92
- export class RouteError extends Error {
110
+ export class RouteError extends BaseTypedError {
93
111
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "route";
112
+ type = RouteError.TYPE;
113
+ static readonly matches = errorMatcher(RouteError.TYPE);
94
114
  path: string;
95
115
 
96
116
  constructor(message: string, path: string) {
@@ -99,18 +119,26 @@ export class RouteError extends Error {
99
119
  }
100
120
  }
101
121
 
102
- export class ControlError extends Error {
122
+ export class ControlError extends BaseTypedError {
103
123
  static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "control";
124
+ type = ControlError.TYPE;
125
+ static readonly matches = errorMatcher(ControlError.TYPE);
104
126
  }
105
127
 
106
128
  export class UnauthorizedError extends ControlError {
107
129
  static readonly TYPE = ControlError.TYPE + ".unauthorized";
130
+ type = UnauthorizedError.TYPE;
131
+ static readonly matches = errorMatcher(UnauthorizedError.TYPE);
108
132
  }
109
133
 
110
134
  /**
111
135
  * Raised when time-series data is not contiguous.
112
136
  */
113
- export class ContiguityError extends Error {}
137
+ export class ContiguityError extends BaseTypedError {
138
+ static readonly TYPE = _FREIGHTER_EXCEPTION_PREFIX + "contiguity";
139
+ type = ContiguityError.TYPE;
140
+ static readonly matches = errorMatcher(ContiguityError.TYPE);
141
+ }
114
142
 
115
143
  const decode = (payload: ErrorPayload): Error | null => {
116
144
  if (!payload.type.startsWith(_FREIGHTER_EXCEPTION_PREFIX)) return null;
@@ -60,4 +60,10 @@ describe("Client", () => {
60
60
  await client.write(start, data.key, 1);
61
61
  });
62
62
  });
63
+ describe("retrieveGroup", () => {
64
+ it("should correctly retrieve the main channel group", async () => {
65
+ const group = await client.channels.retrieveGroup();
66
+ expect(group.name).toEqual("Channels");
67
+ });
68
+ });
63
69
  });
@@ -7,7 +7,7 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import {type StreamClient, UnaryClient } from "@synnaxlabs/freighter";
10
+ import { type StreamClient, UnaryClient } from "@synnaxlabs/freighter";
11
11
  import {
12
12
  type CrudeSeries,
13
13
  type CrudeTimeRange,
@@ -17,13 +17,13 @@ import {
17
17
  TimeSpan,
18
18
  } from "@synnaxlabs/x";
19
19
 
20
- import { type Key, type KeyOrName, KeysOrNames, type Params} from "@/channel/payload";
21
- import { analyzeChannelParams,type Retriever } from "@/channel/retriever";
20
+ import { type Key, type KeyOrName, KeysOrNames, type Params } from "@/channel/payload";
21
+ import { analyzeChannelParams, type Retriever } from "@/channel/retriever";
22
+ import { Deleter } from "@/framer/deleter";
22
23
  import { Frame } from "@/framer/frame";
23
- import { Iterator } from "@/framer/iterator";
24
+ import { Iterator, IteratorConfig } from "@/framer/iterator";
24
25
  import { Streamer, type StreamerConfig } from "@/framer/streamer";
25
- import { Writer, type WriterConfig,WriterMode } from "@/framer/writer";
26
- import { Deleter } from "@/framer/deleter";
26
+ import { Writer, type WriterConfig, WriterMode } from "@/framer/writer";
27
27
 
28
28
  export class Client {
29
29
  private readonly streamClient: StreamClient;
@@ -42,11 +42,16 @@ export class Client {
42
42
  * Opens a new iterator over the given channels within the provided time range.
43
43
  *
44
44
  * @param tr - A time range to iterate over.
45
- * @param keys - A list of channel keys to iterate over.
46
- * @returns a new {@link TypedIterator}.
45
+ * @param channels - A list of channels (by name or key) to iterate over.
46
+ * @param opts - see {@link IteratorConfig}
47
+ * @returns a new {@link Iterator}.
47
48
  */
48
- async openIterator(tr: CrudeTimeRange, channels: Params): Promise<Iterator> {
49
- return await Iterator._open(tr, channels, this.retriever, this.streamClient);
49
+ async openIterator(
50
+ tr: CrudeTimeRange,
51
+ channels: Params,
52
+ opts?: IteratorConfig,
53
+ ): Promise<Iterator> {
54
+ return await Iterator._open(tr, channels, this.retriever, this.streamClient, opts);
50
55
  }
51
56
 
52
57
  /**
@@ -54,7 +59,7 @@ export class Client {
54
59
  *
55
60
  * @param config - The configuration for the created writer, see documentation for
56
61
  * writerConfig for more detail.
57
- * @returns a new {@link RecordWriter}.
62
+ * @returns a new {@link Writer}.
58
63
  */
59
64
  async openWriter(config: WriterConfig | Params): Promise<Writer> {
60
65
  if (Array.isArray(config) || typeof config !== "object")
@@ -175,14 +180,16 @@ export class Client {
175
180
  return frame;
176
181
  }
177
182
 
178
- async delete(
179
- channels: Params,
180
- timeRange : TimeRange,
181
- ): Promise<void> {
182
-
183
+ async delete(channels: Params, timeRange: TimeRange): Promise<void> {
183
184
  const { normalized, variant } = analyzeChannelParams(channels);
184
185
  if (variant === "keys")
185
- return await this.deleter.delete({ keys: normalized as Key[], bounds: timeRange });
186
- return await this.deleter.delete({ names: normalized as string[], bounds: timeRange });
186
+ return await this.deleter.delete({
187
+ keys: normalized as Key[],
188
+ bounds: timeRange,
189
+ });
190
+ return await this.deleter.delete({
191
+ names: normalized as string[],
192
+ bounds: timeRange,
193
+ });
187
194
  }
188
195
  }
@@ -9,9 +9,9 @@
9
9
 
10
10
  import { DataType, Rate, TimeRange, TimeStamp } from "@synnaxlabs/x/telem";
11
11
  import { describe, expect, test } from "vitest";
12
- import { NotFoundError, UnauthorizedError } from "@/errors"
13
12
 
14
13
  import { type channel } from "@/channel";
14
+ import { NotFoundError, UnauthorizedError } from "@/errors";
15
15
  import { newClient } from "@/setupspecs";
16
16
  import { randomSeries } from "@/util/telem";
17
17
 
@@ -31,16 +31,16 @@ const newIndexDataChannelPair = async (): Promise<channel.Channel[]> => {
31
31
  leaseholder: 1,
32
32
  isIndex: true,
33
33
  dataType: DataType.TIMESTAMP,
34
- })
34
+ });
35
35
  const data = await client.channels.create({
36
36
  name: "data",
37
37
  leaseholder: 1,
38
38
  index: ind.key,
39
39
  dataType: DataType.INT64,
40
- })
40
+ });
41
41
 
42
- return [ind, data]
43
- }
42
+ return [ind, data];
43
+ };
44
44
 
45
45
  describe("Deleter", () => {
46
46
  test("Client - basic delete", async () => {
@@ -48,49 +48,49 @@ describe("Deleter", () => {
48
48
  const data = randomSeries(10, ch.dataType);
49
49
  await client.write(TimeStamp.seconds(0), ch.key, data);
50
50
 
51
- await client.delete(ch.key, TimeStamp.seconds(2).range(TimeStamp.seconds(5)))
51
+ await client.delete(ch.key, TimeStamp.seconds(2).range(TimeStamp.seconds(5)));
52
52
 
53
53
  const res = await client.read(TimeRange.MAX, ch.key);
54
54
  expect(res.length).toEqual(data.length - 3);
55
- expect(res.data.slice(0, 2)).toEqual(data.slice(0, 2))
56
- expect(res.data.slice(2)).toEqual(data.slice(5))
55
+ expect(res.data.slice(0, 2)).toEqual(data.slice(0, 2));
56
+ expect(res.data.slice(2)).toEqual(data.slice(5));
57
57
  });
58
58
  test("Client - basic delete by name", async () => {
59
- const ch = await newChannel()
59
+ const ch = await newChannel();
60
60
  const data = randomSeries(10, ch.dataType);
61
61
  await client.write(TimeStamp.seconds(0), ch.key, data);
62
62
 
63
- await client.delete(ch.name, TimeStamp.seconds(2).range(TimeStamp.seconds(5)))
63
+ await client.delete(ch.name, TimeStamp.seconds(2).range(TimeStamp.seconds(5)));
64
64
 
65
65
  const res = await client.read(TimeRange.MAX, ch.key);
66
66
  expect(res.length).toEqual(data.length - 3);
67
- expect(res.data.slice(0, 2)).toEqual(data.slice(0, 2))
68
- expect(res.data.slice(2)).toEqual(data.slice(5))
69
- })
67
+ expect(res.data.slice(0, 2)).toEqual(data.slice(0, 2));
68
+ expect(res.data.slice(2)).toEqual(data.slice(5));
69
+ });
70
70
  test("Client - delete name not found", async () => {
71
71
  const ch = await newChannel();
72
72
  const data = randomSeries(10, ch.dataType);
73
73
  await client.write(TimeStamp.seconds(0), ch.key, data);
74
74
 
75
- await expect(
76
- client.delete(["billy bob", ch.name], TimeRange.MAX)
77
- ).rejects.toThrow(NotFoundError)
75
+ await expect(client.delete(["billy bob", ch.name], TimeRange.MAX)).rejects.toThrow(
76
+ NotFoundError,
77
+ );
78
78
 
79
79
  const res = await client.read(TimeRange.MAX, ch.key);
80
80
  expect(res.data).toEqual(data);
81
- })
81
+ });
82
82
  test("Client - delete key not found", async () => {
83
83
  const ch = await newChannel();
84
84
  const data = randomSeries(10, ch.dataType);
85
85
  await client.write(TimeStamp.seconds(0), ch.key, data);
86
86
 
87
- await expect(
88
- client.delete([ch.key, 1232], TimeRange.MAX)
89
- ).rejects.toThrow(NotFoundError)
87
+ await expect(client.delete([ch.key, 1232], TimeRange.MAX)).rejects.toThrow(
88
+ NotFoundError,
89
+ );
90
90
 
91
91
  const res = await client.read(TimeRange.MAX, ch.key);
92
92
  expect(res.data).toEqual(data);
93
- })
93
+ });
94
94
 
95
95
  test("Client - delete with writer", async () => {
96
96
  const ch = await newChannel();
@@ -101,29 +101,30 @@ describe("Deleter", () => {
101
101
  });
102
102
 
103
103
  await expect(
104
- client.delete(
105
- [ch.key], TimeStamp.seconds(12).range(TimeStamp.seconds(30)))
106
- ).rejects.toThrow(UnauthorizedError)
104
+ client.delete([ch.key], TimeStamp.seconds(12).range(TimeStamp.seconds(30))),
105
+ ).rejects.toThrow(UnauthorizedError);
107
106
 
108
- await writer.close()
109
- })
107
+ await writer.close();
108
+ });
110
109
 
111
110
  test("Client - delete index channel alone", async () => {
112
- const chs = await newIndexDataChannelPair()
113
- const index = chs[0]
114
- const dat = chs[1]
115
- const data = randomSeries(10, dat.dataType)
111
+ const chs = await newIndexDataChannelPair();
112
+ const index = chs[0];
113
+ const dat = chs[1];
114
+ const data = randomSeries(10, dat.dataType);
116
115
 
117
- const time = BigInt64Array.from({ length: 10 },
118
- (_, i) => (TimeStamp.milliseconds(i)).valueOf());
116
+ const time = BigInt64Array.from({ length: 10 }, (_, i) =>
117
+ TimeStamp.milliseconds(i).valueOf(),
118
+ );
119
119
 
120
- await index.write(0, time)
121
- await dat.write(0, data)
120
+ await index.write(0, time);
121
+ await dat.write(0, data);
122
122
 
123
123
  await expect(
124
124
  client.delete(
125
- [index.key], TimeStamp.milliseconds(2).range(TimeStamp.milliseconds(5))
126
- )
127
- ).rejects.toThrow()
128
- })
125
+ [index.key],
126
+ TimeStamp.milliseconds(2).range(TimeStamp.milliseconds(5)),
127
+ ),
128
+ ).rejects.toThrow();
129
+ });
129
130
  });
@@ -8,9 +8,10 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { DataType, Rate, TimeRange, TimeSpan, TimeStamp } from "@synnaxlabs/x/telem";
11
- import { describe, expect,test } from "vitest";
11
+ import { describe, expect, test } from "vitest";
12
12
 
13
13
  import { type channel } from "@/channel";
14
+ import { AUTO_SPAN } from "@/framer/iterator";
14
15
  import { newClient } from "@/setupspecs";
15
16
  import { randomSeries } from "@/util/telem";
16
17
 
@@ -59,4 +60,28 @@ describe("Iterator", () => {
59
60
  await iter.close();
60
61
  }
61
62
  });
63
+ test("chunk size", async () => {
64
+ const ch = await newChannel();
65
+ const data = Float64Array.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
66
+ await ch.write(0, data);
67
+
68
+ const iter = await client.openIterator(TimeRange.MAX, [ch.key], { chunkSize: 4 });
69
+
70
+ try {
71
+ expect(await iter.seekFirst()).toBeTruthy();
72
+
73
+ expect(await iter.next(AUTO_SPAN)).toBeTruthy();
74
+ expect(iter.value.get(ch.key).data).toEqual(Float64Array.of(0, 1, 2, 3));
75
+
76
+ expect(await iter.next(AUTO_SPAN)).toBeTruthy();
77
+ expect(iter.value.get(ch.key).data).toEqual(Float64Array.of(4, 5, 6, 7));
78
+
79
+ expect(await iter.next(AUTO_SPAN)).toBeTruthy();
80
+ expect(iter.value.get(ch.key).data).toEqual(Float64Array.of(8, 9));
81
+
82
+ expect(await iter.next(AUTO_SPAN)).toBeFalsy();
83
+ } finally {
84
+ await iter.close();
85
+ }
86
+ });
62
87
  });
@@ -50,6 +50,7 @@ const reqZ = z.object({
50
50
  bounds: TimeRange.z.optional(),
51
51
  stamp: TimeStamp.z.optional(),
52
52
  keys: z.number().array().optional(),
53
+ chunkSize: z.number().optional(),
53
54
  });
54
55
 
55
56
  type Request = z.infer<typeof reqZ>;
@@ -62,6 +63,13 @@ const resZ = z.object({
62
63
  frame: frameZ.optional(),
63
64
  });
64
65
 
66
+ export interface IteratorConfig {
67
+ /** chunkSize is the maximum number of samples contained per channel in the frame
68
+ * resulting from a call to next with {@link AUTO_SPAN}.
69
+ */
70
+ chunkSize?: number;
71
+ }
72
+
65
73
  /**
66
74
  * Used to iterate over a clusters telemetry in time-order. It should not be
67
75
  * instantiated directly, and should instead be instantiated via the SegmentClient.
@@ -90,13 +98,18 @@ export class Iterator {
90
98
  * channels with the given keys within the provided time range.
91
99
  *
92
100
  * @param tr - The time range to iterate over.
93
- * @param keys - The keys of the channels to iterate over.
101
+ * @param channels - The channels for the iterator to iterate over (can be provided
102
+ * in keys or names).
103
+ * @param retriever - Retriever used to retrieve channel keys from names.
104
+ * @param client - The stream client allowing streaming of iterated data.
105
+ * @param opts - See {@link IteratorConfig}.
94
106
  */
95
107
  static async _open(
96
108
  tr: CrudeTimeRange,
97
109
  channels: Params,
98
110
  retriever: Retriever,
99
111
  client: StreamClient,
112
+ opts: IteratorConfig = {},
100
113
  ): Promise<Iterator> {
101
114
  const adapter = await ReadFrameAdapter.open(retriever, channels);
102
115
  const stream = await client.stream(Iterator.ENDPOINT, reqZ, resZ);
@@ -105,6 +118,7 @@ export class Iterator {
105
118
  command: Command.Open,
106
119
  keys: adapter.keys,
107
120
  bounds: new TimeRange(tr),
121
+ chunkSize: opts.chunkSize ?? 1e5,
108
122
  });
109
123
  return iter;
110
124
  }
@@ -39,7 +39,7 @@ export class StreamProxy<RQ extends z.ZodTypeAny, RS extends z.ZodTypeAny> {
39
39
  Please report this error to the Synnax team. ${JSON.stringify(res)}`,
40
40
  );
41
41
  if (err != null) {
42
- if (err instanceof EOF) return;
42
+ if (EOF.matches(err)) return;
43
43
  throw err;
44
44
  }
45
45
  }
@@ -68,7 +68,7 @@ export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
68
68
  const frame = await this.read();
69
69
  return { done: false, value: frame };
70
70
  } catch (err) {
71
- if (err instanceof EOF) return { done: true, value: undefined };
71
+ if (EOF.matches(err)) return { done: true, value: undefined };
72
72
  throw err;
73
73
  }
74
74
  }
@@ -7,8 +7,8 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { sendRequired,type UnaryClient } from "@synnaxlabs/freighter";
11
- import { toArray,type UnknownRecord } from "@synnaxlabs/x";
10
+ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { toArray, type UnknownRecord } from "@synnaxlabs/x";
12
12
  import { binary } from "@synnaxlabs/x/binary";
13
13
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
14
14
  import { z } from "zod";
@@ -7,18 +7,19 @@
7
7
  // License, use of this software will be governed by the Apache License, Version 2.0,
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
- import { sendRequired,type UnaryClient } from "@synnaxlabs/freighter";
10
+ import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { binary, type observe } from "@synnaxlabs/x";
12
12
  import { type UnknownRecord } from "@synnaxlabs/x/record";
13
13
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
14
- import { type CrudeTimeSpan,TimeSpan } from "@synnaxlabs/x/telem";
14
+ import { type CrudeTimeSpan, TimeSpan } from "@synnaxlabs/x/telem";
15
15
  import { toArray } from "@synnaxlabs/x/toArray";
16
- import { nanoid } from "nanoid";
16
+ import { nanoid } from "nanoid/non-secure";
17
17
  import { z } from "zod";
18
18
 
19
19
  import { framer } from "@/framer";
20
20
  import { type Frame } from "@/framer/frame";
21
21
  import { rack } from "@/hardware/rack";
22
+ import { signals } from "@/signals";
22
23
  import { analyzeParams, checkForMultipleOrNoResults } from "@/util/retrieve";
23
24
  import { nullableArrayZ } from "@/util/zod";
24
25
 
@@ -60,6 +61,7 @@ export const taskZ = z.object({
60
61
  key: taskKeyZ,
61
62
  name: z.string(),
62
63
  type: z.string(),
64
+ internal: z.boolean().optional(),
63
65
  config: z.record(z.unknown()).or(
64
66
  z.string().transform((c) => {
65
67
  if (c === "") return {};
@@ -114,9 +116,10 @@ export class Task<
114
116
  > {
115
117
  readonly key: TaskKey;
116
118
  readonly name: string;
119
+ readonly internal: boolean;
117
120
  readonly type: T;
118
121
  readonly config: C;
119
- readonly state?: State<D>;
122
+ state?: State<D>;
120
123
  private readonly frameClient: framer.Client;
121
124
 
122
125
  constructor(
@@ -125,12 +128,14 @@ export class Task<
125
128
  type: T,
126
129
  config: C,
127
130
  frameClient: framer.Client,
131
+ internal: boolean = false,
128
132
  state?: State<D> | null,
129
133
  ) {
130
134
  this.key = key;
131
135
  this.name = name;
132
136
  this.type = type;
133
137
  this.config = config;
138
+ this.internal = internal;
134
139
  if (state !== null) this.state = state;
135
140
  this.frameClient = frameClient;
136
141
  }
@@ -142,6 +147,7 @@ export class Task<
142
147
  type: this.type,
143
148
  config: this.config,
144
149
  state: this.state,
150
+ internal: this.internal,
145
151
  };
146
152
  }
147
153
 
@@ -282,7 +288,11 @@ export class Client implements AsyncTermSearcher<string, TaskKey, Payload> {
282
288
  }
283
289
 
284
290
  async page(offset: number, limit: number): Promise<Payload[]> {
285
- return this.execRetrieve({ offset, limit });
291
+ return await this.execRetrieve({ offset, limit });
292
+ }
293
+
294
+ async list(options: RetrieveOptions = {}): Promise<Task[]> {
295
+ return this.sugar(await this.execRetrieve(options));
286
296
  }
287
297
 
288
298
  async retrieve<
@@ -343,8 +353,37 @@ export class Client implements AsyncTermSearcher<string, TaskKey, Payload> {
343
353
 
344
354
  private sugar(payloads: Payload[]): Task[] {
345
355
  return payloads.map(
346
- ({ key, name, type, config, state }) =>
347
- new Task(key, name, type, config, this.frameClient, state),
356
+ ({ key, name, type, config, state, internal }) =>
357
+ new Task(key, name, type, config, this.frameClient, internal, state),
358
+ );
359
+ }
360
+
361
+ async openTracker(): Promise<signals.Observable<string, string>> {
362
+ return await signals.openObservable<string, string>(
363
+ this.frameClient,
364
+ "sy_task_set",
365
+ "sy_task_delete",
366
+ (variant, data) =>
367
+ Array.from(data).map((k) => ({
368
+ variant,
369
+ key: k.toString(),
370
+ value: k.toString(),
371
+ })),
372
+ );
373
+ }
374
+
375
+ async openStateObserver<D extends UnknownRecord = UnknownRecord>(): Promise<
376
+ StateObservable<D>
377
+ > {
378
+ return new framer.ObservableStreamer<State<D>>(
379
+ await this.frameClient.openStreamer(TASK_STATE_CHANNEL),
380
+ (frame) => {
381
+ const s = frame.get(TASK_STATE_CHANNEL);
382
+ if (s.length === 0) return [null, false];
383
+ const parse = stateZ.safeParse(s.at(-1));
384
+ if (!parse.success) return [null, false];
385
+ return [parse.data as State<D>, true];
386
+ },
348
387
  );
349
388
  }
350
389
  }
@@ -84,5 +84,17 @@ describe("Hardware", () => {
84
84
  expect(retrieved.state?.variant).toBe(state.variant);
85
85
  });
86
86
  });
87
+ describe("list", () => {
88
+ it("should list all tasks", async () => {
89
+ const t = await client.hardware.racks.create({ name: "test" });
90
+ await t.createTask({
91
+ name: "test",
92
+ config: { a: "dog" },
93
+ type: "ni",
94
+ });
95
+ const tasks = await client.hardware.tasks.list();
96
+ expect(tasks.length).toBeGreaterThan(0);
97
+ });
98
+ });
87
99
  });
88
100
  });