node-red-contrib-questdb 0.6.9 → 0.6.23

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/nodes/questdb.js CHANGED
@@ -31,17 +31,9 @@ module.exports = function (RED) {
31
31
  }
32
32
  }
33
33
 
34
- // Auto-flush options
35
- if (configNode.autoFlush === false) {
36
- connStr += `auto_flush=off;`;
37
- } else {
38
- if (configNode.autoFlushRows) {
39
- connStr += `auto_flush_rows=${configNode.autoFlushRows};`;
40
- }
41
- if (configNode.autoFlushInterval) {
42
- connStr += `auto_flush_interval=${configNode.autoFlushInterval};`;
43
- }
44
- }
34
+ // Always disable sender's built-in auto-flush to prevent uncaught exceptions.
35
+ // We run our own flush timer with error handling in the connection state.
36
+ connStr += `auto_flush=off;`;
45
37
 
46
38
  // Buffer options
47
39
  if (configNode.initBufSize) {
@@ -89,7 +81,7 @@ module.exports = function (RED) {
89
81
 
90
82
  // Auto-flush settings
91
83
  configNode.autoFlush = config.autoFlush !== false;
92
- configNode.autoFlushRows = parseInt(config.autoFlushRows) || 75000;
84
+ configNode.autoFlushRows = parseInt(config.autoFlushRows) || 500;
93
85
  configNode.autoFlushInterval = parseInt(config.autoFlushInterval) || 1000;
94
86
 
95
87
  // Buffer settings
@@ -110,8 +102,11 @@ module.exports = function (RED) {
110
102
  connecting: false,
111
103
  users: 0,
112
104
  reconnectTimer: null,
105
+ flushTimer: null,
106
+ rowCount: 0,
113
107
  emitter: new EventEmitter(),
114
108
  lastError: null,
109
+ lastNotConnectedLog: 0,
115
110
 
116
111
  // Method to update state and emit event
117
112
  updateState: function (newStatus, error = null) {
@@ -171,6 +166,32 @@ module.exports = function (RED) {
171
166
 
172
167
  connectionState.updateState(true);
173
168
 
169
+ // Start managed flush timer (replaces sender's built-in auto-flush)
170
+ if (connectionState.flushTimer) {
171
+ clearInterval(connectionState.flushTimer);
172
+ }
173
+ const flushInterval = configNode.autoFlushInterval || 1000;
174
+ connectionState.flushTimer = setInterval(async () => {
175
+ if (!connectionState.connected || !connectionState.sender) return;
176
+ if (connectionState.rowCount === 0) return;
177
+ try {
178
+ await connectionState.sender.flush();
179
+ connectionState.rowCount = 0;
180
+ } catch (flushErr) {
181
+ const flushErrMsg = formatError(flushErr, { operation: 'auto-flush' });
182
+ RED.log.error(`[QuestDB] Flush failed: ${flushErrMsg}`);
183
+ connectionState.emitter.emit('flushError', flushErr);
184
+ if (isDisconnectionError(flushErr)) {
185
+ connectionState.updateState(false, flushErr.message);
186
+ connectionState.sender = null;
187
+ connectionState.connect();
188
+ }
189
+ }
190
+ }, flushInterval);
191
+
192
+ // Also flush when row count threshold is reached
193
+ connectionState.flushRows = configNode.autoFlushRows || 500;
194
+
174
195
  RED.log.info(
175
196
  `[QuestDB] Connected to ${configNode.protocol}://${configNode.host}:${configNode.port}`
176
197
  );
@@ -196,6 +217,11 @@ module.exports = function (RED) {
196
217
  };
197
218
 
198
219
  connectionState.disconnect = async function () {
220
+ if (connectionState.flushTimer) {
221
+ clearInterval(connectionState.flushTimer);
222
+ connectionState.flushTimer = null;
223
+ }
224
+
199
225
  if (connectionState.reconnectTimer) {
200
226
  clearTimeout(connectionState.reconnectTimer);
201
227
  connectionState.reconnectTimer = null;
@@ -255,10 +281,6 @@ module.exports = function (RED) {
255
281
  return;
256
282
  }
257
283
 
258
- // Configuration from the node properties
259
- node.autoFlush = config.autoFlush !== false; // default true
260
- node.flushInterval = config.flushInterval || 1000;
261
-
262
284
  // Get shared connection
263
285
  const connection = node.questdbConfig.getConnection();
264
286
  if (!connection) {
@@ -306,15 +328,40 @@ module.exports = function (RED) {
306
328
  stateChangeHandler({ status: initialStatus, error: connection.lastError });
307
329
 
308
330
  node.on('input', async function (msg) {
309
- // Auto-reconnect if disconnected
331
+ function sendError(errorText) {
332
+ node.error(errorText, msg);
333
+ node.status({ fill: 'red', shape: 'ring', text: errorText.substring(0, 30) });
334
+ var errMsg = RED.util.cloneMessage(msg);
335
+ errMsg.payload = { success: false, error: errorText };
336
+ node.send([null, errMsg]);
337
+ }
338
+
339
+ // Auto-reconnect if disconnected (rate-limit error logging to once per 5s)
310
340
  if (!connection.connected && !connection.connecting) {
311
341
  connection.connect();
312
- node.error('Not connected to QuestDB, reconnecting...', msg);
342
+ var now = Date.now();
343
+ if (now - connection.lastNotConnectedLog > 5000) {
344
+ connection.lastNotConnectedLog = now;
345
+ sendError('Not connected to QuestDB, reconnecting...');
346
+ } else {
347
+ // Still send to error output but don't log
348
+ var errMsg = RED.util.cloneMessage(msg);
349
+ errMsg.payload = { success: false, error: 'Not connected to QuestDB, reconnecting...' };
350
+ node.send([null, errMsg]);
351
+ }
313
352
  return;
314
353
  }
315
354
 
316
355
  if (!connection.connected || !connection.sender) {
317
- node.error('Not connected to QuestDB', msg);
356
+ var now2 = Date.now();
357
+ if (now2 - connection.lastNotConnectedLog > 5000) {
358
+ connection.lastNotConnectedLog = now2;
359
+ sendError('Not connected to QuestDB');
360
+ } else {
361
+ var errMsg2 = RED.util.cloneMessage(msg);
362
+ errMsg2.payload = { success: false, error: 'Not connected to QuestDB' };
363
+ node.send([null, errMsg2]);
364
+ }
318
365
  return;
319
366
  }
320
367
 
@@ -326,22 +373,21 @@ module.exports = function (RED) {
326
373
  ) {
327
374
  connection.updateState(false);
328
375
  connection.connect();
329
- node.error('TCP transport disconnected, reconnecting...', msg);
376
+ sendError('TCP transport disconnected, reconnecting...');
330
377
  return;
331
378
  }
332
379
 
333
- // Validate message
380
+ // Validate table name
334
381
  const tableName = msg.topic;
335
- if (!tableName) {
336
- node.error(formatError('Topic (table name) not specified', { operation: 'validate' }), msg);
337
- node.status({ fill: 'red', shape: 'ring', text: 'no topic' });
382
+ if (!tableName || typeof tableName !== 'string' || !tableName.trim()) {
383
+ sendError('Table name (msg.topic) is missing or empty');
338
384
  return;
339
385
  }
340
386
 
387
+ // Validate payload
341
388
  const payload = msg.payload;
342
389
  if (payload === undefined || payload === null) {
343
- node.error(formatError('Payload is empty', { operation: 'validate' }), msg);
344
- node.status({ fill: 'red', shape: 'ring', text: 'no payload' });
390
+ sendError('Payload is null or undefined');
345
391
  return;
346
392
  }
347
393
 
@@ -503,15 +549,8 @@ module.exports = function (RED) {
503
549
  node.warn(`Skipping unsupported type '${typeof value}' for column '${key}'`);
504
550
  }
505
551
  }
506
- } else if (!payload.symbols) {
507
- node.error(
508
- formatError(
509
- "Payload must have 'symbols' and/or 'columns' properties, or be a number",
510
- { operation: 'validate', table: tableName }
511
- ),
512
- msg
513
- );
514
- node.status({ fill: 'red', shape: 'ring', text: 'bad format' });
552
+ } else if (!payload.symbols && !payload.columns) {
553
+ sendError("Payload must have 'symbols' and/or 'columns' properties, or be a number");
515
554
  return;
516
555
  }
517
556
 
@@ -544,13 +583,28 @@ module.exports = function (RED) {
544
583
  }
545
584
  }
546
585
 
547
- if (node.autoFlush) {
548
- await connection.sender.flush();
586
+ // Track rows and flush at threshold
587
+ connection.rowCount++;
588
+ if (connection.flushRows && connection.rowCount >= connection.flushRows) {
589
+ try {
590
+ await connection.sender.flush();
591
+ connection.rowCount = 0;
592
+ } catch (flushErr) {
593
+ const flushErrMsg = formatError(flushErr, { operation: 'flush', table: tableName });
594
+ RED.log.error(`[QuestDB] Flush failed: ${flushErrMsg}`);
595
+ if (isDisconnectionError(flushErr)) {
596
+ connection.updateState(false, flushErr.message);
597
+ connection.sender = null;
598
+ connection.connect();
599
+ }
600
+ sendError(flushErrMsg);
601
+ return;
602
+ }
549
603
  }
550
604
 
551
605
  node.status({ fill: 'green', shape: 'dot', text: `sent: ${tableName}` });
552
606
  msg.payload = { success: true, table: tableName };
553
- node.send(msg);
607
+ node.send([msg, null]);
554
608
  } catch (err) {
555
609
  const errMsg = formatError(err, { operation: 'write', table: tableName });
556
610
 
@@ -575,8 +629,7 @@ module.exports = function (RED) {
575
629
  }
576
630
  }
577
631
 
578
- msg.payload = { success: false, error: errMsg };
579
- node.send(msg);
632
+ sendError(errMsg);
580
633
  return; // Early return after error handling
581
634
  }
582
635
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-questdb",
3
- "version": "0.6.9",
3
+ "version": "0.6.23",
4
4
  "description": "Node-RED nodes for writing high-performance time-series data to QuestDB using Influx Line Protocol (ILP). Supports IoT, industrial monitoring, smart buildings, fleet telematics, healthcare, agriculture, and more.",
5
5
  "author": {
6
6
  "name": "Holger Amort"
@@ -50,7 +50,9 @@
50
50
  "nodes": {
51
51
  "questdb": "nodes/questdb.js",
52
52
  "questdb-mapper": "nodes/questdb-mapper.js",
53
- "questdb-type-router": "nodes/questdb-type-router.js"
53
+ "questdb-type-router": "nodes/questdb-type-router.js",
54
+ "questdb-flatten": "nodes/questdb-flatten.js",
55
+ "questdb-value": "nodes/questdb-value.js"
54
56
  }
55
57
  },
56
58
  "engines": {