'use strict';
|
|
const EventEmitter = require('events');
|
const once = require('once');
|
const ready = require('get-ready');
|
const uuid = require('uuid');
|
const debug = require('debug')('ready-callback');
|
|
const defaults = {
|
timeout: 10000,
|
isWeakDep: false,
|
};
|
|
/**
|
* @class Ready
|
*/
|
class Ready extends EventEmitter {
|
|
/**
|
* @constructor
|
* @param {Object} opt
|
* - {Number} [timeout=10000] - emit `ready_timeout` when it doesn't finish but reach the timeout
|
* - {Boolean} [isWeakDep=false] - whether it's a weak dependency
|
* - {Boolean} [lazyStart=false] - will not check cache size automatically, if lazyStart is true
|
*/
|
constructor(opt) {
|
super();
|
ready.mixin(this);
|
|
this.opt = opt || {};
|
this.isError = false;
|
this.cache = new Map();
|
|
if (!this.opt.lazyStart) {
|
this.start();
|
}
|
}
|
|
start() {
|
setImmediate(() => {
|
// fire callback directly when no registered ready callback
|
if (this.cache.size === 0) {
|
debug('Fire callback directly');
|
this.ready(true);
|
}
|
});
|
}
|
|
/**
|
* Mix `ready` and `readyCallback` to `obj`
|
* @method Ready#mixin
|
* @param {Object} obj - The mixed object
|
* @return {Ready} this
|
*/
|
mixin(obj) {
|
// only mixin once
|
if (!obj || this.obj) return null;
|
|
// delegate API to object
|
obj.ready = this.ready.bind(this);
|
obj.readyCallback = this.readyCallback.bind(this);
|
|
// only ready once with error
|
this.once('error', err => obj.ready(err));
|
|
// delegate events
|
if (obj.emit) {
|
this.on('ready_timeout', obj.emit.bind(obj, 'ready_timeout'));
|
this.on('ready_stat', obj.emit.bind(obj, 'ready_stat'));
|
this.on('error', obj.emit.bind(obj, 'error'));
|
}
|
this.obj = obj;
|
|
return this;
|
}
|
|
/**
|
* Create a callback, ready won't be fired until all the callbacks are triggered.
|
* @method Ready#readyCallback
|
* @param {String} name -
|
* @param {Object} opt - the options that will override global
|
* @return {Function} - a callback
|
*/
|
readyCallback(name, opt) {
|
opt = Object.assign({}, defaults, this.opt, opt);
|
const cacheKey = uuid.v1();
|
opt.name = name || cacheKey;
|
const timer = setTimeout(() => this.emit('ready_timeout', opt.name), opt.timeout);
|
const cb = once(err => {
|
if (err != null && !(err instanceof Error)) {
|
err = new Error(err);
|
}
|
clearTimeout(timer);
|
// won't continue to fire after it's error
|
if (this.isError === true) return;
|
// fire callback after all register
|
setImmediate(() => this.readyDone(cacheKey, opt, err));
|
});
|
debug('[%s] Register task id `%s` with %j', cacheKey, opt.name, opt);
|
cb.id = opt.name;
|
this.cache.set(cacheKey, cb);
|
return cb;
|
}
|
|
/**
|
* resolve ths callback when readyCallback be called
|
* @method Ready#readyDone
|
* @private
|
* @param {String} id - unique id generated by readyCallback
|
* @param {Object} opt - the options that will override global
|
* @param {Error} err - err passed by ready callback
|
* @return {Ready} this
|
*/
|
readyDone(id, opt, err) {
|
if (err != null && !opt.isWeakDep) {
|
this.isError = true;
|
debug('[%s] Throw error task id `%s`, error %s', id, opt.name, err);
|
return this.emit('error', err);
|
}
|
|
debug('[%s] End task id `%s`, error %s', id, opt.name, err);
|
this.cache.delete(id);
|
|
this.emit('ready_stat', {
|
id: opt.name,
|
remain: getRemain(this.cache),
|
});
|
|
if (this.cache.size === 0) {
|
debug('[%s] Fire callback async', id);
|
this.ready(true);
|
}
|
return this;
|
}
|
|
}
|
|
// Use ready-callback with options
|
module.exports = opt => new Ready(opt);
|
module.exports.Ready = Ready;
|
|
function getRemain(map) {
|
const names = [];
|
for (const cb of map.values()) {
|
names.push(cb.id);
|
}
|
return names;
|
}
|