'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; }