@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.
- package/CHANGELOG.md +4 -0
- package/package.json +13 -13
- package/plugin/duckdb-parquet-storage.service.js +179 -60
- package/plugin/index.js +82 -74
- package/public/{chunk-D7VDX7ZF.js → chunk-67V4XHCY.js} +1 -1
- package/public/chunk-BTFZS2TW.js +16 -0
- package/public/{chunk-JGGMFMY5.js → chunk-CD5TQSCS.js} +1 -1
- package/public/chunk-FZFDGAQO.js +1 -0
- package/public/{chunk-IYRLINL7.js → chunk-I4SJ5UNN.js} +1 -1
- package/public/{chunk-VCY32MWT.js → chunk-IH4CEW4C.js} +7 -7
- package/public/chunk-ISF5E3CX.js +50 -0
- package/public/{chunk-EQ2N7KDA.js → chunk-KQEEYPK3.js} +2 -2
- package/public/chunk-NFJ4RQSE.js +4 -0
- package/public/{chunk-DEM56G4S.js → chunk-OPTBDYBL.js} +1 -1
- package/public/{chunk-YCEXTKGG.js → chunk-P4CRTB7N.js} +1 -1
- package/public/{chunk-IHURI4IH.js → chunk-P7JKENHI.js} +3 -3
- package/public/chunk-Q2ANAJAD.js +1 -0
- package/public/{chunk-B75MT7ND.js → chunk-R36UY4Q4.js} +1 -1
- package/public/{chunk-CHGXAEKT.js → chunk-RCYOZLZB.js} +1 -1
- package/public/{chunk-KPHICV76.js → chunk-SJFJEOSG.js} +1 -1
- package/public/{chunk-MGPPVLZ7.js → chunk-TBNKOU7M.js} +1 -1
- package/public/chunk-TVNXBPFF.js +6 -0
- package/public/{chunk-S72JTJPN.js → chunk-VPF5756E.js} +1 -1
- package/public/chunk-VXCYPAWR.js +1 -0
- package/public/{chunk-RONXIZ2U.js → chunk-VXTTEFRP.js} +3 -3
- package/public/{chunk-R7RQHWKJ.js → chunk-WH5CIUSB.js} +1 -1
- package/public/{chunk-LQDSU4WS.js → chunk-WQSJFJLW.js} +1 -1
- package/public/{chunk-KZ5DUKAX.js → chunk-XBSU7OGT.js} +1 -1
- package/public/{chunk-CEB42O2C.js → chunk-YI3MZWRZ.js} +1 -1
- package/public/index.html +1 -1
- package/public/main-B6TXB3EB.js +1 -0
- package/public/chunk-A6DQJFP4.js +0 -16
- package/public/chunk-DEGYRCMI.js +0 -1
- package/public/chunk-DYTBBUMI.js +0 -4
- package/public/chunk-FNF7M3AE.js +0 -1
- package/public/chunk-JB4YVVNW.js +0 -1
- package/public/chunk-YKJKIWXO.js +0 -6
- package/public/chunk-ZV7IYYEQ.js +0 -50
- package/public/main-FQESQQV6.js +0 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxtommy/kip",
|
|
3
|
-
"version": "4.5.
|
|
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.
|
|
74
|
-
"@angular/language-service": "21.
|
|
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
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
897
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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-
|
|
614
|
-
server.setPluginStatus(`KIP plugin started with
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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) => {
|