@mxtommy/kip 4.5.1 → 4.5.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/package.json +13 -13
  3. package/plugin/duckdb-parquet-storage.service.js +179 -60
  4. package/plugin/index.js +82 -74
  5. package/public/{chunk-D7VDX7ZF.js → chunk-67V4XHCY.js} +1 -1
  6. package/public/chunk-BTFZS2TW.js +16 -0
  7. package/public/{chunk-JGGMFMY5.js → chunk-CD5TQSCS.js} +1 -1
  8. package/public/chunk-FZFDGAQO.js +1 -0
  9. package/public/{chunk-IYRLINL7.js → chunk-I4SJ5UNN.js} +1 -1
  10. package/public/{chunk-VCY32MWT.js → chunk-IH4CEW4C.js} +7 -7
  11. package/public/chunk-ISF5E3CX.js +50 -0
  12. package/public/{chunk-EQ2N7KDA.js → chunk-KQEEYPK3.js} +2 -2
  13. package/public/chunk-NFJ4RQSE.js +4 -0
  14. package/public/{chunk-DEM56G4S.js → chunk-OPTBDYBL.js} +1 -1
  15. package/public/{chunk-YCEXTKGG.js → chunk-P4CRTB7N.js} +1 -1
  16. package/public/{chunk-IHURI4IH.js → chunk-P7JKENHI.js} +3 -3
  17. package/public/chunk-Q2ANAJAD.js +1 -0
  18. package/public/{chunk-B75MT7ND.js → chunk-R36UY4Q4.js} +1 -1
  19. package/public/{chunk-CHGXAEKT.js → chunk-RCYOZLZB.js} +1 -1
  20. package/public/{chunk-KPHICV76.js → chunk-SJFJEOSG.js} +1 -1
  21. package/public/{chunk-MGPPVLZ7.js → chunk-TBNKOU7M.js} +1 -1
  22. package/public/chunk-TVNXBPFF.js +6 -0
  23. package/public/{chunk-S72JTJPN.js → chunk-VPF5756E.js} +1 -1
  24. package/public/chunk-VXCYPAWR.js +1 -0
  25. package/public/{chunk-RONXIZ2U.js → chunk-VXTTEFRP.js} +3 -3
  26. package/public/{chunk-R7RQHWKJ.js → chunk-WH5CIUSB.js} +1 -1
  27. package/public/{chunk-LQDSU4WS.js → chunk-WQSJFJLW.js} +1 -1
  28. package/public/{chunk-KZ5DUKAX.js → chunk-XBSU7OGT.js} +1 -1
  29. package/public/{chunk-CEB42O2C.js → chunk-YI3MZWRZ.js} +1 -1
  30. package/public/index.html +1 -1
  31. package/public/main-B6TXB3EB.js +1 -0
  32. package/public/chunk-A6DQJFP4.js +0 -16
  33. package/public/chunk-DEGYRCMI.js +0 -1
  34. package/public/chunk-DYTBBUMI.js +0 -4
  35. package/public/chunk-FNF7M3AE.js +0 -1
  36. package/public/chunk-JB4YVVNW.js +0 -1
  37. package/public/chunk-YKJKIWXO.js +0 -6
  38. package/public/chunk-ZV7IYYEQ.js +0 -50
  39. package/public/main-FQESQQV6.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # v4.5.2
2
+ ## Fixes
3
+ * DuckDB initialized when features are not enabled.
4
+ * Parquet data compression and pruning not executing.
1
5
  # v4.5.1
2
6
  ## Fixes
3
7
  * DuckDB dependency causing build and installation errors. Fixes #979
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -66,12 +66,22 @@
66
66
  },
67
67
  "schematics": "tools/schematics/collection.json",
