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