Skip to content
This repository has been archived by the owner on Dec 24, 2024. It is now read-only.

Commit

Permalink
feat: add reduceAsync to the EventsHub
Browse files Browse the repository at this point in the history
  • Loading branch information
homer0 committed Sep 17, 2020
1 parent 389a2b6 commit 5f40019
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 23 deletions.
12 changes: 12 additions & 0 deletions documents/shared/eventsHub.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ console.log(newUsersList);

> `reduce` also supports sending any number of parameters after the target variable.
You can also reduce a variable on an asychroneous way with `reduceAsync`:

```js
events.on('filter-users-list', async (list) => {
const fromAPI = await getUsersToFilterFromSomeAPI('...', { list });
return list.filter((item) => !fromAPI.includes(item));
});

const usersList = ['charito', 'Rosario'];
const newUsersList = await events.reduceAsync('filter-users-list', usersList);
```

## ES Modules

If you are using ESM, you can import the class from the `/esm` sub path:
Expand Down
94 changes: 71 additions & 23 deletions shared/eventsHub.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,44 +198,92 @@ class EventsHub {
* Reduces a target using an event. It's like emit, but the events listener return
* a modified (or not) version of the `target`.
*
* @template T
* @param {string|string[]} event An event name or a list of them.
* @param {*} target The variable to reduce with the listeners.
* @param {T} target The variable to reduce with the listeners.
* @param {...*} args A list of parameters to send to the listeners.
* @returns {*} A version of the `target` processed by the listeners.
* @returns {T} A version of the `target` processed by the listeners.
*/
reduce(event, target, ...args) {
const events = Array.isArray(event) ? event : [event];
let result = target;
events.forEach((name) => {
const subscribers = this.subscribers(name);
if (subscribers.length) {
const toClean = [];
let processed;
if (Array.isArray(result)) {
processed = result.slice();
} else if (typeof result === 'object') {
processed = { ...result };
} else {
processed = result;
}
const toClean = [];
const result = events.reduce(
(eventAcc, eventName) => this.subscribers(eventName).reduce(
(subAcc, subscriber) => {
let useCurrent;
if (Array.isArray(subAcc)) {
useCurrent = subAcc.slice();
} else if (typeof subAcc === 'object') {
useCurrent = { ...subAcc };
} else {
useCurrent = subAcc;
}

subscribers.forEach((subscriber) => {
processed = subscriber(...[processed, ...args]);
const nextStep = subscriber(...[useCurrent, ...args]);
if (subscriber.once) {
toClean.push({
event: name,
event: eventName,
fn: subscriber,
});
}
});

toClean.forEach((info) => this.off(info.event, info.fn));
result = processed;
}
});
return nextStep;
},
eventAcc,
),
target,
);

toClean.forEach((info) => this.off(info.event, info.fn));
return result;
}
/**
* Reduces a target using an event. It's like emit, but the events listener return
* a modified (or not) version of the `target`. This is the version async of `reduce`.
*
* @template T
* @param {string|string[]} event An event name or a list of them.
* @param {T} target The variable to reduce with the listeners.
* @param {...*} args A list of parameters to send to the listeners.
* @returns {Promise<T>} A version of the `target` processed by the listeners.
*/
reduceAsync(event, target, ...args) {
const events = Array.isArray(event) ? event : [event];
const toClean = [];
return events.reduce(
(eventAcc, eventName) => eventAcc.then((eventCurrent) => {
const subscribers = this.subscribers(eventName);
return subscribers.reduce(
(subAcc, subscriber) => subAcc.then((subCurrent) => {
let useCurrent;
if (Array.isArray(subCurrent)) {
useCurrent = subCurrent.slice();
} else if (typeof subCurrent === 'object') {
useCurrent = { ...subCurrent };
} else {
useCurrent = subCurrent;
}

const nextStep = subscriber(...[useCurrent, ...args]);
if (subscriber.once) {
toClean.push({
event: eventName,
fn: subscriber,
});
}

return nextStep;
}),
Promise.resolve(eventCurrent),
);
}),
Promise.resolve(target),
)
.then((result) => {
toClean.forEach((info) => this.off(info.event, info.fn));
return result;
});
}
/**
* Gets all the listeners for an event.
*
Expand Down
221 changes: 221 additions & 0 deletions tests/shared/eventsHub.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,225 @@ describe('EventsHub', () => {
expect(result).toBe(targetInitialValue + argOne + argTwo);
expect(target).toBe(targetInitialValue);
});

it('should allow new subscribers for reduced events (async & number)', async () => {
// Given
const eventName = 'THE EVENT';
const targetInitialValue = 0;
const target = targetInitialValue;
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => Promise.resolve(toReduce + 1));
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventName, subscriber);
result = await sut.reduceAsync(eventName, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(1);
expect(result).toBe(1);
expect(target).toBe(targetInitialValue);
});

it('should allow new subscribers for reduced events (async & array)', async () => {
// Given
const eventName = 'THE EVENT';
const targetInitialValue = ['one', 'two'];
const target = targetInitialValue.slice();
const newValue = 'three';
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => {
toReduce.push(newValue);
return Promise.resolve(toReduce);
});
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventName, subscriber);
result = await sut.reduceAsync(eventName, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(1);
expect(result).toEqual([...targetInitialValue, ...[newValue]]);
expect(target).toEqual(targetInitialValue);
});

it('should allow new subscribers for reduced events (async & object)', async () => {
// Given
const eventName = 'THE EVENT';
const targetInitialValue = { one: 1, two: 2 };
const target = { ...targetInitialValue };
const newValue = { three: 3 };
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => Promise.resolve({ ...toReduce, ...newValue }));
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventName, subscriber);
result = await sut.reduceAsync(eventName, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(1);
expect(result).toEqual({ ...targetInitialValue, ...newValue });
expect(target).toEqual(targetInitialValue);
});

it('should allow a subscriber to unsubscribe after reducing an event once (async)', async () => {
// Given
const eventName = 'THE EVENT';
const targetInitialValue = 0;
const target = targetInitialValue;
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => Promise.resolve(toReduce + 1));
// When
const sut = new EventsHub();
unsubscribe = sut.once(eventName, subscriber);
result = await sut.reduceAsync(eventName, target);
result = await sut.reduceAsync(eventName, result);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(1);
expect(result).toBe(1);
expect(target).toBe(targetInitialValue);
});

it('should allow a subscriber to reduce multiple events (async & number)', async () => {
// Given
const eventOneName = 'FIRST EVENT';
const eventTwoName = 'SECOND EVENT';
const eventNames = [eventOneName, eventTwoName];
const targetInitialValue = 0;
const target = targetInitialValue;
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => Promise.resolve(toReduce + 1));
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventNames, subscriber);
result = await sut.reduceAsync(eventNames, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(eventNames.length);
expect(result).toBe(targetInitialValue + eventNames.length);
expect(target).toBe(targetInitialValue);
});

it('should allow a subscriber to reduce multiple events (async & array)', async () => {
// Given
const eventOneName = 'FIRST EVENT';
const eventTwoName = 'SECOND EVENT';
const eventNames = [eventOneName, eventTwoName];
const targetInitialValue = ['one', 'two'];
const target = targetInitialValue.slice();
let result = null;
let unsubscribe = null;
let counter = -1;
const newValues = ['three', 'four'];
const subscriber = jest.fn((toReduce) => {
counter++;
toReduce.push(newValues[counter]);
return Promise.resolve(toReduce);
});
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventNames, subscriber);
result = await sut.reduceAsync(eventNames, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(eventNames.length);
expect(result).toEqual([...targetInitialValue, ...newValues]);
expect(target).toEqual(targetInitialValue);
});

it('should allow a subscriber to reduce multiple events (async object)', async () => {
// Given
const eventOneName = 'FIRST EVENT';
const eventTwoName = 'SECOND EVENT';
const eventNames = [eventOneName, eventTwoName];
const targetInitialValue = { one: 1, two: 2 };
const target = { ...targetInitialValue };
let result = null;
let unsubscribe = null;
let counter = -1;
const newValues = [{ three: 3 }, { four: 4 }];
const subscriber = jest.fn((toReduce) => {
counter++;
return Promise.resolve({ ...toReduce, ...newValues[counter] });
});
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventNames, subscriber);
result = await sut.reduceAsync(eventNames, target);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(eventNames.length);
expect(result).toEqual(Object.assign({}, targetInitialValue, ...newValues));
expect(target).toEqual(targetInitialValue);
});

it(
'should allow a subscriber to unsubscribe after reducing multiple events once (async)',
async () => {
// Given
const eventOneName = 'FIRST EVENT';
const eventTwoName = 'SECOND EVENT';
const eventNames = [eventOneName, eventTwoName];
const targetInitialValue = 0;
const target = targetInitialValue;
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce) => Promise.resolve(toReduce + 1));
// When
const sut = new EventsHub();
unsubscribe = sut.once(eventNames, subscriber);
result = await sut.reduceAsync(eventNames, target);
result = await sut.reduceAsync(eventNames, result);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(eventNames.length);
expect(result).toBe(targetInitialValue + eventNames.length);
expect(target).toBe(targetInitialValue);
},
);

it(
'should return the original target of a reduced event if there are no subscribers (async)',
async () => {
// Given
const eventName = 'THE EVENT';
const target = 1;
let result = null;
// When
const sut = new EventsHub();
result = await sut.reduceAsync(eventName, target);
// Then
expect(result).toBe(target);
},
);

it(
'should allow subscribers to receive multiple arguments for reduced events (async)',
async () => {
// Given
const eventName = 'THE EVENT';
const targetInitialValue = 0;
const target = targetInitialValue;
const argOne = 1;
const argTwo = 2;
let result = null;
let unsubscribe = null;
const subscriber = jest.fn((toReduce, one, two) => Promise.resolve(toReduce + one + two));
// When
const sut = new EventsHub();
unsubscribe = sut.on(eventName, subscriber);
result = await sut.reduceAsync(eventName, target, argOne, argTwo);
unsubscribe();
// Then
expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledWith(targetInitialValue, argOne, argTwo);
expect(result).toBe(targetInitialValue + argOne + argTwo);
expect(target).toBe(targetInitialValue);
},
);
});

0 comments on commit 5f40019

Please sign in to comment.