bump package version to 1.0.2
[squeep-chores] / lib / chores.js
1 'use strict';
2
3 const { ChoreError } = require('./errors');
4 const { fileScope } = require('@squeep/log-helper');
5 const _fileScope = fileScope(__filename);
6
7 /**
8 * @typedef {object} ConsoleLike
9 * @property {Function} debug debug
10 * @property {Function} error error
11 */
12 /**
13 * @typedef {object} Chore
14 * @property {boolean} isRunning actively being executed
15 * @property {Function} choreFn task handler to invoke
16 * @property {number} intervalMs period to wait between invocations
17 * @property {NodeJS.Timeout=} timeoutObj active timeout
18 * @property {Date=} nextSchedule time of next invocation
19 */
20
21 /**
22 * Thin wrapper for wrangling periodic tasks with setTimeout.
23 */
24 class Chores {
25 /**
26 * @param {ConsoleLike} logger logger object
27 */
28 constructor(logger) {
29 this.logger = logger;
30 this.chores = {};
31 }
32
33 /**
34 *
35 * @param {string} choreName chore name
36 * @returns {Chore} chore
37 */
38 _getChore(choreName) {
39 const chore = this.chores[choreName]; // eslint-disable-line security/detect-object-injection
40 if (!chore) {
41 throw new ChoreError('chore does not exist');
42 }
43 return chore;
44 }
45
46 /**
47 * Register a chore task to be called every intervalMs.
48 * Zero interval will only register task, will not schedule any calls.
49 * @param {string} choreName chore name
50 * @param {() => Promise<void>} choreFn chore function
51 * @param {number} intervalMs invocation period
52 */
53 establishChore(choreName, choreFn, intervalMs = 0) {
54 const _scope = _fileScope('establishChore');
55
56 if (choreName in this.chores) {
57 this.logger.error(_scope, 'chore already exists', { choreName });
58 throw new ChoreError('chore exists');
59 }
60
61 const managedChoreFn = async () => this.runChore(choreName);
62 this.chores[choreName] = { // eslint-disable-line security/detect-object-injection
63 isRunning: false,
64 choreFn: choreFn,
65 intervalMs,
66 timeoutObj: intervalMs ? setTimeout(managedChoreFn, intervalMs).unref() : undefined,
67 nextSchedule: intervalMs ? new Date(Date.now() + intervalMs) : undefined,
68 };
69 }
70
71 /**
72 * Invoke a chore task off-schedule, with any arguments.
73 * Resets timer of any next scheduled call.
74 * @param {string} choreName chore name
75 * @param {...any} choreArgs additional args to pass to chore function
76 * @returns {Promise<void>} nothing
77 */
78 async runChore(choreName, ...choreArgs) {
79 const _scope = _fileScope('runChore');
80
81 const chore = this._getChore(choreName);
82
83 if (chore.isRunning) {
84 this.logger.debug(_scope, 'chore already running, skipping', { choreName });
85 return;
86 }
87
88 chore.isRunning = true;
89 try {
90 await chore.choreFn(...choreArgs);
91 } catch (e) {
92 this.logger.error(_scope, 'chore failed', { choreName, error: e });
93 if (choreArgs.length) {
94 // If args were supplied, was invoked off-schedule, propagate the error.
95 throw e;
96 }
97 } finally {
98 chore.isRunning = false;
99 if (chore.intervalMs) {
100 chore.timeoutObj.refresh();
101 chore.nextSchedule = new Date(Date.now() + chore.intervalMs);
102 }
103 }
104 }
105
106 /**
107 * Stop a chore from being scheduled to run.
108 * @param {string} choreName chore name
109 */
110 stopChore(choreName) {
111 const _scope = _fileScope('stopChore');
112
113 const chore = this._getChore(choreName);
114 clearTimeout(chore.timeoutObj);
115 chore.timeoutObj = undefined;
116 chore.nextSchedule = undefined;
117 this.logger.debug(_scope, 'stopped chore', { chore });
118 }
119
120 stopAllChores() {
121 for (const choreName in this.chores) {
122 this.stopChore(choreName);
123 }
124 }
125
126 } // Chores
127
128 module.exports = Chores;