@uns-kit/cli 2.0.13 → 2.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uns-kit/cli",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
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.13"
29
+ "@uns-kit/core": "2.0.15"
30
30
  },
31
31
  "unsKitPackages": {
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"
32
+ "@uns-kit/core": "2.0.15",
33
+ "@uns-kit/api": "2.0.14",
34
+ "@uns-kit/cron": "2.0.15",
35
+ "@uns-kit/temporal": "2.0.15"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -p tsconfig.build.json",
@@ -14,7 +14,7 @@
14
14
  * pnpm exec tsx src/examples/table-window-load-test.ts
15
15
  *
16
16
  * Optional args:
17
- * --run=all|1|2|3
17
+ * --run=all|1|2|3|4
18
18
  * --mode=window_replace|dedup|append
19
19
  * --bucketMinutes=30
20
20
  * --windowHours=2
@@ -43,8 +43,9 @@ import {
43
43
  import { resolveGeneratedAsset } from "../uns/uns-assets.js";
44
44
  import { GeneratedPhysicalMeasurements } from "../uns/uns-measurements.generated.js";
45
45
 
46
- type RunSelection = "all" | "1" | "2" | "3";
46
+ type RunSelection = "all" | "1" | "2" | "3" | "4";
47
47
  type IngestMode = "append" | "dedup" | "window_replace";
48
+ type IterationIndex = 1 | 2 | 3 | 4;
48
49
 
49
50
  type CliArgs = {
50
51
  run: RunSelection;
@@ -100,7 +101,7 @@ function parseArgs(argv: string[]): Partial<CliArgs> {
100
101
  const pause = map.get("pause");
101
102
 
102
103
  const parsed: Partial<CliArgs> = {};
103
- if (run && (run === "all" || run === "1" || run === "2" || run === "3")) parsed.run = run;
104
+ if (run && (run === "all" || run === "1" || run === "2" || run === "3" || run === "4")) parsed.run = run;
104
105
  if (mode && (mode === "append" || mode === "dedup" || mode === "window_replace")) parsed.mode = mode;
105
106
  if (bucketMinutes) parsed.bucketMinutes = Number(bucketMinutes);
106
107
  if (windowHours) parsed.windowHours = Number(windowHours);
@@ -128,16 +129,24 @@ function sleep(ms: number): Promise<void> {
128
129
  return new Promise((resolve) => setTimeout(resolve, ms));
129
130
  }
130
131
 
131
- function buildIterations(anchorEnd: Date, windowHours: number, stepHours: number): Array<{ idx: 1 | 2 | 3; startMs: number; endMs: number }> {
132
+ function buildIterations(
133
+ anchorEnd: Date,
134
+ windowHours: number,
135
+ stepHours: number,
136
+ ): Array<{ idx: IterationIndex; startMs: number; endMs: number }> {
132
137
  const stepMs = stepHours * 60 * 60 * 1000;
133
138
  const windowMs = windowHours * 60 * 60 * 1000;
134
139
  const end3 = anchorEnd.getTime();
135
140
  const end2 = end3 - stepMs;
136
141
  const end1 = end3 - 2 * stepMs;
142
+ const start1 = end1 - windowMs;
137
143
  return [
138
- { idx: 1, startMs: end1 - windowMs, endMs: end1 },
144
+ { idx: 1, startMs: start1, endMs: end1 },
139
145
  { idx: 2, startMs: end2 - windowMs, endMs: end2 },
140
146
  { idx: 3, startMs: end3 - windowMs, endMs: end3 },
147
+ // Iteration 4: refresh the full timeframe from the earliest bucket of iteration 1
148
+ // to the latest bucket of iteration 3, with a simulated delete of one bucket.
149
+ { idx: 4, startMs: start1, endMs: end3 },
141
150
  ];
142
151
  }
143
152
 
@@ -160,7 +169,13 @@ function simulateScrapKg(producedKg: number, bucketIndex: number, iteration: num
160
169
  return Number(value.toFixed(3));
161
170
  }
162
171
 
163
- function isMissingBucket(iteration: 1 | 2 | 3, bucketStartMs: number, windowStartMs: number, windowEndMs: number, bucketMs: number): boolean {
172
+ function isMissingBucket(
173
+ iteration: Exclude<IterationIndex, 4>,
174
+ bucketStartMs: number,
175
+ windowStartMs: number,
176
+ windowEndMs: number,
177
+ bucketMs: number,
178
+ ): boolean {
164
179
  const oneHourMs = 60 * 60 * 1000;
165
180
  const bucketIndex = Math.floor(bucketStartMs / bucketMs);
166
181
  const inLastHour = bucketStartMs >= windowEndMs - oneHourMs;
@@ -251,18 +266,40 @@ async function main() {
251
266
  const bucketMs = cli.bucketMinutes * 60 * 1000;
252
267
  const iterations = buildIterations(cli.anchorEnd, cli.windowHours, cli.stepHours);
253
268
 
254
- const selected = cli.run === "all" ? new Set([1, 2, 3]) : new Set([Number(cli.run)]);
269
+ const selected = cli.run === "all" ? new Set<IterationIndex>([1, 2, 3, 4]) : new Set<IterationIndex>([Number(cli.run) as IterationIndex]);
255
270
  const selectedIterations = iterations.filter((it) => selected.has(it.idx));
256
271
  const firstIteration = selectedIterations[0]?.idx ?? 1;
257
272
  logger.info(
258
273
  `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
274
  );
275
+ if (cli.mode === "window_replace") {
276
+ logger.info(
277
+ "NOTE: mode=window_replace marks missing buckets in [windowStart, windowEnd) as deleted=true (soft delete) and refreshes published buckets (deleted=false).",
278
+ );
279
+ } else if (cli.mode === "dedup") {
280
+ logger.info(
281
+ "NOTE: mode=dedup upserts by keys; missing buckets are NOT removed unless you publish tombstones (deleted=true) for those buckets.",
282
+ );
283
+ }
284
+
285
+ const fullRefresh = iterations.find((it) => it.idx === 4);
286
+ const deleteBucketStartMs =
287
+ fullRefresh ? fullRefresh.startMs + bucketMs : iterations[0]!.startMs + bucketMs;
260
288
 
261
289
  for (const it of iterations) {
262
290
  if (!selected.has(it.idx)) continue;
263
291
 
264
292
  const windowStartIso = UnsPacket.formatToISO8601(new Date(it.startMs)) as ISO8601;
265
293
  const windowEndIso = UnsPacket.formatToISO8601(new Date(it.endMs)) as ISO8601;
294
+ if (it.idx === 4) {
295
+ const deleteBucketIso = UnsPacket.formatToISO8601(new Date(deleteBucketStartMs)) as ISO8601;
296
+ logger.info(
297
+ `Iteration 4 plan: refresh full timeframe and simulate one deleted bucket at time='${deleteBucketIso}'. ` +
298
+ (cli.mode === "window_replace"
299
+ ? "Delete is simulated by OMITTING the bucket from the snapshot (archiver soft-deletes it: deleted=true)."
300
+ : "Delete is simulated by publishing a tombstone (deleted=true) for that bucket."),
301
+ );
302
+ }
266
303
 
267
304
  if (shouldPause && it.idx !== firstIteration) {
268
305
  await rl.question(
@@ -287,13 +324,47 @@ async function main() {
287
324
  let published = 0;
288
325
  let skipped = 0;
289
326
  let tombstones = 0;
327
+ let simulatedDeletes = 0;
290
328
 
291
329
  for (let startMs = it.startMs; startMs < it.endMs; startMs += bucketMs) {
292
330
  const endMs = startMs + bucketMs;
293
331
  const bucketStartIso = UnsPacket.formatToISO8601(new Date(startMs)) as ISO8601;
294
332
  const bucketEndIso = UnsPacket.formatToISO8601(new Date(endMs)) as ISO8601;
295
333
 
296
- const missing = isMissingBucket(it.idx, startMs, it.startMs, it.endMs, bucketMs);
334
+ const isIteration4 = it.idx === 4;
335
+ const missing = !isIteration4 && isMissingBucket(it.idx as Exclude<IterationIndex, 4>, startMs, it.startMs, it.endMs, bucketMs);
336
+
337
+ if (isIteration4 && startMs === deleteBucketStartMs) {
338
+ // Simulate delete of one bucket.
339
+ if (cli.mode === "window_replace") {
340
+ // For window_replace, omit the bucket from the snapshot (it disappears after window delete).
341
+ simulatedDeletes++;
342
+ skipped++;
343
+ continue;
344
+ }
345
+
346
+ const bucketIndex = Math.floor(startMs / bucketMs);
347
+ const producedKg = simulateProducedKg(bucketIndex, it.idx);
348
+ const scrapKg = simulateScrapKg(producedKg, bucketIndex, it.idx);
349
+ attrs.push({
350
+ attribute,
351
+ table: {
352
+ dataGroup: cli.dataGroup,
353
+ time: bucketStartIso,
354
+ intervalStart: bucketStartIso,
355
+ intervalEnd: bucketEndIso,
356
+ windowStart: windowStartIso,
357
+ windowEnd: windowEndIso,
358
+ deleted: true,
359
+ columns: buildColumns(producedKg, scrapKg),
360
+ },
361
+ });
362
+ simulatedDeletes++;
363
+ tombstones++;
364
+ published++;
365
+ continue;
366
+ }
367
+
297
368
  if (missing) {
298
369
  if (it.idx === 3 && cli.mode === "dedup") {
299
370
  const bucketIndex = Math.floor(startMs / bucketMs);
@@ -339,7 +410,7 @@ async function main() {
339
410
  }
340
411
 
341
412
  logger.info(
342
- `Iteration ${it.idx}: window=[${windowStartIso}, ${windowEndIso}) publish=${published} skipped=${skipped} tombstones=${tombstones}`,
413
+ `Iteration ${it.idx}: window=[${windowStartIso}, ${windowEndIso}) publish=${published} skipped=${skipped} tombstones=${tombstones} deletes=${simulatedDeletes}`,
343
414
  );
344
415
 
345
416
  await mqttOutput.publishMqttMessage(