@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.
@@ -1,21 +1,23 @@
1
- import { type Key, type Params } from '../channel/payload';
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 BackwardFrameAdapter {
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<BackwardFrameAdapter>;
10
+ static open(retriever: Retriever, channels: Params): Promise<ReadFrameAdapter>;
10
11
  update(channels: Params): Promise<void>;
11
- adapt(fr: Frame): Frame;
12
+ adapt(columnsOrData: Frame): Frame;
12
13
  }
13
- export declare class ForwardFrameAdapter {
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<ForwardFrameAdapter>;
19
+ static open(retriever: Retriever, channels: Params): Promise<WriteFrameAdapter>;
19
20
  update(channels: Params): Promise<void>;
20
- adapt(fr: Frame): Frame;
21
+ private fetchChannel;
22
+ adapt(columnsOrData: Params | Record<KeyOrName, CrudeSeries> | CrudeFrame, series?: CrudeSeries | CrudeSeries[]): Promise<Frame>;
21
23
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { type StreamClient } from "@synnaxlabs/freighter";
2
- import { type NativeTypedArray, type Series, type TimeRange, type CrudeTimeStamp, TimeStamp } from "@synnaxlabs/x";
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: NativeTypedArray): Promise<void>;
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;
@@ -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 betwen them.
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
@@ -1,7 +1,7 @@
1
1
  import type { StreamClient } from "@synnaxlabs/freighter";
2
- import { type NativeTypedArray, TimeStamp, type CrudeTimeStamp } from "@synnaxlabs/x";
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: NativeTypedArray): Promise<boolean>;
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 { NativeTypedArray, CrudeDataType, CrudeDensity, CrudeRate, CrudeSize, CrudeTimeSpan, CrudeTimeStamp, SampleValue, TimeStampStringFormat, TZInfo, } from "@synnaxlabs/x";
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';
@@ -1,2 +1,2 @@
1
- import { DataType, NativeTypedArray } from "@synnaxlabs/x";
2
- export declare const randomSeries: (length: number, dataType: DataType) => NativeTypedArray;
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.1"
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.1",
3657
- "resolved": "https://registry.npmjs.org/@synnaxlabs/client/-/client-0.17.1.tgz",
3658
- "integrity": "sha512-8LONAhvloau3ErXMMieLeyDXqA8fPmW1tloE+VRVjUVwHI+K5lnortnN9uddWzZ+re1cgnVEoIpgw/y88hOdpw==",
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",
@@ -11,6 +11,6 @@
11
11
  "author": "",
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
- "@synnaxlabs/client": "^0.17.1"
14
+ "@synnaxlabs/client": "workspace:^"
15
15
  }
16
16
  }
@@ -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.2",
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.1",
22
- "@synnaxlabs/x": "0.14.1"
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",
@@ -11,7 +11,7 @@ import { type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import {
12
12
  DataType,
13
13
  Rate,
14
- type NativeTypedArray,
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: NativeTypedArray): Promise<void> {
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
- private readonly retriever: Retriever;
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(channels: NewPayload | NewPayload[], options: CreateOptions = {}): Promise<Channel | Channel[]> {
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);
@@ -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 { Mutex } from "async-mutex";
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(ClusterRetriever.ENDPOINT, request, reqZ, resZ);
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
- const normal = (toArray(channels) as KeysOrNames).filter((c) => c != 0);
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: typeof normal[0] === "number" ? "keys" : "names",
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
+ });
@@ -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 Key, type Name, type Params } from "@/channel/payload";
11
- import { type Retriever, analyzeParams } from "@/channel/retriever";
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 BackwardFrameAdapter {
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: 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(fr: Frame): Frame {
54
- if (this.adapter == null) return fr;
58
+ adapt(columnsOrData: Frame): Frame {
59
+ if (this.adapter == null) return columnsOrData;
55
60
  const a = this.adapter;
56
- return fr.map((k, arr) => {
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 ForwardFrameAdapter {
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<ForwardFrameAdapter> {
82
- const adapter = new ForwardFrameAdapter(retriever);
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 { variant, normalized } = analyzeParams(channels);
89
- if (variant === "keys") {
90
- this.adapter = null;
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
- adapt(fr: Frame): Frame {
106
- if (this.adapter == null) {
107
- // assert that every col if of type number
108
- fr.columns.forEach((col) => {
109
- if (typeof col !== "number") throw new ValidationError(`Channel ${col} was not provided in the list of channels when opening the writer`);
110
- });
111
- return fr;
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
- const a = this.adapter;
114
- return fr.map((col, arr) => {
115
- if (typeof col === "string") {
116
- const key = a.get(col);
117
- if (key == null) throw new Error(`Channel ${col} not found`);
118
- return [key, arr];
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 [col, arr];
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
  }