@synnaxlabs/client 0.17.2 → 0.17.4
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/README.md +1 -1
- package/dist/channel/client.d.ts +3 -3
- package/dist/channel/retriever.d.ts +1 -0
- package/dist/client.cjs +26 -10
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +4225 -4103
- package/dist/client.js.map +1 -1
- package/dist/framer/adapter.d.ts +10 -8
- package/dist/framer/adapter.spec.d.ts +1 -0
- package/dist/framer/client.d.ts +2 -2
- package/dist/framer/frame.d.ts +1 -1
- package/dist/framer/writer.d.ts +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/util/telem.d.ts +2 -2
- package/examples/node/liveStream.js +41 -0
- package/examples/node/package-lock.json +4 -4
- package/examples/node/package.json +1 -1
- package/examples/node/streamWrite.js +0 -1
- package/package.json +4 -3
- package/src/channel/client.ts +7 -4
- package/src/channel/retriever.ts +35 -8
- package/src/framer/adapter.spec.ts +118 -0
- package/src/framer/adapter.ts +104 -45
- package/src/framer/client.ts +2 -6
- package/src/framer/frame.ts +12 -4
- package/src/framer/iterator.ts +4 -4
- package/src/framer/streamer.ts +4 -4
- package/src/framer/writer.spec.ts +1 -1
- package/src/framer/writer.ts +19 -16
- package/src/index.ts +1 -1
- package/src/util/telem.ts +2 -2
package/dist/framer/adapter.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CrudeSeries } from "@synnaxlabs/x";
|
|
2
|
+
import { type Key, type Params, type KeyOrName } from '../channel/payload';
|
|
2
3
|
import { type Retriever } from '../channel/retriever';
|
|
3
|
-
import { type Frame } from './frame';
|
|
4
|
-
export declare class
|
|
4
|
+
import { type CrudeFrame, Frame } from './frame';
|
|
5
|
+
export declare class ReadFrameAdapter {
|
|
5
6
|
private adapter;
|
|
6
7
|
retriever: Retriever;
|
|
7
8
|
keys: Key[];
|
|
8
9
|
private constructor();
|
|
9
|
-
static open(retriever: Retriever, channels: Params): Promise<
|
|
10
|
+
static open(retriever: Retriever, channels: Params): Promise<ReadFrameAdapter>;
|
|
10
11
|
update(channels: Params): Promise<void>;
|
|
11
|
-
adapt(
|
|
12
|
+
adapt(columnsOrData: Frame): Frame;
|
|
12
13
|
}
|
|
13
|
-
export declare class
|
|
14
|
+
export declare class WriteFrameAdapter {
|
|
14
15
|
private adapter;
|
|
15
16
|
retriever: Retriever;
|
|
16
17
|
keys: Key[];
|
|
17
18
|
private constructor();
|
|
18
|
-
static open(retriever: Retriever, channels: Params): Promise<
|
|
19
|
+
static open(retriever: Retriever, channels: Params): Promise<WriteFrameAdapter>;
|
|
19
20
|
update(channels: Params): Promise<void>;
|
|
20
|
-
|
|
21
|
+
private fetchChannel;
|
|
22
|
+
adapt(columnsOrData: Params | Record<KeyOrName, CrudeSeries> | CrudeFrame, series?: CrudeSeries | CrudeSeries[]): Promise<Frame>;
|
|
21
23
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/framer/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type StreamClient } from "@synnaxlabs/freighter";
|
|
2
|
-
import { type
|
|
2
|
+
import { type TypedArray, type Series, type TimeRange, type CrudeTimeStamp, TimeStamp } from "@synnaxlabs/x";
|
|
3
3
|
import { type KeyOrName, type Params } from '../channel/payload';
|
|
4
4
|
import { type Retriever } from '../channel/retriever';
|
|
5
5
|
import { Frame } from './frame';
|
|
@@ -37,7 +37,7 @@ export declare class Client {
|
|
|
37
37
|
* data type as the channel.
|
|
38
38
|
* @throws if the channel does not exist.
|
|
39
39
|
*/
|
|
40
|
-
write(to: KeyOrName, start: CrudeTimeStamp, data:
|
|
40
|
+
write(to: KeyOrName, start: CrudeTimeStamp, data: TypedArray): Promise<void>;
|
|
41
41
|
read(tr: TimeRange, channel: KeyOrName): Promise<Series>;
|
|
42
42
|
read(tr: TimeRange, channels: Params): Promise<Frame>;
|
|
43
43
|
private readFrame;
|
package/dist/framer/frame.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type CrudeFrame = Frame | FramePayload | Map<KeyOrName, Series[] | Series
|
|
|
14
14
|
*
|
|
15
15
|
* - A frame is weakly aligned if it meets the time range occupied by all arrays of a
|
|
16
16
|
* particular channel is the same for all channels in the frame. This means that the
|
|
17
|
-
* arrays for a particular channel can have gaps
|
|
17
|
+
* arrays for a particular channel can have gaps between them.
|
|
18
18
|
*
|
|
19
19
|
* - A strongly aligned frame means that all channels share the same rate/index and
|
|
20
20
|
* there are no gaps in time between arrays. Strongly aligned frames are natural
|
package/dist/framer/writer.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { StreamClient } from "@synnaxlabs/freighter";
|
|
2
|
-
import {
|
|
2
|
+
import { TimeStamp, type CrudeTimeStamp, type CrudeSeries } from "@synnaxlabs/x";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { type Key, type KeyOrName, type Params } from '../channel/payload';
|
|
4
|
+
import { type KeysOrNames, type Key, type KeyOrName, type Params } from '../channel/payload';
|
|
5
5
|
import { type Retriever } from '../channel/retriever';
|
|
6
6
|
import { Authority } from '../control/authority';
|
|
7
7
|
import { type Subject as ControlSubject } from '../control/state';
|
|
@@ -428,7 +428,8 @@ export declare class Writer {
|
|
|
428
428
|
private readonly adapter;
|
|
429
429
|
private constructor();
|
|
430
430
|
static _open(retriever: Retriever, client: StreamClient, { channels, authorities, controlSubject: subject, start, mode, }: WriterConfig): Promise<Writer>;
|
|
431
|
-
write(channel: KeyOrName, data:
|
|
431
|
+
write(channel: KeyOrName, data: CrudeSeries): Promise<boolean>;
|
|
432
|
+
write(channel: KeysOrNames, data: CrudeSeries[]): Promise<boolean>;
|
|
432
433
|
write(frame: CrudeFrame): Promise<boolean>;
|
|
433
434
|
setAuthority(value: Record<Key, Authority>): Promise<boolean>;
|
|
434
435
|
setMode(mode: WriterMode): Promise<boolean>;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { ontology } from './ontology';
|
|
|
10
10
|
export { control } from './control';
|
|
11
11
|
export { Authority } from './control/authority';
|
|
12
12
|
export { DataType, Density, Rate, Series, TimeRange, TimeSpan, TimeStamp, } from "@synnaxlabs/x";
|
|
13
|
-
export type {
|
|
13
|
+
export type { TypedArray, CrudeDataType, CrudeDensity, CrudeRate, CrudeSize, CrudeTimeSpan, CrudeTimeStamp, SampleValue, TimeStampStringFormat, TZInfo, } from "@synnaxlabs/x";
|
|
14
14
|
export { workspace } from './workspace';
|
|
15
15
|
export { ranger } from './ranger';
|
|
16
16
|
export { label } from './label';
|
package/dist/util/telem.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { DataType,
|
|
2
|
-
export declare const randomSeries: (length: number, dataType: DataType) =>
|
|
1
|
+
import { type DataType, type TypedArray } from "@synnaxlabs/x";
|
|
2
|
+
export declare const randomSeries: (length: number, dataType: DataType) => TypedArray;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Synnax, TimeStamp } from "@synnaxlabs/client";
|
|
2
|
+
|
|
3
|
+
// This example demonstrates how to stream live data from a channel in Synnax.
|
|
4
|
+
// Live-streaming is useful for real-time data processing and analysis, and is an
|
|
5
|
+
// integral part of Synnax's control sequence and data streaming capabilities.
|
|
6
|
+
|
|
7
|
+
// This example is meant to be used in conjunction with the stream_write.py example, and
|
|
8
|
+
// assumes that example is running in a separate terminal.
|
|
9
|
+
|
|
10
|
+
// Connect to a locally running, insecure Synnax cluster. If your connection parameters
|
|
11
|
+
// are different, enter them here.
|
|
12
|
+
const client = new Synnax({
|
|
13
|
+
host: "localhost",
|
|
14
|
+
port: 9090,
|
|
15
|
+
username: "synnax",
|
|
16
|
+
password: "seldon",
|
|
17
|
+
secure: false
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// We can just specify the names of the channels we'd like to stream from.
|
|
21
|
+
const read_from = ["stream_write_example_time", "stream_write_example_data"]
|
|
22
|
+
|
|
23
|
+
const streamer = await client.telem.newStreamer(read_from);
|
|
24
|
+
|
|
25
|
+
// It's very important that we close the streamer when we're done with it to release
|
|
26
|
+
// network connections and other resources, so we wrap the streaming loop in a try-finally
|
|
27
|
+
// block.
|
|
28
|
+
try {
|
|
29
|
+
// Loop through the frames in the streamer. Each iteration will block until a new
|
|
30
|
+
// frame is available, and then we'll just print it out.
|
|
31
|
+
for await (const frame of streamer)
|
|
32
|
+
console.log({
|
|
33
|
+
time: new TimeStamp(frame.get("stream_write_example_time")[0].at(0)).toString(),
|
|
34
|
+
data: frame.get("stream_write_example_data")[0].at(0)
|
|
35
|
+
})
|
|
36
|
+
} finally {
|
|
37
|
+
streamer.close();
|
|
38
|
+
// Close the client when we're done with it.
|
|
39
|
+
client.close();
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@synnaxlabs/client": "^0.17.
|
|
12
|
+
"@synnaxlabs/client": "^0.17.3"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"node_modules/@grpc/grpc-js": {
|
|
@@ -3653,9 +3653,9 @@
|
|
|
3653
3653
|
}
|
|
3654
3654
|
},
|
|
3655
3655
|
"node_modules/@synnaxlabs/client": {
|
|
3656
|
-
"version": "0.17.
|
|
3657
|
-
"resolved": "https://registry.npmjs.org/@synnaxlabs/client/-/client-0.17.
|
|
3658
|
-
"integrity": "sha512-
|
|
3656
|
+
"version": "0.17.3",
|
|
3657
|
+
"resolved": "https://registry.npmjs.org/@synnaxlabs/client/-/client-0.17.3.tgz",
|
|
3658
|
+
"integrity": "sha512-yDlsatfk/Kg0SuiC+LPCb2oZ3ktlmtCAqOglyqaA25RAfTmOY8QicAFvSCafCSweSqLXJ0dFQzniRw001s26sg==",
|
|
3659
3659
|
"dependencies": {
|
|
3660
3660
|
"@synnaxlabs/freighter": "0.9.1",
|
|
3661
3661
|
"@synnaxlabs/x": "0.14.1",
|
|
@@ -58,7 +58,6 @@ try {
|
|
|
58
58
|
[timeChannel.key]: new Series({ data: new timeChannel.dataType.Array([timestamp]) }),
|
|
59
59
|
[dataChannel.key]: new Series({ data: new dataChannel.dataType.Array([data]) })
|
|
60
60
|
});
|
|
61
|
-
console.log(fr.columns);
|
|
62
61
|
await writer.write(fr);
|
|
63
62
|
|
|
64
63
|
if (i % 60 == 0)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnaxlabs/client",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Client Library for Synnax",
|
|
7
7
|
"repository": "https://github.com/synnaxlabs/synnax/tree/main/client/ts",
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"async-mutex": "^0.4.0",
|
|
20
20
|
"zod": "3.22.4",
|
|
21
|
-
"@synnaxlabs/freighter": "0.9.
|
|
22
|
-
"@synnaxlabs/x": "0.14.
|
|
21
|
+
"@synnaxlabs/freighter": "0.9.2",
|
|
22
|
+
"@synnaxlabs/x": "0.14.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^20.10.5",
|
|
26
26
|
"@vitest/coverage-v8": "^1.2.2",
|
|
27
|
+
"eslint": "^8.55.0",
|
|
27
28
|
"typescript": "^5.3.3",
|
|
28
29
|
"vite": "^5.1.2",
|
|
29
30
|
"vitest": "^1.2.2",
|
package/src/channel/client.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { type UnaryClient } from "@synnaxlabs/freighter";
|
|
|
11
11
|
import {
|
|
12
12
|
DataType,
|
|
13
13
|
Rate,
|
|
14
|
-
type
|
|
14
|
+
type TypedArray,
|
|
15
15
|
type CrudeDensity,
|
|
16
16
|
type Series,
|
|
17
17
|
type TimeRange,
|
|
@@ -158,7 +158,7 @@ export class Channel {
|
|
|
158
158
|
* @param start - The starting timestamp of the first sample in data.
|
|
159
159
|
* @param data - THe telemetry to write to the channel.
|
|
160
160
|
*/
|
|
161
|
-
async write(start: CrudeTimeStamp, data:
|
|
161
|
+
async write(start: CrudeTimeStamp, data: TypedArray): Promise<void> {
|
|
162
162
|
return await this.framer.write(this.key, start, data);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -170,7 +170,7 @@ export class Channel {
|
|
|
170
170
|
*/
|
|
171
171
|
export class Client implements AsyncTermSearcher<string, Key, Channel> {
|
|
172
172
|
private readonly frameClient: framer.Client;
|
|
173
|
-
|
|
173
|
+
readonly retriever: Retriever;
|
|
174
174
|
private readonly creator: Creator;
|
|
175
175
|
private readonly client: UnaryClient;
|
|
176
176
|
|
|
@@ -247,7 +247,10 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
|
|
|
247
247
|
*/
|
|
248
248
|
async create(channels: NewPayload[], options?: CreateOptions): Promise<Channel[]>;
|
|
249
249
|
|
|
250
|
-
async create(
|
|
250
|
+
async create(
|
|
251
|
+
channels: NewPayload | NewPayload[],
|
|
252
|
+
options: CreateOptions = {},
|
|
253
|
+
): Promise<Channel | Channel[]> {
|
|
251
254
|
const { retrieveIfNameExists = false } = options;
|
|
252
255
|
const single = !Array.isArray(channels);
|
|
253
256
|
let toCreate = toArray(channels);
|
package/src/channel/retriever.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// included in the file licenses/APL.txt.
|
|
9
9
|
import type { UnaryClient } from "@synnaxlabs/freighter";
|
|
10
10
|
import { debounce, toArray } from "@synnaxlabs/x";
|
|
11
|
+
import { Mutex } from "async-mutex";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
|
|
13
14
|
import {
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
type Payload,
|
|
22
23
|
payload,
|
|
23
24
|
} from "@/channel/payload";
|
|
24
|
-
import {
|
|
25
|
+
import { QueryError, ValidationError } from "@/errors";
|
|
25
26
|
|
|
26
27
|
const reqZ = z.object({
|
|
27
28
|
leaseholder: z.number().optional(),
|
|
@@ -59,15 +60,20 @@ export class ClusterRetriever implements Retriever {
|
|
|
59
60
|
|
|
60
61
|
async retrieve(channels: Params, rangeKey?: string): Promise<Payload[]> {
|
|
61
62
|
const { variant, normalized } = analyzeParams(channels);
|
|
62
|
-
return await this.execute({ [variant]: normalized, rangeKey});
|
|
63
|
+
return await this.execute({ [variant]: normalized, rangeKey });
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
async page(offset: number, limit: number, rangeKey?: string): Promise<Payload[]> {
|
|
66
|
-
return await this.execute({ offset, limit, rangeKey});
|
|
67
|
+
return await this.execute({ offset, limit, rangeKey });
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
private async execute(request: Request): Promise<Payload[]> {
|
|
70
|
-
const [res, err] = await this.client.send(
|
|
71
|
+
const [res, err] = await this.client.send(
|
|
72
|
+
ClusterRetriever.ENDPOINT,
|
|
73
|
+
request,
|
|
74
|
+
reqZ,
|
|
75
|
+
resZ,
|
|
76
|
+
);
|
|
71
77
|
if (err != null) throw err;
|
|
72
78
|
return res.channels;
|
|
73
79
|
}
|
|
@@ -148,10 +154,17 @@ export type ParamAnalysisResult =
|
|
|
148
154
|
};
|
|
149
155
|
|
|
150
156
|
export const analyzeParams = (channels: Params): ParamAnalysisResult => {
|
|
151
|
-
|
|
157
|
+
let normal = (toArray(channels) as KeysOrNames).filter((c) => c !== 0);
|
|
158
|
+
let variant: "names" | "keys" = "keys";
|
|
159
|
+
if (typeof normal[0] === "string") {
|
|
160
|
+
if (isNaN(parseInt(normal[0]))) variant = "names";
|
|
161
|
+
else {
|
|
162
|
+
normal = normal.map((v) => parseInt(v as string));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
152
165
|
return {
|
|
153
166
|
single: !Array.isArray(channels),
|
|
154
|
-
variant
|
|
167
|
+
variant,
|
|
155
168
|
normalized: normal,
|
|
156
169
|
actual: channels,
|
|
157
170
|
} as const as ParamAnalysisResult;
|
|
@@ -187,8 +200,7 @@ export class DebouncedBatchRetriever implements Retriever {
|
|
|
187
200
|
async retrieve(channels: Params): Promise<Payload[]> {
|
|
188
201
|
const { normalized, variant } = analyzeParams(channels);
|
|
189
202
|
// Bypass on name fetches for now.
|
|
190
|
-
if (variant === "names")
|
|
191
|
-
return await this.wrapped.retrieve(normalized);
|
|
203
|
+
if (variant === "names") return await this.wrapped.retrieve(normalized);
|
|
192
204
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
193
205
|
const a = new Promise<Payload[]>((resolve, reject) => {
|
|
194
206
|
void this.mu.runExclusive(() => {
|
|
@@ -216,3 +228,18 @@ export class DebouncedBatchRetriever implements Retriever {
|
|
|
216
228
|
});
|
|
217
229
|
}
|
|
218
230
|
}
|
|
231
|
+
|
|
232
|
+
export const retrieveRequired = async (
|
|
233
|
+
r: Retriever,
|
|
234
|
+
params: Params,
|
|
235
|
+
): Promise<Payload[]> => {
|
|
236
|
+
const { normalized } = analyzeParams(params);
|
|
237
|
+
const results = await r.retrieve(normalized);
|
|
238
|
+
const notFound: KeyOrName[] = [];
|
|
239
|
+
normalized.forEach((v) => {
|
|
240
|
+
if (results.find((c) => c.name === v || c.key === v) == null) notFound.push(v);
|
|
241
|
+
});
|
|
242
|
+
if (notFound.length > 0)
|
|
243
|
+
throw new QueryError(`Could not find channels: ${JSON.stringify(notFound)}`);
|
|
244
|
+
return results;
|
|
245
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DataType, Series, TimeStamp } from "@synnaxlabs/x";
|
|
2
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { type channel } from "@/channel";
|
|
5
|
+
import { WriteFrameAdapter } from "@/framer/adapter";
|
|
6
|
+
import { Frame } from "@/index";
|
|
7
|
+
import { newClient } from "@/setupspecs";
|
|
8
|
+
|
|
9
|
+
const client = newClient();
|
|
10
|
+
|
|
11
|
+
describe("WriteFrameAdapter", () => {
|
|
12
|
+
let timeCh: channel.Channel;
|
|
13
|
+
let dataCh: channel.Channel;
|
|
14
|
+
let adapter: WriteFrameAdapter;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
timeCh = await client.channels.create({
|
|
18
|
+
name: `time-${Math.random()}-${TimeStamp.now().toString()}`,
|
|
19
|
+
dataType: DataType.TIMESTAMP,
|
|
20
|
+
isIndex: true,
|
|
21
|
+
});
|
|
22
|
+
dataCh = await client.channels.create({
|
|
23
|
+
name: `data-${Math.random()}-${TimeStamp.now().toString()}`,
|
|
24
|
+
dataType: DataType.FLOAT32,
|
|
25
|
+
index: timeCh.key,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
adapter = await WriteFrameAdapter.open(client.channels.retriever, [
|
|
29
|
+
timeCh.key,
|
|
30
|
+
dataCh.key,
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should correctly adapt a record of keys to single values", async () => {
|
|
35
|
+
const ts = TimeStamp.now().valueOf();
|
|
36
|
+
const res = await adapter.adapt({
|
|
37
|
+
[timeCh.key]: ts,
|
|
38
|
+
[dataCh.key]: 1,
|
|
39
|
+
});
|
|
40
|
+
expect(res.columns).toHaveLength(2);
|
|
41
|
+
expect(res.series).toHaveLength(2);
|
|
42
|
+
expect(res.get(timeCh.key)).toHaveLength(1);
|
|
43
|
+
expect(res.get(dataCh.key)).toHaveLength(1);
|
|
44
|
+
expect(res.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
45
|
+
expect(res.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should correctly adapt a record of names to single values", async () => {
|
|
49
|
+
const ts = TimeStamp.now().valueOf();
|
|
50
|
+
const res2 = await adapter.adapt({
|
|
51
|
+
[timeCh.name]: ts,
|
|
52
|
+
[dataCh.name]: 1,
|
|
53
|
+
});
|
|
54
|
+
expect(res2.columns).toHaveLength(2);
|
|
55
|
+
expect(res2.series).toHaveLength(2);
|
|
56
|
+
expect(res2.get(timeCh.key)).toHaveLength(1);
|
|
57
|
+
expect(res2.get(dataCh.key)).toHaveLength(1);
|
|
58
|
+
expect(res2.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
59
|
+
expect(res2.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should correctly adapt a single name to a single series", async () => {
|
|
63
|
+
const res3 = await adapter.adapt(dataCh.name, new Series(1));
|
|
64
|
+
expect(res3.columns).toHaveLength(1);
|
|
65
|
+
expect(res3.series).toHaveLength(1);
|
|
66
|
+
expect(res3.get(dataCh.key)).toHaveLength(1);
|
|
67
|
+
expect(res3.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should correctly adapt multiple names to multiple series", async () => {
|
|
71
|
+
const ts = TimeStamp.now().valueOf();
|
|
72
|
+
const res4 = await adapter.adapt(
|
|
73
|
+
[timeCh.name, dataCh.name],
|
|
74
|
+
[new Series(ts), new Series(1)],
|
|
75
|
+
);
|
|
76
|
+
expect(res4.get(timeCh.key)).toHaveLength(1);
|
|
77
|
+
expect(res4.get(dataCh.key)).toHaveLength(1);
|
|
78
|
+
expect(res4.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
79
|
+
expect(res4.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should correctly adapt a frame keyed by name", async () => {
|
|
83
|
+
const ts = TimeStamp.now().valueOf();
|
|
84
|
+
const fr = new Frame({
|
|
85
|
+
[timeCh.name]: new Series(ts),
|
|
86
|
+
[dataCh.name]: new Series(1),
|
|
87
|
+
});
|
|
88
|
+
const res = await adapter.adapt(fr);
|
|
89
|
+
expect(res.columns).toHaveLength(2);
|
|
90
|
+
expect(res.series).toHaveLength(2);
|
|
91
|
+
expect(res.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
92
|
+
expect(res.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should not modify a frame keyed by key", async () => {
|
|
96
|
+
const ts = TimeStamp.now().valueOf();
|
|
97
|
+
const fr = new Frame({
|
|
98
|
+
[timeCh.key]: new Series(ts),
|
|
99
|
+
[dataCh.key]: new Series(1),
|
|
100
|
+
});
|
|
101
|
+
const res = await adapter.adapt(fr);
|
|
102
|
+
expect(res.columns).toHaveLength(2);
|
|
103
|
+
expect(res.series).toHaveLength(2);
|
|
104
|
+
expect(res.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
105
|
+
expect(res.get(dataCh.key)[0].at(0)).toEqual(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should correctly adapt a map of series", async () => {
|
|
109
|
+
const ts = TimeStamp.now().valueOf();
|
|
110
|
+
const m = new Map();
|
|
111
|
+
m.set(timeCh.key, new Series(ts));
|
|
112
|
+
const res = await adapter.adapt(m);
|
|
113
|
+
expect(res.columns).toHaveLength(1);
|
|
114
|
+
expect(res.series).toHaveLength(1);
|
|
115
|
+
expect(res.get(timeCh.key)).toHaveLength(1);
|
|
116
|
+
expect(res.get(timeCh.key)[0].at(0)).toEqual(Number(ts));
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/framer/adapter.ts
CHANGED
|
@@ -7,12 +7,20 @@
|
|
|
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
|
|
11
|
-
|
|
10
|
+
import { type CrudeSeries, Series } from "@synnaxlabs/x";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type Key,
|
|
14
|
+
type Name,
|
|
15
|
+
type Params,
|
|
16
|
+
type KeyOrName,
|
|
17
|
+
type Payload,
|
|
18
|
+
} from "@/channel/payload";
|
|
19
|
+
import { type Retriever, analyzeParams, retrieveRequired } from "@/channel/retriever";
|
|
12
20
|
import { ValidationError } from "@/errors";
|
|
13
|
-
import { type Frame } from "@/framer/frame";
|
|
21
|
+
import { type CrudeFrame, Frame } from "@/framer/frame";
|
|
14
22
|
|
|
15
|
-
export class
|
|
23
|
+
export class ReadFrameAdapter {
|
|
16
24
|
private adapter: Map<Key, Name> | null;
|
|
17
25
|
retriever: Retriever;
|
|
18
26
|
keys: Key[];
|
|
@@ -23,11 +31,8 @@ export class BackwardFrameAdapter {
|
|
|
23
31
|
this.keys = [];
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
static async open(
|
|
27
|
-
retriever
|
|
28
|
-
channels: Params,
|
|
29
|
-
): Promise<BackwardFrameAdapter> {
|
|
30
|
-
const adapter = new BackwardFrameAdapter(retriever);
|
|
34
|
+
static async open(retriever: Retriever, channels: Params): Promise<ReadFrameAdapter> {
|
|
35
|
+
const adapter = new ReadFrameAdapter(retriever);
|
|
31
36
|
await adapter.update(channels);
|
|
32
37
|
return adapter;
|
|
33
38
|
}
|
|
@@ -50,10 +55,10 @@ export class BackwardFrameAdapter {
|
|
|
50
55
|
this.keys = Array.from(this.adapter.keys());
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
adapt(
|
|
54
|
-
if (this.adapter == null) return
|
|
58
|
+
adapt(columnsOrData: Frame): Frame {
|
|
59
|
+
if (this.adapter == null) return columnsOrData;
|
|
55
60
|
const a = this.adapter;
|
|
56
|
-
return
|
|
61
|
+
return columnsOrData.map((k, arr) => {
|
|
57
62
|
if (typeof k === "number") {
|
|
58
63
|
const name = a.get(k);
|
|
59
64
|
if (name == null) throw new Error(`Channel ${k} not found`);
|
|
@@ -64,7 +69,7 @@ export class BackwardFrameAdapter {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
export class
|
|
72
|
+
export class WriteFrameAdapter {
|
|
68
73
|
private adapter: Map<Name, Key> | null;
|
|
69
74
|
retriever: Retriever;
|
|
70
75
|
keys: Key[];
|
|
@@ -78,46 +83,100 @@ export class ForwardFrameAdapter {
|
|
|
78
83
|
static async open(
|
|
79
84
|
retriever: Retriever,
|
|
80
85
|
channels: Params,
|
|
81
|
-
): Promise<
|
|
82
|
-
const adapter = new
|
|
86
|
+
): Promise<WriteFrameAdapter> {
|
|
87
|
+
const adapter = new WriteFrameAdapter(retriever);
|
|
83
88
|
await adapter.update(channels);
|
|
84
89
|
return adapter;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
async update(channels: Params): Promise<void> {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.keys = normalized;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const fetched = await this.retriever.retrieve(normalized);
|
|
95
|
-
const a = new Map<Name, Key>();
|
|
96
|
-
this.adapter = a;
|
|
97
|
-
normalized.forEach((name) => {
|
|
98
|
-
const channel = fetched.find((channel) => channel.name === name);
|
|
99
|
-
if (channel == null) throw new ValidationError(`Channel ${name} was not provided in the list of channels when opening the writer`);
|
|
100
|
-
a.set(channel.name, channel.key);
|
|
101
|
-
});
|
|
102
|
-
this.keys = fetched.map((c) => c.key);
|
|
93
|
+
const results = await retrieveRequired(this.retriever, channels);
|
|
94
|
+
this.adapter = new Map<Name, Key>(results.map((c) => [c.name, c.key]));
|
|
95
|
+
this.keys = results.map((c) => c.key);
|
|
103
96
|
}
|
|
104
97
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
private async fetchChannel(ch: Key | Name): Promise<Payload> {
|
|
99
|
+
const res = await this.retriever.retrieve(ch);
|
|
100
|
+
if (res.length === 0) throw new Error(`Channel ${ch} not found`);
|
|
101
|
+
return res[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async adapt(
|
|
105
|
+
columnsOrData: Params | Record<KeyOrName, CrudeSeries> | CrudeFrame,
|
|
106
|
+
series?: CrudeSeries | CrudeSeries[],
|
|
107
|
+
): Promise<Frame> {
|
|
108
|
+
if (typeof columnsOrData === "string" || typeof columnsOrData === "number") {
|
|
109
|
+
if (series == null)
|
|
110
|
+
throw new ValidationError(`
|
|
111
|
+
Received a single channel name or key but no series.
|
|
112
|
+
`);
|
|
113
|
+
if (Array.isArray(series)) {
|
|
114
|
+
if (series.length > 1) {
|
|
115
|
+
throw new ValidationError(`
|
|
116
|
+
Received a single channel name or key but multiple series.
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
series = series[0] as CrudeSeries;
|
|
120
|
+
}
|
|
121
|
+
const pld = await this.fetchChannel(columnsOrData);
|
|
122
|
+
const s = new Series({ data: series as CrudeSeries, dataType: pld.dataType });
|
|
123
|
+
return new Frame(pld.key, s);
|
|
112
124
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(columnsOrData)) {
|
|
127
|
+
if (series == null)
|
|
128
|
+
throw new ValidationError(`
|
|
129
|
+
Received an array of channel names or keys but no series.
|
|
130
|
+
`);
|
|
131
|
+
if (!Array.isArray(series))
|
|
132
|
+
throw new ValidationError(`
|
|
133
|
+
Received an array of channel names or keys but no array of series.
|
|
134
|
+
`);
|
|
135
|
+
const cols = [];
|
|
136
|
+
const data = [];
|
|
137
|
+
for (let i = 0; i < columnsOrData.length; i++) {
|
|
138
|
+
const pld = await this.fetchChannel(columnsOrData[i]);
|
|
139
|
+
if (i >= series.length) {
|
|
140
|
+
throw new ValidationError(`
|
|
141
|
+
Received an array of channel names or keys but not enough series.
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
const s = new Series({
|
|
145
|
+
data: series[i] as CrudeSeries,
|
|
146
|
+
dataType: pld.dataType,
|
|
147
|
+
});
|
|
148
|
+
cols.push(pld.key);
|
|
149
|
+
data.push(s);
|
|
119
150
|
}
|
|
120
|
-
return
|
|
121
|
-
}
|
|
151
|
+
return new Frame(cols, data);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (columnsOrData instanceof Frame || columnsOrData instanceof Map) {
|
|
155
|
+
const fr = new Frame(columnsOrData);
|
|
156
|
+
if (this.adapter == null) return fr;
|
|
157
|
+
let cols: Key[] = [];
|
|
158
|
+
cols = fr.columns.map((col_) => {
|
|
159
|
+
const col = typeof col_ === "string" ? this.adapter?.get(col_) : col_;
|
|
160
|
+
if (col == null)
|
|
161
|
+
throw new ValidationError(`
|
|
162
|
+
Channel ${col_} was not provided in the list of channels when opening the writer
|
|
163
|
+
`);
|
|
164
|
+
return col;
|
|
165
|
+
});
|
|
166
|
+
return new Frame(cols, fr.series);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cols = [];
|
|
170
|
+
const data = [];
|
|
171
|
+
const kvs = Object.entries(columnsOrData);
|
|
172
|
+
for (let i = 0; i < kvs.length; i++) {
|
|
173
|
+
const [k, v] = kvs[i];
|
|
174
|
+
const pld = await this.fetchChannel(k);
|
|
175
|
+
const s = new Series({ data: v, dataType: pld.dataType });
|
|
176
|
+
cols.push(pld.key);
|
|
177
|
+
data.push(s);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new Frame(cols, data);
|
|
122
181
|
}
|
|
123
182
|
}
|