Skip to content

Add new event package #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 3, 2019
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
173 changes: 173 additions & 0 deletions docs/event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Events

A strict type-safe event system with multiple emitter patterns.

## Installation

```
yarn add @boost/event
```

## Usage

The event system is built around individual `Event` classes that can be instantiated in isolation,
register and unregister their own listeners, and emit values by executing each listener with
arguments. There are multiple [types of events](#types), so choose the best one for each use case.

To begin using events, instantiate an `Event` with a unique name -- the name is purely for debugging
purposes.

```ts
import { Event } from '@boost/event';

const event = new Event<[string]>('example');
```

`Event`s utilize TypeScript generics for typing the arguments passed to listener functions. This can
be defined using a tuple or an array.

```ts
// One argument of type number
new Event<[number]>('foo');

// Two arguments of type number and string
new Event<[number, string]>('bar');

// Three arguments with the last item being optional
new Event<[object, boolean, string?]>('baz');

// Array of any type or size
new Event<unknown[]>('foo');
```

### Registering Listeners

Listeners are simply functions that can be registered to an event using `Event#listen`. The same
listener function reference will only be registered once.

```ts
event.listen(listener);
```

A listener can also be registered to execute only once, using `Event#once`, regardless of how many
times the event has been emitted.

```ts
event.once(listener);
```

### Unregistering Listeners

A listener can be unregistered from an event using `Event#unlisten`. The same listener reference
used to register must also be used for unregistering.

```ts
event.unlisten(listener);
```

### Emitting Events

Emitting is the concept of executing all registered listeners with a set of arguments. This can be
achieved through the `Event#emit` method, which requires an array of values to pass to each listener
as arguments.

```ts
event.emit(['abc']);
```

> The array values and its types should match the [generics defined](#usage) on the constructor.

### Scopes

Scopes are a mechanism for restricting listeners to a unique subset. Scopes are defined as the 2nd
argument to `Event#listen`, `unlisten`, `once`, and `emit`.

```ts
event.listen(listener);
event.listen(listener, 'foo');
event.listen(listener, 'bar');

// Will only execute the 1st listener
event.emit([]);

// Will only execute the 2nd listener
event.emit([], 'foo');
```

## Types

There are 4 types of events that can be instantiated and emitted.

### `Event`

Standard event that executes listeners in the order they were registered.

```ts
import { Event } from '@boost/event';

const event = new Event<[string, number]>('standard');

event.listen(listener);

event.emit(['abc', 123]);
```

### `BailEvent`

Like `Event` but can bail the execution loop early if a listener returns `false`. The `emit` method
will return `true` if a bail occurs.

```ts
import { BailEvent } from '@boost/event';

const event = new BailEvent<[object]>('bail');

// Will execute
event.listen(() => {});

// Will execute and bail
event.listen(() => false);

// Will not execute
event.listen(() => {});

const bailed = event.emit([{ example: true }]);
```

### `ConcurrentEvent`

Executes listeners in parallel and returns a promise with the result of all listeners.

```ts
import { ConcurrentEvent } from '@boost/event';

const event = new ConcurrentEvent<[]>('parallel');

event.listen(doHeavyProcess);
event.listen(doBackgroundJob);

// Async/await
const result = await event.emit([]);

// Promise
event.emit([]).then(result => {});
```

### `WaterfallEvent`

Executes each listener in order, passing the previous listeners return value as an argument to the
next listener.

```ts
import { WaterfallEvent } from '@boost/event';

const event = new WaterfallEvent<number>('waterfall');

event.listen(num => num * 2);
event.listen(num => num * 3);

const result = event.emit(10); // 60
```

> This event only accepts a single argument. The generic type should not be an array, as it types
> the only argument and the return type.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"release": "lerna publish"
},
"devDependencies": {
"@milesj/build-tools": "^0.34.0",
"@milesj/build-tools": "^0.37.2",
"fs-extra": "^7.0.1",
"lerna": "^3.13.1"
},
Expand All @@ -43,6 +43,11 @@
],
"settings": {
"node": true
},
"typescript": {
"exclude": [
"tests/typings.ts"
]
}
}
}
20 changes: 20 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
# 1.11.0

#### 🚀 New

