modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
327 lines (274 loc) • 8.14 kB
JavaScript
// polling-manager.js
class PollingManager {
constructor() {
this.tasks = new Map();
}
addTask(options) {
const { id } = options;
if (!id) throw new Error('Polling task must have an "id".');
if (this.tasks.has(id)) {
throw new Error(`Polling task with id "${id}" already exists.`);
}
const controller = new TaskController(options);
this.tasks.set(id, controller);
if (options.immediate) {
controller.start();
}
}
updateTask(id, newOptions) {
if (!this.tasks.has(id)) {
throw new Error(`Polling task with id "${id}" does not exist.`);
}
this.removeTask(id);
this.addTask({ id, ...newOptions });
}
removeTask(id) {
const task = this.tasks.get(id);
if (task) {
task.stop();
this.tasks.delete(id);
}
}
restartTask(id) {
const task = this.tasks.get(id);
if (task) {
task.stop();
task.start();
}
}
startTask(id) {
this.tasks.get(id)?.start();
}
stopTask(id) {
this.tasks.get(id)?.stop();
}
pauseTask(id) {
this.tasks.get(id)?.pause();
}
resumeTask(id) {
this.tasks.get(id)?.resume();
}
setTaskInterval(id, interval) {
this.tasks.get(id)?.setInterval(interval);
}
isTaskRunning(id) {
return this.tasks.get(id)?.isRunning() ?? false;
}
isTaskPaused(id) {
return this.tasks.get(id)?.isPaused() ?? false;
}
getTaskState(id) {
return this.tasks.get(id)?.getState() ?? null;
}
getTaskStats(id) {
return this.tasks.get(id)?.getStats() ?? null;
}
hasTask(id) {
return this.tasks.has(id);
}
getTaskIds() {
return Array.from(this.tasks.keys());
}
clearAll() {
for (const task of this.tasks.values()) {
task.stop();
}
this.tasks.clear();
}
restartAllTasks() {
for (const task of this.tasks.values()) {
task.stop();
task.start();
}
}
pauseAllTasks() {
for (const task of this.tasks.values()) {
task.pause();
}
}
resumeAllTasks() {
for (const task of this.tasks.values()) {
task.resume();
}
}
startAllTasks() {
for (const task of this.tasks.values()) {
task.start();
}
}
stopAllTasks() {
for (const task of this.tasks.values()) {
task.stop();
}
}
getAllTaskStats() {
const stats = {};
for (const [id, task] of this.tasks.entries()) {
stats[id] = task.getStats();
}
return stats;
}
}
class TaskController {
constructor({
id,
interval,
fn,
onData,
onError,
onStart,
onStop,
onFinish,
onBeforeEach,
onRetry,
shouldRun,
onSuccess,
onFailure,
name = null,
immediate = false,
maxRetries = 0,
backoffDelay = 0,
taskTimeout = 2000
}) {
this.id = id;
this.name = name;
this.fn = Array.isArray(fn) ? fn : [fn];
this.interval = interval;
this.onData = onData;
this.onError = onError;
this.onStart = onStart;
this.onStop = onStop;
this.onFinish = onFinish;
this.onBeforeEach = onBeforeEach;
this.onRetry = onRetry;
this.shouldRun = shouldRun;
this.onSuccess = onSuccess;
this.onFailure = onFailure;
this.maxRetries = maxRetries;
this.backoffDelay = backoffDelay;
this.taskTimeout = taskTimeout;
this.stopped = true;
this.paused = false;
this.loopRunning = false;
this.executionInProgress = false;
this.stats = {
totalRuns: 0,
totalErrors: 0,
lastError: null,
lastResult: null,
lastRunTime: null,
retries: 0,
successes: 0,
failures: 0
};
}
async start() {
if (!this.stopped) return;
this.stopped = false;
this.loopRunning = true;
this.onStart?.();
this._runLoop();
}
stop() {
if (this.stopped) return;
this.stopped = true;
this.loopRunning = false;
this.onStop?.();
}
pause() {
this.paused = true;
}
resume() {
if (!this.stopped && this.paused) {
this.paused = false;
}
}
isRunning() {
return !this.stopped;
}
isPaused() {
return this.paused;
}
setInterval(ms) {
this.interval = ms;
}
getState() {
return {
stopped: this.stopped,
paused: this.paused,
running: this.loopRunning,
inProgress: this.executionInProgress
};
}
getStats() {
return { ...this.stats };
}
async _runLoop() {
while (!this.stopped) {
if (this.paused) {
await this._sleep(this.interval);
continue;
}
if (this.shouldRun && this.shouldRun() === false) {
await this._sleep(this.interval);
continue;
}
this.onBeforeEach?.();
this.executionInProgress = true;
this.stats.totalRuns++;
let success = false;
let results = [];
for (let fnIndex = 0; fnIndex < this.fn.length; fnIndex++) {
let retryCount = 0;
let result = null;
while (!this.stopped && retryCount <= this.maxRetries) {
try {
result = await this._withTimeout(this.fn[fnIndex](), this.taskTimeout);
this.stats.successes++;
this.stats.lastError = null;
success = true;
break;
} catch (err) {
retryCount++;
this.stats.totalErrors++;
this.stats.retries++;
this.stats.lastError = err;
this.onRetry?.(err, fnIndex, retryCount);
if (retryCount > this.maxRetries) {
this.stats.failures++;
this.onFailure?.(err);
this.onError?.(err, fnIndex, retryCount);
} else if (this.backoffDelay > 0) {
await this._sleep(this.backoffDelay);
}
}
}
results.push(result);
}
this.stats.lastResult = results;
this.stats.lastRunTime = Date.now();
this.executionInProgress = false;
this.onData?.(...results);
this.onFinish?.(success, results);
await this._sleep(this.interval);
}
}
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
_withTimeout(promise, timeout) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Task timed out')), timeout);
promise
.then(result => {
clearTimeout(timer);
resolve(result);
})
.catch(err => {
clearTimeout(timer);
reject(err);
});
});
}
}
module.exports = PollingManager