@synnaxlabs/client 0.23.1 → 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.
- package/.turbo/turbo-build.log +5 -5
- package/dist/auth/auth.d.ts +2 -2
- package/dist/channel/client.d.ts +7 -0
- package/dist/channel/client.d.ts.map +1 -1
- package/dist/channel/retriever.d.ts +5 -3
- package/dist/channel/retriever.d.ts.map +1 -1
- package/dist/channel/writer.d.ts +3 -1
- package/dist/channel/writer.d.ts.map +1 -1
- package/dist/client.cjs +22 -18
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4024 -3757
- package/dist/errors.d.ts +33 -8
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.spec.d.ts +2 -0
- package/dist/errors.spec.d.ts.map +1 -0
- package/dist/framer/client.d.ts +6 -5
- package/dist/framer/client.d.ts.map +1 -1
- package/dist/framer/iterator.d.ts +12 -2
- package/dist/framer/iterator.d.ts.map +1 -1
- package/dist/hardware/device/client.d.ts.map +1 -1
- package/dist/hardware/task/client.d.ts +13 -2
- package/dist/hardware/task/client.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/ontology/client.d.ts +4 -4
- package/dist/ontology/group/external.d.ts +1 -0
- package/dist/ontology/group/external.d.ts.map +1 -1
- package/dist/ontology/group/payload.d.ts +2 -2
- package/dist/ontology/group/payload.d.ts.map +1 -1
- package/dist/ontology/group/writer.d.ts.map +1 -1
- package/dist/ontology/payload.d.ts +34 -34
- package/dist/ranger/active.d.ts.map +1 -1
- package/examples/node/basicReadWrite.js +4 -4
- package/examples/node/package-lock.json +8 -8
- package/package.json +5 -5
- package/src/auth/auth.ts +1 -1
- package/src/channel/channel.spec.ts +6 -5
- package/src/channel/client.ts +30 -1
- package/src/channel/retriever.ts +60 -10
- package/src/channel/writer.ts +13 -10
- package/src/client.ts +2 -2
- package/src/errors.spec.ts +40 -0
- package/src/errors.ts +35 -7
- package/src/framer/client.spec.ts +6 -0
- package/src/framer/client.ts +25 -18
- package/src/framer/deleter.spec.ts +39 -38
- package/src/framer/iterator.spec.ts +26 -1
- package/src/framer/iterator.ts +15 -1
- package/src/framer/streamProxy.ts +1 -1
- package/src/framer/streamer.ts +1 -1
- package/src/hardware/device/client.ts +2 -2
- package/src/hardware/task/client.ts +43 -4
- package/src/hardware/task/task.spec.ts +12 -0
- package/src/index.ts +2 -0
- package/src/ontology/group/external.ts +1 -0
- package/src/ontology/group/payload.ts +2 -2
- package/src/ontology/group/writer.ts +2 -2
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
package/src/framer/client.ts
CHANGED
|
@@ -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
|
|
46
|
-
* @
|
|
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(
|
|
49
|
-
|
|
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
|
|
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({
|
|
186
|
-
|
|
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
|
-
|
|
77
|
-
)
|
|
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
|
-
|
|
89
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
126
|
-
|
|
127
|
-
|
|
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
|
});
|
package/src/framer/iterator.ts
CHANGED
|
@@ -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
|
|
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
|
|
42
|
+
if (EOF.matches(err)) return;
|
|
43
43
|
throw err;
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/framer/streamer.ts
CHANGED
|
@@ -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
|
|
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";
|
|
@@ -19,6 +19,7 @@ import { z } from "zod";
|
|
|
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
|
-
|
|
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
|
});
|
package/src/index.ts
CHANGED