UNPKG

ready-callback

Version:
148 lines (124 loc) 3.61 kB
import EventEmitter from 'node:events'; import { debuglog } from 'node:util'; import { randomUUID } from 'node:crypto'; import once from 'once'; import { Ready as ReadyObject, type ReadyFunctionArg } from 'get-ready'; const debug = debuglog('ready-callback'); interface ReadyOption { timeout?: number; isWeakDep?: boolean; lazyStart?: boolean; } interface ReadyCallbackOption { name?: string; timeout?: number; isWeakDep?: boolean; } interface ReadyCallbackFn { (err?: any): void; id: string; } type ReadyCallbackCache = Map<string, ReadyCallbackFn>; const defaults: ReadyCallbackOption = { timeout: 10000, isWeakDep: false, }; /** * @class Ready */ class Ready extends EventEmitter { isError = false; cache: ReadyCallbackCache = new Map(); opt: ReadyOption; obj: any; ready: (flagOrFunction?: ReadyFunctionArg) => void; constructor(opt: ReadyOption = {}) { super(); ReadyObject.mixin(this); this.opt = opt; 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` * @function Ready#mixin * @param {Object} obj - The mixed object * @return {Ready} this */ mixin(obj?: any) { // 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; } readyCallback(name: string, opt: ReadyCallbackOption = {}) { opt = Object.assign({}, defaults, this.opt, opt); const cacheKey = randomUUID(); opt.name = name || cacheKey; const timer = setTimeout(() => this.emit('ready_timeout', opt.name), opt.timeout); const cb = once((err?: any) => { 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)); }) as unknown as ReadyCallbackFn; debug('[%s] Register task id `%s` with %j', cacheKey, opt.name, opt); cb.id = opt.name; this.cache.set(cacheKey, cb); return cb; } readyDone(id: string, opt: ReadyCallbackOption, err?: Error) { 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; } } function getRemain(map: ReadyCallbackCache) { const names: string[] = []; for (const cb of map.values()) { names.push(cb.id); } return names; } export { Ready }; export default function(opt: ReadyOption = {}) { return new Ready(opt); }