pg-boss
Version:
Queueing jobs in Node.js using PostgreSQL like a boss
422 lines (336 loc) • 15.8 kB
JavaScript
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var assert = require('assert');
var EventEmitter = require('events');
var Promise = require('bluebird');
var Worker = require('./worker');
var plans = require('./plans');
var Attorney = require('./attorney');
var completedJobPrefix = plans.completedJobPrefix;
var events = {
error: 'error'
};
var Manager = function (_EventEmitter) {
_inherits(Manager, _EventEmitter);
function Manager(db, config) {
_classCallCheck(this, Manager);
var _this = _possibleConstructorReturn(this, (Manager.__proto__ || Object.getPrototypeOf(Manager)).call(this));
_this.config = config;
_this.db = db;
_this.events = events;
_this.subscriptions = {};
_this.nextJobCommand = plans.fetchNextJob(config.schema);
_this.insertJobCommand = plans.insertJob(config.schema);
_this.completeJobsCommand = plans.completeJobs(config.schema);
_this.cancelJobsCommand = plans.cancelJobs(config.schema);
_this.failJobsCommand = plans.failJobs(config.schema);
_this.deleteQueueCommand = plans.deleteQueue(config.schema);
_this.deleteAllQueuesCommand = plans.deleteAllQueues(config.schema);
// exported api to index
_this.functions = [_this.fetch, _this.complete, _this.cancel, _this.fail, _this.publish, _this.subscribe, _this.unsubscribe, _this.onComplete, _this.offComplete, _this.fetchCompleted, _this.publishDebounced, _this.publishThrottled, _this.publishOnce, _this.publishAfter, _this.deleteQueue, _this.deleteAllQueues];
return _this;
}
_createClass(Manager, [{
key: 'stop',
value: function stop() {
var _this2 = this;
Object.keys(this.subscriptions).forEach(function (name) {
return _this2.unsubscribe(name);
});
this.subscriptions = {};
return Promise.resolve(true);
}
}, {
key: 'subscribe',
value: function subscribe(name) {
var _this3 = this;
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return Attorney.checkSubscribeArgs(name, args).then(function (_ref) {
var options = _ref.options,
callback = _ref.callback;
return _this3.watch(name, options, callback);
});
}
}, {
key: 'onComplete',
value: function onComplete(name) {
var _this4 = this;
for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
return Attorney.checkSubscribeArgs(name, args).then(function (_ref2) {
var options = _ref2.options,
callback = _ref2.callback;
return _this4.watch(completedJobPrefix + name, options, callback);
});
}
}, {
key: 'watch',
value: function watch(name, options, callback) {
var _this5 = this;
// watch() is always nested in a promise, so assert()s are welcome
if ('newJobCheckInterval' in options || 'newJobCheckIntervalSeconds' in options) options = Attorney.applyNewJobCheckInterval(options);else options.newJobCheckInterval = this.config.newJobCheckInterval;
if ('teamConcurrency' in options) {
var teamConcurrencyErrorMessage = 'teamConcurrency must be an integer between 1 and 1000';
assert(Number.isInteger(options.teamConcurrency) && options.teamConcurrency >= 1 && options.teamConcurrency <= 1000, teamConcurrencyErrorMessage);
}
if ('teamSize' in options) {
var teamSizeErrorMessage = 'teamSize must be an integer > 0';
assert(Number.isInteger(options.teamSize) && options.teamSize >= 1, teamSizeErrorMessage);
}
if ('batchSize' in options) {
var batchSizeErrorMessage = 'batchSize must be an integer > 0';
assert(Number.isInteger(options.batchSize) && options.batchSize >= 1, batchSizeErrorMessage);
}
var sendItBruh = function sendItBruh(jobs) {
if (!jobs) return Promise.resolve();
// If you get a batch, for now you should use complete() so you can control
// whether individual or group completion responses apply to your use case
// Failing will fail all fetched jobs
if (options.batchSize) return Promise.all([callback(jobs)]).catch(function (err) {
return _this5.fail(jobs.map(function (job) {
return job.id;
}), err);
});
// either no option was set, or teamSize was used
return Promise.map(jobs, function (job) {
return callback(job).then(function (value) {
return _this5.complete(job.id, value);
}).catch(function (err) {
return _this5.fail(job.id, err);
});
}, { concurrency: options.teamConcurrency || 2 });
};
var onError = function onError(error) {
return _this5.emit(events.error, error);
};
var workerConfig = {
name: name,
fetch: function fetch() {
return _this5.fetch(name, options.batchSize || options.teamSize || 1);
},
onFetch: function onFetch(jobs) {
return sendItBruh(jobs).catch(function (err) {
return null;
});
}, // just send it, bruh
onError: onError,
interval: options.newJobCheckInterval
};
var worker = new Worker(workerConfig);
worker.start();
if (!this.subscriptions[name]) this.subscriptions[name] = { workers: [] };
this.subscriptions[name].workers.push(worker);
return Promise.resolve(true);
}
}, {
key: 'unsubscribe',
value: function unsubscribe(name) {
if (!this.subscriptions[name]) return Promise.reject('No subscriptions for ' + name + ' were found.');
this.subscriptions[name].workers.forEach(function (worker) {
return worker.stop();
});
delete this.subscriptions[name];
return Promise.resolve(true);
}
}, {
key: 'offComplete',
value: function offComplete(name) {
return this.unsubscribe(completedJobPrefix + name);
}
}, {
key: 'publish',
value: function publish() {
var _this6 = this;
for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return Attorney.checkPublishArgs(args).then(function (_ref3) {
var name = _ref3.name,
data = _ref3.data,
options = _ref3.options;
return _this6.createJob(name, data, options);
});
}
}, {
key: 'publishOnce',
value: function publishOnce(name, data, options, key) {
var _this7 = this;
return Attorney.checkPublishArgs([name, data, options]).then(function (_ref4) {
var name = _ref4.name,
data = _ref4.data,
options = _ref4.options;
options.singletonKey = key;
return _this7.createJob(name, data, options);
});
}
}, {
key: 'publishAfter',
value: function publishAfter(name, data, options, after) {
var _this8 = this;
return Attorney.checkPublishArgs([name, data, options]).then(function (_ref5) {
var name = _ref5.name,
data = _ref5.data,
options = _ref5.options;
options.startAfter = after;
return _this8.createJob(name, data, options);
});
}
}, {
key: 'publishThrottled',
value: function publishThrottled(name, data, options, seconds, key) {
var _this9 = this;
return Attorney.checkPublishArgs([name, data, options]).then(function (_ref6) {
var name = _ref6.name,
data = _ref6.data,
options = _ref6.options;
options.singletonSeconds = seconds;
options.singletonNextSlot = false;
options.singletonKey = key;
return _this9.createJob(name, data, options);
});
}
}, {
key: 'publishDebounced',
value: function publishDebounced(name, data, options, seconds, key) {
var _this10 = this;
return Attorney.checkPublishArgs([name, data, options]).then(function (_ref7) {
var name = _ref7.name,
data = _ref7.data,
options = _ref7.options;
options.singletonSeconds = seconds;
options.singletonNextSlot = true;
options.singletonKey = key;
return _this10.createJob(name, data, options);
});
}
}, {
key: 'createJob',
value: function createJob(name, data, options, singletonOffset) {
var _this11 = this;
var startAfter = options.startAfter;
startAfter = startAfter instanceof Date && typeof startAfter.toISOString === 'function' ? startAfter.toISOString() : startAfter > 0 ? '' + startAfter : typeof startAfter === 'string' ? startAfter : null;
if ('retryDelay' in options) assert(Number.isInteger(options.retryDelay) && options.retryDelay >= 0, 'retryDelay must be an integer >= 0');
if ('retryBackoff' in options) assert(options.retryBackoff === true || options.retryBackoff === false, 'retryBackoff must be either true or false');
if ('retryLimit' in options) assert(Number.isInteger(options.retryLimit) && options.retryLimit >= 0, 'retryLimit must be an integer >= 0');
var retryLimit = options.retryLimit || 0;
var retryBackoff = !!options.retryBackoff;
var retryDelay = options.retryDelay || 0;
if (retryBackoff && !retryDelay) retryDelay = 1;
if (retryDelay && !retryLimit) retryLimit = 1;
var expireIn = options.expireIn || '15 minutes';
var priority = options.priority || 0;
var singletonSeconds = options.singletonSeconds > 0 ? options.singletonSeconds : options.singletonMinutes > 0 ? options.singletonMinutes * 60 : options.singletonHours > 0 ? options.singletonHours * 60 * 60 : null;
var singletonKey = options.singletonKey || null;
singletonOffset = singletonOffset || 0;
var id = require('uuid/' + this.config.uuid)();
// ordinals! [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]
var values = [id, name, priority, retryLimit, startAfter, expireIn, data, singletonKey, singletonSeconds, singletonOffset, retryDelay, retryBackoff];
return this.db.executeSql(this.insertJobCommand, values).then(function (result) {
if (result.rowCount === 1) return id;
if (!options.singletonNextSlot) return null;
// delay starting by the offset to honor throttling config
options.startAfter = singletonSeconds;
// toggle off next slot config for round 2
options.singletonNextSlot = false;
var singletonOffset = singletonSeconds;
return _this11.createJob(name, data, options, singletonOffset);
});
}
}, {
key: 'fetch',
value: function fetch(name, batchSize) {
var _this12 = this;
return Attorney.checkFetchArgs(name, batchSize).then(function (values) {
return _this12.db.executeSql(_this12.nextJobCommand, [values.name, values.batchSize || 1]);
}).then(function (result) {
var jobs = result.rows.map(function (job) {
job.done = function (error, response) {
return error ? _this12.fail(job.id, error) : _this12.complete(job.id, response);
};
return job;
});
return jobs.length === 0 ? null : jobs.length === 1 && !batchSize ? jobs[0] : jobs;
});
}
}, {
key: 'fetchCompleted',
value: function fetchCompleted(name, batchSize) {
return this.fetch(completedJobPrefix + name, batchSize);
}
}, {
key: 'mapCompletionIdArg',
value: function mapCompletionIdArg(id, funcName) {
var errorMessage = funcName + '() requires an id';
return Attorney.assertAsync(id, errorMessage).then(function () {
var ids = Array.isArray(id) ? id : [id];
assert(ids.length, errorMessage);
return ids;
});
}
}, {
key: 'mapCompletionDataArg',
value: function mapCompletionDataArg(data) {
if (data === null || typeof data === 'undefined' || typeof data === 'function') return null;
if (data instanceof Error) data = JSON.parse(JSON.stringify(data, Object.getOwnPropertyNames(data)));
return (typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object' && !Array.isArray(data) ? data : { value: data };
}
}, {
key: 'mapCompletionResponse',
value: function mapCompletionResponse(ids, result) {
return {
jobs: ids,
requested: ids.length,
updated: result.rowCount
};
}
}, {
key: 'complete',
value: function complete(id, data) {
var _this13 = this;
return this.mapCompletionIdArg(id, 'complete').then(function (ids) {
return _this13.db.executeSql(_this13.completeJobsCommand, [ids, _this13.mapCompletionDataArg(data)]).then(function (result) {
return _this13.mapCompletionResponse(ids, result);
});
});
}
}, {
key: 'fail',
value: function fail(id, data) {
var _this14 = this;
return this.mapCompletionIdArg(id, 'fail').then(function (ids) {
return _this14.db.executeSql(_this14.failJobsCommand, [ids, _this14.mapCompletionDataArg(data)]).then(function (result) {
return _this14.mapCompletionResponse(ids, result);
});
});
}
}, {
key: 'cancel',
value: function cancel(id) {
var _this15 = this;
return this.mapCompletionIdArg(id, 'cancel').then(function (ids) {
return _this15.db.executeSql(_this15.cancelJobsCommand, [ids]).then(function (result) {
return _this15.mapCompletionResponse(ids, result);
});
});
}
}, {
key: 'deleteQueue',
value: function deleteQueue(queue) {
return this.db.executeSql(this.deleteQueueCommand, [queue]);
}
}, {
key: 'deleteAllQueues',
value: function deleteAllQueues() {
return this.db.executeSql(this.deleteAllQueuesCommand);
}
}]);
return Manager;
}(EventEmitter);
module.exports = Manager;