Skip to content

Commit

Permalink
feat: Class events and EventEmitter (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
offirgolan authored Jul 3, 2018
1 parent eb4e303 commit 0a3d591
Show file tree
Hide file tree
Showing 16 changed files with 622 additions and 154 deletions.
1 change: 0 additions & 1 deletion docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,4 @@
- [API](api.md)
- [Configuration](configuration.md)

- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
32 changes: 32 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@ __Example__
new Polly('<Recording Name>', { /* ... */ });
```

## Events

### create

Emitted when a Polly instance gets created.

!> This is a synchronous event.

__Example__

```js
const listener = polly => { /* Do Something */ };

Polly.on('create', listener);
Polly.off('create', listener);
Polly.once('create', polly => { /* Do Something Once */ });
```

### stop

Emitted when a Polly instance has successfully stopped.

__Example__

```js
const listener = polly => { /* Do Something */ };

Polly.on('stop', listener);
Polly.off('stop', listener);
Polly.once('stop', polly => { /* Do Something Once */ });
```

## Properties

### recordingName
Expand Down
4 changes: 2 additions & 2 deletions packages/@pollyjs/adapter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default class Adapter {

async intercept(pollyRequest) {
pollyRequest.action = ACTIONS.INTERCEPT;
await pollyRequest._invoke('intercept', pollyRequest.response);
await pollyRequest._intercept();

return this.onIntercept(pollyRequest, pollyRequest.response);
}
Expand All @@ -122,7 +122,7 @@ export default class Adapter {
const recordingEntry = await this.persister.findEntry(pollyRequest);

if (recordingEntry) {
await pollyRequest._trigger('beforeReplay', recordingEntry);
await pollyRequest._emit('beforeReplay', recordingEntry);

if (this.shouldReRecord(recordingEntry)) {
return this.record(pollyRequest);
Expand Down
245 changes: 245 additions & 0 deletions packages/@pollyjs/core/src/-private/event-emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { assert } from '@pollyjs/utils';
import isObjectLike from 'lodash-es/isObjectLike';

const EVENTS = Symbol();
const EVENT_NAMES = Symbol();

function assertEventName(eventName, eventNames) {
assert(
`Invalid event name provided. Expected string, received: "${typeof eventName}".`,
typeof eventName === 'string'
);

assert(
`Invalid event name provided: "${eventName}". Possible events: ${[
...eventNames
].join(', ')}.`,
eventNames.has(eventName)
);
}

function assertListener(listener) {
assert(
`Invalid listener provided. Expected function, received: "${typeof listener}".`,
typeof listener === 'function'
);
}

export default class EventEmitter {
/**
* @constructor
* @param {Object} options
* @param {String[]} options.eventNames - Supported events
*/
constructor({ eventNames = [] }) {
assert(
'An array of supported events must be provided via the `eventNames` option.',
eventNames && eventNames.length > 0
);

this[EVENTS] = new Map();
this[EVENT_NAMES] = new Set(eventNames);
}

/**
* Returns an array listing the events for which the emitter has
* registered listeners
*
* @returns {String[]}
*/
eventNames() {
const eventNames = [];

this[EVENTS].forEach(
(_, eventName) =>
this.hasListeners(eventName) && eventNames.push(eventName)
);

return eventNames;
}

/**
* Adds the `listener` function to the end of the listeners array for the
* event named `eventName`
*
* @param {String} eventName - The name of the event
* @param {Function} listener - The callback function
* @returns {EventEmitter}
*/
on(eventName, listener) {
assertEventName(eventName, this[EVENT_NAMES]);
assertListener(listener);

const events = this[EVENTS];

if (!events.has(eventName)) {
events.set(eventName, new Set());
}

events.get(eventName).add(listener);

return this;
}

/**
* Adds a one-time `listener` function for the event named `eventName`.
* The next time `eventName` is triggered, this listener is removed and
* then invoked.
*
* @param {String} eventName - The name of the event
* @param {Function} listener - The callback function
* @returns {EventEmitter}
*/
once(eventName, listener) {
assertEventName(eventName, this[EVENT_NAMES]);
assertListener(listener);

const once = (...args) => {
this.off(eventName, once);

return listener(...args);
};

this.on(eventName, once);

return this;
}

/**
* Removes the specified `listener` from the listener array for
* the event named `eventName`. If `listener` is not provided then it removes
* all listeners, or those of the specified `eventName`.
*
* @param {String} eventName - The name of the event
* @param {Function} [listener] - The callback function
* @returns {EventEmitter}
*/
off(eventName, listener) {
assertEventName(eventName, this[EVENT_NAMES]);

const events = this[EVENTS];

if (this.hasListeners(eventName)) {
if (typeof listener === 'function') {
events.get(eventName).delete(listener);
} else {
events.get(eventName).clear(eventName);
}
}

return this;
}

/**
* Returns a copy of the array of listeners for the event named `eventName`.
*
* @param {String} eventName - The name of the event
* @returns {Function[]}
*/
listeners(eventName) {
assertEventName(eventName, this[EVENT_NAMES]);

return this.hasListeners(eventName) ? [...this[EVENTS].get(eventName)] : [];
}

/**
* Returns `true` if there are any listeners for the event named `eventName`
* or `false` otherwise.
*
* @param {String} eventName - The name of the event
* @returns {Boolean}
*/
hasListeners(eventName) {
assertEventName(eventName, this[EVENT_NAMES]);

const events = this[EVENTS];

return events.has(eventName) && events.get(eventName).size > 0;
}

/**
* Asynchronously calls each of the `listeners` registered for the event named
* `eventName`, in the order they were registered, passing the supplied
* arguments to each.
*
* Returns a promise that will resolve to `true` if the event had listeners,
* `false` otherwise.
*
* @async
* @param {String} eventName - The name of the event
* @param {any} ...args - The arguments to pass to the listeners
* @returns {Promise<Boolean>}
*/
async emit(eventName, ...args) {
assertEventName(eventName, this[EVENT_NAMES]);

if (this.hasListeners(eventName)) {
for (const listener of this.listeners(eventName)) {
await listener(...args);
}

return true;
}

return false;
}

/**
* Asynchronously and concurrently calls each of the `listeners` registered
* for the event named `eventName`, in the order they were registered,
* passing the supplied arguments to each.
*
* Returns a promise that will resolve to `true` if the event had listeners,
* `false` otherwise.
*
* @async
* @param {String} eventName - The name of the event
* @param {any} ...args - The arguments to pass to the listeners
* @returns {Promise<Boolean>}
*/
async emitParallel(eventName, ...args) {
assertEventName(eventName, this[EVENT_NAMES]);

if (this.hasListeners(eventName)) {
await Promise.all(
this.listeners(eventName).map(listener => listener(...args))
);

return true;
}

return false;
}

/**
* Synchronously calls each of the `listeners` registered for the event named
* `eventName`, in the order they were registered, passing the supplied
* arguments to each.
*
* Throws if a listener's return value is promise-like.
*
* Returns `true` if the event had listeners, `false` otherwise.
*
* @param {String} eventName - The name of the event
* @param {any} ...args - The arguments to pass to the listeners
* @returns {Boolean}
*/
emitSync(eventName, ...args) {
assertEventName(eventName, this[EVENT_NAMES]);

if (this.hasListeners(eventName)) {
this.listeners(eventName).forEach(listener => {
const returnValue = listener(...args);

assert(
`Attempted to emit a synchronous event "${eventName}" but an asynchronous listener was called.`,
!(isObjectLike(returnValue) && typeof returnValue.then === 'function')
);
});

return true;
}

return false;
}
}
14 changes: 7 additions & 7 deletions packages/@pollyjs/core/src/-private/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default class PollyRequest extends HTTPBase {

async setup() {
// Trigger the `request` event
await this._trigger('request');
await this._emit('request');

// Setup the response
this.response = new PollyResponse();
Expand Down Expand Up @@ -150,7 +150,7 @@ export default class PollyRequest extends HTTPBase {
this.response.body = body;

// Trigger the `beforeResponse` event
await this._trigger('beforeResponse', this.response);
await this._emit('beforeResponse', this.response);

// End the response so it can no longer be modified
this.response.end();
Expand All @@ -164,19 +164,19 @@ export default class PollyRequest extends HTTPBase {
this.end();

// Trigger the `response` event
await this._trigger('response', this.response);
await this._emit('response', this.response);
}

async serializeBody() {
return serializeRequestBody(this.body);
}

_invoke(methodName, ...args) {
return this[ROUTE].invoke(methodName, this, ...args);
_intercept() {
return this[ROUTE].intercept(this, this.response, ...arguments);
}

_trigger(eventName, ...args) {
return this[ROUTE].trigger(eventName, this, ...args);
_emit(eventName, ...args) {
return this[ROUTE].emit(eventName, this, ...args);
}

_identify() {
Expand Down
Loading

0 comments on commit 0a3d591

Please sign in to comment.