* restructure event-emit directory and convert emitters to promises

- also add emitEventWithAbort function

* implement event emitters as promises

* clean up logic

- return from for loop
- define response as const on each iteration.
- return true if loop completes successfully.

* rename event_emit folder to event-emit
This commit is contained in:
Darren Ethier 2020-03-20 12:46:24 -04:00 committed by GitHub
parent 8ca9fc9b6f
commit e4a82aa1ce
8 changed files with 146 additions and 44 deletions

View File

@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { actions, reducer, emitEvent } from '../event_emit';
import { actions, reducer, emitEvent, emitEventWithAbort } from '../event-emit';
const EMIT_TYPES = {
CHECKOUT_COMPLETE_WITH_SUCCESS: 'checkout_complete',
@ -70,4 +70,10 @@ const emitterSubscribers = ( dispatcher ) => ( {
},
} );
export { EMIT_TYPES, emitterSubscribers, reducer, emitEvent };
export {
EMIT_TYPES,
emitterSubscribers,
reducer,
emitEvent,
emitEventWithAbort,
};

View File

@ -10,6 +10,7 @@ import {
EMIT_TYPES,
emitterSubscribers,
emitEvent,
emitEventWithAbort,
reducer as emitReducer,
} from './event-emit';
@ -124,23 +125,18 @@ export const CheckoutProvider = ( {
useEffect( () => {
const status = checkoutState.status;
if ( status === STATUS.PROCESSING ) {
const error = emitEvent(
emitEventWithAbort(
currentObservers.current,
EMIT_TYPES.CHECKOUT_PROCESSING,
{}
);
//@todo bail if error object detected (see flow).
//Fire off checkoutFail event, and then reset checkout
//status to idle (with hasError flag) - then return from this hook.
// Finally after the event subscribers have processed, do the
// checkout submit sending the order to the server for processing
// and followup on errors from it.
// I'll need to setup a special emitter that aborts on first fail
// instead of hitting all subscribed event observers.
if ( error ) {
dispatchActions.setHasError();
}
dispatch( actions.setComplete() );
).then( ( response ) => {
if ( response !== true ) {
// @todo handle any validation error property values in the
// response
dispatchActions.setHasError();
}
dispatch( actions.setComplete() );
} );
}
if ( checkoutState.isComplete ) {
if ( checkoutState.hasError ) {
@ -154,14 +150,21 @@ export const CheckoutProvider = ( {
currentObservers.current,
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS,
{}
);
}
// all observers have done their thing so let's redirect (if no error)
if ( ! checkoutState.hasError ) {
window.location = checkoutState.redirectUrl;
).then( () => {
// all observers have done their thing so let's redirect
// (if no error)
if ( ! checkoutState.hasError ) {
window.location = checkoutState.redirectUrl;
}
} );
}
}
}, [ checkoutState.status, checkoutState.hasError ] );
}, [
checkoutState.status,
checkoutState.hasError,
checkoutState.isComplete,
checkoutState.redirectUrl,
] );
const onSubmit = () => {
dispatch( actions.setProcessing() );

View File

@ -0,0 +1,63 @@
/**
* Emits events on registered observers for the provided type and passes along
* the provided data.
*
* This event emitter will silently catch promise errors, but doesn't care
* otherwise if any errors are caused by observers. So events that do care
* should use `emitEventWithAbort` instead.
*
* @param {Object} observers The registered observers to omit to.
* @param {string} eventType The event type being emitted.
* @param {*} data Data passed along to the observer when it is
* invoked.
*
* @return {Promise} A promise that resolves to true after all observers have
* executed.
*/
export const emitEvent = async ( observers, eventType, data ) => {
const observersByType = observers[ eventType ] || [];
for ( let i = 0; i < observersByType.length; i++ ) {
try {
await Promise.resolve( observersByType[ i ]( data ) );
} catch ( e ) {
// we don't care about errors blocking execution, but will
// console.error for troubleshooting.
// eslint-disable-next-line no-console
console.error( e );
}
}
return true;
};
/**
* Emits events on registered observers for the provided type and passes along
* the provided data. This event emitter will abort and return any value from
* observers that do not return true.
*
* @param {Object} observers The registered observers to omit to.
* @param {string} eventType The event type being emitted.
* @param {*} data Data passed along to the observer when it is
* invoked.
*
* @return {Promise} Returns a promise that resolves to either boolean or the
* return value of the aborted observer.
*/
export const emitEventWithAbort = async ( observers, eventType, data ) => {
const observersByType = observers[ eventType ] || [];
for ( let i = 0; i < observersByType.length; i++ ) {
try {
const response = await Promise.resolve(
observersByType[ i ]( data )
);
if ( response !== true ) {
return response;
}
} catch ( e ) {
// we don't handle thrown errors but just console.log for
// troubleshooting
// eslint-disable-next-line no-console
console.error( e );
}
}
return true;
};

View File

@ -0,0 +1,2 @@
export * from './reducer';
export * from './emitters';

View File

@ -26,26 +26,6 @@ export const actions = {
},
};
/**
* Emits events on registered observers for the provided type and passes along
* the provided data.
*
* @param {Object} observers The registered observers to omit to.
* @param {string} eventType The event type being emitted.
* @param {*} data Data passed along to the observer when it is
* invoked.
*/
export const emitEvent = ( observers, eventType, data ) => {
const observersByType = observers[ eventType ] || [];
let hasError = false;
observersByType.forEach( ( observer ) => {
if ( typeof observer === 'function' ) {
hasError = !! observer( data, hasError );
}
} );
return hasError;
};
/**
* Handles actions for emmitters
*

View File

@ -0,0 +1,49 @@
/**
* Internal dependencies
*/
import { emitEvent, emitEventWithAbort } from '../emitters';
describe( 'Testing emitters', () => {
let observerMocks = {};
beforeEach( () => {
observerMocks = {
observerA: jest.fn().mockReturnValue( true ),
observerB: jest.fn().mockReturnValue( true ),
observerReturnValue: jest.fn().mockReturnValue( 10 ),
observerPromiseWithReject: jest
.fn()
.mockRejectedValue( 'an error' ),
observerPromiseWithResolvedValue: jest.fn().mockResolvedValue( 10 ),
};
} );
describe( 'Testing emitEvent()', () => {
it( 'invokes all observers', async () => {
const observers = { test: Object.values( observerMocks ) };
const response = await emitEvent( observers, 'test', 'foo' );
expect( console ).toHaveErroredWith( 'an error' );
expect( observerMocks.observerA ).toHaveBeenCalledTimes( 1 );
expect( observerMocks.observerB ).toHaveBeenCalledWith( 'foo' );
expect( response ).toBe( true );
} );
} );
describe( 'Testing emitEventWithAbort()', () => {
it(
'aborts on non truthy value and does not invoke remaining ' +
'observers',
async () => {
const observers = { test: Object.values( observerMocks ) };
const response = await emitEventWithAbort(
observers,
'test',
'foo'
);
expect( console ).not.toHaveErrored();
expect( observerMocks.observerB ).toHaveBeenCalledTimes( 1 );
expect(
observerMocks.observerPromiseWithResolvedValue
).not.toHaveBeenCalled();
expect( response ).toBe( 10 );
}
);
} );
} );

View File

@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { actions, reducer, emitEvent } from '../event_emit';
import { actions, reducer, emitEvent } from '../event-emit';
const EMIT_TYPES = {
SHIPPING_RATES_SUCCESS: 'shipping_rates_success',