68
68
  "devDependencies": {
69
+ "@angular/cdk": "21.2.0",
70
+ "@angular/common": "21.2.0",
71
+ "@angular/compiler": "21.2.0",
72
+ "@angular/core": "21.2.0",
73
+ "@angular/forms": "21.2.0",
74
+ "@angular/material": "21.2.0",
75
+ "@angular/animations": "21.2.0",
76
+ "@angular/platform-browser": "21.2.0",
77
+ "@angular/platform-browser-dynamic": "21.2.0",
78
+ "@angular/router": "21.2.0",
69
79
  "@angular-devkit/build-angular": "^21.1.4",
70
80
  "@angular-devkit/schematics-cli": "^20.1.6",
71
81
  "@angular/build": "^21.1.4",
72
82
  "@angular/cli": "^21.1.4",
73
- "@angular/compiler-cli": "21.1.4",
74
- "@angular/language-service": "21.1.4",
83
+ "@angular/compiler-cli": "21.2.0",
84
+ "@angular/language-service": "21.2.0",
75
85
  "@types/canvas-gauges": "^2.1.8",
76
86
  "@types/d3": "^7.4.3",
77
87
  "@types/jasmine": "~3.6.0",
@@ -97,16 +107,6 @@
97
107
  "sass": "^1.49.9",
98
108
  "ts-node": "^10.9.2",
99
109
  "typescript": "^5.9.3",
100
- "@angular/animations": "21.1.4",
101
- "@angular/cdk": "21.1.4",
102
- "@angular/common": "21.1.4",
103
- "@angular/compiler": "21.1.4",
104
- "@angular/core": "21.1.4",
105
- "@angular/forms": "21.1.4",
106
- "@angular/material": "21.1.4",
107
- "@angular/platform-browser": "21.1.4",
108
- "@angular/platform-browser-dynamic": "21.1.4",
109
- "@angular/router": "21.1.4",
110
110
  "@aziham/chartjs-plugin-streaming": "^3.5.1",
111
111
  "@godind/ng-canvas-gauges": "^6.2.1",
112
112
  "@zakj/no-sleep": "^0.13.5",
@@ -8,13 +8,16 @@ const parquetjs_1 = require("@dsnp/parquetjs");
8
8
  /**
9
9
  * Provides DuckDB storage and Parquet flush support for captured history samples.
10
10
  */
11
+ const DEFAULT_STORAGE_CONFIG = {
12
+ engine: 'duckdb-parquet',
13
+ databaseFile: 'plugin-config-data/kip/historicalData/kip-history.duckdb',
14
+ parquetDirectory: 'plugin-config-data/kip/historicalData/parquet',
15
+ flushIntervalMs: 30_000,
16
+ parquetWindowMs: 60 * 60_000,
17
+ parquetCompression: 'snappy'
18
+ };
11
19
  class DuckDbParquetStorageService {
12
- config = {
13
- engine: 'duckdb-parquet',
14
- databaseFile: 'plugin-config-data/kip/historicalData/kip-history.duckdb',
15
- parquetDirectory: 'plugin-config-data/kip/historicalData/parquet',
16
- flushIntervalMs: 30_000
17
- };
20
+ config = { ...DEFAULT_STORAGE_CONFIG };
18
21
  // 8 hour job interval (8 hours)
19
22
  static EIGHT_HOURS_INTERVAL = 8 * 60 * 60 * 1000;
20
23
  vacuumJob = null;
@@ -61,12 +64,7 @@ class DuckDbParquetStorageService {
61
64
  */
62
65
  configure() {
63
66
  this.initialized = false;
64
- this.config = {
65
- engine: 'duckdb-parquet',
66
- databaseFile: 'plugin-config-data/kip/historicalData/kip-history.duckdb',
67
- parquetDirectory: 'plugin-config-data/kip/historicalData/parquet',
68
- flushIntervalMs: 30_000
69
- };
67
+ this.config = { ...DEFAULT_STORAGE_CONFIG };
70
68
  return this.config;
71
69
  }
72
70
  /**
@@ -165,7 +163,11 @@ class DuckDbParquetStorageService {
165
163
  this.logger.debug('[SERIES STORAGE] Running scheduled prune of expired and orphaned samples');
166
164
  const expired = await this.pruneExpiredSamples(Date.now(), this.lifecycleToken);
167
165
  const orphaned = await this.pruneOrphanedSamples(this.lifecycleToken);
166
+ const parquetRemoved = await this.pruneParquetFilesByRetention(Date.now());
168
167
  this.logger.debug(`[SERIES STORAGE] Pruned ${expired} expired and ${orphaned} orphaned samples`);
168
+ if (parquetRemoved > 0) {
169
+ this.logger.debug(`[SERIES STORAGE] Pruned ${parquetRemoved} parquet files/directories`);
170
+ }
169
171
  });
170
172
  }
171
173
  catch (err) {
@@ -526,6 +528,7 @@ class DuckDbParquetStorageService {
526
528
  return;
527
529
  }
528
530
  await this.runSql(`DELETE FROM history_series WHERE series_id = ${this.escape(seriesId)}`);
531
+ this.deleteParquetSeriesDir(seriesId);
529
532
  }
530
533
  /**
531
534
  * Replaces persisted series definitions with desired set.
@@ -838,64 +841,180 @@ class DuckDbParquetStorageService {
838
841
  await this.runSql(sql);
839
842
  }
840
843
  async exportSeriesRange(seriesId, fromMs, toMs) {
844
+ const windowMs = this.resolveParquetWindowMs();
841
845
  const baseDir = (0, path_1.resolve)(this.config.parquetDirectory);
842
846
  const seriesDir = (0, path_1.join)(baseDir, this.safePath(seriesId));
843
847
  (0, fs_1.mkdirSync)(seriesDir, { recursive: true });
844
- const filePath = (0, path_1.join)(seriesDir, `${fromMs}-${toMs}.parquet`);
845
- const rows = await this.querySql(`
846
- SELECT
847
- series_id,
848
- dataset_uuid,
849
- owner_widget_uuid,
850
- path,
851
- context,
852
- source,
853
- ts_ms,
854
- value
855
- FROM history_samples
856
- WHERE series_id = ${this.escape(seriesId)}
857
- AND ts_ms >= ${Math.trunc(fromMs)}
858
- AND ts_ms <= ${Math.trunc(toMs)}
859
- ORDER BY ts_ms
860
- `);
861
- if (rows.length === 0) {
862
- return;
848
+ const startWindow = Math.floor(fromMs / windowMs) * windowMs;
849
+ const endWindow = Math.floor(toMs / windowMs) * windowMs;
850
+ for (let windowStart = startWindow; windowStart <= endWindow; windowStart += windowMs) {
851
+ const windowEnd = windowStart + windowMs - 1;
852
+ const filePath = (0, path_1.join)(seriesDir, `${windowStart}-${windowEnd}.parquet`);
853
+ const rows = await this.querySql(`
854
+ SELECT
855
+ series_id,
856
+ dataset_uuid,
857
+ owner_widget_uuid,
858
+ path,
859
+ context,
860
+ source,
861
+ ts_ms,
862
+ value
863
+ FROM history_samples
864
+ WHERE series_id = ${this.escape(seriesId)}
865
+ AND ts_ms >= ${Math.trunc(windowStart)}
866
+ AND ts_ms <= ${Math.trunc(windowEnd)}
867
+ ORDER BY ts_ms
868
+ `);
869
+ if (rows.length === 0) {
870
+ (0, fs_1.rmSync)(filePath, { force: true });
871
+ continue;
872
+ }
873
+ const compression = this.resolveParquetCompression();
874
+ const schema = new parquetjs_1.ParquetSchema({
875
+ series_id: { type: 'UTF8', compression },
876
+ dataset_uuid: { type: 'UTF8', compression },
877
+ owner_widget_uuid: { type: 'UTF8', compression },
878
+ path: { type: 'UTF8', compression },
879
+ context: { type: 'UTF8', compression },
880
+ source: { type: 'UTF8', optional: true, compression },
881
+ ts_ms: { type: 'INT64', compression },
882
+ ts: { type: 'TIMESTAMP_MILLIS', compression },
883
+ value: { type: 'DOUBLE', compression }
884
+ });
885
+ const tempPath = `${filePath}.tmp-${Date.now()}`;
886
+ const writer = await parquetjs_1.ParquetWriter.openFile(schema, tempPath);
887
+ try {
888
+ for (const row of rows) {
889
+ const timestampMs = this.toNumberOrUndefined(row.ts_ms);
890
+ const numericValue = this.toNumberOrUndefined(row.value);
891
+ if (timestampMs === undefined || numericValue === undefined) {
892
+ continue;
893
+ }
894
+ await writer.appendRow({
895
+ series_id: row.series_id,
896
+ dataset_uuid: row.dataset_uuid,
897
+ owner_widget_uuid: row.owner_widget_uuid,
898
+ path: row.path,
899
+ context: row.context,
900
+ source: row.source ?? undefined,
901
+ ts_ms: Math.trunc(timestampMs),
902
+ ts: new Date(timestampMs),
903
+ value: numericValue
904
+ });
905
+ }
906
+ }
907
+ finally {
908
+ await writer.close();
909
+ }
910
+ (0, fs_1.rmSync)(filePath, { force: true });
911
+ (0, fs_1.renameSync)(tempPath, filePath);
863
912
  }
864
- const schema = new parquetjs_1.ParquetSchema({
865
- series_id: { type: 'UTF8' },
866
- dataset_uuid: { type: 'UTF8' },
867
- owner_widget_uuid: { type: 'UTF8' },
868
- path: { type: 'UTF8' },
869
- context: { type: 'UTF8' },
870
- source: { type: 'UTF8', optional: true },
871
- ts_ms: { type: 'INT64' },
872
- ts: { type: 'TIMESTAMP_MILLIS' },
873
- value: { type: 'DOUBLE' }
874
- });
875
- const writer = await parquetjs_1.ParquetWriter.openFile(schema, filePath);
876
- try {
877
- for (const row of rows) {
878
- const timestampMs = this.toNumberOrUndefined(row.ts_ms);
879
- const numericValue = this.toNumberOrUndefined(row.value);
880
- if (timestampMs === undefined || numericValue === undefined) {
913
+ }
914
+ resolveParquetWindowMs() {
915
+ const parsed = Number(this.config.parquetWindowMs);
916
+ if (Number.isFinite(parsed) && parsed > 0) {
917
+ return Math.max(1, Math.trunc(parsed));
918
+ }
919
+ return 60 * 60_000;
920
+ }
921
+ resolveParquetCompression() {
922
+ const raw = String(this.config.parquetCompression ?? 'none').trim().toLowerCase();
923
+ if (raw === 'snappy') {
924
+ return 'SNAPPY';
925
+ }
926
+ if (raw === 'gzip') {
927
+ return 'GZIP';
928
+ }
929
+ if (raw === 'brotli') {
930
+ return 'BROTLI';
931
+ }
932
+ if (raw === 'lz4') {
933
+ return 'LZ4';
934
+ }
935
+ if (raw === 'lzo') {
936
+ return 'LZO';
937
+ }
938
+ return undefined;
939
+ }
940
+ pruneParquetFilesByRetention(nowMs) {
941
+ if (!this.isDuckDbParquetEnabled() || !this.connection) {
942
+ return Promise.resolve(0);
943
+ }
944
+ return this.querySql(`
945
+ SELECT series_id, retention_duration_ms
946
+ FROM history_series
947
+ `).then(rows => {
948
+ const baseDir = (0, path_1.resolve)(this.config.parquetDirectory);
949
+ const seriesConfig = new Map(rows.map(row => [this.safePath(row.series_id), row.retention_duration_ms]));
950
+ let removed = 0;
951
+ let seriesDirs = [];
952
+ try {
953
+ seriesDirs = (0, fs_1.readdirSync)(baseDir);
954
+ }
955
+ catch {
956
+ return 0;
957
+ }
958
+ for (const dirName of seriesDirs) {
959
+ const fullDir = (0, path_1.join)(baseDir, dirName);
960
+ let dirStat;
961
+ try {
962
+ dirStat = (0, fs_1.statSync)(fullDir);
963
+ }
964
+ catch {
965
+ continue;
966
+ }
967
+ if (!dirStat.isDirectory()) {
968
+ continue;
969
+ }
970
+ const seriesKey = dirName;
971
+ if (!seriesConfig.has(seriesKey)) {
972
+ (0, fs_1.rmSync)(fullDir, { recursive: true, force: true });
973
+ removed += 1;
881
974
  continue;
882
975
  }
883
- await writer.appendRow({
884
- series_id: row.series_id,
885
- dataset_uuid: row.dataset_uuid,
886
- owner_widget_uuid: row.owner_widget_uuid,
887
- path: row.path,
888
- context: row.context,
889
- source: row.source ?? undefined,
890
- ts_ms: Math.trunc(timestampMs),
891
- ts: new Date(timestampMs),
892
- value: numericValue
976
+ const retentionMs = seriesConfig.get(seriesKey);
977
+ if (!Number.isFinite(retentionMs) || (retentionMs ?? 0) <= 0) {
978
+ continue;
979
+ }
980
+ const cutoff = nowMs - Number(retentionMs);
981
+ let files = [];
982
+ try {
983
+ files = (0, fs_1.readdirSync)(fullDir);
984
+ }
985
+ catch {
986
+ continue;
987
+ }
988
+ files.forEach(fileName => {
989
+ const range = this.parseParquetRangeFromFileName(fileName);
990
+ if (!range) {
991
+ return;
992
+ }
993
+ if (range.toMs < cutoff) {
994
+ (0, fs_1.rmSync)((0, path_1.join)(fullDir, fileName), { force: true });
995
+ removed += 1;
996
+ }
893
997
  });
894
998
  }
999
+ return removed;
1000
+ });
1001
+ }
1002
+ parseParquetRangeFromFileName(fileName) {
1003
+ const match = /^([0-9]+)-([0-9]+)\.parquet$/.exec(fileName);
1004
+ if (!match) {
1005
+ return null;
895
1006
  }
896
- finally {
897
- await writer.close();
1007
+ const fromMs = Number(match[1]);
1008
+ const toMs = Number(match[2]);
1009
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) {
1010
+ return null;
898
1011
  }
1012
+ return { fromMs, toMs };
1013
+ }
1014
+ deleteParquetSeriesDir(seriesId) {
1015
+ const baseDir = (0, path_1.resolve)(this.config.parquetDirectory);
1016
+ const seriesDir = (0, path_1.join)(baseDir, this.safePath(seriesId));
1017
+ (0, fs_1.rmSync)(seriesDir, { recursive: true, force: true });
899
1018
  }
900
1019
  async runSql(sql) {
901
1020
  if (!this.connection) {
package/plugin/index.js CHANGED
@@ -583,87 +583,95 @@ const start = (server) => {
583
583
  historySeriesServiceEnabled = modeConfig.historySeriesServiceEnabled;
584
584
  registerAsHistoryApiProvider = modeConfig.registerAsHistoryApiProvider;
585
585
  logOperationalMode('start-configured');
586
- storageService.setLogger({
587
- debug: (msg) => server.debug(msg),
588
- error: (msg) => server.error(msg)
589
- });
590
- const storageConfig = storageService.configure();
591
- server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} parquetDir=${storageConfig.parquetDirectory} flushMs=${storageConfig.flushIntervalMs}`);
592
- historySeries.setSampleSink(sample => {
593
- storageService.enqueueSample(sample);
594
- });
595
- duckDbInitializationPromise = storageService.initialize();
596
- void duckDbInitializationPromise.then((ready) => {
597
- server.debug(`[KIP][STORAGE] duckdbReady=${ready}`);
598
- if (ready && storageService.isDuckDbParquetEnabled()) {
599
- if (isHistorySeriesServiceEnabled()) {
600
- void storageService.getSeriesDefinitions()
601
- .then((storedSeries) => {
602
- if (storedSeries.length > 0) {
603
- historySeries.reconcileSeries(storedSeries);
604
- rebuildSeriesCaptureSubscriptions();
605
- }
606
- startStorageFlushTimer(storageConfig.flushIntervalMs);
607
- logOperationalMode('duckdb-ready');
608
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
609
- })
610
- .catch((loadError) => {
611
- server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
586
+ const needsDuckDb = historySeriesServiceEnabled || registerAsHistoryApiProvider;
587
+ if (needsDuckDb) {
588
+ storageService.setLogger({
589
+ debug: (msg) => server.debug(msg),
590
+ error: (msg) => server.error(msg)
591
+ });
592
+ const storageConfig = storageService.configure();
593
+ server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} parquetDir=${storageConfig.parquetDirectory} flushMs=${storageConfig.flushIntervalMs} parquetWindowMs=${storageConfig.parquetWindowMs} parquetCompression=${storageConfig.parquetCompression}`);
594
+ historySeries.setSampleSink(sample => {
595
+ storageService.enqueueSample(sample);
596
+ });
597
+ duckDbInitializationPromise = storageService.initialize();
598
+ void duckDbInitializationPromise.then((ready) => {
599
+ server.debug(`[KIP][STORAGE] duckdbReady=${ready}`);
600
+ if (ready && storageService.isDuckDbParquetEnabled()) {
601
+ if (isHistorySeriesServiceEnabled()) {
602
+ void storageService.getSeriesDefinitions()
603
+ .then((storedSeries) => {
604
+ if (storedSeries.length > 0) {
605
+ historySeries.reconcileSeries(storedSeries);
606
+ rebuildSeriesCaptureSubscriptions();
607
+ }
608
+ startStorageFlushTimer(storageConfig.flushIntervalMs);
609
+ logOperationalMode('duckdb-ready');
610
+ server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
611
+ })
612
+ .catch((loadError) => {
613
+ server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
614
+ startStorageFlushTimer(storageConfig.flushIntervalMs);
615
+ logOperationalMode('duckdb-ready-series-load-failed');
616
+ server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
617
+ });
618
+ }
619
+ else {
620
+ historySeries.reconcileSeries([]);
621
+ stopSeriesCapture();
612
622
  startStorageFlushTimer(storageConfig.flushIntervalMs);
613
- logOperationalMode('duckdb-ready-series-load-failed');
614
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
615
- });
623
+ logOperationalMode('duckdb-ready-series-disabled');
624
+ server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
625
+ }
626
+ return;
616
627
  }
617
- else {
618
- historySeries.reconcileSeries([]);
619
- stopSeriesCapture();
620
- startStorageFlushTimer(storageConfig.flushIntervalMs);
621
- logOperationalMode('duckdb-ready-series-disabled');
622
- server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
628
+ if (storageFlushTimer) {
629
+ clearInterval(storageFlushTimer);
630
+ storageFlushTimer = null;
623
631
  }
624
- return;
625
- }
626
- if (storageFlushTimer) {
627
- clearInterval(storageFlushTimer);
628
- storageFlushTimer = null;
629
- }
630
- const initError = storageService.getLastInitError();
631
- if (initError) {
632
- server.setPluginError(`DuckDB unavailable. ${initError}`);
633
- logOperationalMode('duckdb-unavailable');
634
- server.setPluginStatus(`KIP plugin started with DuckDB unavailable. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
632
+ const initError = storageService.getLastInitError();
633
+ if (initError) {
634
+ server.setPluginError(`DuckDB unavailable. ${initError}`);
635
+ logOperationalMode('duckdb-unavailable');
636
+ server.setPluginStatus(`KIP plugin started with DuckDB unavailable. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
637
+ }
638
+ });
639
+ if (retentionSweepTimer) {
640
+ clearInterval(retentionSweepTimer);
635
641
  }
636
- });
637
- if (retentionSweepTimer) {
638
- clearInterval(retentionSweepTimer);
639
- }
640
- retentionSweepTimer = setInterval(() => {
641
- try {
642
- if (storageService.isDuckDbParquetReady()) {
643
- const lifecycleToken = storageService.getLifecycleToken();
644
- void storageService.pruneExpiredSamples(Date.now(), lifecycleToken)
645
- .then(removedPersistedRows => {
646
- if (removedPersistedRows > 0) {
647
- server.debug(`[KIP][RETENTION] pruneExpired removedRows=${removedPersistedRows}`);
648
- }
649
- return storageService.pruneOrphanedSamples(lifecycleToken)
650
- .then(removedOrphanRows => {
651
- if (removedOrphanRows > 0) {
652
- server.debug(`[KIP][RETENTION] pruneOrphaned removedRows=${removedOrphanRows}`);
642
+ retentionSweepTimer = setInterval(() => {
643
+ try {
644
+ if (storageService.isDuckDbParquetReady()) {
645
+ const lifecycleToken = storageService.getLifecycleToken();
646
+ void storageService.pruneExpiredSamples(Date.now(), lifecycleToken)
647
+ .then(removedPersistedRows => {
648
+ if (removedPersistedRows > 0) {
649
+ server.debug(`[KIP][RETENTION] pruneExpired removedRows=${removedPersistedRows}`);
653
650
  }
651
+ return storageService.pruneOrphanedSamples(lifecycleToken)
652
+ .then(removedOrphanRows => {
653
+ if (removedOrphanRows > 0) {
654
+ server.debug(`[KIP][RETENTION] pruneOrphaned removedRows=${removedOrphanRows}`);
655
+ }
656
+ });
657
+ })
658
+ .catch(error => {
659
+ server.error(`[SERIES RETENTION] duckdbPrune failed: ${String(error.message || error)}`);
654
660
  });
655
- })
656
- .catch(error => {
657
- server.error(`[SERIES RETENTION] duckdbPrune failed: ${String(error.message || error)}`);
658
- });
661
+ }
659
662
  }
660
- }
661
- catch (error) {
662
- server.error(`[SERIES RETENTION] sweep failed: ${String(error.message || error)}`);
663
- }
664
- }, 60 * 60_000);
665
- retentionSweepTimer.unref?.();
666
- rebuildSeriesCaptureSubscriptions();
663
+ catch (error) {
664
+ server.error(`[SERIES RETENTION] sweep failed: ${String(error.message || error)}`);
665
+ }
666
+ }, 60 * 60_000);
667
+ retentionSweepTimer.unref?.();
668
+ rebuildSeriesCaptureSubscriptions();
669
+ }
670
+ else {
671
+ server.debug('[KIP][STORAGE] duckdb init skipped reason=config-disabled');
672
+ duckDbInitializationPromise = null;
673
+ stopSeriesCapture();
674
+ }
667
675
  if (server.registerPutHandler) {
668
676
  server.debug(`[KIP][COMMAND] registerPutHandlers context=${PUT_CONTEXT}`);
669
677
  server.registerPutHandler(PUT_CONTEXT, COMMAND_PATHS.SET_DISPLAY, (context, path, value) => {