@synnaxlabs/client 0.15.2 → 0.15.3
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 +6 -6
- package/dist/channel/batchRetriever.spec.d.ts +1 -0
- package/dist/channel/client.d.ts +4 -1
- package/dist/channel/retriever.d.ts +15 -0
- package/dist/client.cjs.js +9 -9
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.es.js +2880 -2657
- package/dist/client.es.js.map +1 -1
- package/package.json +6 -5
- package/src/channel/batchRetriever.spec.ts +95 -0
- package/src/channel/client.ts +15 -3
- package/src/channel/retriever.ts +62 -2
- package/src/client.ts +5 -6
- package/src/hardware/task/retriever.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Client Library for Synnax",
|
|
7
7
|
"repository": "https://github.com/synnaxlabs/synnax/tree/main/client/ts",
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"control systems"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"async-mutex": "^0.4.0",
|
|
19
20
|
"zod": "3.22.4",
|
|
20
|
-
"@synnaxlabs/
|
|
21
|
-
"@synnaxlabs/
|
|
21
|
+
"@synnaxlabs/x": "0.12.0",
|
|
22
|
+
"@synnaxlabs/freighter": "0.7.1"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^20.10.5",
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
"typescript": "^5.3.3",
|
|
27
28
|
"vite": "^5.1.2",
|
|
28
29
|
"vitest": "^1.2.2",
|
|
30
|
+
"@synnaxlabs/tsconfig": "0.0.2",
|
|
29
31
|
"@synnaxlabs/vite-plugin": "0.0.1",
|
|
30
|
-
"eslint-config-synnaxlabs": "0.0.1"
|
|
31
|
-
"@synnaxlabs/tsconfig": "0.0.2"
|
|
32
|
+
"eslint-config-synnaxlabs": "0.0.1"
|
|
32
33
|
},
|
|
33
34
|
"main": "dist/client.cjs.js",
|
|
34
35
|
"module": "dist/client.es.js",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { newClient } from "@/setupspecs";
|
|
3
|
+
import { DebouncedBatchRetriever, Retriever, analyzeParams } from "@/channel/retriever";
|
|
4
|
+
import { Params, Payload } from "@/channel/payload";
|
|
5
|
+
import { DataType, Rate } from "@synnaxlabs/x";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockRetriever implements Retriever {
|
|
10
|
+
func: (channels: Params, rangeKey?: string) => Promise<Payload[]>;
|
|
11
|
+
|
|
12
|
+
constructor(func: (channels: Params, rangeKey?: string) => Promise<Payload[]>) {
|
|
13
|
+
this.func = func;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async search(term: string, rangeKey?: string): Promise<Payload[]> {
|
|
17
|
+
throw new Error("Method not implemented.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async page(offset: number, limit: number, rangeKey?: string): Promise<Payload[]> {
|
|
21
|
+
throw new Error("Method not implemented.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async retrieve(channels: Params, rangeKey?: string): Promise<Payload[]> {
|
|
25
|
+
return this.func(channels, rangeKey);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
describe("channelRetriever", () => {
|
|
32
|
+
it("should batch multiple retrieve requests", async () => {
|
|
33
|
+
const called = vi.fn();
|
|
34
|
+
const base = new MockRetriever(async (batch): Promise<Payload[]> => {
|
|
35
|
+
called(batch);
|
|
36
|
+
const {normalized} = analyzeParams(batch);
|
|
37
|
+
return normalized.map(
|
|
38
|
+
(key) =>
|
|
39
|
+
({
|
|
40
|
+
key: key as number,
|
|
41
|
+
name: `channel-${key}`,
|
|
42
|
+
dataType: DataType.FLOAT32,
|
|
43
|
+
isIndex: false,
|
|
44
|
+
rate: Rate.hz(1),
|
|
45
|
+
leaseholder: 1,
|
|
46
|
+
index:0
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
const retriever = new DebouncedBatchRetriever(base, 10);
|
|
51
|
+
const res = await Promise.all([
|
|
52
|
+
retriever.retrieve([1]),
|
|
53
|
+
retriever.retrieve([2]),
|
|
54
|
+
retriever.retrieve([3, 4]),
|
|
55
|
+
]);
|
|
56
|
+
expect(called).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(called).toHaveBeenCalledWith([1, 2, 3, 4]);
|
|
58
|
+
expect(res.map((r) => r.map((c) => c.key))).toEqual([[1], [2], [3, 4]]);
|
|
59
|
+
});
|
|
60
|
+
it("should only fetch duplicate keys once", async () => {
|
|
61
|
+
const called = vi.fn();
|
|
62
|
+
const base = new MockRetriever(async (batch): Promise<Payload[]> => {
|
|
63
|
+
called(batch);
|
|
64
|
+
const {normalized} = analyzeParams(batch);
|
|
65
|
+
return normalized.map(
|
|
66
|
+
(key) =>
|
|
67
|
+
({
|
|
68
|
+
key: key as number,
|
|
69
|
+
name: `channel-${key}`,
|
|
70
|
+
dataType: DataType.FLOAT32,
|
|
71
|
+
isIndex: false,
|
|
72
|
+
rate: Rate.hz(1),
|
|
73
|
+
leaseholder: 1,
|
|
74
|
+
index:0
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
})
|
|
78
|
+
const retriever = new DebouncedBatchRetriever(base, 10);
|
|
79
|
+
const res = await Promise.all([
|
|
80
|
+
retriever.retrieve([1]),
|
|
81
|
+
retriever.retrieve([2]),
|
|
82
|
+
retriever.retrieve([1, 2]),
|
|
83
|
+
]);
|
|
84
|
+
expect(called).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(called).toHaveBeenCalledWith([1, 2]);
|
|
86
|
+
expect(res.map((r) => r.map((c) => c.key))).toEqual([[1], [2], [1, 2]]);
|
|
87
|
+
});
|
|
88
|
+
it("should throw an error if the fetch fails", async () => {
|
|
89
|
+
const base = new MockRetriever(async (batch): Promise<Payload[]> => {
|
|
90
|
+
throw new Error("failed to fetch");
|
|
91
|
+
});
|
|
92
|
+
const retriever = new DebouncedBatchRetriever(base, 10);
|
|
93
|
+
await expect(retriever.retrieve([1])).rejects.toThrow("failed to fetch");
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/channel/client.ts
CHANGED
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
payload,
|
|
29
29
|
type NewPayload,
|
|
30
30
|
} from "@/channel/payload";
|
|
31
|
-
import { analyzeParams, type Retriever } from "@/channel/retriever";
|
|
31
|
+
import { analyzeParams, CacheRetriever, ClusterRetriever, DebouncedBatchRetriever, type Retriever } from "@/channel/retriever";
|
|
32
32
|
import { QueryError } from "@/errors";
|
|
33
33
|
import { type framer } from "@/framer";
|
|
34
|
+
import { UnaryClient } from "@synnaxlabs/freighter";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Represents a Channel in a Synnax database. It should not be instantiated
|
|
@@ -120,10 +121,17 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
|
|
|
120
121
|
private readonly frameClient: framer.Client;
|
|
121
122
|
private readonly retriever: Retriever;
|
|
122
123
|
private readonly creator: Creator;
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
private readonly client: UnaryClient;
|
|
125
|
+
|
|
126
|
+
constructor(
|
|
127
|
+
segmentClient: framer.Client,
|
|
128
|
+
retriever: Retriever,
|
|
129
|
+
client: UnaryClient,
|
|
130
|
+
creator: Creator
|
|
131
|
+
) {
|
|
125
132
|
this.frameClient = segmentClient;
|
|
126
133
|
this.retriever = retriever;
|
|
134
|
+
this.client = client;
|
|
127
135
|
this.creator = creator;
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -182,6 +190,10 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
|
|
|
182
190
|
return this.sugar(await this.retriever.page(offset, limit, rangeKey));
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
createDebouncedBatchRetriever(deb:number = 10): Retriever {
|
|
194
|
+
return new CacheRetriever(new DebouncedBatchRetriever(new ClusterRetriever(this.client),deb))
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
private sugar(payloads: Payload[]): Channel[] {
|
|
186
198
|
const { frameClient } = this;
|
|
187
199
|
return payloads.map((p) => new Channel({ ...p, frameClient }));
|
package/src/channel/retriever.ts
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
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
|
-
|
|
10
9
|
import type { UnaryClient } from "@synnaxlabs/freighter";
|
|
11
|
-
import { toArray } from "@synnaxlabs/x";
|
|
10
|
+
import { debounce, toArray } from "@synnaxlabs/x";
|
|
12
11
|
import { z } from "zod";
|
|
13
12
|
|
|
14
13
|
import {
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
type Payload,
|
|
23
22
|
payload,
|
|
24
23
|
} from "@/channel/payload";
|
|
24
|
+
import { Mutex } from "async-mutex";
|
|
25
25
|
|
|
26
26
|
const reqZ = z.object({
|
|
27
27
|
leaseholder: z.number().optional(),
|
|
@@ -156,3 +156,63 @@ export const analyzeParams = (channels: Params): ParamAnalysisResult => {
|
|
|
156
156
|
actual: channels,
|
|
157
157
|
} as const as ParamAnalysisResult;
|
|
158
158
|
};
|
|
159
|
+
|
|
160
|
+
export interface PromiseFns<T> {
|
|
161
|
+
resolve: (value: T) => void;
|
|
162
|
+
reject: (reason?: any) => void;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// no interval
|
|
166
|
+
export class DebouncedBatchRetriever implements Retriever {
|
|
167
|
+
private readonly mu = new Mutex();
|
|
168
|
+
private readonly requests = new Map<Keys, PromiseFns<Payload[]>>();
|
|
169
|
+
private readonly wrapped: Retriever;
|
|
170
|
+
private readonly debouncedRun: () => void;
|
|
171
|
+
|
|
172
|
+
constructor(wrapped: Retriever, deb: number) {
|
|
173
|
+
this.wrapped = wrapped;
|
|
174
|
+
this.debouncedRun = debounce(() => {
|
|
175
|
+
void this.run();
|
|
176
|
+
}, deb);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async search(term: string, rangeKey?: string): Promise<Payload[]> {
|
|
180
|
+
return await this.wrapped.search(term, rangeKey);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async page(offset: number, limit: number, rangeKey?: string): Promise<Payload[]> {
|
|
184
|
+
return await this.wrapped.page(offset, limit, rangeKey);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async retrieve(channels: Params): Promise<Payload[]> {
|
|
188
|
+
const { normalized, variant } = analyzeParams(channels);
|
|
189
|
+
// Bypass on name fetches for now.
|
|
190
|
+
if (variant === "names")
|
|
191
|
+
return await this.wrapped.retrieve(normalized);
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
193
|
+
const a = new Promise<Payload[]>((resolve, reject) => {
|
|
194
|
+
void this.mu.runExclusive(() => {
|
|
195
|
+
this.requests.set(normalized, { resolve, reject });
|
|
196
|
+
this.debouncedRun();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
return await a;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async run(): Promise<void> {
|
|
203
|
+
await this.mu.runExclusive(async () => {
|
|
204
|
+
const allKeys = new Set<Key>();
|
|
205
|
+
this.requests.forEach((_, keys) => keys.forEach((k) => allKeys.add(k)));
|
|
206
|
+
try {
|
|
207
|
+
const channels = await this.wrapped.retrieve(Array.from(allKeys));
|
|
208
|
+
this.requests.forEach((fns, keys) =>
|
|
209
|
+
fns.resolve(channels.filter((c) => keys.includes(c.key))),
|
|
210
|
+
);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
this.requests.forEach((fns) => fns.reject(e));
|
|
213
|
+
} finally {
|
|
214
|
+
this.requests.clear();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -20,11 +20,10 @@ import { ontology } from "@/ontology";
|
|
|
20
20
|
import { ranger } from "@/ranger";
|
|
21
21
|
import { Transport } from "@/transport";
|
|
22
22
|
import { workspace } from "@/workspace";
|
|
23
|
-
import { hardware } from "
|
|
24
|
-
import { device } from "
|
|
25
|
-
import { rack } from "
|
|
26
|
-
import { task } from "
|
|
27
|
-
import { randomUUID } from "crypto";
|
|
23
|
+
import { hardware } from "@/hardware";
|
|
24
|
+
import { device } from "@/hardware/device";
|
|
25
|
+
import { rack } from "@/hardware/rack";
|
|
26
|
+
import { task } from "@/hardware/task";
|
|
28
27
|
|
|
29
28
|
export const synnaxPropsZ = z.object({
|
|
30
29
|
host: z.string().min(1),
|
|
@@ -97,7 +96,7 @@ export default class Synnax {
|
|
|
97
96
|
);
|
|
98
97
|
const chCreator = new channel.Creator(this.transport.unary);
|
|
99
98
|
this.telem = new framer.Client(this.transport.stream, chRetriever);
|
|
100
|
-
this.channels = new channel.Client(this.telem, chRetriever, chCreator);
|
|
99
|
+
this.channels = new channel.Client(this.telem, chRetriever, this.transport.unary, chCreator);
|
|
101
100
|
this.connectivity = new connection.Checker(
|
|
102
101
|
this.transport.unary,
|
|
103
102
|
connectivityPollFrequency,
|