@uns-kit/cli 2.0.9 → 2.0.13
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/dist/index.js
CHANGED
|
@@ -171,7 +171,6 @@ function printHelp() {
|
|
|
171
171
|
" create <name> Scaffold a new UNS application\n" +
|
|
172
172
|
" configure [dir] [features...] Configure multiple templates (--all, --overwrite)\n" +
|
|
173
173
|
" configure-templates [dir] [templates...] Copy any template directory (--all, --overwrite)\n" +
|
|
174
|
-
" configure-config [dir] Copy configuration example files\n" +
|
|
175
174
|
" configure-devops [dir] Configure Azure DevOps tooling in an existing project\n" +
|
|
176
175
|
" configure-vscode [dir] Add VS Code workspace configuration files\n" +
|
|
177
176
|
" configure-codegen [dir] Copy GraphQL codegen template and dependencies\n" +
|
|
@@ -187,6 +186,8 @@ async function createProject(projectName) {
|
|
|
187
186
|
await ensureTargetDir(targetDir);
|
|
188
187
|
const templateDir = path.resolve(__dirname, "../templates/default");
|
|
189
188
|
await copyTemplateDirectory(templateDir, targetDir, targetDir);
|
|
189
|
+
// Seed config examples automatically (previously required configure-config).
|
|
190
|
+
await configureConfigFiles(targetDir, { overwrite: false });
|
|
190
191
|
const pkgName = normalizePackageName(projectName);
|
|
191
192
|
await patchPackageJson(targetDir, pkgName);
|
|
192
193
|
await patchConfigJson(targetDir, pkgName);
|
|
@@ -600,7 +601,6 @@ async function configureUnsReference(targetPath, options = {}) {
|
|
|
600
601
|
const configureFeatureHandlers = {
|
|
601
602
|
devops: configureDevops,
|
|
602
603
|
vscode: configureVscode,
|
|
603
|
-
config: configureConfigFiles,
|
|
604
604
|
codegen: configureCodegen,
|
|
605
605
|
api: configureApi,
|
|
606
606
|
cron: configureCron,
|
|
@@ -612,7 +612,6 @@ const AVAILABLE_CONFIGURE_FEATURES = Object.keys(configureFeatureHandlers);
|
|
|
612
612
|
const configureFeatureLabels = {
|
|
613
613
|
devops: "Azure DevOps tooling",
|
|
614
614
|
vscode: "VS Code workspace",
|
|
615
|
-
config: "Configuration example files",
|
|
616
615
|
codegen: "GraphQL codegen tooling",
|
|
617
616
|
api: "UNS API resources",
|
|
618
617
|
cron: "UNS cron resources",
|
|
@@ -808,8 +807,6 @@ const configureFeatureAliases = {
|
|
|
808
807
|
"configure-devops": "devops",
|
|
809
808
|
vscode: "vscode",
|
|
810
809
|
"configure-vscode": "vscode",
|
|
811
|
-
config: "config",
|
|
812
|
-
"configure-config": "config",
|
|
813
810
|
codegen: "codegen",
|
|
814
811
|
"configure-codegen": "codegen",
|
|
815
812
|
api: "api",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uns-kit/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "Command line scaffolding tool for UNS applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"azure-devops-node-api": "^15.1.1",
|
|
29
|
-
"@uns-kit/core": "2.0.
|
|
29
|
+
"@uns-kit/core": "2.0.13"
|
|
30
30
|
},
|
|
31
31
|
"unsKitPackages": {
|
|
32
|
-
"@uns-kit/core": "2.0.
|
|
33
|
-
"@uns-kit/api": "2.0.
|
|
34
|
-
"@uns-kit/cron": "2.0.
|
|
35
|
-
"@uns-kit/temporal": "2.0.
|
|
32
|
+
"@uns-kit/core": "2.0.13",
|
|
33
|
+
"@uns-kit/api": "2.0.13",
|
|
34
|
+
"@uns-kit/cron": "2.0.13",
|
|
35
|
+
"@uns-kit/temporal": "2.0.13"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving-window table load test.
|
|
3
|
+
*
|
|
4
|
+
* Publishes time buckets as UNS table messages with:
|
|
5
|
+
* - `time` (bucket identity; uses `intervalStart`)
|
|
6
|
+
* - `intervalStart` / `intervalEnd` (bucket boundaries)
|
|
7
|
+
* - `windowStart` / `windowEnd` (snapshot window boundaries)
|
|
8
|
+
*
|
|
9
|
+
* This is meant to validate `uns-archiver` ingest modes:
|
|
10
|
+
* - `dedup`: upsert by (eventId + time + symbols). Missing buckets can be marked via `deleted: true`.
|
|
11
|
+
* - `window_replace`: delete rows in the window, then insert the snapshot (omitting missing buckets).
|
|
12
|
+
*
|
|
13
|
+
* Run:
|
|
14
|
+
* pnpm exec tsx src/examples/table-window-load-test.ts
|
|
15
|
+
*
|
|
16
|
+
* Optional args:
|
|
17
|
+
* --run=all|1|2|3
|
|
18
|
+
* --mode=window_replace|dedup|append
|
|
19
|
+
* --bucketMinutes=30
|
|
20
|
+
* --windowHours=2
|
|
21
|
+
* --stepHours=1
|
|
22
|
+
* --anchorEnd=2026-02-04T12:00:00.000Z
|
|
23
|
+
* --dataGroup=production_window
|
|
24
|
+
* --delayMs=0
|
|
25
|
+
* --pause=true
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { randomUUID } from "node:crypto";
|
|
29
|
+
import * as readline from "node:readline/promises";
|
|
30
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
31
|
+
import { ConfigFile, logger } from "@uns-kit/core";
|
|
32
|
+
import UnsMqttProxy from "@uns-kit/core/uns-mqtt/uns-mqtt-proxy.js";
|
|
33
|
+
import { registerAttributeDescriptions, registerObjectTypeDescriptions } from "@uns-kit/core/uns/uns-dictionary-registry.js";
|
|
34
|
+
import { UnsPacket } from "@uns-kit/core/uns/uns-packet.js";
|
|
35
|
+
import { UnsTopics } from "@uns-kit/core/uns/uns-topics.js";
|
|
36
|
+
import type { ISO8601, IUnsMessage, IUnsTableColumn } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
37
|
+
import {
|
|
38
|
+
GeneratedAttributes,
|
|
39
|
+
GeneratedAttributeDescriptions,
|
|
40
|
+
GeneratedObjectTypes,
|
|
41
|
+
GeneratedObjectTypeDescriptions,
|
|
42
|
+
} from "../uns/uns-dictionary.generated.js";
|
|
43
|
+
import { resolveGeneratedAsset } from "../uns/uns-assets.js";
|
|
44
|
+
import { GeneratedPhysicalMeasurements } from "../uns/uns-measurements.generated.js";
|
|
45
|
+
|
|
46
|
+
type RunSelection = "all" | "1" | "2" | "3";
|
|
47
|
+
type IngestMode = "append" | "dedup" | "window_replace";
|
|
48
|
+
|
|
49
|
+
type CliArgs = {
|
|
50
|
+
run: RunSelection;
|
|
51
|
+
mode: IngestMode;
|
|
52
|
+
bucketMinutes: number;
|
|
53
|
+
windowHours: number;
|
|
54
|
+
stepHours: number;
|
|
55
|
+
anchorEnd: Date;
|
|
56
|
+
dataGroup: string;
|
|
57
|
+
delayMs: number;
|
|
58
|
+
pause: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const DEFAULTS: Omit<CliArgs, "anchorEnd"> & { anchorEnd?: Date } = {
|
|
62
|
+
run: "all",
|
|
63
|
+
mode: "window_replace",
|
|
64
|
+
bucketMinutes: 30,
|
|
65
|
+
windowHours: 2,
|
|
66
|
+
stepHours: 1,
|
|
67
|
+
dataGroup: "production_window",
|
|
68
|
+
delayMs: 0,
|
|
69
|
+
pause: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function parseArgs(argv: string[]): Partial<CliArgs> {
|
|
73
|
+
const map = new Map<string, string>();
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
const token = argv[i];
|
|
76
|
+
if (!token?.startsWith("--")) continue;
|
|
77
|
+
const raw = token.slice(2);
|
|
78
|
+
const eqIdx = raw.indexOf("=");
|
|
79
|
+
if (eqIdx !== -1) {
|
|
80
|
+
map.set(raw.slice(0, eqIdx), raw.slice(eqIdx + 1));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const next = argv[i + 1];
|
|
84
|
+
if (next && !next.startsWith("--")) {
|
|
85
|
+
map.set(raw, next);
|
|
86
|
+
i++;
|
|
87
|
+
} else {
|
|
88
|
+
map.set(raw, "true");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const run = map.get("run") ?? map.get("iteration");
|
|
93
|
+
const mode = map.get("mode");
|
|
94
|
+
const bucketMinutes = map.get("bucketMinutes") ?? map.get("bucket-minutes");
|
|
95
|
+
const windowHours = map.get("windowHours") ?? map.get("window-hours");
|
|
96
|
+
const stepHours = map.get("stepHours") ?? map.get("step-hours");
|
|
97
|
+
const anchorEnd = map.get("anchorEnd") ?? map.get("anchor-end");
|
|
98
|
+
const dataGroup = map.get("dataGroup") ?? map.get("data-group");
|
|
99
|
+
const delayMs = map.get("delayMs") ?? map.get("delay-ms");
|
|
100
|
+
const pause = map.get("pause");
|
|
101
|
+
|
|
102
|
+
const parsed: Partial<CliArgs> = {};
|
|
103
|
+
if (run && (run === "all" || run === "1" || run === "2" || run === "3")) parsed.run = run;
|
|
104
|
+
if (mode && (mode === "append" || mode === "dedup" || mode === "window_replace")) parsed.mode = mode;
|
|
105
|
+
if (bucketMinutes) parsed.bucketMinutes = Number(bucketMinutes);
|
|
106
|
+
if (windowHours) parsed.windowHours = Number(windowHours);
|
|
107
|
+
if (stepHours) parsed.stepHours = Number(stepHours);
|
|
108
|
+
if (dataGroup) parsed.dataGroup = dataGroup;
|
|
109
|
+
if (delayMs) parsed.delayMs = Number(delayMs);
|
|
110
|
+
if (pause !== undefined) {
|
|
111
|
+
const normalized = pause.trim().toLowerCase();
|
|
112
|
+
parsed.pause = !(normalized === "false" || normalized === "0" || normalized === "n" || normalized === "no");
|
|
113
|
+
}
|
|
114
|
+
if (anchorEnd) {
|
|
115
|
+
const d = new Date(anchorEnd);
|
|
116
|
+
if (!Number.isNaN(d.getTime())) parsed.anchorEnd = d;
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function floorToHourUtc(date: Date): Date {
|
|
122
|
+
const d = new Date(date);
|
|
123
|
+
d.setUTCMinutes(0, 0, 0);
|
|
124
|
+
return d;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sleep(ms: number): Promise<void> {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildIterations(anchorEnd: Date, windowHours: number, stepHours: number): Array<{ idx: 1 | 2 | 3; startMs: number; endMs: number }> {
|
|
132
|
+
const stepMs = stepHours * 60 * 60 * 1000;
|
|
133
|
+
const windowMs = windowHours * 60 * 60 * 1000;
|
|
134
|
+
const end3 = anchorEnd.getTime();
|
|
135
|
+
const end2 = end3 - stepMs;
|
|
136
|
+
const end1 = end3 - 2 * stepMs;
|
|
137
|
+
return [
|
|
138
|
+
{ idx: 1, startMs: end1 - windowMs, endMs: end1 },
|
|
139
|
+
{ idx: 2, startMs: end2 - windowMs, endMs: end2 },
|
|
140
|
+
{ idx: 3, startMs: end3 - windowMs, endMs: end3 },
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildColumns(producedKg: number, scrapKg: number): IUnsTableColumn[] {
|
|
145
|
+
return [
|
|
146
|
+
{ name: "producedKg", type: "double", value: producedKg, uom: GeneratedPhysicalMeasurements.Kilogram },
|
|
147
|
+
{ name: "scrapKg", type: "double", value: scrapKg, uom: GeneratedPhysicalMeasurements.Kilogram },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function simulateProducedKg(bucketIndex: number, iteration: number): number {
|
|
152
|
+
const wave = Math.sin(bucketIndex / 10) * 3 + Math.sin(bucketIndex / 3) * 0.5;
|
|
153
|
+
const value = 100 + (bucketIndex % 50) * 0.2 + wave + iteration * 0.25;
|
|
154
|
+
return Number(value.toFixed(3));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function simulateScrapKg(producedKg: number, bucketIndex: number, iteration: number): number {
|
|
158
|
+
const ratio = 0.02 + (Math.sin(bucketIndex / 17) + 1) * 0.003;
|
|
159
|
+
const value = producedKg * ratio + iteration * 0.01;
|
|
160
|
+
return Number(value.toFixed(3));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isMissingBucket(iteration: 1 | 2 | 3, bucketStartMs: number, windowStartMs: number, windowEndMs: number, bucketMs: number): boolean {
|
|
164
|
+
const oneHourMs = 60 * 60 * 1000;
|
|
165
|
+
const bucketIndex = Math.floor(bucketStartMs / bucketMs);
|
|
166
|
+
const inLastHour = bucketStartMs >= windowEndMs - oneHourMs;
|
|
167
|
+
const inFirstHour = bucketStartMs < windowStartMs + oneHourMs;
|
|
168
|
+
const missingEveryIter1 = 3;
|
|
169
|
+
const missingEveryIter2 = 4;
|
|
170
|
+
|
|
171
|
+
if (iteration === 1) return inLastHour && bucketIndex % missingEveryIter1 === 0;
|
|
172
|
+
if (iteration === 2) return inLastHour && bucketIndex % missingEveryIter2 === 0;
|
|
173
|
+
return inFirstHour && bucketIndex % missingEveryIter2 === 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function main() {
|
|
177
|
+
const args = { ...DEFAULTS, ...parseArgs(process.argv.slice(2)) } as Omit<CliArgs, "anchorEnd"> & Partial<Pick<CliArgs, "anchorEnd">>;
|
|
178
|
+
const anchorEnd = args.anchorEnd ?? floorToHourUtc(new Date());
|
|
179
|
+
const cli: CliArgs = { ...args, anchorEnd };
|
|
180
|
+
|
|
181
|
+
if (!Number.isFinite(cli.bucketMinutes) || cli.bucketMinutes <= 0) {
|
|
182
|
+
throw new Error(`Invalid --bucketMinutes=${String(cli.bucketMinutes)}`);
|
|
183
|
+
}
|
|
184
|
+
if (!Number.isFinite(cli.windowHours) || cli.windowHours <= 0) {
|
|
185
|
+
throw new Error(`Invalid --windowHours=${String(cli.windowHours)}`);
|
|
186
|
+
}
|
|
187
|
+
if (!Number.isFinite(cli.stepHours) || cli.stepHours <= 0) {
|
|
188
|
+
throw new Error(`Invalid --stepHours=${String(cli.stepHours)}`);
|
|
189
|
+
}
|
|
190
|
+
if (!Number.isFinite(cli.delayMs) || cli.delayMs < 0) {
|
|
191
|
+
throw new Error(`Invalid --delayMs=${String(cli.delayMs)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const config = await ConfigFile.loadConfig();
|
|
195
|
+
registerObjectTypeDescriptions(GeneratedObjectTypeDescriptions);
|
|
196
|
+
registerAttributeDescriptions(GeneratedAttributeDescriptions);
|
|
197
|
+
|
|
198
|
+
const topic: UnsTopics = "enterprise/site/area/line/";
|
|
199
|
+
const asset = resolveGeneratedAsset("asset");
|
|
200
|
+
const assetDescription = "Load test asset";
|
|
201
|
+
const objectType = GeneratedObjectTypes["process-segment"];
|
|
202
|
+
const objectId = "main";
|
|
203
|
+
const attribute = GeneratedAttributes["output-quantity"];
|
|
204
|
+
|
|
205
|
+
const outputHost = config.output?.host!;
|
|
206
|
+
const shouldPause = cli.pause && !!process.stdin.isTTY;
|
|
207
|
+
|
|
208
|
+
const rl = readline.createInterface({ input, output });
|
|
209
|
+
let mqttOutput: UnsMqttProxy | undefined;
|
|
210
|
+
try {
|
|
211
|
+
const proceed =
|
|
212
|
+
!process.stdin.isTTY
|
|
213
|
+
? "y"
|
|
214
|
+
: await rl.question(`Run moving-window table load test on output broker ${outputHost}? (Y/n) `);
|
|
215
|
+
if (proceed.trim() && proceed.trim().toLowerCase() !== "y") {
|
|
216
|
+
logger.info("Aborted.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const processName = config.uns.processName!;
|
|
221
|
+
const instanceName = "tableWindowLoadTest";
|
|
222
|
+
const clientIdBase = config.output?.clientId ?? `${processName}-${instanceName}`;
|
|
223
|
+
const clientId = `${clientIdBase}-${randomUUID()}`;
|
|
224
|
+
mqttOutput = new UnsMqttProxy(
|
|
225
|
+
outputHost,
|
|
226
|
+
processName,
|
|
227
|
+
instanceName,
|
|
228
|
+
{ publishThrottlingDelay: 0, clientId, defaultPublishOptions: { qos: 1 } },
|
|
229
|
+
true,
|
|
230
|
+
);
|
|
231
|
+
mqttOutput.event.on("unsProxyProducedTopics", (event) => {
|
|
232
|
+
void mqttOutput?.publishMessage(event.statusTopic, JSON.stringify(event.producedTopics), {
|
|
233
|
+
retain: true,
|
|
234
|
+
properties: { messageExpiryInterval: 120000 },
|
|
235
|
+
}).catch(() => undefined);
|
|
236
|
+
});
|
|
237
|
+
mqttOutput.event.on("mqttProxyStatus", async (event) => {
|
|
238
|
+
try {
|
|
239
|
+
const time = UnsPacket.formatToISO8601(new Date());
|
|
240
|
+
const unsMessage: IUnsMessage = { data: { time, value: event.value, uom: event.uom } };
|
|
241
|
+
const packet = await UnsPacket.unsPacketFromUnsMessage(unsMessage);
|
|
242
|
+
await mqttOutput?.publishMessage(event.statusTopic, JSON.stringify(packet));
|
|
243
|
+
} catch {
|
|
244
|
+
// ignore status publish errors in load tests
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
// Give the output proxy a moment to connect before publishing.
|
|
248
|
+
// (Same approach as `load-test-data.ts`.)
|
|
249
|
+
await sleep(1000);
|
|
250
|
+
|
|
251
|
+
const bucketMs = cli.bucketMinutes * 60 * 1000;
|
|
252
|
+
const iterations = buildIterations(cli.anchorEnd, cli.windowHours, cli.stepHours);
|
|
253
|
+
|
|
254
|
+
const selected = cli.run === "all" ? new Set([1, 2, 3]) : new Set([Number(cli.run)]);
|
|
255
|
+
const selectedIterations = iterations.filter((it) => selected.has(it.idx));
|
|
256
|
+
const firstIteration = selectedIterations[0]?.idx ?? 1;
|
|
257
|
+
logger.info(
|
|
258
|
+
`Publishing iterations=${Array.from(selected).join(",")} mode=${cli.mode} bucket=${cli.bucketMinutes}min window=${cli.windowHours}h step=${cli.stepHours}h anchorEnd=${cli.anchorEnd.toISOString()} dataGroup=${cli.dataGroup} pause=${shouldPause}`,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
for (const it of iterations) {
|
|
262
|
+
if (!selected.has(it.idx)) continue;
|
|
263
|
+
|
|
264
|
+
const windowStartIso = UnsPacket.formatToISO8601(new Date(it.startMs)) as ISO8601;
|
|
265
|
+
const windowEndIso = UnsPacket.formatToISO8601(new Date(it.endMs)) as ISO8601;
|
|
266
|
+
|
|
267
|
+
if (shouldPause && it.idx !== firstIteration) {
|
|
268
|
+
await rl.question(
|
|
269
|
+
`[pause] Inspect QuestDB now (after previous iteration). Press Enter to run iteration ${it.idx} with window [${windowStartIso}, ${windowEndIso})...`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const attrs: Array<{
|
|
274
|
+
attribute: typeof attribute;
|
|
275
|
+
table: {
|
|
276
|
+
dataGroup: string;
|
|
277
|
+
time: ISO8601;
|
|
278
|
+
intervalStart: ISO8601;
|
|
279
|
+
intervalEnd: ISO8601;
|
|
280
|
+
windowStart: ISO8601;
|
|
281
|
+
windowEnd: ISO8601;
|
|
282
|
+
deleted?: boolean;
|
|
283
|
+
columns: IUnsTableColumn[];
|
|
284
|
+
};
|
|
285
|
+
}> = [];
|
|
286
|
+
|
|
287
|
+
let published = 0;
|
|
288
|
+
let skipped = 0;
|
|
289
|
+
let tombstones = 0;
|
|
290
|
+
|
|
291
|
+
for (let startMs = it.startMs; startMs < it.endMs; startMs += bucketMs) {
|
|
292
|
+
const endMs = startMs + bucketMs;
|
|
293
|
+
const bucketStartIso = UnsPacket.formatToISO8601(new Date(startMs)) as ISO8601;
|
|
294
|
+
const bucketEndIso = UnsPacket.formatToISO8601(new Date(endMs)) as ISO8601;
|
|
295
|
+
|
|
296
|
+
const missing = isMissingBucket(it.idx, startMs, it.startMs, it.endMs, bucketMs);
|
|
297
|
+
if (missing) {
|
|
298
|
+
if (it.idx === 3 && cli.mode === "dedup") {
|
|
299
|
+
const bucketIndex = Math.floor(startMs / bucketMs);
|
|
300
|
+
const producedKg = simulateProducedKg(bucketIndex, it.idx);
|
|
301
|
+
const scrapKg = simulateScrapKg(producedKg, bucketIndex, it.idx);
|
|
302
|
+
attrs.push({
|
|
303
|
+
attribute,
|
|
304
|
+
table: {
|
|
305
|
+
dataGroup: cli.dataGroup,
|
|
306
|
+
time: bucketStartIso,
|
|
307
|
+
intervalStart: bucketStartIso,
|
|
308
|
+
intervalEnd: bucketEndIso,
|
|
309
|
+
windowStart: windowStartIso,
|
|
310
|
+
windowEnd: windowEndIso,
|
|
311
|
+
deleted: true,
|
|
312
|
+
columns: buildColumns(producedKg, scrapKg),
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
tombstones++;
|
|
316
|
+
published++;
|
|
317
|
+
} else {
|
|
318
|
+
skipped++;
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const bucketIndex = Math.floor(startMs / bucketMs);
|
|
324
|
+
const producedKg = simulateProducedKg(bucketIndex, it.idx);
|
|
325
|
+
const scrapKg = simulateScrapKg(producedKg, bucketIndex, it.idx);
|
|
326
|
+
attrs.push({
|
|
327
|
+
attribute,
|
|
328
|
+
table: {
|
|
329
|
+
dataGroup: cli.dataGroup,
|
|
330
|
+
time: bucketStartIso,
|
|
331
|
+
intervalStart: bucketStartIso,
|
|
332
|
+
intervalEnd: bucketEndIso,
|
|
333
|
+
windowStart: windowStartIso,
|
|
334
|
+
windowEnd: windowEndIso,
|
|
335
|
+
columns: buildColumns(producedKg, scrapKg),
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
published++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger.info(
|
|
342
|
+
`Iteration ${it.idx}: window=[${windowStartIso}, ${windowEndIso}) publish=${published} skipped=${skipped} tombstones=${tombstones}`,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
await mqttOutput.publishMqttMessage(
|
|
346
|
+
{
|
|
347
|
+
topic,
|
|
348
|
+
asset,
|
|
349
|
+
assetDescription,
|
|
350
|
+
objectType,
|
|
351
|
+
objectId,
|
|
352
|
+
attributes: attrs,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (cli.delayMs > 0) {
|
|
357
|
+
await sleep(cli.delayMs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Give the worker queue a moment to publish before exiting.
|
|
362
|
+
await sleep(500);
|
|
363
|
+
} finally {
|
|
364
|
+
rl.close();
|
|
365
|
+
await mqttOutput?.stop();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
main().catch((error) => {
|
|
370
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
371
|
+
logger.error(`Table window load test failed: ${reason.message}`);
|
|
372
|
+
process.exitCode = 1;
|
|
373
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Python bytecode
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
ENV/
|
|
11
|
+
.venv*/
|
|
12
|
+
|
|
13
|
+
# Packaging / build artifacts
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
.eggs/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
*.egg
|
|
19
|
+
pip-wheel-metadata/
|
|
20
|
+
.wheels/
|
|
21
|
+
|
|
22
|
+
# Tests / coverage
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.coverage*
|
|
25
|
+
htmlcov/
|
|
26
|
+
.hypothesis/
|
|
27
|
+
|
|
28
|
+
# Tool caches
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
|
|
32
|
+
# Logs
|
|
33
|
+
*.log
|
|
34
|
+
|
|
35
|
+
# Editor / OS
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
*.swp
|
|
41
|
+
|
|
42
|
+
# Local configuration
|
|
43
|
+
config.json
|
|
44
|
+
config-*.json
|
|
45
|
+
|
|
46
|
+
# Temp
|
|
47
|
+
/tmp/
|