Skip to content

Commit

Permalink
Merge pull request #5 from rstgroup/errorHandling
Browse files Browse the repository at this point in the history
error handling on emit
  • Loading branch information
mprzodala authored Oct 25, 2023
2 parents 1adb3d1 + 8ccebf2 commit b5bd62c
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 14 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"repository": "https://github.com/rstgroup/eventrix",
"homepage": "https://eventrix.js.org",
"scripts": {
"test": "jest",
"test": "jest --runInBand",
"test:watch": "jest --watch",
"test:clean": "jest --clearCache",
"build": "webpack --config webpack-prod.config.js --stats-error-details",
"coverage": "jest --coverage",
"coverage": "jest --coverage --runInBand",
"coverage:report": "cat ./coverage/lcov.info | coveralls",
"lint": "eslint src --ext ts,tsx,js",
"lint:fix": "eslint src --ext ts,tsx,js --fix "
Expand Down
33 changes: 33 additions & 0 deletions src/Eventrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,37 @@ describe('Eventrix', () => {
expect(scopedInstance4.getParent()?.getParent()?.getParent()?.getParent()?.getEventNameWithScope('')).toEqual('');
expect(scopedInstance4.getFirstParent().getEventNameWithScope('')).toEqual('');
});

describe('should run error callback', () => {
it('when emit catch error on receiver failed', () => {
const eventName = 'setBar';
const eventData = 'test';
const eventsReceiver = new EventsReceiver(eventName, (name, data, store) => {
throw 'failedReceiver';
});
const errorCallback = jest.fn();
eventrix.onError(errorCallback);
eventrix.useReceiver(eventsReceiver);
return eventrix.emit(eventName, eventData).catch(() => {
expect(errorCallback).toHaveBeenCalledWith('failedReceiver', eventName, eventData, initialState);
expect(eventrix.getState()).toEqual(initialState);
});
});

it('when emit catch error on listener failed', () => {
const eventName = 'setBar';
const eventData = 'test';

const failedListener = () => {
throw 'failedListener';
};
const errorCallback = jest.fn();
eventrix.onError(errorCallback);
eventrix.listen(eventName, failedListener);
return eventrix.emit(eventName, eventData).catch(() => {
expect(errorCallback).toHaveBeenCalledWith('failedListener', eventName, eventData, initialState);
expect(eventrix.getState()).toEqual(initialState);
});
});
});
});
5 changes: 5 additions & 0 deletions src/Eventrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EventsListenerI,
EventsReceiverI,
EmitArgumentsI,
ErrorCallback,
EventrixOptionsI,
ScopesI,
} from './interfaces';
Expand Down Expand Up @@ -35,6 +36,7 @@ class Eventrix<InitialStateI = any> implements EventrixI {
this.unlisten = this.unlisten.bind(this);
this.useReceiver = this.useReceiver.bind(this);
this.removeReceiver = this.removeReceiver.bind(this);
this.onError = this.onError.bind(this);
}
private getEventName(eventName: string): string {
if (!this.eventScope) {
Expand Down Expand Up @@ -113,6 +115,9 @@ class Eventrix<InitialStateI = any> implements EventrixI {
scopedInstance.stateManager = this.stateManager;
return scopedInstance;
}
onError(errorCallback: ErrorCallback<InitialStateI>) {
this.eventsEmitter.onError(errorCallback);
}
}

export default Eventrix;
52 changes: 40 additions & 12 deletions src/EventsEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import get from 'lodash/get';
import { isPromise } from './helpers';
import { EventsEmitterI, EventsListenerI, StateManagerI } from './interfaces';
import { EventsEmitterI, EventsListenerI, StateManagerI, ErrorCallback } from './interfaces';

class EventsEmitter implements EventsEmitterI {
listeners: {
Expand All @@ -10,12 +10,27 @@ class EventsEmitter implements EventsEmitterI {
matchedListenersCache: {
[key: string]: string[];
};
errorCallbacks: Set<ErrorCallback>;

constructor() {
this.listeners = {};
this.matchedListenersCache = {};
this.errorCallbacks = new Set();

this.emit = this.emit.bind(this);
this.emitWild = this.emitWild.bind(this);
this.onError = this.onError.bind(this);
this.handleError = this.handleError.bind(this);
}

onError(errorCallback: ErrorCallback) {
this.errorCallbacks.add(errorCallback);
}

handleError(error: Error, eventName: string, eventData: any, state: any): void {
this.errorCallbacks.forEach((callback: ErrorCallback) => {
callback(error, eventName, eventData, state);
});
}

useStore(stateManager: StateManagerI): void {
Expand Down Expand Up @@ -93,21 +108,34 @@ class EventsEmitter implements EventsEmitterI {
}

emitWild<EventDataI>(name: string, data: EventDataI): void {
const matchedEvents = this.getMatchedListeners(name);
return matchedEvents.forEach((eventName) => {
this.runListeners(eventName, this.getEventData(name, eventName, data), []);
});
try {
const matchedEvents = this.getMatchedListeners(name);
return matchedEvents.forEach((eventName) => {
this.runListeners(eventName, this.getEventData(name, eventName, data), []);
});
} catch (error) {
if (this.errorCallbacks.size > 0) {
this.handleError(error, name, data, this.stateManager!.getState());
}
}
}

emit<EventDataI = any>(name: string, data: EventDataI): Promise<any> {
const receiversResponse = this.stateManager?.runReceivers<EventDataI>(name, data);
if (isPromise(receiversResponse)) {
return receiversResponse.then((receiversData: any) => {
this.runListeners(name, data, receiversData);
});
try {
const receiversResponse = this.stateManager?.runReceivers<EventDataI>(name, data);
if (isPromise(receiversResponse)) {
return receiversResponse.then((receiversData: any) => {
this.runListeners(name, data, receiversData);
});
}
this.runListeners(name, data, receiversResponse);
return Promise.resolve(receiversResponse);
} catch (error) {
if (this.errorCallbacks.size > 0) {
this.handleError(error, name, data, this.stateManager!.getState());
}
return Promise.reject(error);
}
this.runListeners(name, data, receiversResponse);
return Promise.resolve(receiversResponse);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ export interface EventsEmitterI {
matchedListenersCache: {
[key: string]: string[];
};
errorCallbacks: Set<ErrorCallback>;
stateManager?: StateManagerI;
onError(errorCallback: ErrorCallback): void;
handleError(error: Error, eventName: string, eventData: any, state: any): void;
emit<EventDataI = any>(eventName: string, eventData?: EventDataI): Promise<any>;
emitWild<EventDataI = any>(eventName: string, eventData: EventDataI): void;
listen(eventName: string, listener: EventsListenerI<any>): void;
Expand All @@ -183,6 +186,7 @@ export interface EventsEmitterI {
runListeners<EventDataI>(name: string, data: EventDataI, receiversData: any[]): void;
emitWild<EventDataI>(name: string, data: EventDataI): void;
useStore(stateManager: StateManagerI): void;
onError(errorCallback: ErrorCallback): void;
}

export interface FetchHandlersI<DataI = any, ResponseI = any, EventDataI = any> {
Expand Down Expand Up @@ -310,3 +314,7 @@ export interface RequestHandlerInstance {
isAnyPending(): boolean;
isPending(requestId: string): boolean;
}

export interface ErrorCallback<StateI = any> {
(error: Error, eventName: string, eventData: any, state: StateI): void;
}
60 changes: 60 additions & 0 deletions src/react/context/context.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { render } from '@testing-library/react';
import EventrixProvider from '../context/EventrixProvider';
import useEvent from '../hooks/useEvent';
import EventrixScope from '../context/EventrixScope';
import { defaultEventrixInstance } from './context';

describe('context with default eventrix instance', () => {
const ItemComponent = ({ callback }: any) => {
useEvent('testEvent', callback);
return <div>Test Item Component</div>;
};
// @ts-ignore
const TestContainer = ({ children }: any) => <EventrixProvider>{children}</EventrixProvider>;

it('should invoke callback when event emitted', () => {
const eventrixInstance = defaultEventrixInstance;
const callbackMock = jest.fn();

render(
<TestContainer>
<ItemComponent callback={callbackMock} />
</TestContainer>,
);
eventrixInstance.emit('testEvent', 'test');
expect(callbackMock).toHaveBeenCalledWith('test', []);
});

it('should invoke callback when event emitted with scope', () => {
const eventrixInstance = defaultEventrixInstance;
const callbackMock = jest.fn();

render(
<TestContainer>
<EventrixScope event="Test">
<ItemComponent callback={callbackMock} />
</EventrixScope>
</TestContainer>,
);
eventrixInstance.emit('Test:testEvent', 'test');
expect(callbackMock).toHaveBeenCalledWith('test', []);
});

it('should invoke callback when event emitted with deep scope', () => {
const eventrixInstance = defaultEventrixInstance;
const callbackMock = jest.fn();

render(
<TestContainer>
<EventrixScope event="Test">
<EventrixScope event="List">
<ItemComponent callback={callbackMock} />
</EventrixScope>
</EventrixScope>
</TestContainer>,
);
eventrixInstance.emit('Test:List:testEvent', 'test');
expect(callbackMock).toHaveBeenCalledWith('test', []);
});
});

0 comments on commit b5bd62c

Please sign in to comment.