Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 77 additions & 18 deletions src/flare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
FlareHandler,
FlareInterceptor,
FlareMiddleware,
FlareObservationSource,
FlareObservationType,
FlareObserver,
} from './types';

export class Flare<E extends Record<string, any>> {
Expand All @@ -14,6 +17,7 @@ export class Flare<E extends Record<string, any>> {

private interceptors: FlareInterceptor<E>[] = [];
private middlewares: FlareMiddleware<E>[] = [];
private observers: FlareObserver<E, any>[] = [];

in(interceptor: FlareInterceptor<E>): void {
this.interceptors.push(interceptor);
Expand All @@ -28,16 +32,26 @@ export class Flare<E extends Record<string, any>> {
payload: E[K],
options: FlareFireOptions = {},
): Promise<string | void> {
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);

Expand All @@ -61,6 +75,10 @@ export class Flare<E extends Record<string, any>> {
return () => this.release(event, handler);
}

observe<K extends keyof E>(observer: FlareObserver<E, K>): void {
this.observers.push(observer);
}

release<K extends keyof E>(
event: K,
handler: FlareHandler<E[K]>,
Expand Down Expand Up @@ -91,10 +109,13 @@ export class Flare<E extends Record<string, any>> {
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!
}
}
}
Expand All @@ -104,16 +125,20 @@ export class Flare<E extends Record<string, any>> {
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<K extends keyof E>(event: K, payload: E[K]): Promise<{ stopped: boolean, newPayload: E[K] }> {
private async handleMiddlewares<K extends keyof E>(event: K, payload: E[K])
: Promise<{ stopped: boolean, newPayload: E[K], stoppedMiddleware?: FlareMiddleware<E> }> {
const middlewares = this.middlewares;

const dis = this;
let stopped = false;
let newPayload = payload;
let stoppedMiddleware: FlareMiddleware<E> | undefined = undefined;

if (middlewares.length === 0) return { stopped, newPayload };

Expand All @@ -122,21 +147,31 @@ export class Flare<E extends Record<string, any>> {

await executeMiddleware(0);

return { stopped, newPayload };

return { stopped, newPayload, stoppedMiddleware };

async function executeMiddleware(index: number): Promise<void> {
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<K extends keyof E>(
event: K,
newPayload: E[K],
payload: E[K],
fireOptions: FlareFireOptions,
handlerOptionsSet: Set<HandlerOptionsPair<E, K>>
) {
Expand All @@ -146,25 +181,49 @@ export class Flare<E extends Record<string, any>> {
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<T, K extends keyof E>(
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<K extends keyof E>(
Expand Down
48 changes: 39 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,42 @@ export type FlareInterceptor<E> = {
after?<K extends keyof E>(event: K, payload: E[K]): void;
};

export type FlareMiddleware<E extends Record<string, any>> = <K extends keyof E>(
context: {
event: K;
payload: E[K];
stop: () => void;
set: (newPayload: E[K]) => void;
},
next: () => Promise<void>
) => void | Promise<void>;
export type FlareMiddleware<E extends Record<string, any>> = {
id?: string;
fn: <K extends keyof E>(
context: {
event: K;
payload: E[K];
stop: () => void;
set: (newPayload: E[K]) => void;
},
next: () => Promise<void>
) => void | Promise<void>
};

export type FlareObserver<E, K extends keyof E> = {
id?: string;
fn: <T>(
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'
};
18 changes: 18 additions & 0 deletions test/observe.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});