- Added a new package, [@boost/event](https://www.npmjs.com/package/@boost/event), to provide a
type-safe static event system. The old event emitter is deprecated, so please migrate to the new
system!
- Added `onError`, `onRoutine`, `onRoutines`, `onStart`, `onStop`, `onTask`, and `onTasks` events
to `Console`.
- Added `onRoutine`, `onRoutines`,`onTask`, and `onTasks` events to `Executor`.
- Added `onCommand` and `onCommandData` events to `Routine`.
- Added `onFail`, `onPass`, `onRun`, and `onSkip` to `Task` and `Routine`.
- Added `onExit` to `Tool`.
- Tasks and Routines can be skipped during their run process if an `onRun` event listener returns
`false`.

#### 🛠 Internal

- Started writing [documentation using GitBook](https://milesj.gitbook.io/boost/).

# 1.10.1 - 2019-03-24

#### 🐞 Fixes
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"access": "public"
},
"dependencies": {
"@boost/event": "^0.0.0",
"@types/cli-truncate": "^1.1.0",
"@types/debug": "^4.1.2",
"@types/execa": "^0.9.0",
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/Console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import exit from 'exit';
import cliSize from 'term-size';
import ansiEscapes from 'ansi-escapes';
import { Event } from '@boost/event';
import Emitter from './Emitter';
import Task from './Task';
import Tool from './Tool';
import Output from './Output';
import Routine from './Routine';
import SignalError from './SignalError';
import { Debugger } from './types';

Expand Down Expand Up @@ -41,6 +44,20 @@ export default class Console extends Emitter {

logs: string[] = [];

onError: Event<[Error]>;

onRoutine: Event<[Routine<any, any>, unknown, boolean]>;

onRoutines: Event<[Routine<any, any>[], unknown, boolean]>;

onStart: Event<unknown[]>;

onStop: Event<[Error | null]>;

onTask: Event<[Task<any>, unknown, boolean]>;

onTasks: Event<[Task<any>[], unknown, boolean]>;

outputQueue: Output[] = [];

tool: Tool<any>;
Expand All @@ -63,6 +80,14 @@ export default class Console extends Emitter {
this.tool = tool;
this.writers = testWriters;

this.onError = new Event('error');
this.onRoutine = new Event('routine');
this.onRoutines = new Event('routines');
this.onStart = new Event('start');
this.onStop = new Event('stop');
this.onTask = new Event('task');
this.onTasks = new Event('tasks');

// istanbul ignore next
if (process.env.NODE_ENV !== 'test') {
process
Expand Down Expand Up @@ -324,6 +349,7 @@ export default class Console extends Emitter {
}

this.emit('error', [error]);
this.onError.emit([error]);
} else {
if (this.logs.length > 0) {
this.out(`\n${this.logs.join('\n')}\n`);
Expand Down Expand Up @@ -371,6 +397,7 @@ export default class Console extends Emitter {

this.debug('Starting console render loop');
this.emit('start', args);
this.onStart.emit(args);
this.wrapStreams();
this.displayHeader();
this.state.started = true;
Expand Down Expand Up @@ -411,6 +438,7 @@ export default class Console extends Emitter {
this.renderFinalOutput(error);
this.unwrapStreams();
this.emit('stop', [error]);
this.onStop.emit([error]);
this.state.stopped = true;
this.state.started = false;
}
Expand Down
34 changes: 18 additions & 16 deletions packages/core/src/Emitter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import camelCase from 'lodash/camelCase';
import upperFirst from 'lodash/upperFirst';
import { EVENT_NAME_PATTERN } from './constants';

export type EventArguments = any[];
Expand Down Expand Up @@ -25,22 +27,6 @@ export default class Emitter {
return this;
}

/**
* Syncronously execute listeners for the defined event and arguments,
* with the ability to intercept and abort early with a value.
*/
// emitCascade<T>(name: string, args: EventArguments = []): T | void {
// let value;

// Array.from(this.getListeners(this.createEventName(name))).some(listener => {
// value = listener(...args);

// return typeof value !== 'undefined';
// });

// return value;
// }

/**
* Return all event names with registered listeners.
*/
Expand Down Expand Up @@ -83,6 +69,22 @@ export default class Emitter {
throw new TypeError(`Invalid event listener for "${eventName}", must be a function.`);
}

let eventProp = eventName;
const args = ['listener'];

if (eventName.includes('.')) {
const [scope, event] = eventName.split('.', 2);

args.push(`'${scope}'`);
eventProp = event;
}

console.warn(
`Boost emitter has been deprecated. Please migrate \`on('${eventName}', listener)\` to the new @boost/event system, \`on${upperFirst(
camelCase(eventProp),
)}.listen(${args.join(', ')})\`.'`,
);

this.getListeners(eventName).add(listener);

return this;
Expand Down
Loading