ready-callback
Version:
Launch server after all async task ready
148 lines (124 loc) • 3.61 kB
text/typescript
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);
}