@transitive-sdk/clickhouse 0.4.3 → 0.5.1

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/index.js CHANGED
@@ -47,21 +47,22 @@ class ClickHouse {
47
47
 
48
48
  mqttHistoryTable = null; // name of the table used for MQTT history, if used
49
49
  topics = {}; // list of topics registered for storage, as object for de-duplication
50
+ rowCache = {}; // cache of rows awaiting insertion, by table
50
51
 
51
52
  /** Create the client, connecting to Clickhouse */
52
- async init({ url, dbName, user, password } = {}) {
53
+ async init({ url, dbName, user, password, interval } = {}) {
53
54
 
54
55
  const _url = url || process.env.CLICKHOUSE_URL || 'http://clickhouse:8123';
55
56
  const _dbName = dbName || process.env.CLICKHOUSE_DB || 'default';
56
57
  const _user = user || process.env.CLICKHOUSE_USER || 'default';
58
+ interval ||= 10_000;
57
59
 
58
60
  const {hostname, port} = URL.parse(_url);
59
- const interval = 200;
60
- await waitPort({ host: hostname, port: Number(port || 80), interval }, 10000);
61
+ const waitInterval = 200;
62
+ await waitPort({ host: hostname, port: Number(port || 80), waitInterval },
63
+ 10000);
61
64
  await new Promise(done => setTimeout(done, 200));
62
65
 
63
- // console.debug(`Creating ClickHouse client for URL: ${_url}, DB: ${_dbName}, User: ${_user}`);
64
-
65
66
  this._client = createClient({
66
67
  url: _url,
67
68
  max_open_connections: 10,
@@ -86,6 +87,9 @@ class ClickHouse {
86
87
  });
87
88
 
88
89
  await this._client.query({ query: 'SELECT 1' });
90
+
91
+ // start interval for batch insertion
92
+ setInterval(this.batchInsertCache.bind(this), interval);
89
93
  }
90
94
 
91
95
  /** Get the Clickhouse client (from @clickhouse/client) */
@@ -142,7 +146,7 @@ class ClickHouse {
142
146
  * @param {string} orgId - organization ID to add to each row
143
147
  * @param {string} deviceId - device ID to add to each row
144
148
  */
145
- async insert(tableName, rows, orgId, deviceId) {
149
+ insert(tableName, rows, orgId, deviceId) {
146
150
  // assert that orgId and deviceId are provided
147
151
  if (!orgId || !deviceId) {
148
152
  throw new Error('Both orgId and deviceId must be provided for multi-tenant insert');
@@ -155,16 +159,13 @@ class ClickHouse {
155
159
  DeviceId: deviceId
156
160
  }));
157
161
 
158
- return await this.client.insert({
159
- table: tableName,
160
- values: rowsWithIds,
161
- format: 'JSONEachRow'
162
- });
162
+ return this.addToCache(tableName, rowsWithIds);
163
163
  }
164
164
 
165
165
  /* Enable history recording. Ensure the mqtt_history table exists with the
166
166
  * correct schema, set dataCache, and subscribe to changes.
167
167
  * @param {object} options = {dataCache, tableName, ttlDays}
168
+ * @param {number} interval = ms interval between batch insertions
168
169
  */
169
170
  async enableHistory(options) {
170
171
  const { dataCache, tableName = 'mqtt_history' } = options;
@@ -241,7 +242,7 @@ class ClickHouse {
241
242
  // it matches any of the registered topics (this avoid duplicate triggers),
242
243
  // then store to ClickHouse with current timestamp.
243
244
  dataCache.subscribe((changes) => {
244
- _.forEach(changes, async (value, topic) => {
245
+ _.forEach(changes, (value, topic) => {
245
246
 
246
247
  const matched =
247
248
  _.some(this.topics, (_true, selector) => topicMatch(selector, topic));
@@ -257,24 +258,40 @@ class ClickHouse {
257
258
  row.Payload = JSON.stringify(value);
258
259
  } // else: omit
259
260
 
260
- try {
261
- await this.client.insert({
262
- table: this.mqttHistoryTable,
263
- values: [row],
264
- format: 'JSONEachRow'
265
- });
266
- } catch (error) {
267
- console.error('Error inserting MQTT message into ClickHouse:', error.message);
268
- }
261
+ // cache it for batch insertion
262
+ this.addToCache(this.mqttHistoryTable, [row]);
269
263
  })
270
264
  });
271
-
272
-
273
265
  }
274
266
 
275
267
  this.mqttHistoryTable = tableName;
276
268
  }
277
269
 
270
+ /** Add the given rows to the cache for batch-insertion to the given table */
271
+ addToCache(table, rows) {
272
+ this.rowCache[this.mqttHistoryTable] ||= [];
273
+ this.rowCache[this.mqttHistoryTable].push(...rows);
274
+ }
275
+
276
+ /** Function responsible fgor inserting all cached rows */
277
+ batchInsertCache() {
278
+ _.forEach(this.rowCache, (rows, table) => {
279
+ if (rows.length == 0) return;
280
+
281
+ try {
282
+ this.rowCache[table] = [];
283
+ this.client.insert({
284
+ table,
285
+ values: rows,
286
+ format: 'JSONEachRow'
287
+ });
288
+ } catch (error) {
289
+ console.error(`Error inserting ${rows.length} rows into ${table}`, error.message);
290
+ }
291
+ });
292
+ }
293
+
294
+
278
295
  /* Register an MQTT topic for storage in ClickHouse subscribes to the topic
279
296
  * and stores incoming messages JSON.stringify'd in a ClickHouse table.
280
297
  * Retrieve using `queryMQTTHistory`, or, when quering directly, e.g., from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitive-sdk/clickhouse",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "A tiny ClickHouse utility class for use in the Transitive framework.",
5
5
  "homepage": "https://transitiverobotics.com",
6
6
  "repository": {
@@ -48,7 +48,7 @@ describe('ClickHouse', function() {
48
48
  const dataCache = new DataCache({});
49
49
 
50
50
  before(async () => {
51
- await clickhouse.init({ url: CLICKHOUSE_URL });
51
+ await clickhouse.init({ url: CLICKHOUSE_URL, interval: 100 });
52
52
  /* Register for `insert` events on ClickHouse client */
53
53
  emitter = interceptInserts();
54
54
 
@@ -63,7 +63,7 @@ describe('ClickHouse', function() {
63
63
  await clickhouse.enableHistory({
64
64
  dataCache,
65
65
  tableName: TABLE_NAME
66
- });
66
+ }, 100);
67
67
 
68
68
  await clickhouse.registerMqttTopicForStorage(STANDARD_TOPIC_PATTERN);
69
69
  });
@@ -130,7 +130,6 @@ describe('ClickHouse', function() {
130
130
  await new Promise(resolve => setTimeout(resolve, 10));
131
131
  dataCache.update([org, 'device1', 'data'], null);
132
132
  await once(emitter, 'insert');
133
- await once(emitter, 'insert');
134
133
 
135
134
  const rows = await queryRowsByOrg(org);
136
135