@synnaxlabs/client 0.48.0 → 0.49.2
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 +6 -6
- package/dist/client.cjs +33 -31
- package/dist/client.js +6522 -6167
- package/dist/src/access/client.d.ts +3 -1
- package/dist/src/access/client.d.ts.map +1 -1
- package/dist/src/access/enforce.d.ts +35 -0
- package/dist/src/access/enforce.d.ts.map +1 -0
- package/dist/src/access/enforce.spec.d.ts +2 -0
- package/dist/src/access/enforce.spec.d.ts.map +1 -0
- package/dist/src/access/external.d.ts +3 -0
- package/dist/src/access/external.d.ts.map +1 -1
- package/dist/src/access/payload.d.ts +0 -6
- package/dist/src/access/payload.d.ts.map +1 -1
- package/dist/src/access/policy/access.spec.d.ts +2 -0
- package/dist/src/access/policy/access.spec.d.ts.map +1 -0
- package/dist/src/access/policy/client.d.ts +485 -31
- package/dist/src/access/policy/client.d.ts.map +1 -1
- package/dist/src/access/policy/payload.d.ts +36 -113
- package/dist/src/access/policy/payload.d.ts.map +1 -1
- package/dist/src/access/role/client.d.ts +135 -0
- package/dist/src/access/role/client.d.ts.map +1 -0
- package/dist/src/access/role/external.d.ts.map +1 -0
- package/dist/src/access/role/index.d.ts +2 -0
- package/dist/src/access/role/index.d.ts.map +1 -0
- package/dist/src/access/role/payload.d.ts +27 -0
- package/dist/src/access/role/payload.d.ts.map +1 -0
- package/dist/src/access/role/role.spec.d.ts +2 -0
- package/dist/src/access/role/role.spec.d.ts.map +1 -0
- package/dist/src/arc/access.spec.d.ts +2 -0
- package/dist/src/arc/access.spec.d.ts.map +1 -0
- package/dist/src/arc/client.d.ts +5 -14
- package/dist/src/arc/client.d.ts.map +1 -1
- package/dist/src/arc/payload.d.ts +11 -2
- package/dist/src/arc/payload.d.ts.map +1 -1
- package/dist/src/auth/auth.d.ts +5 -3
- package/dist/src/auth/auth.d.ts.map +1 -1
- package/dist/src/channel/access.spec.d.ts +2 -0
- package/dist/src/channel/access.spec.d.ts.map +1 -0
- package/dist/src/channel/client.d.ts +0 -1
- package/dist/src/channel/client.d.ts.map +1 -1
- package/dist/src/channel/payload.d.ts +18 -8
- package/dist/src/channel/payload.d.ts.map +1 -1
- package/dist/src/channel/payload.spec.d.ts +2 -0
- package/dist/src/channel/payload.spec.d.ts.map +1 -0
- package/dist/src/channel/retriever.d.ts +4 -6
- package/dist/src/channel/retriever.d.ts.map +1 -1
- package/dist/src/channel/writer.d.ts.map +1 -1
- package/dist/src/client.d.ts +9 -5
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/device/access.spec.d.ts +2 -0
- package/dist/src/device/access.spec.d.ts.map +1 -0
- package/dist/src/{hardware/device → device}/client.d.ts +14 -7
- package/dist/src/device/client.d.ts.map +1 -0
- package/dist/src/device/device.spec.d.ts.map +1 -0
- package/dist/src/device/external.d.ts.map +1 -0
- package/dist/src/device/index.d.ts.map +1 -0
- package/dist/src/{hardware/device → device}/payload.d.ts +1 -1
- package/dist/src/device/payload.d.ts.map +1 -0
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/framer/client.d.ts +11 -1
- package/dist/src/framer/client.d.ts.map +1 -1
- package/dist/src/framer/frame.d.ts +10 -5
- package/dist/src/framer/frame.d.ts.map +1 -1
- package/dist/src/framer/iterator.d.ts +3 -3
- package/dist/src/framer/reader.d.ts +16 -0
- package/dist/src/framer/reader.d.ts.map +1 -0
- package/dist/src/framer/reader.spec.d.ts +2 -0
- package/dist/src/framer/reader.spec.d.ts.map +1 -0
- package/dist/src/framer/streamer.d.ts +24 -21
- package/dist/src/framer/streamer.d.ts.map +1 -1
- package/dist/src/framer/writer.d.ts +13 -13
- package/dist/src/index.d.ts +4 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/label/access.spec.d.ts +2 -0
- package/dist/src/label/access.spec.d.ts.map +1 -0
- package/dist/src/label/client.d.ts +20 -11
- package/dist/src/label/client.d.ts.map +1 -1
- package/dist/src/ontology/client.d.ts +6 -6
- package/dist/src/ontology/client.d.ts.map +1 -1
- package/dist/src/ontology/group/access.spec.d.ts +2 -0
- package/dist/src/ontology/group/access.spec.d.ts.map +1 -0
- package/dist/src/ontology/group/client.d.ts +2 -2
- package/dist/src/ontology/group/client.d.ts.map +1 -1
- package/dist/src/ontology/group/payload.d.ts +1 -2
- package/dist/src/ontology/group/payload.d.ts.map +1 -1
- package/dist/src/ontology/payload.d.ts +23 -17
- package/dist/src/ontology/payload.d.ts.map +1 -1
- package/dist/src/ontology/writer.d.ts +10 -10
- package/dist/src/ontology/writer.d.ts.map +1 -1
- package/dist/src/rack/access.spec.d.ts +2 -0
- package/dist/src/rack/access.spec.d.ts.map +1 -0
- package/dist/src/{hardware/rack → rack}/client.d.ts +15 -8
- package/dist/src/rack/client.d.ts.map +1 -0
- package/dist/src/rack/external.d.ts.map +1 -0
- package/dist/src/rack/index.d.ts.map +1 -0
- package/dist/src/{hardware/rack → rack}/payload.d.ts +1 -1
- package/dist/src/rack/payload.d.ts.map +1 -0
- package/dist/src/rack/rack.spec.d.ts.map +1 -0
- package/dist/src/ranger/access.spec.d.ts +2 -0
- package/dist/src/ranger/access.spec.d.ts.map +1 -0
- package/dist/src/ranger/alias.d.ts +1 -8
- package/dist/src/ranger/alias.d.ts.map +1 -1
- package/dist/src/ranger/client.d.ts +12 -5
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/ranger/kv.d.ts +0 -3
- package/dist/src/ranger/kv.d.ts.map +1 -1
- package/dist/src/ranger/writer.d.ts +2 -2
- package/dist/src/ranger/writer.d.ts.map +1 -1
- package/dist/src/status/access.spec.d.ts +2 -0
- package/dist/src/status/access.spec.d.ts.map +1 -0
- package/dist/src/status/client.d.ts +4 -4
- package/dist/src/status/client.d.ts.map +1 -1
- package/dist/src/status/payload.d.ts +9 -2
- package/dist/src/status/payload.d.ts.map +1 -1
- package/dist/src/task/access.spec.d.ts +2 -0
- package/dist/src/task/access.spec.d.ts.map +1 -0
- package/dist/src/{hardware/task → task}/client.d.ts +26 -15
- package/dist/src/task/client.d.ts.map +1 -0
- package/dist/src/task/external.d.ts +3 -0
- package/dist/src/task/external.d.ts.map +1 -0
- package/dist/src/task/index.d.ts.map +1 -0
- package/dist/src/{hardware/task → task}/payload.d.ts +45 -6
- package/dist/src/task/payload.d.ts.map +1 -0
- package/dist/src/task/task.spec.d.ts.map +1 -0
- package/dist/src/testutil/access.d.ts +4 -0
- package/dist/src/testutil/access.d.ts.map +1 -0
- package/dist/src/transport.d.ts.map +1 -1
- package/dist/src/user/access.spec.d.ts +2 -0
- package/dist/src/user/access.spec.d.ts.map +1 -0
- package/dist/src/user/client.d.ts +10 -1
- package/dist/src/user/client.d.ts.map +1 -1
- package/dist/src/user/external.d.ts +1 -1
- package/dist/src/user/external.d.ts.map +1 -1
- package/dist/src/user/payload.d.ts.map +1 -1
- package/dist/src/workspace/access.spec.d.ts +2 -0
- package/dist/src/workspace/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/client.d.ts +10 -5
- package/dist/src/workspace/client.d.ts.map +1 -1
- package/dist/src/workspace/lineplot/access.spec.d.ts +2 -0
- package/dist/src/workspace/lineplot/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/lineplot/client.d.ts +8 -1
- package/dist/src/workspace/lineplot/client.d.ts.map +1 -1
- package/dist/src/workspace/log/access.spec.d.ts +2 -0
- package/dist/src/workspace/log/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/log/client.d.ts +8 -1
- package/dist/src/workspace/log/client.d.ts.map +1 -1
- package/dist/src/workspace/schematic/access.spec.d.ts +2 -0
- package/dist/src/workspace/schematic/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/schematic/client.d.ts +8 -1
- package/dist/src/workspace/schematic/client.d.ts.map +1 -1
- package/dist/src/workspace/schematic/symbol/access.spec.d.ts +2 -0
- package/dist/src/workspace/schematic/symbol/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/schematic/symbol/client.d.ts +1 -5
- package/dist/src/workspace/schematic/symbol/client.d.ts.map +1 -1
- package/dist/src/workspace/schematic/symbol/payload.d.ts +2 -2
- package/dist/src/workspace/table/access.spec.d.ts +2 -0
- package/dist/src/workspace/table/access.spec.d.ts.map +1 -0
- package/dist/src/workspace/table/client.d.ts +8 -1
- package/dist/src/workspace/table/client.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/access/client.ts +5 -2
- package/src/access/enforce.spec.ts +189 -0
- package/src/access/enforce.ts +84 -0
- package/src/access/external.ts +3 -0
- package/src/access/payload.ts +1 -13
- package/src/access/policy/access.spec.ts +147 -0
- package/src/access/policy/client.ts +21 -25
- package/src/access/policy/payload.ts +9 -5
- package/src/access/role/client.ts +135 -0
- package/src/access/role/external.ts +11 -0
- package/src/{hardware → access/role}/index.ts +1 -1
- package/src/access/role/payload.ts +32 -0
- package/src/access/role/role.spec.ts +95 -0
- package/src/arc/access.spec.ts +143 -0
- package/src/arc/client.ts +7 -31
- package/src/arc/payload.ts +4 -0
- package/src/auth/auth.ts +33 -11
- package/src/channel/access.spec.ts +116 -0
- package/src/channel/channel.spec.ts +63 -73
- package/src/channel/client.ts +2 -8
- package/src/channel/payload.spec.ts +171 -0
- package/src/channel/payload.ts +35 -7
- package/src/channel/retriever.ts +10 -11
- package/src/channel/writer.ts +3 -7
- package/src/client.ts +14 -18
- package/src/device/access.spec.ts +159 -0
- package/src/{hardware/device → device}/client.ts +12 -21
- package/src/{hardware/device → device}/device.spec.ts +70 -34
- package/src/device/external.ts +11 -0
- package/src/{hardware/rack → device}/index.ts +1 -1
- package/src/{hardware/device → device}/payload.ts +3 -3
- package/src/errors.ts +2 -0
- package/src/framer/adapter.spec.ts +14 -14
- package/src/framer/client.spec.ts +14 -20
- package/src/framer/client.ts +15 -20
- package/src/framer/deleter.spec.ts +1 -1
- package/src/framer/frame.spec.ts +131 -0
- package/src/framer/frame.ts +10 -2
- package/src/framer/iterator.ts +3 -3
- package/src/framer/reader.spec.ts +736 -0
- package/src/framer/reader.ts +265 -0
- package/src/framer/streamer.spec.ts +100 -12
- package/src/framer/streamer.ts +29 -9
- package/src/framer/writer.spec.ts +5 -5
- package/src/index.ts +4 -5
- package/src/label/access.spec.ts +109 -0
- package/src/label/client.ts +10 -14
- package/src/ontology/client.ts +4 -6
- package/src/ontology/group/access.spec.ts +77 -0
- package/src/ontology/group/client.ts +3 -7
- package/src/ontology/group/group.spec.ts +18 -0
- package/src/ontology/group/payload.ts +2 -2
- package/src/ontology/ontology.spec.ts +2 -0
- package/src/ontology/payload.ts +18 -2
- package/src/ontology/writer.ts +3 -7
- package/src/rack/access.spec.ts +102 -0
- package/src/{hardware/rack → rack}/client.ts +14 -19
- package/src/{hardware/device/index.ts → rack/external.ts} +2 -1
- package/src/{hardware/external.ts → rack/index.ts} +1 -1
- package/src/{hardware/rack → rack}/payload.ts +2 -2
- package/src/{hardware/rack → rack}/rack.spec.ts +43 -17
- package/src/ranger/access.spec.ts +115 -0
- package/src/ranger/alias.ts +6 -14
- package/src/ranger/client.ts +13 -14
- package/src/ranger/kv.ts +7 -9
- package/src/ranger/ranger.spec.ts +4 -4
- package/src/ranger/writer.ts +3 -7
- package/src/status/access.spec.ts +129 -0
- package/src/status/client.ts +5 -9
- package/src/status/payload.ts +3 -2
- package/src/task/access.spec.ts +131 -0
- package/src/{hardware/task → task}/client.ts +50 -25
- package/src/task/external.ts +11 -0
- package/src/{hardware/task → task}/index.ts +1 -1
- package/src/{hardware/task → task}/payload.ts +22 -3
- package/src/{hardware/task → task}/task.spec.ts +197 -34
- package/src/testutil/access.ts +34 -0
- package/src/testutil/channels.ts +3 -3
- package/src/transport.ts +1 -3
- package/src/user/access.spec.ts +107 -0
- package/src/user/client.ts +10 -12
- package/src/user/external.ts +12 -1
- package/src/user/payload.ts +3 -5
- package/src/workspace/access.spec.ts +108 -0
- package/src/workspace/client.ts +11 -27
- package/src/workspace/lineplot/access.spec.ts +134 -0
- package/src/workspace/lineplot/client.ts +8 -13
- package/src/workspace/log/access.spec.ts +134 -0
- package/src/workspace/log/client.ts +8 -13
- package/src/workspace/schematic/access.spec.ts +134 -0
- package/src/workspace/schematic/client.ts +9 -18
- package/src/workspace/schematic/symbol/access.spec.ts +172 -0
- package/src/workspace/schematic/symbol/client.ts +6 -17
- package/src/workspace/schematic/symbol/payload.ts +1 -1
- package/src/workspace/table/access.spec.ts +134 -0
- package/src/workspace/table/client.ts +8 -13
- package/dist/src/access/policy/policy.spec.d.ts +0 -2
- package/dist/src/access/policy/policy.spec.d.ts.map +0 -1
- package/dist/src/hardware/client.d.ts +0 -10
- package/dist/src/hardware/client.d.ts.map +0 -1
- package/dist/src/hardware/device/client.d.ts.map +0 -1
- package/dist/src/hardware/device/device.spec.d.ts.map +0 -1
- package/dist/src/hardware/device/external.d.ts.map +0 -1
- package/dist/src/hardware/device/index.d.ts.map +0 -1
- package/dist/src/hardware/device/payload.d.ts.map +0 -1
- package/dist/src/hardware/external.d.ts +0 -2
- package/dist/src/hardware/external.d.ts.map +0 -1
- package/dist/src/hardware/index.d.ts +0 -2
- package/dist/src/hardware/index.d.ts.map +0 -1
- package/dist/src/hardware/rack/client.d.ts.map +0 -1
- package/dist/src/hardware/rack/external.d.ts.map +0 -1
- package/dist/src/hardware/rack/index.d.ts.map +0 -1
- package/dist/src/hardware/rack/payload.d.ts.map +0 -1
- package/dist/src/hardware/rack/rack.spec.d.ts.map +0 -1
- package/dist/src/hardware/task/client.d.ts.map +0 -1
- package/dist/src/hardware/task/external.d.ts.map +0 -1
- package/dist/src/hardware/task/index.d.ts.map +0 -1
- package/dist/src/hardware/task/payload.d.ts.map +0 -1
- package/dist/src/hardware/task/task.spec.d.ts.map +0 -1
- package/dist/src/user/retriever.d.ts +0 -16
- package/dist/src/user/retriever.d.ts.map +0 -1
- package/dist/src/user/writer.d.ts +0 -11
- package/dist/src/user/writer.d.ts.map +0 -1
- package/src/access/policy/policy.spec.ts +0 -329
- package/src/hardware/client.ts +0 -24
- package/src/hardware/device/external.ts +0 -11
- package/src/hardware/rack/external.ts +0 -11
- package/src/hardware/task/external.ts +0 -11
- package/src/user/retriever.ts +0 -41
- package/src/user/writer.ts +0 -84
- /package/dist/src/{hardware/device → access/role}/external.d.ts +0 -0
- /package/dist/src/{hardware/device → device}/device.spec.d.ts +0 -0
- /package/dist/src/{hardware/rack → device}/external.d.ts +0 -0
- /package/dist/src/{hardware/device → device}/index.d.ts +0 -0
- /package/dist/src/{hardware/task → rack}/external.d.ts +0 -0
- /package/dist/src/{hardware/rack → rack}/index.d.ts +0 -0
- /package/dist/src/{hardware/rack → rack}/rack.spec.d.ts +0 -0
- /package/dist/src/{hardware/task → task}/index.d.ts +0 -0
- /package/dist/src/{hardware/task → task}/task.spec.d.ts +0 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// Copyright 2025 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { DataType, id, runtime, TimeSpan, TimeStamp } from "@synnaxlabs/x";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { type channel } from "@/channel";
|
|
14
|
+
import { createTestClient } from "@/testutil/client";
|
|
15
|
+
|
|
16
|
+
const client = createTestClient();
|
|
17
|
+
|
|
18
|
+
const delimiter = runtime.getOS() === "Windows" ? "\r\n" : "\n";
|
|
19
|
+
|
|
20
|
+
/** Helper to collect stream into a string */
|
|
21
|
+
const streamToString = async (stream: ReadableStream<Uint8Array>): Promise<string> => {
|
|
22
|
+
const reader = stream.getReader();
|
|
23
|
+
const chunks: Uint8Array[] = [];
|
|
24
|
+
while (true) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
chunks.push(value);
|
|
28
|
+
}
|
|
29
|
+
const decoder = new TextDecoder();
|
|
30
|
+
return chunks.map((c) => decoder.decode(c)).join("");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const parseCSV = (csv: string): string[][] => {
|
|
34
|
+
const lines = csv.trim().split(delimiter);
|
|
35
|
+
return lines.map((line) => line.split(","));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const streamToRecords = async (
|
|
39
|
+
stream: ReadableStream<Uint8Array>,
|
|
40
|
+
): Promise<string[][]> => {
|
|
41
|
+
const csv = await streamToString(stream);
|
|
42
|
+
return parseCSV(csv);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("Reader", () => {
|
|
46
|
+
describe("CSV", () => {
|
|
47
|
+
it("should export channels with the same index", async () => {
|
|
48
|
+
const index = await client.channels.create({
|
|
49
|
+
name: id.create(),
|
|
50
|
+
dataType: DataType.TIMESTAMP,
|
|
51
|
+
isIndex: true,
|
|
52
|
+
});
|
|
53
|
+
const data1 = await client.channels.create({
|
|
54
|
+
name: id.create(),
|
|
55
|
+
dataType: DataType.FLOAT64,
|
|
56
|
+
index: index.key,
|
|
57
|
+
});
|
|
58
|
+
const data2 = await client.channels.create({
|
|
59
|
+
name: id.create(),
|
|
60
|
+
dataType: DataType.FLOAT64,
|
|
61
|
+
index: index.key,
|
|
62
|
+
});
|
|
63
|
+
const start = TimeStamp.seconds(1);
|
|
64
|
+
const writer = await client.openWriter({
|
|
65
|
+
start,
|
|
66
|
+
channels: [index.key, data1.key, data2.key],
|
|
67
|
+
});
|
|
68
|
+
await writer.write({
|
|
69
|
+
[index.key]: [TimeStamp.seconds(1), TimeStamp.seconds(2), TimeStamp.seconds(3)],
|
|
70
|
+
[data1.key]: [10, 20, 30],
|
|
71
|
+
[data2.key]: [100, 200, 300],
|
|
72
|
+
});
|
|
73
|
+
await writer.commit();
|
|
74
|
+
await writer.close();
|
|
75
|
+
const stream = await client.read({
|
|
76
|
+
channels: [index.key, data1.key, data2.key],
|
|
77
|
+
timeRange: { start: TimeStamp.seconds(0), end: TimeStamp.seconds(10) },
|
|
78
|
+
channelNames: new Map([
|
|
79
|
+
[index.key, "Time"],
|
|
80
|
+
[data1.key, "Sensor1"],
|
|
81
|
+
[data2.key, "Sensor2"],
|
|
82
|
+
]),
|
|
83
|
+
responseType: "csv",
|
|
84
|
+
});
|
|
85
|
+
const records = await streamToRecords(stream);
|
|
86
|
+
expect(records).toEqual([
|
|
87
|
+
["Time", "Sensor1", "Sensor2"],
|
|
88
|
+
["1000000000", "10", "100"],
|
|
89
|
+
["2000000000", "20", "200"],
|
|
90
|
+
["3000000000", "30", "300"],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
it("should export multiple channels with different indexes", async () => {
|
|
94
|
+
const index1 = await client.channels.create({
|
|
95
|
+
name: id.create(),
|
|
96
|
+
dataType: DataType.TIMESTAMP,
|
|
97
|
+
isIndex: true,
|
|
98
|
+
});
|
|
99
|
+
const data1 = await client.channels.create({
|
|
100
|
+
name: id.create(),
|
|
101
|
+
dataType: DataType.FLOAT64,
|
|
102
|
+
index: index1.key,
|
|
103
|
+
});
|
|
104
|
+
const index2 = await client.channels.create({
|
|
105
|
+
name: id.create(),
|
|
106
|
+
dataType: DataType.TIMESTAMP,
|
|
107
|
+
isIndex: true,
|
|
108
|
+
});
|
|
109
|
+
const data2 = await client.channels.create({
|
|
110
|
+
name: id.create(),
|
|
111
|
+
dataType: DataType.FLOAT64,
|
|
112
|
+
index: index2.key,
|
|
113
|
+
});
|
|
114
|
+
// Write to first group - timestamps 1, 3, 5
|
|
115
|
+
const writer1 = await client.openWriter({
|
|
116
|
+
start: TimeStamp.seconds(1),
|
|
117
|
+
channels: [index1.key, data1.key],
|
|
118
|
+
});
|
|
119
|
+
await writer1.write({
|
|
120
|
+
[index1.key]: [
|
|
121
|
+
TimeStamp.seconds(1),
|
|
122
|
+
TimeStamp.seconds(3),
|
|
123
|
+
TimeStamp.seconds(5),
|
|
124
|
+
],
|
|
125
|
+
[data1.key]: [100, 300, 500],
|
|
126
|
+
});
|
|
127
|
+
await writer1.commit();
|
|
128
|
+
await writer1.close();
|
|
129
|
+
|
|
130
|
+
// Write to second group - timestamps 2, 4, 6
|
|
131
|
+
const writer2 = await client.openWriter({
|
|
132
|
+
start: TimeStamp.seconds(2),
|
|
133
|
+
channels: [index2.key, data2.key],
|
|
134
|
+
});
|
|
135
|
+
await writer2.write({
|
|
136
|
+
[index2.key]: [
|
|
137
|
+
TimeStamp.seconds(2),
|
|
138
|
+
TimeStamp.seconds(4),
|
|
139
|
+
TimeStamp.seconds(6),
|
|
140
|
+
],
|
|
141
|
+
[data2.key]: [200, 400, 600],
|
|
142
|
+
});
|
|
143
|
+
await writer2.commit();
|
|
144
|
+
await writer2.close();
|
|
145
|
+
const stream = await client.read({
|
|
146
|
+
channels: [data1.key, data2.key], // Just data channels - indexes auto-included
|
|
147
|
+
timeRange: { start: TimeStamp.seconds(0), end: TimeStamp.seconds(10) },
|
|
148
|
+
channelNames: new Map([
|
|
149
|
+
[index1.key, "Time1"],
|
|
150
|
+
[data1.key, "Data1"],
|
|
151
|
+
[index2.key, "Time2"],
|
|
152
|
+
[data2.key, "Data2"],
|
|
153
|
+
]),
|
|
154
|
+
responseType: "csv",
|
|
155
|
+
});
|
|
156
|
+
const records = await streamToRecords(stream);
|
|
157
|
+
expect(records).toEqual([
|
|
158
|
+
["Time1", "Data1", "Time2", "Data2"],
|
|
159
|
+
["1000000000", "100", "", ""],
|
|
160
|
+
["", "", "2000000000", "200"],
|
|
161
|
+
["3000000000", "300", "", ""],
|
|
162
|
+
["", "", "4000000000", "400"],
|
|
163
|
+
["5000000000", "500", "", ""],
|
|
164
|
+
["", "", "6000000000", "600"],
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
it("should handle channels at different uneven rates with correct row ordering", async () => {
|
|
168
|
+
const indexFast = await client.channels.create({
|
|
169
|
+
name: id.create(),
|
|
170
|
+
dataType: DataType.TIMESTAMP,
|
|
171
|
+
isIndex: true,
|
|
172
|
+
});
|
|
173
|
+
const dataFast = await client.channels.create({
|
|
174
|
+
name: id.create(),
|
|
175
|
+
dataType: DataType.FLOAT64,
|
|
176
|
+
index: indexFast.key,
|
|
177
|
+
});
|
|
178
|
+
const indexSlow = await client.channels.create({
|
|
179
|
+
name: id.create(),
|
|
180
|
+
dataType: DataType.TIMESTAMP,
|
|
181
|
+
isIndex: true,
|
|
182
|
+
});
|
|
183
|
+
const dataSlow = await client.channels.create({
|
|
184
|
+
name: id.create(),
|
|
185
|
+
dataType: DataType.FLOAT64,
|
|
186
|
+
index: indexSlow.key,
|
|
187
|
+
});
|
|
188
|
+
const baseTime = TimeStamp.nanoseconds(0);
|
|
189
|
+
// Write fast data: 0ns, 1ns, 2ns, 3ns, 4ns, 5ns
|
|
190
|
+
const writerFast = await client.openWriter({
|
|
191
|
+
start: baseTime,
|
|
192
|
+
channels: [indexFast.key, dataFast.key],
|
|
193
|
+
});
|
|
194
|
+
await writerFast.write({
|
|
195
|
+
[indexFast.key]: [
|
|
196
|
+
TimeStamp.nanoseconds(0),
|
|
197
|
+
TimeStamp.nanoseconds(1),
|
|
198
|
+
TimeStamp.nanoseconds(2),
|
|
199
|
+
TimeStamp.nanoseconds(3),
|
|
200
|
+
TimeStamp.nanoseconds(4),
|
|
201
|
+
TimeStamp.nanoseconds(5),
|
|
202
|
+
],
|
|
203
|
+
[dataFast.key]: [1.0, 1.1, 1.2, 1.3, 1.4, 1.5],
|
|
204
|
+
});
|
|
205
|
+
await writerFast.commit();
|
|
206
|
+
await writerFast.close();
|
|
207
|
+
|
|
208
|
+
// Write slow data: 0ns, 5ns
|
|
209
|
+
const writerSlow = await client.openWriter({
|
|
210
|
+
start: baseTime,
|
|
211
|
+
channels: [indexSlow.key, dataSlow.key],
|
|
212
|
+
});
|
|
213
|
+
await writerSlow.write({
|
|
214
|
+
[indexSlow.key]: [TimeStamp.nanoseconds(0), TimeStamp.nanoseconds(5)],
|
|
215
|
+
[dataSlow.key]: [2.0, 2.5],
|
|
216
|
+
});
|
|
217
|
+
await writerSlow.commit();
|
|
218
|
+
await writerSlow.close();
|
|
219
|
+
|
|
220
|
+
const stream = await client.read({
|
|
221
|
+
channels: [dataFast.key, dataSlow.key],
|
|
222
|
+
timeRange: {
|
|
223
|
+
start: baseTime,
|
|
224
|
+
end: TimeStamp.nanoseconds(6),
|
|
225
|
+
},
|
|
226
|
+
responseType: "csv",
|
|
227
|
+
});
|
|
228
|
+
const records = await streamToRecords(stream);
|
|
229
|
+
expect(records).toEqual([
|
|
230
|
+
[indexFast.name, dataFast.name, indexSlow.name, dataSlow.name],
|
|
231
|
+
["0", "1", "0", "2"],
|
|
232
|
+
["1", "1.1", "", ""],
|
|
233
|
+
["2", "1.2", "", ""],
|
|
234
|
+
["3", "1.3", "", ""],
|
|
235
|
+
["4", "1.4", "", ""],
|
|
236
|
+
["5", "1.5", "5", "2.5"],
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
it("should handle large amounts of channels", async () => {
|
|
240
|
+
const numGroups = 5;
|
|
241
|
+
const channelsPerGroup = 3;
|
|
242
|
+
const dataKeys: channel.Keys = [];
|
|
243
|
+
const expectedColumns = numGroups * (1 + channelsPerGroup);
|
|
244
|
+
|
|
245
|
+
// Store timestamps written per group for building expected rows later
|
|
246
|
+
interface GroupWrite {
|
|
247
|
+
groupIdx: number;
|
|
248
|
+
timestamps: bigint[];
|
|
249
|
+
values: number[][]; // values[sampleIdx][channelIdx]
|
|
250
|
+
}
|
|
251
|
+
const groupWrites: GroupWrite[] = [];
|
|
252
|
+
|
|
253
|
+
for (let g = 0; g < numGroups; g++) {
|
|
254
|
+
const index = await client.channels.create({
|
|
255
|
+
name: id.create(),
|
|
256
|
+
dataType: DataType.TIMESTAMP,
|
|
257
|
+
isIndex: true,
|
|
258
|
+
});
|
|
259
|
+
const groupChannels: channel.Keys = [index.key];
|
|
260
|
+
for (let c = 0; c < channelsPerGroup; c++) {
|
|
261
|
+
const data = await client.channels.create({
|
|
262
|
+
name: id.create(),
|
|
263
|
+
dataType: DataType.FLOAT64,
|
|
264
|
+
index: index.key,
|
|
265
|
+
});
|
|
266
|
+
dataKeys.push(data.key);
|
|
267
|
+
groupChannels.push(data.key);
|
|
268
|
+
}
|
|
269
|
+
const writer = await client.openWriter({
|
|
270
|
+
start: TimeStamp.seconds(g + 1),
|
|
271
|
+
channels: groupChannels,
|
|
272
|
+
});
|
|
273
|
+
// Write two timestamps for this group
|
|
274
|
+
const ts1 = TimeStamp.seconds(g + 1);
|
|
275
|
+
const ts2 = TimeStamp.seconds(g + 2);
|
|
276
|
+
const writeData: Record<number, unknown[]> = {
|
|
277
|
+
[index.key]: [ts1, ts2],
|
|
278
|
+
};
|
|
279
|
+
// Write sample values for all channels
|
|
280
|
+
for (let c = 0; c < channelsPerGroup; c++)
|
|
281
|
+
writeData[groupChannels[c + 1]] = [g * 10 + c, g * 10 + c + 1];
|
|
282
|
+
|
|
283
|
+
await writer.write(writeData);
|
|
284
|
+
await writer.commit();
|
|
285
|
+
await writer.close();
|
|
286
|
+
|
|
287
|
+
// Store the write info
|
|
288
|
+
groupWrites.push({
|
|
289
|
+
groupIdx: g,
|
|
290
|
+
timestamps: [ts1.valueOf(), ts2.valueOf()],
|
|
291
|
+
values: [
|
|
292
|
+
Array.from({ length: channelsPerGroup }, (_, c) => g * 10 + c),
|
|
293
|
+
Array.from({ length: channelsPerGroup }, (_, c) => g * 10 + c + 1),
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Build expected rows AFTER all groups created (now we know total columns)
|
|
299
|
+
const rowsByTime = new Map<string, string[]>();
|
|
300
|
+
for (const gw of groupWrites)
|
|
301
|
+
for (let i = 0; i < gw.timestamps.length; i++) {
|
|
302
|
+
const timeStr = gw.timestamps[i].toString();
|
|
303
|
+
if (!rowsByTime.has(timeStr))
|
|
304
|
+
rowsByTime.set(timeStr, Array(expectedColumns).fill(""));
|
|
305
|
+
|
|
306
|
+
const row = rowsByTime.get(timeStr)!;
|
|
307
|
+
const colOffset = gw.groupIdx * (1 + channelsPerGroup);
|
|
308
|
+
row[colOffset] = timeStr; // index timestamp
|
|
309
|
+
for (let c = 0; c < channelsPerGroup; c++)
|
|
310
|
+
row[colOffset + 1 + c] = gw.values[i][c].toString();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Compose expected rows in time order (ascending)
|
|
314
|
+
const sortedTimes = Array.from(rowsByTime.keys())
|
|
315
|
+
.map((k) => BigInt(k))
|
|
316
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
317
|
+
.map((k) => k.toString());
|
|
318
|
+
|
|
319
|
+
const expectedRows: string[][] = [];
|
|
320
|
+
for (const timeStr of sortedTimes) expectedRows.push(rowsByTime.get(timeStr)!);
|
|
321
|
+
|
|
322
|
+
const stream = await client.read({
|
|
323
|
+
channels: dataKeys,
|
|
324
|
+
timeRange: { start: TimeStamp.seconds(0), end: TimeStamp.seconds(20) },
|
|
325
|
+
responseType: "csv",
|
|
326
|
+
});
|
|
327
|
+
const rows = await streamToRecords(stream);
|
|
328
|
+
// There should be a header and at least the expected number of rows
|
|
329
|
+
expect(rows.length).toBeGreaterThan(1);
|
|
330
|
+
expect(rows.slice(1)).toEqual(expectedRows);
|
|
331
|
+
// Each row should have columns for all groups (index + data channels each)
|
|
332
|
+
rows.forEach((row) => {
|
|
333
|
+
expect(row).toHaveLength(expectedColumns);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should handle empty data gracefully", async () => {
|
|
338
|
+
const index = await client.channels.create({
|
|
339
|
+
name: id.create(),
|
|
340
|
+
dataType: DataType.TIMESTAMP,
|
|
341
|
+
isIndex: true,
|
|
342
|
+
});
|
|
343
|
+
const data = await client.channels.create({
|
|
344
|
+
name: id.create(),
|
|
345
|
+
dataType: DataType.FLOAT64,
|
|
346
|
+
index: index.key,
|
|
347
|
+
});
|
|
348
|
+
const stream = await client.read({
|
|
349
|
+
channels: [data.key],
|
|
350
|
+
timeRange: { start: TimeStamp.seconds(0), end: TimeStamp.seconds(10) },
|
|
351
|
+
responseType: "csv",
|
|
352
|
+
});
|
|
353
|
+
const rows = await streamToRecords(stream);
|
|
354
|
+
expect(rows).toEqual([[index.name, data.name]]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should use channel names as default headers", async () => {
|
|
358
|
+
const uniqueSuffix = id.create();
|
|
359
|
+
const indexName = `my_timestamp_${uniqueSuffix}`;
|
|
360
|
+
const dataName = `my_sensor_data_${uniqueSuffix}`;
|
|
361
|
+
const index = await client.channels.create({
|
|
362
|
+
name: indexName,
|
|
363
|
+
dataType: DataType.TIMESTAMP,
|
|
364
|
+
isIndex: true,
|
|
365
|
+
});
|
|
366
|
+
const data = await client.channels.create({
|
|
367
|
+
name: dataName,
|
|
368
|
+
dataType: DataType.FLOAT64,
|
|
369
|
+
index: index.key,
|
|
370
|
+
});
|
|
371
|
+
const writer = await client.openWriter({
|
|
372
|
+
start: TimeStamp.nanoseconds(1),
|
|
373
|
+
channels: [index.key, data.key],
|
|
374
|
+
});
|
|
375
|
+
await writer.write({
|
|
376
|
+
[index.key]: [TimeStamp.nanoseconds(1)],
|
|
377
|
+
[data.key]: [42],
|
|
378
|
+
});
|
|
379
|
+
await writer.commit();
|
|
380
|
+
await writer.close();
|
|
381
|
+
const stream = await client.read({
|
|
382
|
+
channels: [data.key],
|
|
383
|
+
timeRange: { start: TimeStamp.seconds(0), end: TimeStamp.seconds(10) },
|
|
384
|
+
responseType: "csv",
|
|
385
|
+
});
|
|
386
|
+
const records = await streamToRecords(stream);
|
|
387
|
+
expect(records).toEqual([
|
|
388
|
+
[index.name, data.name],
|
|
389
|
+
["1", "42"],
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should handle large dataset requiring multiple iterator calls", async () => {
|
|
394
|
+
// Create 4 groups with different indexes at different rates
|
|
395
|
+
const numGroups = 4;
|
|
396
|
+
const samplesPerGroup = [3000, 2500, 2000, 1500]; // Different sample counts
|
|
397
|
+
const channelsPerGroup = 3;
|
|
398
|
+
|
|
399
|
+
interface GroupInfo {
|
|
400
|
+
indexKey: number;
|
|
401
|
+
dataKeys: number[];
|
|
402
|
+
baseTime: TimeStamp;
|
|
403
|
+
sampleCount: number;
|
|
404
|
+
intervalMs: number;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const groups: GroupInfo[] = [];
|
|
408
|
+
const allDataKeys: number[] = [];
|
|
409
|
+
|
|
410
|
+
// Create channels for each group
|
|
411
|
+
for (let g = 0; g < numGroups; g++) {
|
|
412
|
+
const index = await client.channels.create({
|
|
413
|
+
name: `stress_index_${id.create()}`,
|
|
414
|
+
dataType: DataType.TIMESTAMP,
|
|
415
|
+
isIndex: true,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const dataKeys: number[] = [];
|
|
419
|
+
for (let c = 0; c < channelsPerGroup; c++) {
|
|
420
|
+
const data = await client.channels.create({
|
|
421
|
+
name: `stress_data_${id.create()}`,
|
|
422
|
+
dataType: DataType.FLOAT64,
|
|
423
|
+
index: index.key,
|
|
424
|
+
});
|
|
425
|
+
dataKeys.push(data.key);
|
|
426
|
+
allDataKeys.push(data.key);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Different base times and intervals to create interleaving
|
|
430
|
+
const baseTime = TimeStamp.seconds(1000).add(TimeSpan.milliseconds(g * 7));
|
|
431
|
+
const intervalMs = 10 + g * 3; // 10ms, 13ms, 16ms, 19ms intervals
|
|
432
|
+
|
|
433
|
+
groups.push({
|
|
434
|
+
indexKey: index.key,
|
|
435
|
+
dataKeys,
|
|
436
|
+
baseTime,
|
|
437
|
+
sampleCount: samplesPerGroup[g],
|
|
438
|
+
intervalMs,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Write data to each group in parallel using Promise.all
|
|
443
|
+
await Promise.all(
|
|
444
|
+
groups.map(async (group) => {
|
|
445
|
+
const writer = await client.openWriter({
|
|
446
|
+
start: group.baseTime,
|
|
447
|
+
channels: [group.indexKey, ...group.dataKeys],
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Write in batches to avoid memory issues
|
|
451
|
+
const batchSize = 500;
|
|
452
|
+
for (
|
|
453
|
+
let batchStart = 0;
|
|
454
|
+
batchStart < group.sampleCount;
|
|
455
|
+
batchStart += batchSize
|
|
456
|
+
) {
|
|
457
|
+
const batchEnd = Math.min(batchStart + batchSize, group.sampleCount);
|
|
458
|
+
const timestamps: TimeStamp[] = [];
|
|
459
|
+
const dataArrays: number[][] = group.dataKeys.map(() => []);
|
|
460
|
+
|
|
461
|
+
for (let i = batchStart; i < batchEnd; i++) {
|
|
462
|
+
timestamps.push(
|
|
463
|
+
group.baseTime.add(TimeSpan.milliseconds(i * group.intervalMs)),
|
|
464
|
+
);
|
|
465
|
+
group.dataKeys.forEach((_, c) => {
|
|
466
|
+
dataArrays[c].push(i * 100 + c);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const writeData: Record<number, unknown[]> = {
|
|
471
|
+
[group.indexKey]: timestamps,
|
|
472
|
+
};
|
|
473
|
+
group.dataKeys.forEach((key, c) => {
|
|
474
|
+
writeData[key] = dataArrays[c];
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await writer.write(writeData);
|
|
478
|
+
}
|
|
479
|
+
await writer.commit();
|
|
480
|
+
await writer.close();
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
// Calculate expected total samples across all groups
|
|
484
|
+
const totalSamples = samplesPerGroup.reduce((a, b) => a + b, 0);
|
|
485
|
+
|
|
486
|
+
// Export the data
|
|
487
|
+
const stream = await client.read({
|
|
488
|
+
channels: allDataKeys,
|
|
489
|
+
timeRange: {
|
|
490
|
+
start: TimeStamp.seconds(999),
|
|
491
|
+
end: TimeStamp.seconds(1100),
|
|
492
|
+
},
|
|
493
|
+
responseType: "csv",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Collect all chunks and track streaming behavior
|
|
497
|
+
const reader = stream.getReader();
|
|
498
|
+
const chunks: Uint8Array[] = [];
|
|
499
|
+
let chunkCount = 0;
|
|
500
|
+
|
|
501
|
+
while (true) {
|
|
502
|
+
const { done, value } = await reader.read();
|
|
503
|
+
if (done) break;
|
|
504
|
+
chunks.push(value);
|
|
505
|
+
chunkCount++;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Verify multiple chunks were produced (proves streaming worked)
|
|
509
|
+
expect(chunkCount).toBeGreaterThan(1);
|
|
510
|
+
|
|
511
|
+
// Decode and parse the full CSV
|
|
512
|
+
const decoder = new TextDecoder();
|
|
513
|
+
const csv = chunks.map((c) => decoder.decode(c)).join("");
|
|
514
|
+
const lines = csv.trim().split(delimiter);
|
|
515
|
+
|
|
516
|
+
// Header + data rows (some timestamps may align, so rows <= totalSamples)
|
|
517
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
518
|
+
expect(lines.length).toBeLessThanOrEqual(totalSamples + 1);
|
|
519
|
+
|
|
520
|
+
// Verify header has correct number of columns
|
|
521
|
+
// Each group has: 1 index + channelsPerGroup data channels
|
|
522
|
+
const expectedColumns = numGroups * (1 + channelsPerGroup);
|
|
523
|
+
const headerColumns = lines[0].split(",");
|
|
524
|
+
expect(headerColumns).toHaveLength(expectedColumns);
|
|
525
|
+
|
|
526
|
+
// Verify all data rows have correct column count
|
|
527
|
+
for (let i = 1; i < lines.length; i++) {
|
|
528
|
+
const cols = lines[i].split(",");
|
|
529
|
+
expect(cols).toHaveLength(expectedColumns);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Verify timestamps are in ascending order
|
|
533
|
+
const rows = parseCSV(csv);
|
|
534
|
+
let lastTimestamp: bigint | null = null;
|
|
535
|
+
for (let i = 1; i < rows.length; i++)
|
|
536
|
+
// Find the first non-empty timestamp in this row
|
|
537
|
+
for (let g = 0; g < numGroups; g++) {
|
|
538
|
+
const tsCol = g * (1 + channelsPerGroup);
|
|
539
|
+
const tsStr = rows[i][tsCol];
|
|
540
|
+
if (tsStr === "") continue;
|
|
541
|
+
const ts = BigInt(tsStr);
|
|
542
|
+
if (lastTimestamp !== null) expect(ts).toBeGreaterThanOrEqual(lastTimestamp);
|
|
543
|
+
lastTimestamp = ts;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Verify some specific data integrity
|
|
548
|
+
// First row should have data from at least one group
|
|
549
|
+
const firstDataRow = rows[1];
|
|
550
|
+
const nonEmptyValues = firstDataRow.filter((v) => v !== "");
|
|
551
|
+
expect(nonEmptyValues.length).toBeGreaterThan(0);
|
|
552
|
+
});
|
|
553
|
+
it(
|
|
554
|
+
"should handle large dense and sparse indexes with correct ordering and merging",
|
|
555
|
+
{ timeout: 15_000 },
|
|
556
|
+
async () => {
|
|
557
|
+
const denseSamples = 100_000;
|
|
558
|
+
const sparseStep = 1_000;
|
|
559
|
+
const sparseSamples = denseSamples / sparseStep;
|
|
560
|
+
|
|
561
|
+
// Fast (dense) index + data
|
|
562
|
+
const indexFast = await client.channels.create({
|
|
563
|
+
name: `dense_index_${id.create()}`,
|
|
564
|
+
dataType: DataType.TIMESTAMP,
|
|
565
|
+
isIndex: true,
|
|
566
|
+
});
|
|
567
|
+
const dataFast = await client.channels.create({
|
|
568
|
+
name: `dense_data_${id.create()}`,
|
|
569
|
+
dataType: DataType.FLOAT64,
|
|
570
|
+
index: indexFast.key,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Slow (sparse) index + data
|
|
574
|
+
const indexSlow = await client.channels.create({
|
|
575
|
+
name: `sparse_index_${id.create()}`,
|
|
576
|
+
dataType: DataType.TIMESTAMP,
|
|
577
|
+
isIndex: true,
|
|
578
|
+
});
|
|
579
|
+
const dataSlow = await client.channels.create({
|
|
580
|
+
name: `sparse_data_${id.create()}`,
|
|
581
|
+
dataType: DataType.FLOAT64,
|
|
582
|
+
index: indexSlow.key,
|
|
583
|
+
});
|
|
584
|
+
const baseTime = TimeStamp.seconds(0);
|
|
585
|
+
const denseWriter = await client.openWriter({
|
|
586
|
+
start: baseTime,
|
|
587
|
+
channels: [indexFast.key, dataFast.key],
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const denseBatchSize = 10_000;
|
|
591
|
+
for (
|
|
592
|
+
let batchStart = 1;
|
|
593
|
+
batchStart <= denseSamples;
|
|
594
|
+
batchStart += denseBatchSize
|
|
595
|
+
) {
|
|
596
|
+
const batchEnd = Math.min(batchStart + denseBatchSize - 1, denseSamples);
|
|
597
|
+
const tsBatch: TimeStamp[] = [];
|
|
598
|
+
const valBatch: number[] = [];
|
|
599
|
+
|
|
600
|
+
for (let i = batchStart; i <= batchEnd; i++) {
|
|
601
|
+
// baseTime + i ns => underlying raw timestamps ~ [1..1_000_000]
|
|
602
|
+
tsBatch.push(baseTime.add(TimeSpan.nanoseconds(i)));
|
|
603
|
+
valBatch.push(i); // arbitrary data value
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
await denseWriter.write({
|
|
607
|
+
[indexFast.key]: tsBatch,
|
|
608
|
+
[dataFast.key]: valBatch,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
await denseWriter.commit();
|
|
612
|
+
await denseWriter.close();
|
|
613
|
+
|
|
614
|
+
// ---- Write sparse channel: timestamps 1..1_000_000 every 1000 ----
|
|
615
|
+
const sparseWriter = await client.openWriter({
|
|
616
|
+
start: baseTime,
|
|
617
|
+
channels: [indexSlow.key, dataSlow.key],
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const sparseBatchSize = 1000; // at most 1000 sparse points total anyway
|
|
621
|
+
for (
|
|
622
|
+
let batchStart = 0;
|
|
623
|
+
batchStart < sparseSamples;
|
|
624
|
+
batchStart += sparseBatchSize
|
|
625
|
+
) {
|
|
626
|
+
const batchEnd = Math.min(batchStart + sparseBatchSize, sparseSamples);
|
|
627
|
+
const tsBatch: TimeStamp[] = [];
|
|
628
|
+
const valBatch: number[] = [];
|
|
629
|
+
|
|
630
|
+
for (let j = batchStart; j < batchEnd; j++) {
|
|
631
|
+
const logicalTs = (j + 1) * sparseStep; // 1000, 2000, ..., 1_000_000
|
|
632
|
+
tsBatch.push(baseTime.add(TimeSpan.nanoseconds(logicalTs)));
|
|
633
|
+
valBatch.push(logicalTs); // arbitrary data value
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
await sparseWriter.write({
|
|
637
|
+
[indexSlow.key]: tsBatch,
|
|
638
|
+
[dataSlow.key]: valBatch,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
await sparseWriter.commit();
|
|
642
|
+
await sparseWriter.close();
|
|
643
|
+
|
|
644
|
+
// ---- Export CSV with explicit headers so we know column order ----
|
|
645
|
+
const stream = await client.read({
|
|
646
|
+
channels: [dataFast.key, dataSlow.key],
|
|
647
|
+
timeRange: {
|
|
648
|
+
start: baseTime,
|
|
649
|
+
end: baseTime.add(TimeSpan.nanoseconds(denseSamples + 1)),
|
|
650
|
+
},
|
|
651
|
+
channelNames: new Map([
|
|
652
|
+
[indexFast.key, "FastTime"],
|
|
653
|
+
[dataFast.key, "FastValue"],
|
|
654
|
+
[indexSlow.key, "SlowTime"],
|
|
655
|
+
[dataSlow.key, "SlowValue"],
|
|
656
|
+
]),
|
|
657
|
+
responseType: "csv",
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const reader = stream.getReader();
|
|
661
|
+
const decoder = new TextDecoder();
|
|
662
|
+
|
|
663
|
+
let buffer = "";
|
|
664
|
+
let chunkCount = 0;
|
|
665
|
+
let isHeader = true;
|
|
666
|
+
let totalRows = 0; // data rows only (exclude header)
|
|
667
|
+
let sparseRows = 0;
|
|
668
|
+
let lastTimestamp: bigint | null = null;
|
|
669
|
+
|
|
670
|
+
while (true) {
|
|
671
|
+
const { done, value } = await reader.read();
|
|
672
|
+
if (done) break;
|
|
673
|
+
chunkCount++;
|
|
674
|
+
|
|
675
|
+
buffer += decoder.decode(value);
|
|
676
|
+
|
|
677
|
+
while (true) {
|
|
678
|
+
const idx = buffer.indexOf(delimiter);
|
|
679
|
+
if (idx === -1) break;
|
|
680
|
+
const line = buffer.slice(0, idx);
|
|
681
|
+
buffer = buffer.slice(idx + delimiter.length);
|
|
682
|
+
if (line === "") continue;
|
|
683
|
+
if (isHeader) {
|
|
684
|
+
const headerCols = line.split(",");
|
|
685
|
+
expect(headerCols).toEqual([
|
|
686
|
+
"FastTime",
|
|
687
|
+
"FastValue",
|
|
688
|
+
"SlowTime",
|
|
689
|
+
"SlowValue",
|
|
690
|
+
]);
|
|
691
|
+
isHeader = false;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
totalRows++;
|
|
696
|
+
|
|
697
|
+
const cols = line.split(",");
|
|
698
|
+
expect(cols).toHaveLength(4);
|
|
699
|
+
|
|
700
|
+
const fastTsStr = cols[0];
|
|
701
|
+
const slowTsStr = cols[2];
|
|
702
|
+
const fastValStr = cols[1];
|
|
703
|
+
const slowValStr = cols[3];
|
|
704
|
+
|
|
705
|
+
// Dense channel should always have a timestamp and value
|
|
706
|
+
expect(fastTsStr).not.toBe("");
|
|
707
|
+
expect(fastValStr).not.toBe("");
|
|
708
|
+
|
|
709
|
+
const ts = BigInt(fastTsStr);
|
|
710
|
+
if (lastTimestamp !== null) expect(ts).toBeGreaterThan(lastTimestamp);
|
|
711
|
+
lastTimestamp = ts;
|
|
712
|
+
|
|
713
|
+
// Sparse channel only has data every 1000 "ticks"
|
|
714
|
+
if (slowValStr !== "") {
|
|
715
|
+
sparseRows++;
|
|
716
|
+
// When sparse has data, its timestamp should match dense's timestamp
|
|
717
|
+
expect(slowTsStr).toBe(fastTsStr);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Handle any final line without trailing CRLF
|
|
723
|
+
if (buffer.trim().length > 0) if (!isHeader) totalRows++;
|
|
724
|
+
|
|
725
|
+
// We should have streamed multiple chunks (proves AUTO_SPAN / multi-frame)
|
|
726
|
+
expect(chunkCount).toBeGreaterThan(1);
|
|
727
|
+
|
|
728
|
+
// One row per dense timestamp
|
|
729
|
+
expect(totalRows).toBe(denseSamples);
|
|
730
|
+
|
|
731
|
+
// One row per sparse timestamp (merged into dense rows)
|
|
732
|
+
expect(sparseRows).toBe(sparseSamples);
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
});
|