@midwayjs/event-bus
Version:
Midway event bus
667 lines • 27.7 kB
JavaScript
"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