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 { #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: } * * @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(emitter: Promise<{ default: TEmitter; }> | TEmitter, options?: EventEmitterOptionsType & { emitterParams?: Parameters; }): Promise>; 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>; /** * 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>; /** * [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>; /** * 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; /** * 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; /** * 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; /** * 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(channels: string[] | string, key: string, ...data: any[]): Promise; /** * 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(key: string, ...data: any[]): Promise; /** * 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