@halsystems/red-bacnet 1.4.7 → 2.0.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0]
4
+ ### Changed
5
+ - Replaced independent node `CoalescedJobQueue` with global queue with priority
6
+
7
+ ## [2.0.0-alpha.0]
8
+ ### Changed
9
+ - Initial testing: replaced independent node `CoalescedJobQueue` with global queue with priority
10
+
3
11
  ## [1.4.7]
4
12
  ### Added
5
13
  - Added CICD workflow
package/ROOT CHANGED
@@ -0,0 +1 @@
1
+ ROOT
@@ -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',
@@ -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
- // ---------------------------------- export ----------------------------------
11
- module.exports = {
12
- BaseJob: class {
13
- constructor() {
14
- if (new.target === module.exports.BaseJob) {
15
- throw new TypeError(ERR_CONSTRUCT_ABSTRACT_JOB);
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
- async onStart() {
20
- throw new Error(ERR_ONSTART_NOT_IMPLEMENTED);
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
- async execute() {
24
- throw new Error(ERR_EXECUTE_NOT_IMPLEMENTED);
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
- async onStop() {
28
- throw new Error(ERR_ONSTOP_NOT_IMPLEMENTED);
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
- CoalescedJobQueue: class {
33
- isRunning = false
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
- * Create a new CoalescedJob.
37
- * @param {number} [delay=1000] - Delay in milliseconds between coalesced job runs.
38
- * @param {boolean} [showLog=true] - Whether to log coalesced jobs.
39
- */
40
- constructor(delay = 1000, showLog = true,) {
41
- this.queue = [];
42
- this.delay = delay;
43
- this.showLog = showLog;
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
- * Start indefinitely running the first job in the queue.
48
- * If there are no jobs in the queue, wait for the delay before checking again.
49
- * @async
50
- */
51
- async run() {
52
- this.isRunning = true;
53
- while (this.isRunning) {
54
- if (this.queue.length > 0) {
55
- const job = this.queue.shift();
56
- try {
57
- await job.task.execute();
58
- } catch (error) {
59
- console.error(`Error executing job ${job.id}:`, error);
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
- * Add a job to the queue.
68
- * If the job with the same id is already in the queue, do not add it.
69
- * If showLog is true, log a message when a job is coalesced.
70
- * @param {Object} job - { id: string, task: BaseJob }
71
- */
72
- addJob(job) {
73
- if (this.queue.map(item => item.id).includes(job.id)) {
74
- if (this.showLog)
75
- print(`Coalesced job: ${job.id}`, true)
76
- return
77
- }
78
-
79
- this.queue.push({
80
- id: job.id,
81
- task: job.task
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
- * Stops the job queue process.
87
- */
88
- stop() {
89
- this.isRunning = false;
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
@@ -2,7 +2,6 @@
2
2
  "compilerOptions": {
3
3
  "module": "commonjs",
4
4
  "target": "ES2022",
5
- "baseUrl": ".",
6
5
  "paths": {
7
6
  "@root/*": ["./*"],
8
7
  "@tests/*": ["./tests/*"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "1.4.7",
3
+ "version": "2.0.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -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 { CoalescedJobQueue } = require('@root/common/job/core.js')
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 = new CoalescedJobQueue();
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
- if (this.job.queue.length > 0) {
63
- print('Coalesced job: discoverDevices', true)
64
- return
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: (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'task',
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>task</code> if not provided.</dd>
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 { CoalescedJobQueue } = require('@root/common/job/core.js')
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 = new CoalescedJobQueue();
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: (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'task',
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>task</code> if not provided.</dd>
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>
@@ -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 { CoalescedJobQueue } = require('@root/common/job/core.js')
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 = new CoalescedJobQueue();
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: (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'task',
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>task</code> if not provided.</dd>
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 { CoalescedJobQueue } = require('@root/common/job/core.js')
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 = new CoalescedJobQueue();
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: (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : 'task',
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) => {