@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +28 -18
  3. package/plugin/duckdb-parquet-storage.service.js +199 -104
  4. package/plugin/index.js +137 -109
  5. package/public/{chunk-D7VDX7ZF.js → chunk-67V4XHCY.js} +1 -1
  6. package/public/chunk-BTFZS2TW.js +16 -0
  7. package/public/{chunk-UYIJND2R.js → chunk-CD5TQSCS.js} +1 -1
  8. package/public/chunk-FZFDGAQO.js +1 -0
  9. package/public/{chunk-EDNYYQIZ.js → chunk-I4SJ5UNN.js} +1 -1
  10. package/public/{chunk-2ICAVOT2.js → chunk-IH4CEW4C.js} +7 -7
  11. package/public/chunk-ISF5E3CX.js +50 -0
  12. package/public/{chunk-6XFWUUDD.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-DD4F6F4S.js → chunk-VXTTEFRP.js} +8 -8
  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/.github/ISSUE_TEMPLATE/bug_report.yml +0 -84
  33. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  34. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -35
  35. package/.github/copilot-instructions.md +0 -218
  36. package/.github/instructions/angular.instructions.md +0 -123
  37. package/.github/instructions/best-practices.instructions.md +0 -59
  38. package/.github/instructions/project.instructions.md +0 -468
  39. package/.github/workflows/ci.yml +0 -37
  40. package/docs/widget-schematic.md +0 -102
  41. package/images/ActionSidenav.png +0 -0
  42. package/images/ChartplotterMode.png +0 -0
  43. package/images/KIPDemo.png +0 -0
  44. package/images/KipBrightness-1024.png +0 -0
  45. package/images/KipConfig-Units-1024.png +0 -0
  46. package/images/KipConfig-display-1024x488.png +0 -0
  47. package/images/KipFreeboard-SK-1024.png +0 -0
  48. package/images/KipGaugeSample1-1024x545.png +0 -0
  49. package/images/KipGaugeSample2-1024x488.png +0 -0
  50. package/images/KipGaugeSample3-1024x508.png +0 -0
  51. package/images/KipNightMode-1024.png +0 -0
  52. package/images/KipWidgetConfig-layout-1024.png +0 -0
  53. package/images/KipWidgetConfig-paths-1024x488.png +0 -0
  54. package/images/Options.png +0 -0
  55. package/images/exterior_user_installs.png +0 -0
  56. package/images/formfactor.png +0 -0
  57. package/plugin-config-data/kip/historicalData/kip-history.duckdb +0 -0
  58. package/plugin-config-data/kip/historicalData/parquet/chart-1/1772344583976-1772344583976.parquet +0 -0
  59. package/plugin-config-data/kip/historicalData/parquet/live-1/1771408800000-1771408890000.parquet +0 -0
  60. package/plugin-config-data/kip/historicalData/parquet/live-1/1771412400000-1771412490000.parquet +0 -0
  61. package/plugin-config-data/kip/historicalData/parquet/live-1/1771419600000-1771419650000.parquet +0 -0
  62. package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584154-1772344584154.parquet +0 -0
  63. package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584191-1772344584191.parquet +0 -0
  64. package/plugin-config-data/kip/historicalData/parquet/live-1/1772344584268-1772344584268.parquet +0 -0
  65. package/plugin-config-data/kip/historicalData/parquet/live-2/1771502400000-1771502400000.parquet +0 -0
  66. package/plugin-config-data/kip/historicalData/parquet/live-3/1771408800000-1771408890000.parquet +0 -0
  67. package/plugin-config-data/kip/historicalData/parquet/live-3/1771412400000-1771412490000.parquet +0 -0
  68. package/plugin-config-data/kip/historicalData/parquet/live-3/1771419600000-1771419650000.parquet +0 -0
  69. package/plugin-config-data/kip/historicalData/parquet/live-3/1772344584268-1772344584268.parquet +0 -0
  70. package/plugin-config-data/kip/historicalData/parquet/live-4/1771408800000-1771408890000.parquet +0 -0
  71. package/plugin-config-data/kip/historicalData/parquet/live-4/1771412400000-1771412490000.parquet +0 -0
  72. package/plugin-config-data/kip/historicalData/parquet/live-4/1771419600000-1771419650000.parquet +0 -0
  73. package/plugin-config-data/kip/historicalData/parquet/live-5/1771412400000-1771412490000.parquet +0 -0
  74. package/plugin-config-data/kip/historicalData/parquet/live-5/1771419600000-1771419650000.parquet +0 -0
  75. package/plugin-config-data/kip/historicalData/parquet/live-6/1771419600000-1771419650000.parquet +0 -0
  76. package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771408800000-1771408890000.parquet +0 -0
  77. package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771412400000-1771412490000.parquet +0 -0
  78. package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1771419600000-1771419650000.parquet +0 -0
  79. package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1772344584191-1772344584191.parquet +0 -0
  80. package/plugin-config-data/kip/historicalData/parquet/live-prefixed-1/1772344584268-1772344584268.parquet +0 -0
  81. package/public/chunk-A6DQJFP4.js +0 -16
  82. package/public/chunk-DEGYRCMI.js +0 -1
  83. package/public/chunk-DYTBBUMI.js +0 -4
  84. package/public/chunk-FNF7M3AE.js +0 -1
  85. package/public/chunk-J3LDKVIS.js +0 -50
  86. package/public/chunk-JB4YVVNW.js +0 -1
  87. package/public/chunk-YKJKIWXO.js +0 -6
  88. package/public/main-EG2WF4EO.js +0 -1
  89. package/tools/schematics/collection.json +0 -9
  90. package/tools/schematics/create-host2-widget/files/readme/README.md.template +0 -109
  91. package/tools/schematics/create-host2-widget/files/spec/widget-__name@dasherize__.component.spec.ts +0 -38
  92. package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.html +0 -6
  93. package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.scss +0 -5
  94. package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.ts.template +0 -94
  95. package/tools/schematics/create-host2-widget/index.js +0 -138
  96. package/tools/schematics/create-host2-widget/schema.json +0 -89
  97. package/tools/schematics/create-host2-widget/test/create-host2-widget.spec.ts +0 -70
  98. 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.0",
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.1.4",
65
- "@angular/language-service": "21.1.4",
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 duckdb = __importStar(require("duckdb"));
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 plugin settings into the storage backend configuration.
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(settings) {
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 = new duckdbModule.Database(dbPath);
126
- const maybeConnection = typeof this.db.connect === 'function'
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
- await new Promise((resolvePromise) => {
816
- connection.close(() => resolvePromise());
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 filePath = (0, path_1.join)(seriesDir, `${fromMs}-${toMs}.parquet`);
883
- const escapedSeries = this.escape(seriesId);
884
- const escapedFile = this.escapePath(filePath);
885
- const sql = `
886
- COPY (
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 = ${escapedSeries}
899
- AND ts_ms >= ${Math.trunc(fromMs)}
900
- AND ts_ms <= ${Math.trunc(toMs)}
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
- ) TO '${escapedFile}' (FORMAT PARQUET)
903
- `;
904
- await this.runSql(sql);
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 new Promise((resolvePromise, rejectPromise) => {
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
- return new Promise((resolvePromise, rejectPromise) => {
925
- this.connection?.all(sql, (error, rows) => {
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';