@mxtommy/kip 4.5.0 → 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 +9 -0
- package/package.json +28 -18
- package/plugin/duckdb-parquet-storage.service.js +199 -104
- package/plugin/index.js +137 -109
- package/public/{chunk-D7VDX7ZF.js → chunk-67V4XHCY.js} +1 -1
- package/public/chunk-BTFZS2TW.js +16 -0
- package/public/{chunk-UYIJND2R.js → chunk-CD5TQSCS.js} +1 -1
- package/public/chunk-FZFDGAQO.js +1 -0
- package/public/{chunk-EDNYYQIZ.js → chunk-I4SJ5UNN.js} +1 -1
- package/public/{chunk-2ICAVOT2.js → chunk-IH4CEW4C.js} +7 -7
- package/public/chunk-ISF5E3CX.js +50 -0
- package/public/{chunk-6XFWUUDD.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-DD4F6F4S.js → chunk-VXTTEFRP.js} +8 -8
- 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/.github/ISSUE_TEMPLATE/bug_report.yml +0 -84
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -35
- package/.github/copilot-instructions.md +0 -218
- package/.github/instructions/angular.instructions.md +0 -123
- package/.github/instructions/best-practices.instructions.md +0 -59
- package/.github/instructions/project.instructions.md +0 -468
- package/.github/workflows/ci.yml +0 -37
- package/docs/widget-schematic.md +0 -102
- package/images/ActionSidenav.png +0 -0
- package/images/ChartplotterMode.png +0 -0
- package/images/KIPDemo.png +0 -0
- package/images/KipBrightness-1024.png +0 -0
- package/images/KipConfig-Units-1024.png +0 -0
- package/images/KipConfig-display-1024x488.png +0 -0
- package/images/KipFreeboard-SK-1024.png +0 -0
- package/images/KipGaugeSample1-1024x545.png +0 -0
- package/images/KipGaugeSample2-1024x488.png +0 -0
- package/images/KipGaugeSample3-1024x508.png +0 -0
- package/images/KipNightMode-1024.png +0 -0
- package/images/KipWidgetConfig-layout-1024.png +0 -0
- package/images/KipWidgetConfig-paths-1024x488.png +0 -0
- package/images/Options.png +0 -0
- package/images/exterior_user_installs.png +0 -0
- package/images/formfactor.png +0 -0
- package/plugin-config-data/kip/historicalData/kip-history.duckdb +0 -0
- package/plugin-config-data/kip/historicalData/parquet/chart-1/1772344583976-1772344583976.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1771408800000-1771408890000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1771412400000-1771412490000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584154-1772344584154.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584191-1772344584191.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584268-1772344584268.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-2/1771502400000-1771502400000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-3/1771408800000-1771408890000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-3/1771412400000-1771412490000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-3/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-3/1772344584268-1772344584268.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-4/1771408800000-1771408890000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-4/1771412400000-1771412490000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-4/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-5/1771412400000-1771412490000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-5/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-6/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771408800000-1771408890000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771412400000-1771412490000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771419600000-1771419650000.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1772344584191-1772344584191.parquet +0 -0
- package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1772344584268-1772344584268.parquet +0 -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-J3LDKVIS.js +0 -50
- package/public/chunk-JB4YVVNW.js +0 -1
- package/public/chunk-YKJKIWXO.js +0 -6
- package/public/main-EG2WF4EO.js +0 -1
- package/tools/schematics/collection.json +0 -9
- package/tools/schematics/create-host2-widget/files/readme/README.md.template +0 -109
- package/tools/schematics/create-host2-widget/files/spec/widget-__name@dasherize__.component.spec.ts +0 -38
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.html +0 -6
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.scss +0 -5
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.ts.template +0 -94
- package/tools/schematics/create-host2-widget/index.js +0 -138
- package/tools/schematics/create-host2-widget/schema.json +0 -89
- package/tools/schematics/create-host2-widget/test/create-host2-widget.spec.ts +0 -70
- package/tools/schematics/create-host2-widget/utils/formatting.js +0 -119
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# v4.5.2
|
|
2
|
+
## Fixes
|
|
3
|
+
* DuckDB initialized when features are not enabled.
|
|
4
|
+
* Parquet data compression and pruning not executing.
|
|
5
|
+
# v4.5.1
|
|
6
|
+
## Fixes
|
|
7
|
+
* DuckDB dependency causing build and installation errors. Fixes #979
|
|
8
|
+
* Reduced installation size. Fixes #980
|
|
1
9
|
# v4.5.0
|
|
2
10
|
## New Features
|
|
3
11
|
* Effortlessly review your vessel’s history with the new Widget Historical Charts—automatically track, store, and visualize key data. Instantly access up to the last full day of performance: just two-finger tap or right-click any widget to open a seamless history dialog—no setup, no clutter, just the trends you need. (Requires Signal K v2.22.1)
|
|
@@ -7,6 +15,7 @@
|
|
|
7
15
|
* Added "Days" as a selectable time scale in the Data Chart widget.
|
|
8
16
|
* Improved integration by validating server plugin presence, plugin state, and configuration.
|
|
9
17
|
* Added a Node-RED introduction guide to the Help section.
|
|
18
|
+
* Migrated KIP plugin historical storage internals from `duckdb` to `@duckdb/node-api` and Parquet export writing to `@dsnp/parquetjs`.
|
|
10
19
|
## Fixes
|
|
11
20
|
* Improved KIP plugin OpenAPI compatibility.
|
|
12
21
|
* Resolved slow Data Inspector performance caused by high resource usage in deep loop logic.
|
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": {
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
"displayName": "KIP Instrument MFD"
|
|
41
41
|
},
|
|
42
42
|
"main": "plugin/index.js",
|
|
43
|
+
"files": [
|
|
44
|
+
"plugin/**",
|
|
45
|
+
"public/**",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"CONTRIBUTORS.md",
|
|
50
|
+
"package.json"
|
|
51
|
+
],
|
|
43
52
|
"signalk-plugin-enabled-by-default": true,
|
|
44
53
|
"scripts": {
|
|
45
54
|
"test": "CI=1 ng test --watch=false",
|
|
@@ -57,12 +66,22 @@
|
|
|
57
66
|
},
|
|
58
67
|
"schematics": "tools/schematics/collection.json",
|
|
59
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",
|
|
60
79
|
"@angular-devkit/build-angular": "^21.1.4",
|
|
61
80
|
"@angular-devkit/schematics-cli": "^20.1.6",
|
|
62
81
|
"@angular/build": "^21.1.4",
|
|
63
82
|
"@angular/cli": "^21.1.4",
|
|
64
|
-
"@angular/compiler-cli": "21.
|
|
65
|
-
"@angular/language-service": "21.
|
|
83
|
+
"@angular/compiler-cli": "21.2.0",
|
|
84
|
+
"@angular/language-service": "21.2.0",
|
|
66
85
|
"@types/canvas-gauges": "^2.1.8",
|
|
67
86
|
"@types/d3": "^7.4.3",
|
|
68
87
|
"@types/jasmine": "~3.6.0",
|
|
@@ -87,22 +106,9 @@
|
|
|
87
106
|
"pwa-asset-generator": "^8.1.1",
|
|
88
107
|
"sass": "^1.49.9",
|
|
89
108
|
"ts-node": "^10.9.2",
|
|
90
|
-
"typescript": "^5.9.3"
|
|
91
|
-
},
|
|
92
|
-
"dependencies": {
|
|
93
|
-
"@angular/animations": "21.1.4",
|
|
94
|
-
"@angular/cdk": "21.1.4",
|
|
95
|
-
"@angular/common": "21.1.4",
|
|
96
|
-
"@angular/compiler": "21.1.4",
|
|
97
|
-
"@angular/core": "21.1.4",
|
|
98
|
-
"@angular/forms": "21.1.4",
|
|
99
|
-
"@angular/material": "21.1.4",
|
|
100
|
-
"@angular/platform-browser": "21.1.4",
|
|
101
|
-
"@angular/platform-browser-dynamic": "21.1.4",
|
|
102
|
-
"@angular/router": "21.1.4",
|
|
109
|
+
"typescript": "^5.9.3",
|
|
103
110
|
"@aziham/chartjs-plugin-streaming": "^3.5.1",
|
|
104
111
|
"@godind/ng-canvas-gauges": "^6.2.1",
|
|
105
|
-
"@signalk/server-api": "^2.22.0",
|
|
106
112
|
"@zakj/no-sleep": "^0.13.5",
|
|
107
113
|
"chart.js": "^4.5.1",
|
|
108
114
|
"chartjs-adapter-date-fns": "^3.0.0",
|
|
@@ -112,7 +118,6 @@
|
|
|
112
118
|
"core-js": "^3.13.1",
|
|
113
119
|
"d3": "^7.9.0",
|
|
114
120
|
"date-fns": "^2.30.0",
|
|
115
|
-
"duckdb": "^1.4.4",
|
|
116
121
|
"gridstack": "^12.3.3",
|
|
117
122
|
"js-quantities": "^1.8.0",
|
|
118
123
|
"lodash-es": "^4.17.23",
|
|
@@ -124,5 +129,10 @@
|
|
|
124
129
|
"steelseries": "^2.0.9",
|
|
125
130
|
"tslib": "^2.6.2",
|
|
126
131
|
"zone.js": "^0.15.1"
|
|
132
|
+
},
|
|
133
|
+
"dependencies": {
|
|
134
|
+
"@dsnp/parquetjs": "^1.8.7",
|
|
135
|
+
"@duckdb/node-api": "^1.4.4-r.2",
|
|
136
|
+
"@signalk/server-api": "^2.22.0"
|
|
127
137
|
}
|
|
128
138
|
}
|
|
@@ -1,52 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.DuckDbParquetStorageService = void 0;
|
|
37
4
|
const fs_1 = require("fs");
|
|
38
5
|
const path_1 = require("path");
|
|
39
|
-
const
|
|
6
|
+
const node_api_1 = require("@duckdb/node-api");
|
|
7
|
+
const parquetjs_1 = require("@dsnp/parquetjs");
|
|
40
8
|
/**
|
|
41
9
|
* Provides DuckDB storage and Parquet flush support for captured history samples.
|
|
42
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
|
+
};
|
|
43
19
|
class DuckDbParquetStorageService {
|
|
44
|
-
config = {
|
|
45
|
-
engine: 'duckdb-parquet',
|
|
46
|
-
databaseFile: 'plugin-config-data/kip/historicalData/kip-history.duckdb',
|
|
47
|
-
parquetDirectory: 'plugin-config-data/kip/historicalData/parquet',
|
|
48
|
-
flushIntervalMs: 30_000
|
|
49
|
-
};
|
|
20
|
+
config = { ...DEFAULT_STORAGE_CONFIG };
|
|
50
21
|
// 8 hour job interval (8 hours)
|
|
51
22
|
static EIGHT_HOURS_INTERVAL = 8 * 60 * 60 * 1000;
|
|
52
23
|
vacuumJob = null;
|
|
@@ -83,24 +54,17 @@ class DuckDbParquetStorageService {
|
|
|
83
54
|
this.logger = logger;
|
|
84
55
|
}
|
|
85
56
|
/**
|
|
86
|
-
* Applies
|
|
57
|
+
* Applies the fixed storage backend configuration.
|
|
87
58
|
*
|
|
88
|
-
* @param {unknown} settings Plugin settings payload from Signal K (ignored for fixed storage defaults).
|
|
89
59
|
* @returns {IDuckDbParquetStorageConfig} Fixed storage configuration.
|
|
90
60
|
*
|
|
91
61
|
* @example
|
|
92
|
-
* const cfg = storage.configure(
|
|
62
|
+
* const cfg = storage.configure();
|
|
93
63
|
* console.log(cfg.engine);
|
|
94
64
|
*/
|
|
95
|
-
configure(
|
|
96
|
-
void settings;
|
|
65
|
+
configure() {
|
|
97
66
|
this.initialized = false;
|
|
98
|
-
this.config = {
|
|
99
|
-
engine: 'duckdb-parquet',
|
|
100
|
-
databaseFile: 'plugin-config-data/kip/historicalData/kip-history.duckdb',
|
|
101
|
-
parquetDirectory: 'plugin-config-data/kip/historicalData/parquet',
|
|
102
|
-
flushIntervalMs: 30_000
|
|
103
|
-
};
|
|
67
|
+
this.config = { ...DEFAULT_STORAGE_CONFIG };
|
|
104
68
|
return this.config;
|
|
105
69
|
}
|
|
106
70
|
/**
|
|
@@ -118,21 +82,11 @@ class DuckDbParquetStorageService {
|
|
|
118
82
|
this.initialized = false;
|
|
119
83
|
this.lifecycleToken += 1;
|
|
120
84
|
try {
|
|
121
|
-
const duckdbModule = duckdb;
|
|
122
85
|
const dbPath = (0, path_1.resolve)(this.config.databaseFile);
|
|
123
86
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(dbPath), { recursive: true });
|
|
124
87
|
(0, fs_1.mkdirSync)((0, path_1.resolve)(this.config.parquetDirectory), { recursive: true });
|
|
125
|
-
this.db =
|
|
126
|
-
|
|
127
|
-
? this.db.connect()
|
|
128
|
-
: this.db;
|
|
129
|
-
if (!maybeConnection
|
|
130
|
-
|| typeof maybeConnection.run !== 'function'
|
|
131
|
-
|| typeof maybeConnection.all !== 'function'
|
|
132
|
-
|| typeof maybeConnection.close !== 'function') {
|
|
133
|
-
throw new Error('DuckDB connection API is unavailable in this runtime');
|
|
134
|
-
}
|
|
135
|
-
this.connection = maybeConnection;
|
|
88
|
+
this.db = await node_api_1.DuckDBInstance.create(dbPath);
|
|
89
|
+
this.connection = await this.db.connect();
|
|
136
90
|
await this.createCoreTables();
|
|
137
91
|
await this.runSql('CREATE INDEX IF NOT EXISTS idx_history_series_scope_ts ON history_samples(series_id, ts_ms)');
|
|
138
92
|
await this.runSql('CREATE INDEX IF NOT EXISTS idx_history_series_scope_id ON history_series(series_id)');
|
|
@@ -154,7 +108,7 @@ class DuckDbParquetStorageService {
|
|
|
154
108
|
const message = error?.message ?? String(error);
|
|
155
109
|
this.lastInitError = message;
|
|
156
110
|
this.logger.error(`[SERIES STORAGE] DuckDB initialization failed: ${message}`);
|
|
157
|
-
this.logger.error('[SERIES STORAGE] DuckDB is required. Install runtime dependency with: npm i duckdb in the installed plugin directory, then restart Signal K.');
|
|
111
|
+
this.logger.error('[SERIES STORAGE] DuckDB Node API is required. Install runtime dependency with: npm i @duckdb/node-api in the installed plugin directory, then restart Signal K.');
|
|
158
112
|
this.connection = null;
|
|
159
113
|
this.db = null;
|
|
160
114
|
this.pendingRows = [];
|
|
@@ -209,7 +163,11 @@ class DuckDbParquetStorageService {
|
|
|
209
163
|
this.logger.debug('[SERIES STORAGE] Running scheduled prune of expired and orphaned samples');
|
|
210
164
|
const expired = await this.pruneExpiredSamples(Date.now(), this.lifecycleToken);
|
|
211
165
|
const orphaned = await this.pruneOrphanedSamples(this.lifecycleToken);
|
|
166
|
+
const parquetRemoved = await this.pruneParquetFilesByRetention(Date.now());
|
|
212
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
|
+
}
|
|
213
171
|
});
|
|
214
172
|
}
|
|
215
173
|
catch (err) {
|
|
@@ -570,6 +528,7 @@ class DuckDbParquetStorageService {
|
|
|
570
528
|
return;
|
|
571
529
|
}
|
|
572
530
|
await this.runSql(`DELETE FROM history_series WHERE series_id = ${this.escape(seriesId)}`);
|
|
531
|
+
this.deleteParquetSeriesDir(seriesId);
|
|
573
532
|
}
|
|
574
533
|
/**
|
|
575
534
|
* Replaces persisted series definitions with desired set.
|
|
@@ -811,10 +770,20 @@ class DuckDbParquetStorageService {
|
|
|
811
770
|
return;
|
|
812
771
|
}
|
|
813
772
|
const connection = this.connection;
|
|
773
|
+
const db = this.db;
|
|
814
774
|
this.connection = null;
|
|
815
|
-
|
|
816
|
-
connection.
|
|
817
|
-
}
|
|
775
|
+
try {
|
|
776
|
+
connection.disconnectSync();
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
// ignore disconnect failures during shutdown
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
db?.closeSync();
|
|
783
|
+
}
|
|
784
|
+
catch {
|
|
785
|
+
// ignore close failures during shutdown
|
|
786
|
+
}
|
|
818
787
|
this.db = null;
|
|
819
788
|
}
|
|
820
789
|
async createCoreTables() {
|
|
@@ -850,10 +819,6 @@ class DuckDbParquetStorageService {
|
|
|
850
819
|
)
|
|
851
820
|
`);
|
|
852
821
|
}
|
|
853
|
-
async countRows(tableName) {
|
|
854
|
-
const rows = await this.querySql(`SELECT COUNT(*) AS removed_rows FROM ${tableName}`);
|
|
855
|
-
return this.toNumberOrUndefined(rows[0]?.removed_rows) ?? 0;
|
|
856
|
-
}
|
|
857
822
|
async insertRows(rows) {
|
|
858
823
|
if (rows.length === 0) {
|
|
859
824
|
return;
|
|
@@ -876,14 +841,16 @@ class DuckDbParquetStorageService {
|
|
|
876
841
|
await this.runSql(sql);
|
|
877
842
|
}
|
|
878
843
|
async exportSeriesRange(seriesId, fromMs, toMs) {
|
|
844
|
+
const windowMs = this.resolveParquetWindowMs();
|
|
879
845
|
const baseDir = (0, path_1.resolve)(this.config.parquetDirectory);
|
|
880
846
|
const seriesDir = (0, path_1.join)(baseDir, this.safePath(seriesId));
|
|
881
847
|
(0, fs_1.mkdirSync)(seriesDir, { recursive: true });
|
|
882
|
-
const
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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(`
|
|
887
854
|
SELECT
|
|
888
855
|
series_id,
|
|
889
856
|
dataset_uuid,
|
|
@@ -892,44 +859,175 @@ class DuckDbParquetStorageService {
|
|
|
892
859
|
context,
|
|
893
860
|
source,
|
|
894
861
|
ts_ms,
|
|
895
|
-
to_timestamp(ts_ms / 1000.0) AS ts,
|
|
896
862
|
value
|
|
897
863
|
FROM history_samples
|
|
898
|
-
WHERE series_id = ${
|
|
899
|
-
AND ts_ms >= ${Math.trunc(
|
|
900
|
-
AND ts_ms <= ${Math.trunc(
|
|
864
|
+
WHERE series_id = ${this.escape(seriesId)}
|
|
865
|
+
AND ts_ms >= ${Math.trunc(windowStart)}
|
|
866
|
+
AND ts_ms <= ${Math.trunc(windowEnd)}
|
|
901
867
|
ORDER BY ts_ms
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
|
|
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);
|
|
912
|
+
}
|
|
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;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
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
|
+
}
|
|
997
|
+
});
|
|
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;
|
|
1006
|
+
}
|
|
1007
|
+
const fromMs = Number(match[1]);
|
|
1008
|
+
const toMs = Number(match[2]);
|
|
1009
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) {
|
|
1010
|
+
return null;
|
|
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 });
|
|
905
1018
|
}
|
|
906
1019
|
async runSql(sql) {
|
|
907
1020
|
if (!this.connection) {
|
|
908
1021
|
throw new Error('DuckDB connection is not initialized');
|
|
909
1022
|
}
|
|
910
|
-
await
|
|
911
|
-
this.connection?.run(sql, (error) => {
|
|
912
|
-
if (error) {
|
|
913
|
-
rejectPromise(error);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
resolvePromise();
|
|
917
|
-
});
|
|
918
|
-
});
|
|
1023
|
+
await this.connection.run(sql);
|
|
919
1024
|
}
|
|
920
1025
|
async querySql(sql) {
|
|
921
1026
|
if (!this.connection) {
|
|
922
1027
|
throw new Error('DuckDB connection is not initialized');
|
|
923
1028
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (error) {
|
|
927
|
-
rejectPromise(error);
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
resolvePromise((rows ?? []));
|
|
931
|
-
});
|
|
932
|
-
});
|
|
1029
|
+
const result = await this.connection.runAndReadAll(sql);
|
|
1030
|
+
return result.getRowObjectsJson();
|
|
933
1031
|
}
|
|
934
1032
|
async selectRowsForPaths(paths, context, fromMs, toMs) {
|
|
935
1033
|
const rowsByPath = new Map();
|
|
@@ -1142,9 +1240,6 @@ class DuckDbParquetStorageService {
|
|
|
1142
1240
|
escape(value) {
|
|
1143
1241
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1144
1242
|
}
|
|
1145
|
-
escapePath(value) {
|
|
1146
|
-
return String(value).replace(/'/g, "''");
|
|
1147
|
-
}
|
|
1148
1243
|
nullableString(value) {
|
|
1149
1244
|
if (value === undefined || value === null || value === '') {
|
|
1150
1245
|
return 'NULL';
|