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/icons/questdb-logo.png +0 -0
- package/nodes/questdb-flatten.html +45 -0
- package/nodes/questdb-flatten.js +15 -0
- package/nodes/questdb-mapper.html +281 -277
- package/nodes/questdb-type-router.html +2 -1
- package/nodes/questdb-value.html +145 -0
- package/nodes/questdb-value.js +280 -0
- package/nodes/questdb.html +290 -289
- package/nodes/questdb.js +93 -40
- package/package.json +4 -2
package/nodes/questdb.js
CHANGED
|
@@ -31,17 +31,9 @@ module.exports = function (RED) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
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) ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
+
sendError('TCP transport disconnected, reconnecting...');
|
|
330
377
|
return;
|
|
331
378
|
}
|
|
332
379
|
|
|
333
|
-
// Validate
|
|
380
|
+
// Validate table name
|
|
334
381
|
const tableName = msg.topic;
|
|
335
|
-
if (!tableName) {
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|