UNPKG

@midwayjs/event-bus

Version:
667 lines 27.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AbstractEventBus = exports.AckResponder = exports.createWaitHandler = void 0; // event bus const interface_1 = require("./interface"); const util_1 = require("util"); const error_1 = require("./error"); const DEFAULT_LISTENER_KEY = '_default_'; const END_FLAG = null; function revertError(errorObj) { const error = new Error(); error.name = errorObj.name; error.message = errorObj.message; error.stack = errorObj.stack; return error; } class MessageLimitSet extends Set { constructor(limit) { super(); this.limit = limit; } add(value) { if (this.size < this.limit) { return super.add(value); } } } async function createWaitHandler(checkHandler, options = {}) { await new Promise((resolve, reject) => { const timeoutHandler = setTimeout(() => { clearInterval(handler); clearTimeout(timeoutHandler); reject(new error_1.EventBusTimeoutError()); }, options.timeout || 5000); const handler = setInterval(() => { if (checkHandler()) { clearInterval(handler); clearTimeout(timeoutHandler); resolve(true); } }, options.timeoutCheckInterval || 500); }); } exports.createWaitHandler = createWaitHandler; class ChunkIterator { constructor(debugLogger) { this.debugLogger = debugLogger; this.buffer = []; } publish(data) { this.buffer.push(data); // check buffer and resolve defer if (this.waitingPromiseDeferred) { const w = this.waitingPromiseDeferred; this.waitingPromiseDeferred = null; // 这里要从 buffer 取 w.resolve(this.buffer.shift()); } } error(err) { if (this.waitingPromiseDeferred) { const w = this.waitingPromiseDeferred; this.waitingPromiseDeferred = null; w.reject(err); } else { this.errorRisen = err; } this.clear(); } [Symbol.asyncIterator]() { return this; } async tryToGetNextData() { if (this.errorRisen) { // 如果在循环时发现之前已经有接到错误,直接抛出 throw this.errorRisen; } /** * 1、如果在循环前有数据,就从 buffer 中取 * 2、如果 buffer 里没数据,这里就要等待数据,在 publish 的时候直接 resolve */ if (this.buffer.length > 0) { return this.buffer.shift(); } else { const deferred = {}; deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); this.waitingPromiseDeferred = deferred; return deferred.promise; } } async next() { this.debugLogger('1 ChunkIterator run next and wait data'); const { data, isEnd } = await this.tryToGetNextData(); this.debugLogger('2 ChunkIterator get data', data, isEnd); if (isEnd) { this.clear(); return { value: undefined, done: true }; } else { return { value: data, done: false }; } } clear() { this.buffer.length = 0; } } class AckResponder { constructor() { this.isEndFlag = false; } onData(dataHandler) { this.dataHandler = dataHandler; } onError(errorHandler) { this.errorHandler = errorHandler; } end(data) { if (!this.isEndFlag) { this.isEndFlag = true; if (data) { this.sendData(data); } this.sendData(END_FLAG); } } sendData(data) { if (this.dataHandler) { this.dataHandler(data); } } send(data) { if (!this.isEndFlag) { this.sendData(data); } } error(err) { if (!this.isEndFlag) { if (this.errorHandler) { this.errorHandler(err); } } } isEnd() { return this.isEndFlag; } } exports.AckResponder = AckResponder; class AbstractEventBus { constructor(options = {}) { this.options = options; this.isInited = false; this.workers = []; this.stopping = false; this.workerReady = new Map(); this.topicListener = new Map(); this.topicMessageCache = new Map(); this.asyncMessageMap = new Map(); this.eventListenerMap = new Map(); this.debugLogger = this.createDebugger(); this.debugLogger(`Start EventBus(${this.constructor.name}) in ${this.isWorker() ? 'worker' : 'main'}`); this.eventListenerMap.set(interface_1.ListenerType.Error, (err) => { console.error(err); }); this.listener = (message, responder) => { var _a, _b; const listeners = this.topicListener.get(((_a = message.messageOptions) === null || _a === void 0 ? void 0 : _a.topic) || DEFAULT_LISTENER_KEY); if (listeners) { for (const listener of listeners) { if (listener['_subscribeOnce']) { listeners.delete(listener); } // eslint-disable-next-line no-async-promise-executor new Promise(async (resolve, reject) => { try { await resolve(listener(message, responder)); } catch (e) { reject(e); } }).catch(err => { if (responder) { responder.error(err); } else { this.eventListenerMap.get(interface_1.ListenerType.Error)(err); } }); } } else { const topic = ((_b = message.messageOptions) === null || _b === void 0 ? void 0 : _b.topic) || DEFAULT_LISTENER_KEY; if (!this.topicMessageCache.has(topic)) { this.topicMessageCache.set(topic, new MessageLimitSet(10)); } this.topicMessageCache.get(topic).add({ message, responder }); } }; // bind event center this.setupEventBind(); } createDebugger() { return (0, util_1.debuglog)(`midway:event-bus:${this.isWorker() ? 'worker' : 'main '}`); } debugDataflow(message) { if (message.messageCategory === interface_1.MessageCategory.IN) { if (this.isMain()) { return `${message.message.type}|${message.messageCategory}: worker => main(△)`; } else { return `${message.message.type}|${message.messageCategory}: main => worker(△)`; } } else { if (this.isMain()) { return `${message.message.type}|${message.messageCategory}: main(△) => worker`; } else { return `${message.message.type}|${message.messageCategory}: worker(△) => main`; } } } async start(err) { this.isInited = true; if (this.isMain()) { await createWaitHandler(() => this.isAllWorkerReady(), { timeout: this.options.initTimeout, timeoutCheckInterval: this.options.initTimeoutCheckInterval, }); } else { // listen main => worker in worker this.workerSubscribeMessage((message) => { this.transit({ messageCategory: interface_1.MessageCategory.IN, message, }); }); // worker => main this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId: this.generateMessageId(), workerId: this.getWorkerId(), type: interface_1.MessageType.Inited, body: this.isInited, error: err ? { name: err.name, message: err.message, stack: err.stack, } : undefined, }, }); } } addWorker(worker) { this.debugLogger(`Add worker(${this.getWorkerId(worker)})`); if (!this.workerReady.has(this.getWorkerId(worker))) { this.debugLogger(`Init worker(${this.getWorkerId(worker)}) status = false`); this.workerReady.set(this.getWorkerId(worker), { worker, ready: false, }); } else { this.debugLogger(`Skip init worker(${this.getWorkerId(worker)}) status`); } worker === null || worker === void 0 ? void 0 : worker['on']('exit', async (exitCode) => { if (!this.stopping) { // remove ready status this.workerReady.delete(this.getWorkerId(worker)); // remove worker const idx = this.workers.findIndex(item => this.getWorkerId(item) === this.getWorkerId(worker)); this.workers.splice(idx, 1); } }); // listen worker => main in main this.workerListenMessage(worker, (message) => { this.transit({ messageCategory: interface_1.MessageCategory.IN, message, }); }); this.workers.push(worker); } isAllWorkerReady() { for (const [workerId, value] of this.workerReady) { if (!value || !value.ready) { this.debugLogger(`Worker(${workerId}) not ready.`); return false; } } if (this.workerReady.size > 0) { this.debugLogger(`All worker(size=${this.workerReady.size}) is ready.`); } return true; } setupEventBind() { this.messageReceiver = (message) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; if (!message.message || !message.message.messageId) { // ignore unvalid format message return; } this.debugLogger('EventCenter(%s) message = %j', this.debugDataflow(message), message); const originMessage = message.message; if (message.messageCategory === interface_1.MessageCategory.OUT) { // out operation if (originMessage.type === interface_1.MessageType.Invoke || originMessage.type === interface_1.MessageType.Request || originMessage.type === interface_1.MessageType.Response || originMessage.type === interface_1.MessageType.Broadcast) { this.postMessage(originMessage); (_a = this.eventListenerMap.get(interface_1.ListenerType.Request)) === null || _a === void 0 ? void 0 : _a(originMessage); } else if (originMessage.type === interface_1.MessageType.Inited) { this.postMessage(originMessage); (_b = this.eventListenerMap.get(interface_1.ListenerType.Inited)) === null || _b === void 0 ? void 0 : _b(originMessage); } } else if (message.messageCategory === interface_1.MessageCategory.IN) { // in operation if (originMessage.type === interface_1.MessageType.Invoke) { const isChunk = ((_c = originMessage.messageOptions) === null || _c === void 0 ? void 0 : _c['isChunk']) === true; const responder = new AckResponder(); responder.onData(data => { var _a; this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId: this.generateMessageId(), workerId: this.getWorkerId(), type: interface_1.MessageType.Response, body: data, messageOptions: { relatedMessageId: originMessage.messageId, isChunk, topic: (_a = originMessage.messageOptions) === null || _a === void 0 ? void 0 : _a.topic, }, }, }); if (!isChunk) { // auto run end in normal invoke mode responder.end(); } }); responder.onError(err => { // publish error this.publish(err, { relatedMessageId: originMessage.messageId, isChunk, }); responder.end(); }); (_d = this.listener) === null || _d === void 0 ? void 0 : _d.call(this, originMessage, responder); (_e = this.eventListenerMap.get(interface_1.ListenerType.Subscribe)) === null || _e === void 0 ? void 0 : _e(originMessage); } else if (originMessage.type === interface_1.MessageType.Request) { (_f = this.listener) === null || _f === void 0 ? void 0 : _f.call(this, originMessage); (_g = this.eventListenerMap.get(interface_1.ListenerType.Subscribe)) === null || _g === void 0 ? void 0 : _g(originMessage); } else if (originMessage.type === interface_1.MessageType.Broadcast) { if (this.isMain()) { if (originMessage.messageOptions['includeMainFromWorker'] === true) { (_h = this.listener) === null || _h === void 0 ? void 0 : _h.call(this, originMessage); (_j = this.eventListenerMap.get(interface_1.ListenerType.Subscribe)) === null || _j === void 0 ? void 0 : _j(originMessage); } this.broadcast(originMessage.body, { ...originMessage.messageOptions, relatedMessageId: originMessage.messageId, relatedWorkerId: originMessage.workerId, }); } else { (_k = this.listener) === null || _k === void 0 ? void 0 : _k.call(this, originMessage); (_l = this.eventListenerMap.get(interface_1.ListenerType.Subscribe)) === null || _l === void 0 ? void 0 : _l(originMessage); } } else if (originMessage.type === interface_1.MessageType.Response) { if ((_m = originMessage.messageOptions) === null || _m === void 0 ? void 0 : _m.relatedMessageId) { // worker => main with invoke const asyncResolve = this.asyncMessageMap.get(originMessage.messageOptions.relatedMessageId); const isChunk = originMessage.messageOptions['isChunk'] === true; if (asyncResolve) { if (!isChunk || (isChunk && originMessage.body === END_FLAG)) { this.asyncMessageMap.delete(originMessage.messageOptions.relatedMessageId); } asyncResolve(originMessage.error ? revertError(originMessage.error) : undefined, originMessage.body, isChunk ? originMessage.body === END_FLAG : true); } else { // not found and ignore } } else { (_o = this.listener) === null || _o === void 0 ? void 0 : _o.call(this, originMessage); (_p = this.eventListenerMap.get(interface_1.ListenerType.Subscribe)) === null || _p === void 0 ? void 0 : _p(originMessage); } } else if (originMessage.type === interface_1.MessageType.Inited) { if (this.isMain()) { // trigger in worker if (originMessage.error) { this.debugLogger(`got worker ${originMessage.workerId} ready failed`); this.eventListenerMap.get(interface_1.ListenerType.Error)(revertError(originMessage.error)); } else { (_q = this.eventListenerMap.get(interface_1.ListenerType.Inited)) === null || _q === void 0 ? void 0 : _q(originMessage); // got init status from worker this.workerReady.get(originMessage.workerId).ready = true; this.debugLogger(`got worker ${originMessage.workerId} ready`); } } else { // ignore } } } }; } transit(message) { this.messageReceiver(message); } subscribe(listener, options = {}) { const topic = options.topic || DEFAULT_LISTENER_KEY; if (!this.topicListener.has(topic)) { this.topicListener.set(topic, new Set()); } if (options.subscribeOnce) { listener['_subscribeOnce'] = true; } this.topicListener.get(topic).add(listener); listener['_abortController'] = { abort: () => { this.topicListener.get(topic).delete(listener); }, }; // if topic has cache if (this.topicMessageCache.has(topic) && this.topicMessageCache.get(topic).size > 0) { // loop topic cache and trigger listener for (const data of this.topicMessageCache.get(topic)) { this.listener(data.message, data.responder); if (options.subscribeOnce) { break; } } this.topicMessageCache.get(topic).clear(); } return listener['_abortController']; } subscribeOnce(listener, options = {}) { options.subscribeOnce = true; return this.subscribe(listener, options); } publish(data, publishOptions = {}) { if (data instanceof Error) { this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId: publishOptions.relatedMessageId || this.generateMessageId(), workerId: this.getWorkerId(), type: this.isMain() ? interface_1.MessageType.Request : interface_1.MessageType.Response, body: undefined, error: { name: data.name, message: data.message, stack: data.stack, }, messageOptions: publishOptions, }, }); } else { this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId: publishOptions.relatedMessageId || this.generateMessageId(), workerId: this.getWorkerId(), type: this.isMain() ? interface_1.MessageType.Request : interface_1.MessageType.Response, body: data, messageOptions: publishOptions, }, }); } } publishAsync(data, publishOptions = {}) { return new Promise((resolve, reject) => { const messageId = publishOptions.relatedMessageId || this.generateMessageId(); this.useTimeout(messageId, publishOptions.timeout, resolve, reject); this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId, workerId: this.getWorkerId(), type: interface_1.MessageType.Invoke, body: data, messageOptions: { topic: publishOptions.topic, dispatchToken: publishOptions.dispatchToken, }, }, }); }); } publishChunk(data, publishOptions = {}) { const messageId = publishOptions.relatedMessageId || this.generateMessageId(); const iterator = new ChunkIterator(this.debugLogger); this.useTimeout(messageId, publishOptions.timeout, (data, isEnd) => { iterator.publish({ data, isEnd, }); }, err => { iterator.error(err); }); this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId, workerId: this.getWorkerId(), type: this.isMain() ? interface_1.MessageType.Invoke : interface_1.MessageType.Response, body: data, messageOptions: { isChunk: true, topic: publishOptions.topic, }, }, }); return iterator; } useTimeout(messageId, timeout = 5000, successHandler, errorHandler) { const handler = setTimeout(() => { clearTimeout(handler); this.asyncMessageMap.delete(messageId); errorHandler(new error_1.EventBusPublishTimeoutError(messageId)); }, timeout); this.asyncMessageMap.set(messageId, (err, data, isEnd) => { if (isEnd || err) { clearTimeout(handler); } if (err) { errorHandler(err); } else { successHandler(data, isEnd); } }); } broadcast(data, options = {}) { if (this.isWorker()) { options = { includeMainFromWorker: false, includeSelfFromWorker: false, ...options, }; } this.transit({ messageCategory: interface_1.MessageCategory.OUT, message: { messageId: options.relatedMessageId || this.generateMessageId(), workerId: this.getWorkerId(), type: interface_1.MessageType.Broadcast, body: data, messageOptions: options, }, }); } postMessage(message) { var _a, _b, _c, _d, _e, _f, _g, _h; if (this.isMain()) { if (this.workers.length > 0) { if (message.type === interface_1.MessageType.Broadcast) { if (message.messageOptions && message.messageOptions['relatedWorkerId']) { this.workers.forEach(w => { if (message.messageOptions['includeSelfFromWorker'] === false && this.getWorkerId(w) === message.messageOptions['relatedWorkerId']) { return; } else { this.mainSendMessage(w, message); } }); } else { this.workers.forEach(w => this.mainSendMessage(w, message)); } } else if ((_a = message.messageOptions) === null || _a === void 0 ? void 0 : _a['targetWorkerId']) { const targetWorker = (_c = this.workerReady.get((_b = message.messageOptions) === null || _b === void 0 ? void 0 : _b['targetWorkerId'])) === null || _c === void 0 ? void 0 : _c.worker; if (!targetWorker) { throw new error_1.EventBusPublishSpecifyWorkerError((_d = message.messageOptions) === null || _d === void 0 ? void 0 : _d['targetWorkerId']); } this.mainSendMessage(targetWorker, message); } else { if (this.options.dispatchStrategy) { const selectedWorker = this.options.dispatchStrategy(this.workers, (_e = message.messageOptions) === null || _e === void 0 ? void 0 : _e.dispatchToken); if (selectedWorker) { try { this.mainSendMessage(selectedWorker, message); } catch (err) { (_f = this.eventListenerMap.get(interface_1.ListenerType.Error)) === null || _f === void 0 ? void 0 : _f(new error_1.EventBusMainPostError(message, err)); } } else { throw new error_1.EventBusDispatchStrategyError(); } } else { // round ring const [worker, ...otherWorkers] = this.workers; try { this.mainSendMessage(worker, message); } catch (err) { (_g = this.eventListenerMap.get(interface_1.ListenerType.Error)) === null || _g === void 0 ? void 0 : _g(new error_1.EventBusMainPostError(message, err)); } this.workers = [...otherWorkers, worker]; } } } } else { try { this.workerSendMessage(message); } catch (err) { (_h = this.eventListenerMap.get(interface_1.ListenerType.Error)) === null || _h === void 0 ? void 0 : _h(new error_1.EventBusWorkerPostError(message, err)); } } } onInited(listener) { this.eventListenerMap.set(interface_1.ListenerType.Inited, listener); } onPublish(listener) { this.eventListenerMap.set(interface_1.ListenerType.Request, listener); } onSubscribe(listener) { this.eventListenerMap.set(interface_1.ListenerType.Subscribe, listener); } onError(listener) { this.eventListenerMap.set(interface_1.ListenerType.Error, listener); } async stop() { this.stopping = true; this.workerReady.clear(); this.eventListenerMap.clear(); this.listener = null; this.workers.length = 0; } generateMessageId() { return Math.random().toString(36).substring(2); } } exports.AbstractEventBus = AbstractEventBus; //# sourceMappingURL=base.js.map