Skip to content

Commit 0e594c4

Browse files
committed
TaskRunner, Task class and index
1 parent eff5fbb commit 0e594c4

3 files changed

Lines changed: 329 additions & 0 deletions

File tree

index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Task runner and task constructors
3+
* @type {{Task: Task, TaskRunner: TaskRunner}}
4+
*/
5+
module.exports = {
6+
Task : require( './src/classes/Task' ),
7+
TaskRunner : require( './src/classes/TaskRunner' ),
8+
};

src/classes/Task.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Requires
3+
*/
4+
const { Exception, Timer, strand } = require( '@squirrel-forge/node-util' );
5+
const { isPojo, cloneObject, mergeObject } = require( '@squirrel-forge/node-objection' );
6+
7+
/**
8+
* Task exception
9+
* @class
10+
*/
11+
class TaskException extends Exception {}
12+
13+
/**
14+
* Task class
15+
* @abstract
16+
* @class
17+
*/
18+
class Task {
19+
20+
/**
21+
* Constructor
22+
* @constructor
23+
* @param {TaskRunner} runner - Runner instance
24+
* @param {null|Object} options - Task options object
25+
* @param {Object} defaults - Task default options
26+
*/
27+
constructor( runner, options = null, defaults = {} ) {
28+
29+
// Require task id
30+
if ( !defaults.id ) {
31+
defaults.id = strand();
32+
}
33+
34+
/**
35+
* Timer
36+
* @public
37+
* @type {Timer}
38+
*/
39+
this.timer = new Timer();
40+
41+
/**
42+
* Runner instance
43+
* @public
44+
* @property
45+
* @type {TaskRunner}
46+
*/
47+
this.runner = runner;
48+
49+
/**
50+
* Options defults
51+
* @protected
52+
* @property
53+
* @type {Object}
54+
*/
55+
this._defaults = defaults;
56+
57+
/**
58+
* Options
59+
* @protected
60+
* @property
61+
* @type {Object}
62+
*/
63+
this._ = cloneObject( this._defaults, true );
64+
65+
// Apply custom options
66+
if ( options && isPojo( options ) ) {
67+
mergeObject( this._, options, true, true, true, true );
68+
}
69+
}
70+
71+
/**
72+
* Generate stats object
73+
* @param {Object} data - Stats data
74+
* @return {Object} - Stats data
75+
*/
76+
stats( data = {} ) {
77+
78+
// Force id and set processing time
79+
data.id = this._.id;
80+
data.time = this.timer.end( 'construct' );
81+
return data;
82+
}
83+
84+
/**
85+
* Run task
86+
* @public
87+
* @abstract
88+
* @return {Promise<null|Object>} - Null on fail, stats object on success
89+
*/
90+
async run() {
91+
throw new TaskException( 'Task must implement a run method' );
92+
}
93+
}
94+
95+
// Export Exception as static property constructor
96+
Task.TaskException = TaskException;
97+
module.exports = Task;

