@palmares/events
Version:
This is the events framework for palmares, it's responsible for handling everything that is Pub/Sub like websockets, pub/sub like redis, and other types of asynchronous background tasks
649 lines (645 loc) • 30.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/events/exceptions.ts
var NoLayerError = class _NoLayerError extends Error {
static {
__name(this, "NoLayerError");
}
constructor() {
super("Your emitter does not have a layer. You should add a layer before trying to emit an event to the layer");
this.name = _NoLayerError.name;
}
};
// src/utils/uuid.ts
function uuid() {
let date = (/* @__PURE__ */ new Date()).getTime();
const browserOrNodePerformance = globalThis.performance;
let performanceDate = (
// eslint-disable-next-line ts/no-unnecessary-condition
browserOrNodePerformance && browserOrNodePerformance.now && browserOrNodePerformance.now() * 1e3 || 0
);
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (character) => {
let randomNumber = Math.random() * 16;
if (date > 0) {
randomNumber = (date + randomNumber) % 16 | 0;
date = Math.floor(date / 16);
} else {
randomNumber = (performanceDate + randomNumber) % 16 | 0;
performanceDate = Math.floor(performanceDate / 16);
}
return (character === "x" ? randomNumber : randomNumber & 3 | 8).toString(16);
});
}
__name(uuid, "uuid");
// src/events/index.ts
var EventEmitter = class {
static {
__name(this, "EventEmitter");
}
$$type = "$PEventEmitter";
emitter;
layer;
resultsEventName;
#channels;
#unsubscribeByChannel = {};
#pendingHandlerIdForResultKey = /* @__PURE__ */ new Map();
#pendingResults = {};
#pingTimeout = 1e3;
#resultsTimeout = 5e3;
#delimiter = ".";
#wildcards = false;
#groupByKeys = {};
#groups = {};
/**
* Factory method for the building the emitter, we need this because we need to add results listener and layer
* listeners to the function and both operations are async.
*
* Be aware that you need to pass the emitter, the constructor, and not the instance, you can pass the parameters
* of the emitter inside of options: { customParams: <your_params_for_the_emitter> }
*
* @param emitter - The emitter constructor so we build it inside here or a default export by using
* `import('./my-custom-emitter.ts')`
* @param options - Custom options for the emitter, on here you can pass a layer instance, wildcards options and
* customize the timeout for the results to be retrieved.
*
* @returns - This is a factory method so we create a new EventEmitter instance.
*/
static async new(emitter, options) {
const { emitterParams, ...optionsForConstructor } = options || {};
if (emitter instanceof Promise) emitter = (await emitter).default;
const emitterCustomParams = emitterParams || [];
const emitterInstance = await emitter.new(...emitterCustomParams);
const eventEmitterInstance = new this(emitterInstance, optionsForConstructor);
eventEmitterInstance.resultsEventName = `resultsOfEmitter-${uuid()}`;
await eventEmitterInstance.addRawEventListenerWithoutResult(eventEmitterInstance.resultsEventName, eventEmitterInstance.resultsListener.bind(eventEmitterInstance));
if (options?.layer?.use) {
eventEmitterInstance.layer = await Promise.resolve(options.layer.use);
await eventEmitterInstance.addChannelListeners(options.layer.channels || [
"all"
]);
}
return eventEmitterInstance;
}
constructor(emitterInstance, options) {
this.emitter = emitterInstance;
if (options?.wildcards?.delimiter) this.#delimiter = options.wildcards.delimiter;
if (options?.wildcards?.use) this.#wildcards = options.wildcards.use;
if (typeof options?.results?.pingTimeout === "number") this.#pingTimeout = options.results.pingTimeout;
if (typeof options?.results?.timeout === "number") this.#resultsTimeout = options.results.timeout;
if (options?.layer?.channels) this.#channels = options.layer.channels;
}
get hasLayer() {
return this.layer?.["$$type"] === "$PEventEmitter";
}
get channels() {
return Array.isArray(this.#channels) ? [
...this.#channels
] : [];
}
/**
* This is responsible fo retrieving the response of the emitted event, when the event
* finishes processing it'll send a response to this function (this is handler for a specific
* event inside of the event emitter).
*/
resultsListener(handlerId, resultId, _, result) {
const hasPendingResultsForId = (
// eslint-disable-next-line ts/no-unnecessary-condition
typeof this.#pendingResults[resultId] === "object" && this.#pendingResults[resultId] !== void 0
);
if (hasPendingResultsForId) this.#pendingResults[resultId][handlerId] = result;
}
/**
* Adds the event listener without the wildcards, self explanatory, so we will
* not append the groupId to stuff like '*' or '**' or 'create.*'
*
* We just append the groupId to the key and that's it. It shouldn't be called directly
* because there is no other reason for this instead of organizing the callback in the right keys.
*
* @param handlerGroupId - As explained, the groupId is the actual event that will be fired in the actual emitter.
* @param handlerId - The id of the handler so we can retrieve the function quickly.
* @param key - The actual key that the `handlerGroupId` refers to.
* @param callback - The function (wrapped or not) that will be fired when we emit an event.
*/
#addListenerWithoutWildcards(handlerGroupId, handlerId, key, callback) {
if (key in this.#groupByKeys === false) this.#groupByKeys[key] = /* @__PURE__ */ new Set([
handlerGroupId
]);
if (this.#groups[handlerGroupId]) this.#groups[handlerGroupId].listeners[handlerId] = callback;
else {
this.#groups[handlerGroupId] = {
listeners: {
[handlerId]: callback
},
keys: /* @__PURE__ */ new Set([
key
])
};
}
}
/**
* This will add the listener with the wildcards, what we do in order to be able to save the list with the
* wildcards is that we save all of the events tied to the same group.
*
* Think that we are appending the event with the keyword: `create.users`. The group id refers to all of the
* handlers that will listen for this particular event.
*
* What we do is that we append the same groupId to multiple keys like for example:
*
* `create.user`: new Set(`group-h123-huasd1-123890-1023098`),
* `create.*`: new Set(`group-h123-huasd1-123890-1023098`),
* `create.**`: new Set(`group-h123-huasd1-123890-1023098`),
* `**`: new Set(`group-h123-huasd1-123890-1023098`)
*
* @param handlerGroupId - The groupId is the actual event that will be fired in the actual emitter.
* @param handlerId - The id of the handler that is the actual function that will fire the event.
* @param key - This will be like `create.user` in the example above.
* @param callback - The function appended to the handlerId.
*/
#addListenerWithWildcards(handlerGroupId, handlerId, key, callback) {
const splittedKey = key.split(this.#delimiter);
const allKeysOfKey = [
"**"
];
if (this.#groupByKeys["**"]) this.#groupByKeys["**"].add(handlerGroupId);
else this.#groupByKeys["**"] = /* @__PURE__ */ new Set([
handlerGroupId
]);
let appendedKey = "";
for (let i = 0; i < splittedKey.length; i++) {
const isLastKey = i === splittedKey.length - 1;
const eachKey = splittedKey[i];
const newAppendedKey = `${appendedKey}${i > 0 ? this.#delimiter : ""}${eachKey}`;
if (isLastKey) {
const wildCardLastKey = `${appendedKey}${i > 0 ? this.#delimiter : ""}*`;
if (this.#groupByKeys[wildCardLastKey]) this.#groupByKeys[wildCardLastKey].add(handlerGroupId);
else this.#groupByKeys[wildCardLastKey] = /* @__PURE__ */ new Set([
handlerGroupId
]);
const completeKey = newAppendedKey;
if (this.#groupByKeys[completeKey]) this.#groupByKeys[completeKey].add(handlerGroupId);
else this.#groupByKeys[completeKey] = /* @__PURE__ */ new Set([
handlerGroupId
]);
allKeysOfKey.push(wildCardLastKey, completeKey);
} else {
const deepNestedKey = `${newAppendedKey}${this.#delimiter}**`;
if (this.#groupByKeys[deepNestedKey]) this.#groupByKeys[deepNestedKey].add(handlerGroupId);
else this.#groupByKeys[deepNestedKey] = /* @__PURE__ */ new Set([
handlerGroupId
]);
allKeysOfKey.push(deepNestedKey);
}
appendedKey = newAppendedKey;
}
if (this.#groups[handlerGroupId]) this.#groups[handlerGroupId].listeners[handlerId] = callback;
else {
this.#groups[handlerGroupId] = {
listeners: {
[handlerId]: callback
},
keys: new Set(allKeysOfKey)
};
}
}
/**
* This will prevent that we call the same function for the same handler twice, it'll also nicely organize the data.
* stuff like `resultsEventName`, `resultKey` and `channelLayer` should not be passed to the user defined callback.
* It's for internal usage only, so you see that if `isResultWrapped` is false we will just pass the data to the
* callback and nothing else.
*
* @param handlerId - The id of the handler that is being called.
* @param callback - The function (wrapped or not) that will be called when we fire the event.
* @param isResultWrapped - Boolean value, if it's result wrapped we will send the `resultsEventName`,
* `resultKey`, `channelLayer` to the callback, otherwise we will just send the data. We try to not send unnecessary
* data to the callback, all of the values should be used by the handler itself.
*
* @returns - Return the callback wrapped, so we can notify the class that we are working on a result for a particular
* resultKey, this is nice so if the system receives multiple requests it won't do nothing.
*/
// eslint-disable-next-line ts/require-await
async #wrapInPendingHandlerToPreventMultipleCalls(handlerId, callback, isResultWrapped = true) {
const preventMultipleCallsWrappedCallback = /* @__PURE__ */ __name(async (resultsEventName, resultKey, channelLayer, ...data) => {
const isThisHandlerAlreadyWorkingForAResponse = this.#pendingHandlerIdForResultKey.has(handlerId) && this.#pendingHandlerIdForResultKey.get(handlerId) === resultKey;
if (isThisHandlerAlreadyWorkingForAResponse === false) {
this.#pendingHandlerIdForResultKey.set(handlerId, resultKey);
if (isResultWrapped) await Promise.resolve(callback(resultsEventName, resultKey, channelLayer, ...data));
else await Promise.resolve(callback(...data));
this.#pendingHandlerIdForResultKey.delete(handlerId);
}
}, "preventMultipleCallsWrappedCallback");
return preventMultipleCallsWrappedCallback.bind(this);
}
/**
* This works similar to a promise in javascript, what we do is that we send the emitter
* that we are working on a response.
*
* When we work on a response we notify with `pending`, after the result is finished we notify
* the result with `completed`. This is why we use the `resultsEventName` and `resultKey` for.
* This way we are able to notify the emitter that we are working on a response for it.
*
* @param handlerId - We need the handlerId so we know that exactly that this handler that is
* working on a response.
*/
async #wrapInResultCallback(handlerId, callback) {
const resultWrappedCallback = /* @__PURE__ */ __name(async (resultsEventName, resultKey, channelLayer, ...data) => {
this.emitResult(resultsEventName, handlerId, resultKey, channelLayer, {
status: "pending"
});
try {
const result = await Promise.resolve(callback(...data));
this.emitResult(resultsEventName, handlerId, resultKey, channelLayer, {
status: "completed",
result
});
} catch (e) {
this.emitResult(resultsEventName, handlerId, resultKey, channelLayer, {
status: "failed"
});
throw e;
}
}, "resultWrappedCallback");
return this.#wrapInPendingHandlerToPreventMultipleCalls(handlerId, resultWrappedCallback.bind(this), true);
}
/**
* This will subscribe a listener (function) to an specific event (key). When this key is emitted, either from
* a channel or from the emitter itself, the listener (function) will be called.
*
* Returning a value from the function will emit a result back to the caller.
*
* IMPORTANT: The data received and the return value must be JSON serializable values. This means you cannot expect
* to receive a callback or function in your listener. As well as this, you can't return a function, can't return
* a class. It needs to be JSON serializable.
*
* @param key - The key that will be used to emit the event.
* @param callback - The function that will be called when the event is emitted.
*
* @returns - A unsubscribe function that if called, will remove the listener from the emitter.
*/
async addEventListener(key, callback) {
return this.#addEventListenerWithOptions(key, {
useResult: true,
wildcards: this.#wildcards,
usePreventMultipleCalls: true
}, callback);
}
/**
* This method will subscribe a listener that will not emit a result back to the caller. So it might
* be useful for listeners where performance does matter and needs to be taken aware of.
*
* @param key - The key that will be used to emit the event.
* @param callback - The function that will be called when the event is emitted.
*
* @returns - A unsubscribe function that if called, will remove the listener from the emitter.
*/
async addEventListenerWithoutResult(key, callback) {
return this.#addEventListenerWithOptions(key, {
useResult: false,
wildcards: this.#wildcards,
usePreventMultipleCalls: true
}, callback);
}
/**
* [INTERNAL] This will subscribe a listener (function) to an specific event (key) without worrying about the result.
* This is mostly used for internal usage, we do not need to wrap the `results` listener and
* `layerListener` to send the results. Actually if we did this we might would end up in a loop.
*
* So in other words, this adds the key and the listener `raw`, so not wrapped in anything and without
* the wildcards.
*
* @param key - The key that will be used to emit the event.
* @param callback - The function that will be called when the event is emitted.
*
* @returns - Returns the unsubscribe function that should be called to unsubscribe the listener.
*/
async addRawEventListenerWithoutResult(key, callback) {
return this.#addEventListenerWithOptions(key, {
useResult: false,
wildcards: false,
usePreventMultipleCalls: false
}, callback);
}
/**
* Adds the event listeners with custom options, as you see this function is 100% private, we do not want to
* expose this to the user because we want to keep the api as simple as possible, and this might bring more
* confusion than making it simple. We created this function so we can reuse it if you are adding an event
* listener with the `addEventListener` or with the `addRawEventListenerWithoutResult`.
*
* @param key - The key that will be used to fire the event.
* @param options - The options that will be used to fire the event.
* @param options.useResult - If the event listener will be wrapped with the result emitter.
* @param options.wildcards - If the event listener accepts wildcards or not.
* @param callback - The function (not wrapped) that will be fired when we emit an event.
*
* @returns - Returns the unsubscribe function that should be called to unsubscribe the listener.
*/
async #addEventListenerWithOptions(key, options, callback) {
const handlerGroupId = key in this.#groupByKeys ? [
...this.#groupByKeys[key].values()
][0] : `group-${uuid()}`;
const handlerId = `handler-${uuid()}`;
if (options.useResult) callback = await this.#wrapInResultCallback(handlerId, callback);
else if (options.usePreventMultipleCalls) {
callback = await this.#wrapInPendingHandlerToPreventMultipleCalls(handlerId, callback, false);
}
if (options.wildcards) {
this.#addListenerWithWildcards(handlerGroupId, handlerId, key, callback);
} else {
this.#addListenerWithoutWildcards(handlerGroupId, handlerId, key, callback);
}
await this.emitter.addEventListener(handlerGroupId, key, callback);
return this.#unsubscribe({
handlerGroupId,
key,
handlerId
});
}
/**
* This will either unsubscribe all listeners or all of the listeners of a specific key. We pass an object here
* to prevent undesired behavior, if for some reason key is undefined we will not remove all of the listeners you need
* to explicitly define the key that you want to remove.
*
* @param options - The options of the listeners we want to remove.
* @param options.key - The key that you want to remove from the emitter.
*/
async unsubscribeAll(options) {
const isKeyDefined = typeof options?.key === "string";
const isToRemoveAll = options === void 0;
const doesKeyExists = isKeyDefined && options.key in this.#groupByKeys;
if (doesKeyExists) {
const groupIds = this.#groupByKeys[options.key];
const promises = [];
for (const groupId of groupIds) {
promises.push((async () => {
const groupKeysAndListeners = this.#groups[groupId];
if (groupKeysAndListeners) {
const listeners = Object.values(groupKeysAndListeners.listeners);
const listenerRemovalPromises = listeners.map(async (listener) => {
await this.emitter.removeEventListener(groupId, options.key, listener);
});
const keysToRemove = groupKeysAndListeners.keys.values();
for (const keyBeingRemoved of keysToRemove) {
if (this.#groupByKeys[keyBeingRemoved].size === 1) delete this.#groupByKeys[keyBeingRemoved];
else this.#groupByKeys[keyBeingRemoved].delete(groupId);
}
await Promise.all(listenerRemovalPromises);
delete this.#groups[groupId];
}
})());
}
await Promise.all(promises);
} else if (isToRemoveAll) {
if (this.#wildcards) await this.unsubscribeAll({
key: "**"
});
else {
const promises = [];
const keysToRemove = Object.keys(this.#groupByKeys);
for (const key of keysToRemove) {
if (this.resultsEventName !== key) promises.push(this.unsubscribeAll({
key
}));
}
await Promise.all(promises);
}
}
}
/**
* Unsubscribes this emitter from a specific channel inside of the layer. If it doesn't exist it will do nothing.
*
* @param channel - The channel that you want to unsubscribe from.
*/
async unsubscribeFromChannel(channel) {
if (channel in this.#unsubscribeByChannel) await this.#unsubscribeByChannel[channel]();
}
/**
* When unsubscribing we need to remove the listener from the groups array. This is exactly what this do.
* We remove all of the keys from the groupKeys. Also if the #groupByKeys becomes empty we make sure to remove
* it from the #groupByKeys object.
*
* @param handlerGroupId - The group id that we want to remove from the #groups and #groupByKeys.
*/
#unsubscribeGroup(handlerGroupId) {
for (const keyToRemoveIdFrom of this.#groups[handlerGroupId].keys) {
this.#groupByKeys[keyToRemoveIdFrom].delete(handlerGroupId);
const isHandlerByKeyEmpty = this.#groupByKeys[keyToRemoveIdFrom].size === 0;
if (isHandlerByKeyEmpty) delete this.#groupByKeys[keyToRemoveIdFrom];
}
delete this.#groups[handlerGroupId];
}
/**
* This function is called when we append a new listener using the `addEventListeners` functions.
*
* @example
* ```ts
* const unsubscribe = await emitter.addEventListener('customEventName', () => { console.log('hello world') });
*
* await unsubscribe();
* ```
*
* You see that on addEventListener what we return a function to unsubscribe the listener. The unsubscribe function
* is returned from this method.
*
* To unsubscribe the listener we need to remove it from the emitter and from the #groups and #groupByKeys objects.
* But all of this logic is handled here. You see that we bind the function we return to the `this` context of the
* emitter instance. So that the `this` context will always be the emitter instance.
*
* @param handlerGroupId - The group id that we want to remove from the emitter (remember, groupIds are like the
* 'eventName')
* @param key - The original key that we used to add the listener.
* @param handlerId - The id of the handler that we want to remove from the emitter.
*/
// eslint-disable-next-line ts/require-await
async #unsubscribe({ handlerGroupId, key, handlerId }) {
const unsubscribeHandlerFunction = /* @__PURE__ */ __name(async () => {
const doesGroupStillExists = handlerGroupId in this.#groups;
const doesHandlerStillExists = doesGroupStillExists && handlerId in this.#groups[handlerGroupId].listeners;
const isLastListenerFromGroup = doesHandlerStillExists && Object.keys(this.#groups[handlerGroupId].listeners).length === 1;
if (doesHandlerStillExists) {
const listener = this.#groups[handlerGroupId].listeners[handlerId];
await this.emitter.removeEventListener(handlerGroupId, key, listener);
delete this.#groups[handlerGroupId].listeners[handlerId];
if (isLastListenerFromGroup) this.#unsubscribeGroup(handlerGroupId);
}
}, "unsubscribeHandlerFunction");
return unsubscribeHandlerFunction.bind(this);
}
#getOriginalKeyFromGroup(key, groupId) {
if (groupId in this.#groups) {
const groupKeys = this.#groups[groupId].keys;
let originalKey;
for (originalKey of groupKeys) ;
return originalKey;
}
return key;
}
/**
* Emits the event to the `this.emitter.emit`
*
* @param resultsEventName - this is the handler you will call with the result, it'll
* it's just one for every emitter, so each emitter instance define it's own resultsEventName
* @param resultKey - This is the key of the result, when you all `.emit()` we will create a key
* meaning that we will populate the contents of this key with the results.
*/
// eslint-disable-next-line ts/require-await
async emitEventToEmitter(key, resultsEventName, resultKey, channelLayer, ...data) {
const groupIdsToEmitEventTo = (this.#groupByKeys[key] || /* @__PURE__ */ new Set()).values();
for (const groupId of groupIdsToEmitEventTo) {
const groupListenersIds = Object.keys(this.#groups[groupId].listeners || {});
const areAllListenersBeingHandled = groupListenersIds.every((handlerId) => this.#pendingHandlerIdForResultKey.get(handlerId) === resultKey);
if (areAllListenersBeingHandled === false) {
const originalKey = this.#getOriginalKeyFromGroup(key, groupId);
this.emitter.emit(groupId, originalKey, resultsEventName, resultKey, channelLayer, ...data);
}
}
}
/**
* This is responsible to wait for the result of the fired event. First we fire the event, then we will iterate
* over and over again over `this.#pendingResults` until we receive the result of the event that we fired.
*
* We will loop until we receive the result of the event, or we can timeout with both `#resultsTimeout` or
* `#pingTimeout`. The first one is how long we will wait to retrieve a response. The second one is how long we will
* wait until we receive that a listener is working on a response. The second one is useful when the event fired does
* not have any handlers.
*
* @param resultKey - In other words, the key of the event that was fired. But generally speaking this is the id
* that we will append the results to.
*
* @returns - A Promise that resolves to an array of the results of the event that was fired.
*/
async #fetchResultForEmit(resultKey) {
return new Promise((resolve, reject) => {
function keepAlive(startTimer) {
try {
const hasReachedTimeout = Date.now() - startTimer > this.#resultsTimeout;
const hasResultForKey = Object.keys(this.#pendingResults[resultKey]).length > 0;
const resultsAsArray = Object.values(this.#pendingResults[resultKey] || {});
const allResultsConcluded = hasResultForKey && resultsAsArray.every(({ status }) => status !== "pending");
const isPingTimeoutPassed = Date.now() - startTimer > this.#pingTimeout;
const hasReachedPingTimeout = hasResultForKey === false && isPingTimeoutPassed;
if (allResultsConcluded && isPingTimeoutPassed || hasReachedTimeout) {
delete this.#pendingResults[resultKey];
return resolve(resultsAsArray.filter(({ status }) => status === "completed").map(({ result }) => result));
} else if (hasReachedPingTimeout) {
return resolve([]);
} else setTimeout(() => keepAlive.bind(this)(startTimer), 0);
} catch (e) {
reject(e);
}
}
__name(keepAlive, "keepAlive");
keepAlive.bind(this)(Date.now());
});
}
/**
* Emits some data to a channel, a channel is something that should be defined in the layer, This will fire the event
* in the layer calling all subscribed listeners. By doing this you can call the `emit` method on multiple machines
* inside of the server.
*
* @param channel - The channel to emit the event to.
* @param key - The key to send events to.
* @param data - The data to send over to the listeners. (IT SHOULD BE JSON SERIALIZABLE)
*
* @return - A promise that will wait for a return of the emitters.
*/
async emitToChannel(channels, key, ...data) {
const resultKey = `emittedToChannelResultKey-${uuid()}`;
this.#pendingResults[resultKey] = {};
if (this.layer) {
const channelsAsArray = Array.isArray(channels) ? channels : [
channels
];
const filteredChannels = channelsAsArray.filter((channel) => this.#channels.includes(channel));
for (const channel of filteredChannels) {
this.layer.emitEventToEmitter(channel, this.resultsEventName, resultKey, channel, {
key,
data
});
}
return this.#fetchResultForEmit(resultKey);
} else {
throw new NoLayerError();
}
}
/**
* When we emit the event we will return a promise, this promise will wait
* for the results of the listeners to be sent back to the application. With this
* we are able to retrieve the results of the connected listeners.
*
* @param key - The key to send events to.
* @param data - The data to send over to the listeners. (IT SHOULD BE JSON SERIALIZABLE)
*
* @return - A promise that will wait for a return of the emitters.
*/
async emit(key, ...data) {
const resultKey = `emittedResultKey-${uuid()}`;
this.#pendingResults[resultKey] = {};
this.emitEventToEmitter(key, this.resultsEventName, resultKey, null, ...data);
return this.#fetchResultForEmit(resultKey);
}
/**
* Function that is used to emit the result back to the caller this way we can distribute the callers
* and send the response back to the caller as it was on the same system / machine.
*
* If it is in an emitter we will send this response through the layer, otherwise it'll send normally
* in the event emitter.
*
* @param resultsEventName - The results event name is a listener that exists for all emitters inside
* of the application. This is a unique ID. Each EventEmitter instance has it's own. So what we do is that
* we send this over the network and guarantee that only who sent the event will receive this response back.
* @param handlerId - An id of the function (listener) that is working for the result of this event.
* @param resultKey - The id of the result. When you emit an event it'll generate a key. This is the key
* that we use to guarantee that the value received is from this event.
* @param channelLayer - Only needed when using `layers`, but this is the channel that we should broadcast
* this response to. For example if we emitted the event for every listener in the `users` channel, we should
* guarantee that we are sending the response back to the `users` channel so that the `resultEventName` can
* catch this value and do something with it.
* @param data - This is the actual data that you are sending over the network, generally speaking it'll be,
* most of the times, an array since we are spreading it over.
*/
emitResult(resultsEventName, handlerId, resultKey, channelLayer, ...data) {
if (this.layer && channelLayer !== null) {
this.layer.emitEventToEmitter(channelLayer, handlerId, resultKey, channelLayer, {
key: resultsEventName,
data
});
} else if (channelLayer === null) {
this.emitEventToEmitter(resultsEventName, handlerId, resultKey, null, ...data);
}
}
/**
* This is used to append listener functions to the layer, you see that the layer calls the emitter.
*
* So what we are doing is: We are appending the callback to the layer, from the emitter, when this function
* is called we will call the `this.emitEventToEmitter` from the emitter itself AND NOT the layer.
*
* @returns - Returns the callback that will be called when we fire the emitter.
*/
// eslint-disable-next-line ts/require-await
async #getLayerListener() {
function layerListener(resultsEventName, resultKey, channel, data) {
this.emitEventToEmitter(data.key, resultsEventName, resultKey, channel, ...data.data || []);
}
__name(layerListener, "layerListener");
return layerListener.bind(this);
}
/**
* Appends the listeners to the layer, this way we will be able to connect two different emitters together.
* Those 2 different emitters might be on the same machine or a completely different machine (if we are using
* RedisEmitter)
*
* @param channels - The channels that your emitter will listen to. This means that when we receive an event on
* a specific channel and this emitter has handlers for this event, we will emit the event.
*/
async addChannelListeners(channels) {
const promises = channels.map(async (channel) => {
if (this.layer) {
const unsubscribe = await this.layer.addRawEventListenerWithoutResult(channel, await this.#getLayerListener());
this.#unsubscribeByChannel[channel] = unsubscribe;
}
});
await Promise.all(promises);
}
};
export {
EventEmitter
};