@synnaxlabs/client 0.17.6 → 0.18.1

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.
@@ -400,4 +400,110 @@ describe("framer.Frame", () => {
400
400
  expect(f.latest()).toEqual({ 12: 3, 13: 3 });
401
401
  });
402
402
  });
403
+
404
+ describe("sample access", () => {
405
+ it("should return the sample at the given index", () => {
406
+ const f = new framer.Frame(
407
+ new Map([
408
+ [
409
+ 12,
410
+ [
411
+ new Series({
412
+ data: new Float32Array([1, 2, 3]),
413
+ timeRange: new TimeRange(40, 50000),
414
+ }),
415
+ ],
416
+ ],
417
+ [
418
+ 13,
419
+ [
420
+ new Series({
421
+ data: new Float32Array([1, 2, 3]),
422
+ timeRange: new TimeRange(500, 50001),
423
+ }),
424
+ ],
425
+ ],
426
+ ]),
427
+ );
428
+ expect(f.get(12).at(0)).toEqual(1);
429
+ });
430
+ });
431
+
432
+ describe("at", () => {
433
+ it("should return the sample at the given index", () => {
434
+ const f = new framer.Frame(
435
+ new Map([
436
+ [
437
+ 12,
438
+ [
439
+ new Series({
440
+ data: new Float32Array([1, 2, 3]),
441
+ timeRange: new TimeRange(40, 50000),
442
+ }),
443
+ ],
444
+ ],
445
+ [
446
+ 13,
447
+ [
448
+ new Series({
449
+ data: new Float32Array([1, 2, 3]),
450
+ timeRange: new TimeRange(500, 50001),
451
+ }),
452
+ ],
453
+ ],
454
+ ]),
455
+ );
456
+ expect(f.at(0)).toEqual({ 12: 1, 13: 1 });
457
+ });
458
+ it("should throw an error if required is true and the index is out of bounds", () => {
459
+ const f = new framer.Frame(
460
+ new Map([
461
+ [
462
+ 12,
463
+ [
464
+ new Series({
465
+ data: new Float32Array([1, 2, 3]),
466
+ timeRange: new TimeRange(40, 50000),
467
+ }),
468
+ ],
469
+ ],
470
+ [
471
+ 13,
472
+ [
473
+ new Series({
474
+ data: new Float32Array([1, 2, 3]),
475
+ timeRange: new TimeRange(500, 50001),
476
+ }),
477
+ ],
478
+ ],
479
+ ]),
480
+ );
481
+ expect(() => f.at(3, true)).toThrow();
482
+ });
483
+ it("should return undefined if required is false and the index is out of bounds", () => {
484
+ const f = new framer.Frame(
485
+ new Map([
486
+ [
487
+ 12,
488
+ [
489
+ new Series({
490
+ data: new Float32Array([1, 2, 3]),
491
+ timeRange: new TimeRange(40, 50000),
492
+ }),
493
+ ],
494
+ ],
495
+ [
496
+ 13,
497
+ [
498
+ new Series({
499
+ data: new Float32Array([1, 2]),
500
+ timeRange: new TimeRange(500, 50001),
501
+ }),
502
+ ],
503
+ ],
504
+ ]),
505
+ );
506
+ expect(f.at(2)).toEqual({ 12: 3, 13: undefined });
507
+ });
508
+ });
403
509
  });
@@ -16,6 +16,7 @@ import {
16
16
  unique,
17
17
  TimeStamp,
18
18
  type TelemValue,
19
+ MultiSeries,
19
20
  } from "@synnaxlabs/x";
20
21
  import { z } from "zod";
21
22
 
@@ -34,6 +35,7 @@ const columnType = (columns: Params): ColumnType => {
34
35
  const arrKeys = toArray(columns);
35
36
  if (arrKeys.length === 0) return null;
36
37
  if (typeof arrKeys[0] === "number") return "key";
38
+ if (!isNaN(parseInt(arrKeys[0]))) return "key";
37
39
  return "name";
38
40
  };
39
41
 
@@ -252,18 +254,11 @@ export class Frame {
252
254
  }
253
255
  const group = this.get(col);
254
256
  if (group == null) return TimeRange.ZERO;
255
- return new TimeRange(
256
- group[0].timeRange.start,
257
- group[group.length - 1].timeRange.end,
258
- );
257
+ return group.timeRange;
259
258
  }