src/classes/TaskRunner.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* Requires
3+
*/
4+
const { Exception } = require( '@squirrel-forge/node-util' );
5+
const { isPojo } = require( '@squirrel-forge/node-objection' );
6+
7+
/**
8+
* TaskRunner exception
9+
* @class
10+
*/
11+
class TaskRunnerException extends Exception {}
12+
13+
/**
14+
* @typedef {Object|Array} TaskMap
15+
*/
16+
17+
/**
18+
* TaskRunner class
19+
* @class
20+
*/
21+
class TaskRunner {
22+
23+
/**
24+
* Constructor
25+
* @constructor
26+
* @param {boolean} strict - Strict mode, default: true
27+
* @param {Function} notify - Notification callback for non strict mode
28+
* @param {Function} taskParser - Parses task data before processing
29+
*/
30+
constructor( strict = true, notify = null, taskParser = null ) {
31+
32+
/**
33+
* Strict mode
34+
* @protected
35+
* @property
36+
* @type {boolean}
37+
*/
38+
this._strict = strict;
39+
40+
/**
41+
* Notify in non strict callback
42+
* @protected
43+
* @property
44+
* @type {Function}
45+
*/
46+
this._notify = notify;
47+
48+
/**
49+
* Parse/modify task data before construction
50+
* @protected
51+
* @property
52+
* @type {Function}
53+
*/
54+
this._parser = taskParser;
55+
56+
/**
57+
* Available task types
58+
* @protected
59+
* @property
60+
* @type {Object}
61+
*/
62+
this._types = {};
63+
}
64+
65+
/**
66+
* Throw or notify on error
67+
* @public
68+
* @param {TaskRunnerException} err - Exception instance
69+
* @throws {TaskRunnerException|TaskException}
70+
* @return {void}
71+
*/
72+
error( err ) {
73+
if ( this._strict ) throw err;
74+
if ( this._notify ) this._notify( err );
75+
}
76+
77+
/**
78+
* Register task constructor
79+
* @public
80+
* @param {string} name Task type
81+
* @param {Function} TaskConstructor - Task constructor
82+
* @param {boolean} replace - Replace existing task constructor
83+
* @return {void}
84+
*/
85+
register( name, TaskConstructor, replace = false ) {
86+
if ( typeof TaskConstructor !== 'function' ) {
87+
this.error( new TaskRunnerException( 'Task constructor must be a constructor: ' + name ) );
88+
return;
89+
}
90+
if ( !replace && this._types[ name ] ) {
91+
this.error( new TaskRunnerException( 'Task constructor already defined: ' + name ) );
92+
} else {
93+
this._types[ name ] = TaskConstructor;
94+
}
95+
}
96+
97+
/**
98+
* Get constructor
99+
* @public
100+
* @param {string} name - Task type
101+
* @return {null|Function} - Task constructor if available
102+
*/
103+
getTaskConstructor( name ) {
104+
if ( this._types[ name ] ) {
105+
return this._types[ name ];
106+
}
107+
return null;
108+
}
109+
110+
/**
111+
* Run tasks in parallel
112+
* @public
113+
* @param {Array<Task|TaskMap>} taskMap - Task map
114+
* @return {Promise<Object[]>} - Array of nulls and stats objects
115+
*/
116+
parallel( taskMap ) {
117+
const tasks = [];
118+
119+
// Run in parallel and collect promises
120+
for ( let i = 0; i < taskMap.length; i++ ) {
121+
tasks.push( this.run( taskMap[ i ] ) );
122+
}
123+
return Promise.all( tasks );
124+
}
125+
126+
/**
127+
* Run tasks in sequence
128+
* @public
129+
* @param {Object} taskMap - Task map
130+
* @return {Promise<Object>} - Object of nulls and stats objects
131+
*/
132+
async sequence( taskMap ) {
133+
const stats = {};
134+
const entries = Object.entries( taskMap );
135+
136+
// Process each map/task in order
137+
for ( let i = 0; i < entries.length; i++ ) {
138+
const [ name, value ] = entries[ i ];
139+
stats[ name ] = await this.run( value );
140+
}
141+
return stats;
142+
}
143+
144+
/**
145+
* Construct and run task
146+
* @public
147+
* @param {Object} taskData - Task data object
148+
* @return {Promise<null|Object>} - Null or stats object
149+
*/
150+
async task( taskData ) {
151+
152+
// Allow external parser to modify the taskData object
153+
if ( this._parser ) {
154+
this._parser( taskData );
155+
}
156+
157+
// Use custom id if set, useful when when running the same task type multiple times
158+
if ( taskData.id ) {
159+
taskData.options.id = taskData.id;
160+
}
161+
162+
// Get the task constructor
163+
const TaskConstructor = this.getTaskConstructor( taskData.type );
164+
if ( !TaskConstructor ) {
165+
this.error( new TaskRunnerException( 'Invalid or unknown task type: ' + taskData.type ) );
166+
return null;
167+
}
168+
169+
// Attempt to create task
170+
let stats = null, task;
171+
try {
172+
task = new TaskConstructor( this, taskData.options );
173+
} catch ( e ) {
174+
this.error( new TaskRunnerException( 'Failed to construct task: ' + taskData.type, e ) );
175+
return null;
176+
}
177+
178+
// Attempt to run task
179+
try {
180+
stats = await task.run( ...taskData.args );
181+
} catch ( e ) {
182+
this.error( new TaskRunnerException( 'Failed to run task: ' + taskData.type, e ) );
183+
return null;
184+
}
185+
return stats;
186+
}
187+
188+
/**
189+
* Run task map
190+
* @public
191+
* @param {TaskMap} taskMap - Task map
192+
* @return {Promise<any[]|null|Object>} - Null, stats object or Array/Object map of nulls and stats objects
193+
*/
194+
async run( taskMap ) {
195+
let stats;
196+
197+
// Arrays are run in parallel, order does not matter
198+
if ( taskMap instanceof Array ) {
199+
stats = await this.parallel( taskMap );
200+
} else if ( isPojo( taskMap ) ) {
201+
202+
// Assume it's a task if it has a type property
203+
if ( taskMap.type ) {
204+
stats = await this.task( taskMap );
205+
} else {
206+
207+
// All other objects are assumed sequence maps and are processed in order
208+
stats = await this.sequence( taskMap );
209+
}
210+
} else {
211+
212+
// If we have invalid data we might break
213+
this.error( new TaskRunnerException( 'Invalid taskMap type: ' + typeof taskMap ) );
214+
return null;
215+
}
216+
217+
// Return stats object
218+
return stats;
219+
}
220+
}
221+
222+
// Export Exception as static property constructor
223+
TaskRunner.TaskRunnerException = TaskRunnerException;
224+
module.exports = TaskRunner;

0 commit comments

Comments
 (0)