@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
315 lines • 18.4 kB
TypeScript
import type { EventEmitterOptionsType } from './types';
import type { Emitter } from '../emitter';
/**
* This class is responsible for appending listeners (functions) and sending events to them
* when needed.
*
* WHY THIS IS NEEDED SINCE NODE AND THE BROWSER HAS IT'S OWN EVENTS SYSTEM?
* We do not try to change the default event system of node, or libraries like EventEmitter2,
* instead we try to work with them.
*
* We use them to emit the events locally inside of the application but we use this class to emit
* and listen to events distributed to multiple machines. For example, if we need to send an event
* between one machine over the other we will need to append a layer to the event emitter. This layer
* will receive an event and dispatch it to the responsible listeners. By default a layer is just another
* event emitter. Who defines the behavior of the layer is the emitter and not the layer, so who defines
* the listeners of this layer is the emitter by itself.
*
* >>> FOR MAINTAINERS <<<
* This class can be kinda hard to debug, specially with layers on top of it, so we need to dissect it first
* before you start working on it.
*
* - 1: Simple event/emitter:
* When you initialize this class you need to pass an Emitter instance. Emitter will be the interface that we use
* for pub/sub. Emitter can be for example Redis, EventEmitter2, node's EventEmitter from 'events'. This interface
* will be available right below EventEmitter class/instance.
* This means that all of the logic is extracted away from the emitter interface and should be implemented here. One
* of those special logics are wildcards.
* To save an event to the emitter like 'EventEmitter2' we will have some work to do. We don't save it `raw`,
* but instead we save a representation of the event. First things first we need to separate it between groups and
* handlers.
*
* - Groups:
* T.L.D.R.: This is the name of the event.
* a groupId is the name of the event, so for example: for the event 'create.user', we transform 'create.user' to
* a uuid `124002c4-3719-4c9b-a88e-f743b67f1686`, this means that on the emitter what we will be firing is the
* `124002c4-3719-4c9b-a88e-f743b67f1686` event and not directly `create.user`. In other words we need to guarantee that
* we do this conversion when firing the event. To help us with that we use the `this.#groupByKeys`, this means that for
* `create.user`, or `create.**`, or `create.*` we need to fire the emit action to the following groups. You will see
* that for most functions we just need to do is get the groupIds and fire it.
*
* ```ts
* const key = `create.user`
* const groupIdsToEmitEventTo = (this.#groupByKeys[key] || new Set()).values();
* ```
*
* This guarantees that for the specific key we will call the emitters correctly. The nicest thing about doing this way
* is that it's really easy to store this data since most of them are just strings so stuff like wildcards are like:
*
* ```ts
* {
* 'results-b35ab092-48f3-472f-be2c-48ee2ea0df91': Set(1) { '30e6d1c4-2470-4cff-8ccb-a48b6378dd67' },
* '**': Set(1) { 'ad14de8c-9104-4eaf-9c14-9c7384cc0473' },
* 'create.**': Set(1) { 'ad14de8c-9104-4eaf-9c14-9c7384cc0473' },
* 'create.*': Set(1) { 'ad14de8c-9104-4eaf-9c14-9c7384cc0473' },
* 'create.user': Set(1) { 'ad14de8c-9104-4eaf-9c14-9c7384cc0473' }
* }
* ```
*
* You see that for `create.*`, 'create.**', 'create.user' we are pointing to the same group? That's the general idea.
*
* - Handlers:
* Handlers are the functions, that is being called, it doesn't have any usage for the `emitter` instance. Our usage for
* it is internal like for example removing a handler. Most APIs for event emitters work like:
*
* ```
* const emitter = new EventEmitter2()
*
* const callback = (value1, value2) => {
* console.log(this.event, value1, value2);
* }
* emitter.on('foo', callback);
* emitter.removeListener('foo', callback);
* ```
*
* Do you see that we need to pass the function there to remove the listener? That's what we try to solve by storing it.
* By transforming this handler to an id we can easily find for it with a O(n) algorithm that retrieves the handler and
* removes it. The other usage of handlers is on results we will cover it on the next topic.
*
* - 2: Emitting an event and waiting for a result.
* Your first though might be? WHAT, how's that even possible? We can't know an event has fired or even the result of
* it, specially on distributed systems.
*
* That's not really magic it's really simple actually.
* When we add a new listener you see that we wrap the function (callback) to another function
* (see #wrapInResultCallback).
* What this function do is that it has a lifecycle, similar to a promise in javascript: `pending`, `completed`,
* `failed`.
* What's the idea?
* When we call the for example `emitter.emit('create.user', 1)` we will call this function after
* creating a resultKey, the emitter by itself, when we initialize the class, will also hold a `resultsEventName`.
* Why both? The second one is a listener, a listener that will only listen for results of this emitter. The second one
* is needed because a single emitter can send multiple events at the same time, se we need to differ between them.
*
* Continuing on, we called `emitter.emit('create.user', 1)`, created the resultId, and sent the resultsEventName to
* the listener. After calling we return to the user a promise, inside of this promise there will be a recursion that
* iterates over for each tick of the event loop. (see #fetchResultForEmit). Inside of this promise be aware of
* `pingTimeout` and `resultsTimeout`.
* Ping is how long we will wait to be notified that ""someone"" is working on the result, this is needed for cases
* when the event simply don't exist so we don't wait for too long. The second one is `resultsTimeout`, as you might
* have guessed, means how long we will wait for a result.
*
* Now let's jump to the listener itself, you see that the first thing we do is to emit an event TO THE
* `resultsEventName` (remember, that's the listener for the results), this event will have the following structure:
* { status: string; data: any } When the listener receives this event, it'll append this result to `#pendingResults`
* the Promise (that will not be resolved just yet), will iterate over and it'll see that some listener is working
* on the response for this emit. When this finishes we enter the `waiting` stage. So now we will wait until all pending
* results have finished or until we reach the resultsTimeout. This get's kinda complicated when adding a layer.
*
* - 3: Layers, what makes this almost unstoppable and where things gets kinda complicated.
* Layers are just EventEmitter instances, a layer will be able to make distributed systems fully in sync with each
* other.
* But how?
* Generally speaking a layer will be using `RedisEmitter` or `KafkaEmitter` or basically any type of Pub/Sub or
* messaging service.
*
* A layer works by channels, channels enables the user to separate the logic between each of them, for example:
* if we have have a chat, we might end up having multiple rooms, `room1` would be the first channel and `room2`
* would be the second channel. If we want to broadcast an event to `room1` layer we can do that by just emitting
* the event to it. If we want `room2` to be broadcasted we can send an event directly to it.
* You will see that when layers are defined, emitting events are done inside of the layer, and not inside of the
* EventEmitter instance.
* In other words, what we are doing is: Every event that will be emitted from the emitter will actually be sent
* to the layer. The layer broadcasts the events to `room1` for example, `room1` so we emit it, when we emit a
* broadcast will be fired, this message received it'll be handled by
*
* ```
* this.emitEventToEmitter
* ```
*
* in other words, i'll be handled by the emitter (the local one) itself.
*
* This might become easier with an example:
* - Call `emitter.emitToChannel(['birds', 'users'], 'create.user', { id: 1, name: 'Nicolas'})`
* - Send the key ('create.user') and the data both to `bird` and `user` keys INSIDE of the layer
* - `Bird` handler points to a function defined in birdsEmitter, so when we are receiving this value we are
* handling inside of `birdsEmitter`.
* - on `birdsEmitter` instance, we get get the original key (so `create.user`) and we will be able to make
* it work normally as the layer didn't exist.
* - When we notify about the response we follow the same thing.
*
* IMPORTANT: Your emitter cannot receive responses from channels it's not subscribed to.
*
* ```
* const emitter = await EventEmitter.new(EventEmitter2Emitter, {
* layer: {
* use: layer,
* channels: ['birds'],
* },
* wildcards: { use: true },
* });
*
* const result = await emitter.emitToChannel(['users', 'birds'], 'create.*');
* // We totally ignore 'users channel' on this case
* ```
*
* IMPORTANT: We can't rely on the data inside of this class, when working with events
* we are working with distributed systems, the data might not be inside of here, so this means
* a listener might be in other machine. So when working with them we do not have to rely too much on
* internal data for the class.
*/
export declare class EventEmitter<TEmitter extends Emitter = Emitter> {
#private;
protected $$type: string;
emitter: TEmitter;
protected layer?: EventEmitter;
private resultsEventName;
/**
* 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 new<TEmitter extends typeof Emitter = typeof Emitter>(emitter: Promise<{
default: TEmitter;
}> | TEmitter, options?: EventEmitterOptionsType & {
emitterParams?: Parameters<TEmitter['new']>;
}): Promise<EventEmitter<Emitter>>;
constructor(emitterInstance: TEmitter, options?: EventEmitterOptionsType);
get hasLayer(): boolean;
get channels(): string[];
/**
* 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).
*/
private resultsListener;
/**
* 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.
*/
addEventListener(key: string, callback: (...args: any) => any): Promise<() => Promise<void>>;
/**
* 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.
*/
addEventListenerWithoutResult(key: string, callback: (...args: any) => any): Promise<() => Promise<void>>;
/**
* [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.
*/
protected addRawEventListenerWithoutResult(key: string, callback: (...args: any) => any): Promise<() => Promise<void>>;
/**
* 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.
*/
unsubscribeAll(options?: {
key: string;
}): Promise<void>;
/**
* 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.
*/
unsubscribeFromChannel(channel: string): Promise<void>;
/**
* 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.
*/
protected emitEventToEmitter(key: string, resultsEventName: string, resultKey: string, channelLayer: string | null, ...data: any[]): Promise<void>;
/**
* 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.
*/
emitToChannel<TResult = unknown>(channels: string[] | string, key: string, ...data: any[]): Promise<TResult[]>;
/**
* 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.
*/
emit<TResult = unknown>(key: string, ...data: any[]): Promise<TResult[]>;
/**
* 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.
*/
protected emitResult(resultsEventName: string, handlerId: string, resultKey: string, channelLayer: string | null, ...data: any[]): void;
/**
* 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.
*/
private addChannelListeners;
}
//# sourceMappingURL=index.d.ts.map