@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@synnaxlabs/client",
3
3
  "private": false,
4
- "version": "0.15.2",
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/freighter": "0.7.1",
21
- "@synnaxlabs/x": "0.12.0"
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
+ });
@@ -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
- constructor(segmentClient: framer.Client, retriever: Retriever, creator: Creator) {
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 }));
@@ -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 "./hardware";
24
- import { device } from "./hardware/device";
25
- import { rack } from "./hardware/rack";
26
- import { task } from "./hardware/task";
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,
@@ -21,7 +21,7 @@ const retrieveReqZ = z.object({
21
21
  });
22
22
 
23
23
  const rerieveResS = z.object({
24
- tasks: taskZ.array(),
24
+ tasks: z.union([taskZ.array(), z.null().transform(() => [])]),
25
25
  });
26
26
 
27
27
  export type RetrieveRequest = z.infer<typeof retrieveReqZ>;