diff --git a/src/flare.ts b/src/flare.ts index 66d4431..34f978f 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -5,6 +5,9 @@ import { FlareHandler, FlareInterceptor, FlareMiddleware, + FlareObservationSource, + FlareObservationType, + FlareObserver, } from './types'; export class Flare> { @@ -14,6 +17,7 @@ export class Flare> { private interceptors: FlareInterceptor[] = []; private middlewares: FlareMiddleware[] = []; + private observers: FlareObserver[] = []; in(interceptor: FlareInterceptor): void { this.interceptors.push(interceptor); @@ -28,16 +32,26 @@ export class Flare> { payload: E[K], options: FlareFireOptions = {}, ): Promise { + this.handleObservers(event, payload, FlareObservationType.Info, undefined, FlareObservationSource.Flare, undefined); + const beforeResult = this.handleBeforeInterceptors(event, payload); if (beforeResult) return beforeResult; - const { stopped, newPayload } = await this.handleMiddlewares(event, payload); + const { stopped, newPayload, stoppedMiddleware } = await this.handleMiddlewares(event, payload); + if (stopped) { - return Promise.resolve(`Event "${String(event)}" was stopped by middleware.`); + const message = `Event "${String(event)}" was stopped by middleware${stoppedMiddleware ? ` (${stoppedMiddleware.id})` : ''}.`; + this.handleObservers(event, payload, FlareObservationType.Warning, stoppedMiddleware?.id, FlareObservationSource.Middleware, message); + return Promise.resolve(message); } const handlerOptionsSet = this.handlerOptionsStore[event]; - if (!handlerOptionsSet) return Promise.resolve(`No handlers found for event "${String(event)}".`); + + if (!handlerOptionsSet) { + const message = `No handlers found for event "${String(event)}".`; + this.handleObservers(event, payload, FlareObservationType.Warning, undefined, FlareObservationSource.Flare, message); + return Promise.resolve(message); + } await this.handleExecute(event, newPayload, options, handlerOptionsSet); @@ -61,6 +75,10 @@ export class Flare> { return () => this.release(event, handler); } + observe(observer: FlareObserver): void { + this.observers.push(observer); + } + release( event: K, handler: FlareHandler, @@ -91,10 +109,13 @@ export class Flare> { try { const shouldContinue = interceptor.before?.(event, payload); if (shouldContinue === false) { - return Promise.resolve(`Event "${String(event)}" was cancelled by interceptor ${interceptor.id ? `(${interceptor.id})` : ""}.`); + const message = `Event "${String(event)}" was cancelled by interceptor${interceptor.id ? ` (${interceptor.id})` : ""}.`; + this.handleObservers(event, payload, FlareObservationType.Warning, interceptor.id, FlareObservationSource.Interceptor, message); + return Promise.resolve(message); } } catch (error) { - // TODO: handle interceptor exception + this.handleObservers(event, payload, FlareObservationType.Error, interceptor.id, FlareObservationSource.Interceptor, error); + // the event flow should continue even if an interceptor fails! } } } @@ -104,16 +125,20 @@ export class Flare> { try { interceptor.after?.(event, payload); } catch (error) { - // TODO: handle interceptor exception + this.handleObservers(event, payload, FlareObservationType.Error, interceptor.id, FlareObservationSource.Interceptor, error); + // the event flow should continue even if an interceptor fails! } } } - private async handleMiddlewares(event: K, payload: E[K]): Promise<{ stopped: boolean, newPayload: E[K] }> { + private async handleMiddlewares(event: K, payload: E[K]) + : Promise<{ stopped: boolean, newPayload: E[K], stoppedMiddleware?: FlareMiddleware }> { const middlewares = this.middlewares; + const dis = this; let stopped = false; let newPayload = payload; + let stoppedMiddleware: FlareMiddleware | undefined = undefined; if (middlewares.length === 0) return { stopped, newPayload }; @@ -122,21 +147,31 @@ export class Flare> { await executeMiddleware(0); - return { stopped, newPayload }; - + return { stopped, newPayload, stoppedMiddleware }; async function executeMiddleware(index: number): Promise { + if (stopped) { + stoppedMiddleware = middlewares[index - 1]; + return; + } + if (index < middlewares.length) { const middleware = middlewares[index]; const context = { event, payload: newPayload, stop, set }; - await middleware(context, () => executeMiddleware(index + 1)); + try { + await middleware.fn(context, () => executeMiddleware(index + 1)); + } catch (error) { + dis.handleObservers(event, payload, FlareObservationType.Error, middleware.id, FlareObservationSource.Middleware, error); + // the event flow should continue if a middleware fails! + return Promise.reject(error); + } } }; } private async handleExecute( event: K, - newPayload: E[K], + payload: E[K], fireOptions: FlareFireOptions, handlerOptionsSet: Set> ) { @@ -146,25 +181,49 @@ export class Flare> { await Promise.allSettled( Array.from(handlerOptionsSet).map(async (handlerOptions) => { try { - await this.execute(event, newPayload, timeout, handlerOptions); - } catch (err) { - // TODO: handle or log exception - if (haltOnError) throw err; + await this.execute(event, payload, timeout, handlerOptions); + } catch (error) { + this.handleObservers(event, payload, FlareObservationType.Error, undefined, FlareObservationSource.Handler, error); + if (haltOnError) throw error; } }) ); } else { for (const handlerOptions of handlerOptionsSet) { try { - await this.execute(event, newPayload, timeout, handlerOptions); - } catch (err) { - // TODO: handle or log exception + await this.execute(event, payload, timeout, handlerOptions); + } catch (error) { + this.handleObservers(event, payload, FlareObservationType.Error, undefined, FlareObservationSource.Handler, error); if (haltOnError) break; } } } } + private async handleObservers( + event: K, + payload: E[K], + type: FlareObservationType, + sourceId: string | undefined, + source: FlareObservationSource, + arg?: T + ) { + for (const observer of this.observers) { + try { + const context = { + event, + payload, + timestamp: Date.now(), + type, + sourceId, + source, + }; + observer.fn(context, arg); + } catch (error) { + } + } + } + // ==================== internal methods ==================== private async execute( diff --git a/src/types.ts b/src/types.ts index 4439a8e..de79249 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,12 +24,42 @@ export type FlareInterceptor = { after?(event: K, payload: E[K]): void; }; -export type FlareMiddleware> = ( - context: { - event: K; - payload: E[K]; - stop: () => void; - set: (newPayload: E[K]) => void; - }, - next: () => Promise -) => void | Promise; +export type FlareMiddleware> = { + id?: string; + fn: ( + context: { + event: K; + payload: E[K]; + stop: () => void; + set: (newPayload: E[K]) => void; + }, + next: () => Promise + ) => void | Promise +}; + +export type FlareObserver = { + id?: string; + fn: ( + context: { + event: K, + payload: E[K], + timestamp: number, + type: FlareObservationType, + sourceId: string | undefined, + source: FlareObservationSource, + }, + arg: T) => void; +} + +export enum FlareObservationType { + Info = 'info', + Warning = 'warning', + Error = 'error' +}; + +export enum FlareObservationSource { + Interceptor = 'interceptor', + Middleware = 'middleware', + Handler = 'handler', + Flare = 'flare' +}; diff --git a/test/observe.test.ts b/test/observe.test.ts new file mode 100644 index 0000000..2d02f0a --- /dev/null +++ b/test/observe.test.ts @@ -0,0 +1,18 @@ +import { flare } from '../src'; + +test('observe', () => { + const PAYLOAD = 'PAYLOAD'; + const EVENT_NAME = 'EVENT_NAME'; + + flare.observe({ + id: 'ID', + fn: (context, arg) => { + console.log('context:', context); + console.log('arg:', arg); + expect(context.event).toBe(EVENT_NAME); + expect(context.payload).toBe(PAYLOAD); + } + }); + + flare.fire(EVENT_NAME, PAYLOAD); +});