@synnaxlabs/client 0.17.5 → 0.18.0
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/dist/client.cjs +14 -14
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +2497 -2352
- package/dist/client.js.map +1 -1
- package/dist/framer/client.d.ts +28 -7
- package/dist/framer/frame.d.ts +5 -3
- package/dist/framer/streamer.d.ts +5 -1
- package/dist/framer/writer.d.ts +13 -13
- package/dist/index.d.ts +2 -2
- package/examples/node/liveStream.js +1 -6
- package/examples/node/package-lock.json +4 -4
- package/examples/node/package.json +1 -1
- package/examples/node/seriesAndFrames.js +0 -0
- package/package.json +5 -5
- package/src/control/state.ts +1 -1
- package/src/framer/adapter.spec.ts +12 -12
- package/src/framer/client.ts +43 -30
- package/src/framer/frame.spec.ts +133 -0
- package/src/framer/frame.ts +20 -13
- package/src/framer/iterator.spec.ts +3 -3
- package/src/framer/streamer.spec.ts +16 -4
- package/src/framer/streamer.ts +7 -3
- package/src/framer/writer.spec.ts +4 -4
- package/src/framer/writer.ts +14 -14
- package/src/index.ts +3 -1
- package/src/ontology/signals.ts +11 -11
- package/src/signals/observable.ts +3 -3
package/src/framer/frame.ts
CHANGED
|
@@ -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
|
|
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
|
|
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):
|
|
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):
|
|
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.
|
|
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.
|
|
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)
|
|
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.
|
|
29
|
+
const streamer = await client.telem.openStreamer(ch.key);
|
|
30
30
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
31
|
-
const writer = await client.telem.
|
|
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)
|
|
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
|
});
|
package/src/framer/streamer.ts
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
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.
|
|
52
|
-
const writer = await client.telem.
|
|
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,
|
package/src/framer/writer.ts
CHANGED
|
@@ -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
|
|
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
|
|
90
|
-
* comes at the cost of
|
|
91
|
-
* writing large volumes of data (such as recording telemetry from a sensor or
|
|
92
|
-
* data
|
|
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
|
|
99
|
-
* for any channels specified. If the writer opens
|
|
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
|
|
107
|
-
* caller can check for errors by calling the error
|
|
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
|
|
114
|
-
* return false. After the caller acknowledges the
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
TelemValue,
|
|
49
|
+
NumericTelemValue,
|
|
48
50
|
TimeStampStringFormat,
|
|
49
51
|
TZInfo,
|
|
50
52
|
} from "@synnaxlabs/x";
|
package/src/ontology/signals.ts
CHANGED
|
@@ -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
|
|
76
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
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.
|
|
74
|
+
const stream = await client.openStreamer([
|
|
75
75
|
setChannel,
|
|
76
76
|
deleteChannel,
|
|
77
77
|
] as channel.Keys);
|