260
259
 
261
- latest(): Record<string, TelemValue> {
262
- return Object.fromEntries(
263
- this.columns
264
- .map((c, i) => [c, this.series[i].at(-1)])
265
- .filter(([_, v]) => v != null),
266
- );
260
+ latest(): Record<string, TelemValue | undefined> {
261
+ return this.at(-1);
267
262
  }
268
263
 
269
264
  get timeRanges(): TimeRange[] {
@@ -274,7 +269,7 @@ export class Frame {
274
269
  * @returns lazy arrays matching the given channel key or name.
275
270
  * @param key the channel key or name.
276
271
  */
277
- get(key: KeyOrName): Series[];
272
+ get(key: KeyOrName): MultiSeries;
278
273
 
279
274
  /**
280
275
  * @returns a frame with the given channel keys or names.
@@ -282,9 +277,9 @@ export class Frame {
282
277
  */
283
278
  get(keys: Keys | Names): Frame;
284
279
 
285
- get(key: KeyOrName | Keys | Names): Series[] | Frame {
280
+ get(key: KeyOrName | Keys | Names): MultiSeries | Frame {
286
281
  if (Array.isArray(key)) return this.filter((k) => (key as Keys).includes(k as Key));
287
- return this.series.filter((_, i) => this.columns[i] === key);
282
+ return new MultiSeries(this.series.filter((_, i) => this.columns[i] === key));
288
283
  }
289
284
 
290
285
  /**
@@ -364,6 +359,18 @@ export class Frame {
364
359
  });
365
360
  }
366
361
 
362
+ at(index: number, required: true): Record<KeyOrName, TelemValue>;
363
+
364
+ at(index: number, required?: false): Record<KeyOrName, TelemValue | undefined>;
365
+
366
+ at(index: number, required = false): Record<KeyOrName, TelemValue | undefined> {
367
+ const res: Record<KeyOrName, TelemValue> = {};
368
+ this.uniqueColumns.forEach((k) => {
369
+ res[k] = this.get(k).at(index, required as true);
370
+ });
371
+ return res;
372
+ }
373
+
367
374
  /**
368
375
  * @returns a new frame containing all typed arrays in the current frame that pass
369
376
  * the provided filter function.
@@ -28,7 +28,7 @@ const newChannel = async (): Promise<channel.Channel> => {
28
28
  describe("Iterator", () => {
29
29
  test("happy path", async () => {
30
30
  const ch = await newChannel();
31
- const writer = await client.telem.newWriter({
31
+ const writer = await client.telem.openWriter({
32
32
  start: TimeStamp.SECOND,
33
33
  channels: ch.key,
34
34
  });
@@ -42,7 +42,7 @@ describe("Iterator", () => {
42
42
  await writer.close();
43
43
  }
44
44
 
45
- const iter = await client.telem.newIterator(
45
+ const iter = await client.telem.openIterator(
46
46
  new TimeRange(TimeSpan.ZERO, TimeSpan.seconds(4)),
47
47
  [ch.key],
48
48
  );
@@ -52,7 +52,7 @@ describe("Iterator", () => {
52
52
  let c = 0;
53
53
  while (await iter.next(TimeSpan.seconds(1))) {
54
54
  c++;
55
- expect(iter.value.get(ch.key)[0]).toHaveLength(25);
55
+ expect(iter.value.get(ch.key)).toHaveLength(25);
56
56
  }
57
57
  expect(c).toEqual(3);
58
58
  } finally {
@@ -8,7 +8,7 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { DataType, Rate, TimeStamp } from "@synnaxlabs/x";
11
- import { describe, test, expect } from "vitest";
11
+ import { describe, test, expect, it } from "vitest";
12
12
 
13
13
  import { type channel } from "@/channel";
14
14
  import { newClient } from "@/setupspecs";
@@ -26,9 +26,9 @@ const newChannel = async (): Promise<channel.Channel> =>
26
26
  describe("Streamer", () => {
27
27
  test("happy path", async () => {
28
28
  const ch = await newChannel();
29
- const streamer = await client.telem.newStreamer(ch.key);
29
+ const streamer = await client.telem.openStreamer(ch.key);
30
30
  await new Promise((resolve) => setTimeout(resolve, 100));
31
- const writer = await client.telem.newWriter({
31
+ const writer = await client.telem.openWriter({
32
32
  start: TimeStamp.now(),
33
33
  channels: ch.key,
34
34
  });
@@ -38,6 +38,18 @@ describe("Streamer", () => {
38
38
  await writer.close();
39
39
  }
40
40
  const d = await streamer.read();
41
- expect(d.get(ch.key)[0].data).toEqual(new Float64Array([1, 2, 3]));
41
+ expect(Array.from(d.get(ch.key))).toEqual([1, 2, 3]);
42
+ });
43
+ test("open with config", async () => {
44
+ const ch = await newChannel();
45
+ await expect(
46
+ client.telem.openStreamer({
47
+ channels: ch.key,
48
+ from: TimeStamp.now(),
49
+ }),
50
+ ).resolves.not.toThrow();
51
+ });
52
+ it("should not throw an error when the streamer is opened with zero channels", async () => {
53
+ await expect(client.telem.openStreamer([])).resolves.not.toThrow();
42
54
  });
43
55
  });
@@ -29,6 +29,11 @@ const resZ = z.object({
29
29
 
30
30
  const ENDPOINT = "/frame/stream";
31
31
 
32
+ export interface StreamerConfig {
33
+ channels: Params;
34
+ from?: CrudeTimeStamp;
35
+ }
36
+
32
37
  export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
33
38
  private readonly stream: StreamProxy<typeof reqZ, typeof resZ>;
34
39
  private readonly adapter: ReadFrameAdapter;
@@ -46,15 +51,14 @@ export class Streamer implements AsyncIterator<Frame>, AsyncIterable<Frame> {
46
51
  }
47
52
 
48
53
  static async _open(
49
- start: CrudeTimeStamp,
50
- channels: Params,
51
54
  retriever: Retriever,
52
55
  client: StreamClient,
56
+ { channels, from }: StreamerConfig,
53
57
  ): Promise<Streamer> {
54
58
  const adapter = await ReadFrameAdapter.open(retriever, channels);
55
59
  const stream = await client.stream(ENDPOINT, reqZ, resZ);
56
60
  const streamer = new Streamer(stream, adapter);
57
- stream.send({ start: new TimeStamp(start), keys: adapter.keys });
61
+ stream.send({ start: new TimeStamp(from), keys: adapter.keys });
58
62
  return streamer;
59
63
  }
60
64
 
@@ -29,7 +29,7 @@ describe("Writer", () => {
29
29
  describe("Writer", () => {
30
30
  test("basic write", async () => {
31
31
  const ch = await newChannel();
32
- const writer = await client.telem.newWriter({ start: 0, channels: ch.key });
32
+ const writer = await client.telem.openWriter({ start: 0, channels: ch.key });
33
33
  try {
34
34
  await writer.write(ch.key, randomSeries(10, ch.dataType));
35
35
  await writer.commit();
@@ -40,7 +40,7 @@ describe("Writer", () => {
40
40
  });
41
41
  test("write to unknown channel key", async () => {
42
42
  const ch = await newChannel();
43
- const writer = await client.telem.newWriter({ start: 0, channels: ch.key });
43
+ const writer = await client.telem.openWriter({ start: 0, channels: ch.key });
44
44
  await expect(
45
45
  writer.write("billy bob", randomSeries(10, DataType.FLOAT64)),
46
46
  ).rejects.toThrow("Channel billy bob not found");
@@ -48,8 +48,8 @@ describe("Writer", () => {
48
48
  });
49
49
  test("stream when mode is set ot persist only", async () => {
50
50
  const ch = await newChannel();
51
- const stream = await client.telem.newStreamer(ch.key);
52
- const writer = await client.telem.newWriter({
51
+ const stream = await client.telem.openStreamer(ch.key);
52
+ const writer = await client.telem.openWriter({
53
53
  start: 0,
54
54
  channels: ch.key,
55
55
  mode: WriterMode.PersistOnly,
@@ -74,8 +74,8 @@ const resZ = z.object({
74
74
  type Response = z.infer<typeof resZ>;
75
75
 
76
76
  export interface WriterConfig {
77
- start: CrudeTimeStamp;
78
77
  channels: Params;
78
+ start?: CrudeTimeStamp;
79
79
  controlSubject?: ControlSubject;
80
80
  authorities?: Authority | Authority[];
81
81
  mode?: WriterMode;
@@ -83,35 +83,35 @@ export interface WriterConfig {
83
83
 
84
84
  /**
85
85
  * Writer is used to write telemetry to a set of channels in time order.
86
- * It should not be instantiated directly, and should instead be instantited via the
86
+ * It should not be instantiated directly, and should instead be instantiated via the
87
87
  * FramerClient {@link FrameClient#openWriter}.
88
88
  *
89
- * The writer is a streaming protocol that is heavily optimized for prerformance. This
90
- * comes at the cost of icnreased complexity, and should only be used directly when
91
- * writing large volumes of data (such as recording telemetry from a sensor or ingsting
92
- * data froma file). Simpler methods (such as the frame client's write method) should
89
+ * The writer is a streaming protocol that is heavily optimized for performance. This
90
+ * comes at the cost of increased complexity, and should only be used directly when
91
+ * writing large volumes of data (such as recording telemetry from a sensor or ingesting
92
+ * data from file). Simpler methods (such as the frame client's write method) should
93
93
  * be used for most use cases.
94
94
  *
95
95
  * The protocol is as follows:
96
96
  *
97
97
  * 1. The writer is opened with a starting timestamp and a list of channel keys. The
98
- * writer will fail to open if the starting timstamp overlaps with any existing telemetry
99
- * for any channels specified. If the writer opens successfuly, the caller is then
98
+ * writer will fail to open if the starting timestamp overlaps with any existing telemetry
99
+ * for any channels specified. If the writer opens successfully, the caller is then
100
100
  * free to write frames to the writer.
101
101
  *
102
102
  * 2. To write a frame, the caller can use the write method and follow the validation
103
103
  * rules described in its method's documentation. This process is asynchronous, meaning
104
104
  * that write calls may return before teh frame has been written to the cluster. This
105
105
  * also means that the writer can accumulate an error after write is called. If the writer
106
- * accumulates an erorr, all subsequent write and commit calls will return False. The
107
- * caller can check for errors by calling the error mehtod, which returns the accumulated
106
+ * accumulates an error, all subsequent write and commit calls will return False. The
107
+ * caller can check for errors by calling the error method, which returns the accumulated
108
108
  * error and resets the writer for future use. The caller can also check for errors by
109
109
  * closing the writer, which will throw any accumulated error.
110
110
  *
111
111
  * 3. To commit the written frames to the cluster, the caller can call the commit method.
112
112
  * Unlike write, commit is synchronous, meaning that it will not return until the frames
113
- * have been written to the cluster. If the writer has accumulated an erorr, commit will
114
- * return false. After the caller acknowledges the erorr, they can attempt to commit again.
113
+ * have been written to the cluster. If the writer has accumulated an error, commit will
114
+ * return false. After the caller acknowledges the error, they can attempt to commit again.
115
115
  * Commit can be called several times throughout a writer's lifetime, and will only
116
116
  * commit the frames that have been written since the last commit.
117
117
  *
@@ -137,10 +137,10 @@ export class Writer {
137
137
  client: StreamClient,
138
138
  {
139
139
  channels,
140
+ start = TimeStamp.now(),
140
141
  authorities = Authority.Absolute,
141
142
  controlSubject: subject,
142
- start,
143
- mode,
143
+ mode = WriterMode.PersistStream,
144
144
  }: WriterConfig,
145
145
  ): Promise<Writer> {
146
146
  const adapter = await WriteFrameAdapter.open(retriever, channels);
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  TimeRange,
36
36
  TimeSpan,
37
37
  TimeStamp,
38
+ MultiSeries,
38
39
  } from "@synnaxlabs/x";
39
40
  export type {
40
41
  TypedArray,
@@ -44,7 +45,8 @@ export type {
44
45
  CrudeSize,
45
46
  CrudeTimeSpan,
46
47
  CrudeTimeStamp,
47
- SampleValue,
48
+ TelemValue,
49
+ NumericTelemValue,
48
50
  TimeStampStringFormat,
49
51
  TZInfo,
50
52
  } from "@synnaxlabs/x";
@@ -72,15 +72,15 @@ export class ChangeTracker {
72
72
  if (allResources.length > 0) this.resourceObs.notify(resSets.concat(resDeletes));
73
73
  const relSets = this.parseRelationshipSets(frame);
74
74
  const relDeletes = this.parseRelationshipDeletes(frame);
75
- const allRels = relSets.concat(relDeletes);
76
- if (allRels.length > 0) this.relationshipObs.notify(relSets.concat(relDeletes));
75
+ const allRelationships = relSets.concat(relDeletes);
76
+ if (allRelationships.length > 0)
77
+ this.relationshipObs.notify(relSets.concat(relDeletes));
77
78
  }
78
79
 
79
80
  private parseRelationshipSets(frame: Frame): RelationshipChange[] {
80
81
  const relationships = frame.get(RELATIONSHIP_SET_NAME);
81
82
  if (relationships.length === 0) return [];
82
- // We should only ever get one series of relationships
83
- return relationships[0].toStrings().map((rel) => ({
83
+ return Array.from(relationships.as("string")).map((rel) => ({
84
84
  variant: "set",
85
85
  key: parseRelationship(rel),
86
86
  value: undefined,
@@ -90,8 +90,7 @@ export class ChangeTracker {
90
90
  private parseRelationshipDeletes(frame: Frame): RelationshipChange[] {
91
91
  const relationships = frame.get(RELATIONSHIP_DELETE_NAME);
92
92
  if (relationships.length === 0) return [];
93
- // We should only ever get one series of relationships
94
- return relationships[0].toStrings().map((rel) => ({
93
+ return Array.from(relationships.as("string")).map((rel) => ({
95
94
  variant: "delete",
96
95
  key: parseRelationship(rel),
97
96
  }));
@@ -101,7 +100,7 @@ export class ChangeTracker {
101
100
  const sets = frame.get(RESOURCE_SET_NAME);
102
101
  if (sets.length === 0) return [];
103
102
  // We should only ever get one series of sets
104
- const ids = sets[0].toStrings().map((id) => new ID(id));
103
+ const ids = Array.from(sets.as("string")).map((id: string) => new ID(id));
105
104
  try {
106
105
  const resources = await this.retriever.retrieve(ids);
107
106
  return resources.map((resource) => ({
@@ -122,13 +121,14 @@ export class ChangeTracker {
122
121
  const deletes = frame.get(RESOURCE_DELETE_NAME);
123
122
  if (deletes.length === 0) return [];
124
123
  // We should only ever get one series of deletes
125
- return deletes[0]
126
- .toStrings()
127
- .map((str) => ({ variant: "delete", key: new ID(str) }));
124
+ return Array.from(deletes.as("string")).map((str) => ({
125
+ variant: "delete",
126
+ key: new ID(str),
127
+ }));
128
128
  }
129
129
 
130
130
  static async open(client: FrameClient, retriever: Retriever): Promise<ChangeTracker> {
131
- const streamer = await client.newStreamer([
131
+ const streamer = await client.openStreamer([
132
132
  RESOURCE_SET_NAME,
133
133
  RESOURCE_DELETE_NAME,
134
134
  RELATIONSHIP_SET_NAME,
@@ -55,11 +55,11 @@ export class Observable<K, V>
55
55
  const changes: Array<change.Change<K, V>> = [];
56
56
  if (this.deleteChannel != null) {
57
57
  const deletes = frame.get(this.deleteChannel);
58
- changes.push(...deletes.flatMap((s) => this.decoder("delete", s)));
58
+ changes.push(...deletes.series.flatMap((s) => this.decoder("delete", s)));
59
59
  }
60
60
  if (this.setChannel != null) {
61
61
  const sets = frame.get(this.setChannel);
62
- changes.push(...sets.flatMap((s) => this.decoder("set", s)));
62
+ changes.push(...sets.series.flatMap((s) => this.decoder("set", s)));
63
63
  }
64
64
  this.base.notify(changes);
65
65
  }
@@ -71,7 +71,7 @@ export class Observable<K, V>
71
71
  deleteChannel: channel.Key | channel.Name,
72
72
  ecd: Decoder<K, V>,
73
73
  ): Promise<Observable<K, V>> {
74
- const stream = await client.newStreamer([
74
+ const stream = await client.openStreamer([
75
75
  setChannel,
76
76
  deleteChannel,
77
77
  ] as channel.Keys);