@halsystems/red-bacnet 1.4.6 → 2.0.0-alpha.0
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 +5 -1
- package/ROOT +1 -0
- package/common/core/constant.js +1 -0
- package/common/job/core.js +203 -68
- package/common/job/global_queue_manager.js +112 -0
- package/jsconfig.json +0 -1
- package/package.json +1 -1
- package/red-bacnet/discover_device.html +6 -0
- package/red-bacnet/discover_device.js +14 -9
- package/red-bacnet/discover_point.html +3 -1
- package/red-bacnet/discover_point.js +17 -6
- package/red-bacnet/read_point.html +4 -1
- package/red-bacnet/read_point.js +17 -6
- package/red-bacnet/write_point.html +4 -1
- package/red-bacnet/write_point.js +16 -6
package/CHANGELOG.md
CHANGED
package/ROOT
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ROOT
|
package/common/core/constant.js
CHANGED
|
@@ -27,6 +27,7 @@ module.exports = {
|
|
|
27
27
|
ERR_WRITE_POINT_INVALID_PRIORITY: 'Write point invalid priority',
|
|
28
28
|
ERR_EMPTY_DATA: 'Empty data',
|
|
29
29
|
ERR_WRITING_POINT: 'Error writing point',
|
|
30
|
+
ERR_GLOBAL_QUEUE_WORKER_ERROR: 'Global queue worker error',
|
|
30
31
|
|
|
31
32
|
// job error
|
|
32
33
|
ERR_CONSTRUCT_ABSTRACT_JOB: 'Cannot construct abstract Job class',
|
package/common/job/core.js
CHANGED
|
@@ -7,86 +7,221 @@ const {
|
|
|
7
7
|
ERR_ONSTOP_NOT_IMPLEMENTED, ERR_EXECUTE_NOT_IMPLEMENTED
|
|
8
8
|
} = require('@root/common/core/constant.js')
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Abstract base class for job implementations.
|
|
12
|
+
*
|
|
13
|
+
* This is an abstract class that cannot be instantiated directly. Subclasses must implement
|
|
14
|
+
* the lifecycle methods: onStart(), execute(), and onStop().
|
|
15
|
+
*
|
|
16
|
+
* @abstract
|
|
17
|
+
* @class BaseJob
|
|
18
|
+
* @example
|
|
19
|
+
* class MyJob extends BaseJob {
|
|
20
|
+
* async onStart() {
|
|
21
|
+
* // Initialize job resources
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* async execute() {
|
|
25
|
+
* // Perform the job's main work
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* async onStop() {
|
|
29
|
+
* // Clean up job resources
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
class BaseJob {
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new BaseJob instance.
|
|
36
|
+
*
|
|
37
|
+
* @constructor
|
|
38
|
+
* @throws {TypeError} If attempting to instantiate BaseJob directly (must subclass it).
|
|
39
|
+
*/
|
|
40
|
+
constructor() {
|
|
41
|
+
if (new.target === BaseJob) {
|
|
42
|
+
throw new TypeError(ERR_CONSTRUCT_ABSTRACT_JOB);
|
|
17
43
|
}
|
|
44
|
+
}
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Lifecycle method called when the job starts.
|
|
48
|
+
*
|
|
49
|
+
* Subclasses should override this method to perform initialization tasks such as
|
|
50
|
+
* setting up resources, connections, or state.
|
|
51
|
+
*
|
|
52
|
+
* @async
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
* @throws {Error} If not implemented by subclass.
|
|
55
|
+
*/
|
|
56
|
+
async onStart() {
|
|
57
|
+
throw new Error(ERR_ONSTART_NOT_IMPLEMENTED);
|
|
58
|
+
}
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Main execution method for the job.
|
|
62
|
+
*
|
|
63
|
+
* Subclasses must override this method to implement the core job logic.
|
|
64
|
+
* This method is called by the job queue to execute the job's work.
|
|
65
|
+
*
|
|
66
|
+
* @async
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
* @throws {Error} If not implemented by subclass.
|
|
69
|
+
*/
|
|
70
|
+
async execute() {
|
|
71
|
+
throw new Error(ERR_EXECUTE_NOT_IMPLEMENTED);
|
|
72
|
+
}
|
|
26
73
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Lifecycle method called when the job stops.
|
|
76
|
+
*
|
|
77
|
+
* Subclasses should override this method to perform cleanup tasks such as
|
|
78
|
+
* closing connections, releasing resources, or saving state.
|
|
79
|
+
*
|
|
80
|
+
* @async
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
* @throws {Error} If not implemented by subclass.
|
|
83
|
+
*/
|
|
84
|
+
async onStop() {
|
|
85
|
+
throw new Error(ERR_ONSTOP_NOT_IMPLEMENTED);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
31
88
|
|
|
32
|
-
|
|
33
|
-
|
|
89
|
+
/**
|
|
90
|
+
* A priority-based job queue that coalesces duplicate jobs.
|
|
91
|
+
*
|
|
92
|
+
* This class manages a queue of jobs that are executed asynchronously with support for:
|
|
93
|
+
* - Priority-based execution (lower priority number = higher execution priority)
|
|
94
|
+
* - Job coalescing (duplicate job IDs are not added to the queue)
|
|
95
|
+
* - FIFO ordering for jobs with equal priority
|
|
96
|
+
* - Configurable delay between job processing cycles
|
|
97
|
+
*
|
|
98
|
+
* @class CoalescedJobQueue
|
|
99
|
+
* @example
|
|
100
|
+
* const queue = new CoalescedJobQueue(1000, true, 5);
|
|
101
|
+
* queue.addJob({ id: 'job1', task: myJobInstance, priority: 2 });
|
|
102
|
+
* queue.addJob({ id: 'job2', task: anotherJobInstance, priority: 5 });
|
|
103
|
+
* await queue.run(); // Starts processing jobs indefinitely
|
|
104
|
+
*/
|
|
105
|
+
class CoalescedJobQueue {
|
|
106
|
+
/**
|
|
107
|
+
* Whether the job queue is currently running.
|
|
108
|
+
* @type {boolean}
|
|
109
|
+
*/
|
|
110
|
+
isRunning = false
|
|
34
111
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new CoalescedJobQueue instance.
|
|
114
|
+
*
|
|
115
|
+
* @constructor
|
|
116
|
+
* @param {number} [delay=100] - Delay in milliseconds between job processing cycles.
|
|
117
|
+
* @param {boolean} [showLog=true] - Whether to log when jobs are coalesced (duplicate IDs).
|
|
118
|
+
* @param {number} [defaultPriority=10] - Default priority for jobs when not specified (0-10, lower = higher priority).
|
|
119
|
+
*/
|
|
120
|
+
constructor(delay = 100, showLog = true, defaultPriority = 10) {
|
|
121
|
+
this.queue = [];
|
|
122
|
+
this.delay = delay;
|
|
123
|
+
this.showLog = showLog;
|
|
124
|
+
this.defaultPriority = defaultPriority;
|
|
125
|
+
}
|
|
45
126
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Starts indefinitely running jobs from the queue.
|
|
129
|
+
*
|
|
130
|
+
* Continuously processes jobs in the queue until stop() is called. Jobs are executed
|
|
131
|
+
* in priority order (lower priority number = higher execution priority). If multiple
|
|
132
|
+
* jobs have the same priority, they are executed in FIFO order (earliest first).
|
|
133
|
+
* If the queue is empty, waits for the configured delay before checking again.
|
|
134
|
+
*
|
|
135
|
+
* @async
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
* @example
|
|
138
|
+
* const queue = new CoalescedJobQueue();
|
|
139
|
+
* // Start the queue in the background
|
|
140
|
+
* queue.run().catch(err => console.error('Queue error:', err));
|
|
141
|
+
* // Add jobs...
|
|
142
|
+
* // Later, stop the queue
|
|
143
|
+
* queue.stop();
|
|
144
|
+
*/
|
|
145
|
+
async run() {
|
|
146
|
+
this.isRunning = true;
|
|
147
|
+
while (this.isRunning) {
|
|
148
|
+
if (this.queue.length > 0) {
|
|
149
|
+
// Get the job with the lowest priority number (highest priority)
|
|
150
|
+
// If priorities are equal, use FIFO (earliest timestamp first)
|
|
151
|
+
const jobIndex = this.queue.reduce((minIndex, job, index) => {
|
|
152
|
+
const minJob = this.queue[minIndex];
|
|
153
|
+
if (job.priority < minJob.priority) {
|
|
154
|
+
return index;
|
|
155
|
+
} else if (job.priority === minJob.priority && job.timestamp < minJob.timestamp) {
|
|
156
|
+
return index;
|
|
60
157
|
}
|
|
158
|
+
return minIndex;
|
|
159
|
+
}, 0);
|
|
160
|
+
|
|
161
|
+
const job = this.queue.splice(jobIndex, 1)[0];
|
|
162
|
+
try {
|
|
163
|
+
await job.task.execute();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`Error executing job ${job.id}:`, error);
|
|
61
166
|
}
|
|
62
|
-
await new Promise(resolve => setTimeout(resolve, this.delay));
|
|
63
167
|
}
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, this.delay));
|
|
64
169
|
}
|
|
170
|
+
}
|
|
65
171
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Adds a job to the queue with optional priority.
|
|
174
|
+
*
|
|
175
|
+
* If a job with the same ID already exists in the queue, it will not be added again
|
|
176
|
+
* (job coalescing). If showLog is enabled, a message will be logged when a job is coalesced.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} job - The job object to add to the queue.
|
|
179
|
+
* @param {string} job.id - Unique identifier for the job. Duplicate IDs will be coalesced.
|
|
180
|
+
* @param {Object} job.task - The job task instance (must be a subclass of BaseJob).
|
|
181
|
+
* @param {number} [job.priority] - Priority level for execution (0-10, lower = higher priority).
|
|
182
|
+
* If not specified, uses this.defaultPriority.
|
|
183
|
+
* @returns {void}
|
|
184
|
+
* @example
|
|
185
|
+
* queue.addJob({
|
|
186
|
+
* id: 'discover-points',
|
|
187
|
+
* task: new DiscoverPointsJob(),
|
|
188
|
+
* priority: 3
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
addJob(job) {
|
|
192
|
+
if (this.queue.map(item => item.id).includes(job.id)) {
|
|
193
|
+
if (this.showLog)
|
|
194
|
+
print(`Coalesced job: ${job.id}`, true)
|
|
195
|
+
return
|
|
83
196
|
}
|
|
84
197
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
198
|
+
const priority = typeof job.priority === 'number' ? job.priority : this.defaultPriority;
|
|
199
|
+
|
|
200
|
+
this.queue.push({
|
|
201
|
+
id: job.id,
|
|
202
|
+
task: job.task,
|
|
203
|
+
priority: priority,
|
|
204
|
+
timestamp: Date.now()
|
|
205
|
+
})
|
|
91
206
|
}
|
|
92
|
-
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stops the job queue process.
|
|
210
|
+
*
|
|
211
|
+
* Sets isRunning to false, which will cause the run() loop to exit after the current
|
|
212
|
+
* job completes and the next delay cycle finishes.
|
|
213
|
+
*
|
|
214
|
+
* @returns {void}
|
|
215
|
+
* @example
|
|
216
|
+
* queue.stop();
|
|
217
|
+
*/
|
|
218
|
+
stop() {
|
|
219
|
+
this.isRunning = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------- export ----------------------------------
|
|
224
|
+
module.exports = {
|
|
225
|
+
BaseJob,
|
|
226
|
+
CoalescedJobQueue
|
|
227
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./_alias.js');
|
|
4
|
+
|
|
5
|
+
const { CoalescedJobQueue } = require('@root/common/job/core.js');
|
|
6
|
+
const { ERR_GLOBAL_QUEUE_WORKER_ERROR } = require('@root/common/core/constant.js');
|
|
7
|
+
|
|
8
|
+
// ---------------------------------- Singleton State ----------------------------------
|
|
9
|
+
let globalQueue = null;
|
|
10
|
+
let workerPromise = null;
|
|
11
|
+
let workerRunning = false;
|
|
12
|
+
let stopping = false;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------- Worker Control ----------------------------------
|
|
15
|
+
function startWorker() {
|
|
16
|
+
if (!globalQueue || stopping || workerRunning) return;
|
|
17
|
+
|
|
18
|
+
workerRunning = true;
|
|
19
|
+
|
|
20
|
+
const loop = async () => {
|
|
21
|
+
try {
|
|
22
|
+
await globalQueue.run();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`${ERR_GLOBAL_QUEUE_WORKER_ERROR}:`, error);
|
|
25
|
+
} finally {
|
|
26
|
+
workerRunning = false;
|
|
27
|
+
workerPromise = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Self-healing: restart after short delay if still active
|
|
31
|
+
if (globalQueue && !stopping) {
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
33
|
+
startWorker();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
workerPromise = loop();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------- Teardown Helper ----------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Shared teardown logic used by both shutdown() and reset().
|
|
43
|
+
* Returns a promise that resolves once any in-flight worker has finished.
|
|
44
|
+
*/
|
|
45
|
+
async function teardown() {
|
|
46
|
+
stopping = true;
|
|
47
|
+
|
|
48
|
+
if (globalQueue) {
|
|
49
|
+
globalQueue.stop();
|
|
50
|
+
globalQueue = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Await in-flight work rather than abandoning it
|
|
54
|
+
if (workerPromise) {
|
|
55
|
+
try {
|
|
56
|
+
await workerPromise;
|
|
57
|
+
} catch {
|
|
58
|
+
// Worker errors are already logged inside loop(); swallow here
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
workerPromise = null;
|
|
63
|
+
workerRunning = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------- Module Exports ----------------------------------
|
|
67
|
+
module.exports = {
|
|
68
|
+
/**
|
|
69
|
+
* Get or create the global queue singleton.
|
|
70
|
+
* Safe to call multiple times; starts the worker if not already running.
|
|
71
|
+
*/
|
|
72
|
+
getQueue() {
|
|
73
|
+
if (!globalQueue) {
|
|
74
|
+
stopping = false;
|
|
75
|
+
globalQueue = new CoalescedJobQueue(100, true, 5);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!globalQueue.isRunning && !stopping) {
|
|
79
|
+
startWorker();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return globalQueue;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize queue (Node-RED startup hook).
|
|
87
|
+
*/
|
|
88
|
+
async initialize() {
|
|
89
|
+
this.getQueue();
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Graceful shutdown. Stops the queue and awaits any in-flight worker.
|
|
94
|
+
* After this, the module is inert until getQueue() is called again.
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
98
|
+
async shutdown() {
|
|
99
|
+
await teardown();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Full cycle reset (testing utility).
|
|
104
|
+
* Tears down the current queue and immediately initialises a fresh one.
|
|
105
|
+
*
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
async reset() {
|
|
109
|
+
await teardown();
|
|
110
|
+
this.getQueue();
|
|
111
|
+
},
|
|
112
|
+
};
|
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -82,6 +82,12 @@
|
|
|
82
82
|
|
|
83
83
|
<dt class="optional">msg.highLimit <span class="property-type">number</span></dt>
|
|
84
84
|
<dd>Default config will be used if not configured.</dd>
|
|
85
|
+
|
|
86
|
+
<dt class="optional">msg.id <span class="property-type">string | number</span></dt>
|
|
87
|
+
<dd> Job ID. Default to <code>discoverDevices</code> if not provided.</dd>
|
|
88
|
+
|
|
89
|
+
<dt class="optional">msg.priority <span class="property-type">number</span></dt>
|
|
90
|
+
<dd> Job priority in queue. Default to <code>5</code> if not provided.</dd>
|
|
85
91
|
</dl>
|
|
86
92
|
|
|
87
93
|
<h3>Outputs</h3>
|
|
@@ -6,7 +6,7 @@ const EventEmitter = require('events');
|
|
|
6
6
|
const { print } = require('@root/common/core/util.js')
|
|
7
7
|
const { nowFormatted } = require('@root/common/core/util.js')
|
|
8
8
|
const { DiscoverDeviceJob } = require('@root/common/job/discover_device.js')
|
|
9
|
-
const {
|
|
9
|
+
const { getQueue } = require('@root/common/job/global_queue_manager.js')
|
|
10
10
|
const {
|
|
11
11
|
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT, EVENT_CLOSE
|
|
12
12
|
} = require('@root/common/core/constant.js')
|
|
@@ -32,9 +32,8 @@ module.exports = function (RED) {
|
|
|
32
32
|
// events
|
|
33
33
|
this.#subscribeListeners();
|
|
34
34
|
|
|
35
|
-
// configure job queue
|
|
36
|
-
this.job =
|
|
37
|
-
this.job.run();
|
|
35
|
+
// configure job queue - use global queue
|
|
36
|
+
this.job = getQueue();
|
|
38
37
|
|
|
39
38
|
// init task
|
|
40
39
|
this.task = new DiscoverDeviceJob(
|
|
@@ -59,9 +58,12 @@ module.exports = function (RED) {
|
|
|
59
58
|
as a result, workaround is introduce before strategy to reuse discoverDeviceJob instead
|
|
60
59
|
of creating new one every time onInput is triggered
|
|
61
60
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'discoverDevices';
|
|
62
|
+
|
|
63
|
+
if (this.job.queue.map(item => item.id).includes(jobId)) {
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
print(`Coalesced job: ${jobId}`, true)
|
|
66
|
+
return;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
this.task.network = (msg.network === undefined) ? this.network : msg.network;
|
|
@@ -69,9 +71,12 @@ module.exports = function (RED) {
|
|
|
69
71
|
this.task.highLimit = (msg.highLimit === undefined) ? this.highLimit : msg.highLimit;
|
|
70
72
|
|
|
71
73
|
this.job.addJob({
|
|
72
|
-
id:
|
|
73
|
-
task: this.task
|
|
74
|
+
id: jobId,
|
|
75
|
+
task: this.task,
|
|
76
|
+
priority: Number.isFinite(msg.priority) ? msg.priority : 5,
|
|
74
77
|
});
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
|
|
75
80
|
});
|
|
76
81
|
|
|
77
82
|
// @ts-ignore
|
|
@@ -101,7 +101,9 @@
|
|
|
101
101
|
<dt>msg.devices <span class="property-type">array</span></dt>
|
|
102
102
|
<dd> A list of devices generated from discover device node (modify values if required)</dd>
|
|
103
103
|
<dt class="optional">msg.id <span class="property-type">string | number</span></dt>
|
|
104
|
-
<dd> Job ID. Default to <code>
|
|
104
|
+
<dd> Job ID. Default to <code>discoverPoints</code> if not provided.</dd>
|
|
105
|
+
<dt class="optional">msg.priority <span class="property-type">number</span></dt>
|
|
106
|
+
<dd> Job priority in queue. Default to <code>5</code> if not provided.</dd>
|
|
105
107
|
</dl>
|
|
106
108
|
|
|
107
109
|
<h3>Outputs</h3>
|
|
@@ -3,9 +3,10 @@ require('./_alias.js');
|
|
|
3
3
|
|
|
4
4
|
const EventEmitter = require('events');
|
|
5
5
|
|
|
6
|
+
const { print } = require('@root/common/core/util.js')
|
|
6
7
|
const { nowFormatted } = require('@root/common/core/util.js')
|
|
7
8
|
const { DiscoverPointJob } = require('@root/common/job/discover_point.js')
|
|
8
|
-
const {
|
|
9
|
+
const { getQueue } = require('@root/common/job/global_queue_manager.js')
|
|
9
10
|
const {
|
|
10
11
|
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT
|
|
11
12
|
} = require('@root/common/core/constant.js')
|
|
@@ -30,9 +31,8 @@ module.exports = function (RED) {
|
|
|
30
31
|
// events
|
|
31
32
|
this.#subscribeListeners();
|
|
32
33
|
|
|
33
|
-
// configure job queue
|
|
34
|
-
this.job =
|
|
35
|
-
this.job.run();
|
|
34
|
+
// configure job queue - use global queue
|
|
35
|
+
this.job = getQueue();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
#subscribeListeners() {
|
|
@@ -41,6 +41,14 @@ module.exports = function (RED) {
|
|
|
41
41
|
*/
|
|
42
42
|
// @ts-ignore
|
|
43
43
|
this.on(EVENT_INPUT, async function (msg) {
|
|
44
|
+
const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'discoverPoints';
|
|
45
|
+
|
|
46
|
+
if (this.job.queue.map(item => item.id).includes(jobId)) {
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
print(`Coalesced job: ${jobId}`, true)
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
const task = new DiscoverPointJob(
|
|
45
53
|
this.client, this.#eventEmitter, msg?.devices, this.discoverMode, this.readMethod,
|
|
46
54
|
this.groupExportDeviceCount, this.maxConcurrentDeviceRead,
|
|
@@ -48,9 +56,12 @@ module.exports = function (RED) {
|
|
|
48
56
|
);
|
|
49
57
|
|
|
50
58
|
this.job.addJob({
|
|
51
|
-
id:
|
|
52
|
-
task: task
|
|
59
|
+
id: jobId,
|
|
60
|
+
task: task,
|
|
61
|
+
priority: Number.isFinite(msg.priority) ? msg.priority : 5,
|
|
53
62
|
});
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
|
|
54
65
|
});
|
|
55
66
|
|
|
56
67
|
this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
|
|
@@ -88,7 +88,10 @@
|
|
|
88
88
|
<dd> A list of points generated from discover point node (modify values if required)</dd>
|
|
89
89
|
|
|
90
90
|
<dt class="optional">msg.id <span class="property-type">string | number</span></dt>
|
|
91
|
-
<dd> Job ID. Default to <code>
|
|
91
|
+
<dd> Job ID. Default to <code>readPoints</code> if not provided.</dd>
|
|
92
|
+
|
|
93
|
+
<dt class="optional">msg.priority <span class="property-type">number</span></dt>
|
|
94
|
+
<dd> Job priority in queue. Default to <code>2</code> if not provided.</dd>
|
|
92
95
|
</dl>
|
|
93
96
|
|
|
94
97
|
<h3>Outputs</h3>
|
package/red-bacnet/read_point.js
CHANGED
|
@@ -3,9 +3,10 @@ require('./_alias.js');
|
|
|
3
3
|
|
|
4
4
|
const EventEmitter = require('events');
|
|
5
5
|
|
|
6
|
+
const { print } = require('@root/common/core/util.js')
|
|
6
7
|
const { nowFormatted } = require('@root/common/core/util.js')
|
|
7
8
|
const { ReadPointJob } = require('@root/common/job/read_point.js')
|
|
8
|
-
const {
|
|
9
|
+
const { getQueue } = require('@root/common/job/global_queue_manager.js')
|
|
9
10
|
const {
|
|
10
11
|
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT
|
|
11
12
|
} = require('@root/common/core/constant.js')
|
|
@@ -29,9 +30,8 @@ module.exports = function (RED) {
|
|
|
29
30
|
// events
|
|
30
31
|
this.#subscribeListeners();
|
|
31
32
|
|
|
32
|
-
// configure job queue
|
|
33
|
-
this.job =
|
|
34
|
-
this.job.run();
|
|
33
|
+
// configure job queue - use global queue
|
|
34
|
+
this.job = getQueue();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
#subscribeListeners() {
|
|
@@ -51,10 +51,21 @@ module.exports = function (RED) {
|
|
|
51
51
|
this.concurrentTaskDelay
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
+
const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'readPoints';
|
|
55
|
+
|
|
56
|
+
if (this.job.queue.map(item => item.id).includes(jobId)) {
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
print(`Coalesced job: ${jobId}`, true)
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
this.job.addJob({
|
|
55
|
-
id:
|
|
56
|
-
task: task
|
|
63
|
+
id: jobId,
|
|
64
|
+
task: task,
|
|
65
|
+
priority: Number.isFinite(msg.priority) ? msg.priority : 2,
|
|
57
66
|
});
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
|
|
58
69
|
});
|
|
59
70
|
|
|
60
71
|
this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
|
|
@@ -76,7 +76,10 @@
|
|
|
76
76
|
<dd> Eg: {'VAV_1.ZoneTemp_SP': 21.5, ...}</dd>
|
|
77
77
|
|
|
78
78
|
<dt class="optional">msg.id <span class="property-type">string | number</span></dt>
|
|
79
|
-
<dd> Job ID. Default to <code>
|
|
79
|
+
<dd> Job ID. Default to <code>writePoints</code> if not provided.</dd>
|
|
80
|
+
|
|
81
|
+
<dt class="optional">msg.priority <span class="property-type">number</span></dt>
|
|
82
|
+
<dd> Job priority in queue. Default to <code>2</code> if not provided.</dd>
|
|
80
83
|
</dl>
|
|
81
84
|
|
|
82
85
|
<h3>Details</h3>
|
|
@@ -3,9 +3,10 @@ require('./_alias.js');
|
|
|
3
3
|
|
|
4
4
|
const EventEmitter = require('events');
|
|
5
5
|
|
|
6
|
+
const { print } = require('@root/common/core/util.js')
|
|
6
7
|
const { nowFormatted } = require('@root/common/core/util.js')
|
|
7
8
|
const { WritePointJob } = require('@root/common/job/write_point.js')
|
|
8
|
-
const {
|
|
9
|
+
const { getQueue } = require('@root/common/job/global_queue_manager.js')
|
|
9
10
|
const {
|
|
10
11
|
EVENT_UPDATE_STATUS, EVENT_ERROR, EVENT_INPUT, EVENT_OUTPUT
|
|
11
12
|
} = require('@root/common/core/constant.js')
|
|
@@ -28,9 +29,8 @@ module.exports = function (RED) {
|
|
|
28
29
|
// events
|
|
29
30
|
this.#subscribeListeners();
|
|
30
31
|
|
|
31
|
-
// configure job queue
|
|
32
|
-
this.job =
|
|
33
|
-
this.job.run();
|
|
32
|
+
// configure job queue - use global queue
|
|
33
|
+
this.job = getQueue();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
#subscribeListeners() {
|
|
@@ -49,11 +49,21 @@ module.exports = function (RED) {
|
|
|
49
49
|
this.maxConcurrentPointWrite,
|
|
50
50
|
this.concurrentTaskDelay
|
|
51
51
|
);
|
|
52
|
+
const jobId = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'writePoints';
|
|
53
|
+
|
|
54
|
+
if (this.job.queue.map(item => item.id).includes(jobId)) {
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
print(`Coalesced job: ${jobId}`, true)
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
52
59
|
|
|
53
60
|
this.job.addJob({
|
|
54
|
-
id:
|
|
55
|
-
task: task
|
|
61
|
+
id: jobId,
|
|
62
|
+
task: task,
|
|
63
|
+
priority: Number.isFinite(msg.priority) ? msg.priority : 2,
|
|
56
64
|
});
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
this.status({ fill: 'yellow', shape: 'dot', text: `in queue` });
|
|
57
67
|
});
|
|
58
68
|
|
|
59
69
|
this.#eventEmitter.on(EVENT_OUTPUT, (data) => {
|