Skip to content
This repository has been archived by the owner on Jan 28, 2020. It is now read-only.

Commit

Permalink
Merge pull request #7 from elderapo/from-event-emitter
Browse files Browse the repository at this point in the history
feat(*): fromEventEmitter
  • Loading branch information
elderapo authored Jul 8, 2018
2 parents 70b31ce + 3737d44 commit 5fc59e6
Show file tree
Hide file tree
Showing 16 changed files with 1,179 additions and 680 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/examples/**/*.ts
67 changes: 26 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ yarn add @elderapo/typed-event-emitter
npm install @elderapo/typed-event-emitter
```

### Basic usage
### Usage

```typescript
import { TypedEventEmitter } from "@elderapo/typed-event-emitter";
Expand Down Expand Up @@ -56,56 +56,41 @@ removeListener2();
removeListener3();
```

### Advanced usage
### [More examples](https://github.com/elderapo/typed-event-emitter/tree/master/src/examples)

### Cavecats:

```typescript
import { TypedEventEmitter } from "@elderapo/typed-event-emitter";
import { EventEmitter } from "events";

enum IncommingEvent {
SomeData,
SomeOtherData,
SomeOtherOtherData = "some-other-other-data"
}
// some event emitter from external library
const ee = new EventEmitter();

type IncommingEvents = {
[IncommingEvent.SomeData]: number[];
[IncommingEvent.SomeOtherData]: {};
[IncommingEvent.SomeOtherOtherData]: { name: string; age: number };
};
setInterval(() => {
ee.emit("0"); // <-- notice "0" instead of 0
}, 500);

// `OutgoingEvent.SendName = 3;` so it doesn't overlap with `IncommingEvent.SomeData = 0;`
// Remove or set it to `0` and see what happens with `IMyEventEmitterEvents` :)
enum OutgoingEvent {
SendName = 3,
SendAge,
SendPerson = "send-person"
enum Event {
SomeEvent // === 0 number
}

type OutgoingEvents = {
[OutgoingEvent.SendName]: string;
[OutgoingEvent.SendAge]: number;
[OutgoingEvent.SendPerson]: { name: string; age: number };
type Events = {
[Event.SomeEvent]: void;
};

// It's important to use interfaces instead of `type Combined = OutgoingEvents & IncommingEvents`
// while combining `Events` to keep 100% type safety!
interface IMyEventEmitterEvents extends OutgoingEvents, IncommingEvents {}

class BidirectionalCommunication extends TypedEventEmitter<
IMyEventEmitterEvents
> {}

const ee = new BidirectionalCommunication();

ee.emit(OutgoingEvent.SendName, "name"); // ok
ee.emit(OutgoingEvent.SendName, 123); // wrong type - TS error
const tee = TypedEventEmitter.fromEventEmitter<Events>(ee);

ee.emit(OutgoingEvent.SendAge, 20); // ok
ee.emit(OutgoingEvent.SendAge, "20"); // wrong type - TS error
/*
For EventEmitter "0" and 0 are the same because it uses basic {} as key/value:
ee.emit(OutgoingEvent.SendPerson, { name: "name", age: 123 }); // ok
ee.emit(OutgoingEvent.SendPerson, { name: "name" }); // wrong type - TS error
const a = {};
a[123] = 123;
a["123"]++;
a[123] === 124; // true
*/

ee.on(IncommingEvent.SomeData, payload => {}); // type === number[]
ee.on(IncommingEvent.SomeOtherData, payload => {}); // type === {}
ee.on(IncommingEvent.SomeOtherOtherData, payload => {}); // type === { name: string; age: number };
tee.on(Event.SomeEvent, () => {
console.log(`Received event!`);
});
```
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"version": "1.2.0",
"description": "",
"keywords": [],
"main": "dist/typed-event-emitter.umd.js",
"module": "dist/typed-event-emitter.es5.js",
"typings": "dist/types/typed-event-emitter.d.ts",
"main": "dist/TypedEventEmitter.umd.js",
"module": "dist/TypedEventEmitter.es5.js",
"typings": "dist/types/TypedEventEmitter.d.ts",
"files": [
"dist"
],
Expand All @@ -22,7 +22,7 @@
"scripts": {
"lint": "tslint -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
"prebuild": "rimraf dist",
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --excludePrivate --out docs --target es6 --theme minimal --mode file src",
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --excludePrivate --out docs --target es6 --theme minimal --mode file src/TypedEventEmitter.ts",
"start": "rollup -c rollup.config.ts -w",
"test": "jest",
"test:watch": "jest --watch",
Expand Down Expand Up @@ -95,18 +95,19 @@
"rollup": "^0.62.0",
"rollup-plugin-commonjs": "^9.0.0",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript2": "^0.15.1",
"semantic-release": "^15.6.3",
"travis-deploy-once": "^5.0.1",
"ts-jest": "^23.0.0",
"ts-node": "^7.0.0",
"tslint": "5.10.0",
"tslint-config-prettier": "^1.1.0",
"tslint-config-standard": "^7.0.0",
"typedoc": "^0.11.0",
"typescript": "^2.9.2",
"validate-commit-msg": "^2.12.2",
"travis-deploy-once": "^5.0.1"
"validate-commit-msg": "^2.12.2"
}
}
32 changes: 17 additions & 15 deletions rollup.config.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import sourceMaps from 'rollup-plugin-sourcemaps'
import camelCase from 'lodash.camelcase'
import typescript from 'rollup-plugin-typescript2'
import json from 'rollup-plugin-json'
import resolve from "rollup-plugin-node-resolve";
import commonjs from "rollup-plugin-commonjs";
import sourceMaps from "rollup-plugin-sourcemaps";
import camelCase from "lodash.camelcase";
import typescript from "rollup-plugin-typescript2";
import json from "rollup-plugin-json";
import builtins from "rollup-plugin-node-builtins";

const pkg = require('./package.json')
const pkg = require("./package.json");

const libraryName = 'typed-event-emitter'
const libraryName = "typed-event-emitter";

export default {
input: `src/${libraryName}.ts`,
input: `src/TypedEventEmitter.ts`,
output: [
{ file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true },
{ file: pkg.main, name: camelCase(libraryName), format: "umd", sourcemap: true },
{ file: pkg.module, format: "es", sourcemap: true }
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
include: "src/**"
},
plugins: [
// Allow json resolution
builtins(),
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
Expand All @@ -33,6 +35,6 @@ export default {
resolve(),

// Resolve source maps to the original source
sourceMaps(),
],
}
sourceMaps()
]
};
125 changes: 125 additions & 0 deletions src/TypedEventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { EventEmitter } from "events";

export type RemoveEventListener = () => void;
export type EventHandler = (payload: any) => void;

export enum InternalEventEmitterEvent {
NewListener = "newListener",
RemoveListener = "removeListener"
}

export type InternalEventEmitterEvents = {
[InternalEventEmitterEvent.NewListener]: EventHandler;
[InternalEventEmitterEvent.RemoveListener]: { event: any; listener: EventHandler };
};

export class TypedEventEmitter<
T,
Events extends InternalEventEmitterEvents & T = InternalEventEmitterEvents & T
> {
constructor(private internalEventEmitter: EventEmitter = new EventEmitter()) {
this.interceptEmit();
}

static fromEventEmitter<T>(eventEmitter: EventEmitter): TypedEventEmitter<T> {
return new TypedEventEmitter<T>(eventEmitter);
}

public on<Event extends keyof Events>(
event: Event,
listener: (payload: Events[Event]) => void
): RemoveEventListener {
this.internalEventEmitter.on(this.castTypedEventToEvent(event), listener);
return () => this.removeListener(event, listener);
}

public once<Event extends keyof Events>(
event: Event,
listener: (payload: Events[Event]) => void
): RemoveEventListener {
this.internalEventEmitter.once(this.castTypedEventToEvent(event), listener);
return () => this.removeListener(event, listener);
}

public prependListener<Event extends keyof Events>(
event: Event,
listener: (payload: Events[Event]) => void
): RemoveEventListener {
this.internalEventEmitter.prependListener(this.castTypedEventToEvent(event), listener);
return () => this.removeListener(event, listener);
}

public prependOnceListener<Event extends keyof Events>(
event: Event,
listener: (payload: Events[Event]) => void
): RemoveEventListener {
this.internalEventEmitter.prependOnceListener(this.castTypedEventToEvent(event), listener);
return () => this.removeListener(event, listener);
}

public removeListener<Event extends keyof Events>(
event: Event,
listenerFunc: (payload: Events[Event]) => void
): void {
this.internalEventEmitter.removeListener(this.castTypedEventToEvent(event), listenerFunc);
}

public removeAllListeners<Event extends keyof Events>(event?: Event): void {
if (typeof event === "undefined") {
this.internalEventEmitter.removeAllListeners();
return;
}

this.internalEventEmitter.removeAllListeners(this.castTypedEventToEvent(event));
}

public setMaxListeners(n: number): void {
this.internalEventEmitter.setMaxListeners(n);
}

public getMaxListeners(): number {
return this.internalEventEmitter.getMaxListeners();
}

public listeners<Event extends keyof Events>(event: Event): EventHandler[] {
return this.internalEventEmitter.listeners(this.castTypedEventToEvent(event)) as EventHandler[];
}

public emit<Event extends keyof Events>(event: Event, payload: Events[Event]): void {
this.internalEventEmitter.emit(this.castTypedEventToEvent(event), payload);
}

public eventIdentifiers(): Array<keyof Events> {
return this.internalEventEmitter.eventNames().map(eventIndetifier => {
if (typeof eventIndetifier === "symbol") {
return eventIndetifier;
}

const n = parseFloat(eventIndetifier);

return Number.isNaN(n) ? eventIndetifier : n;
}) as Array<keyof Events>;
}

public listenerCount<Event extends keyof Events>(event: Event): number {
return this.internalEventEmitter.listenerCount(this.castTypedEventToEvent(event));
}

private castTypedEventToEvent<Event extends keyof Events>(event: Event): string | symbol {
return event as string | symbol;
}

private interceptEmit() {
let originalFunc: Function = this.internalEventEmitter.emit.bind(this.internalEventEmitter);

this.internalEventEmitter.emit = (...args: any[]) => {
if (args[0] === InternalEventEmitterEvent.RemoveListener) {
return originalFunc(args[0], {
event: args[1],
listener: args[2]
});
}
return originalFunc(...args);
};
}
}
34 changes: 34 additions & 0 deletions src/examples/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TypedEventEmitter } from "../TypedEventEmitter";

enum Event {
SomeEvent0,
SomeEvent1,
SomeEvent_StringKey = "lalala"
}

type Events = {
[Event.SomeEvent0]: string;
[Event.SomeEvent1]: number;
[Event.SomeEvent_StringKey]: { name: string; age: number };
};

const ee = new TypedEventEmitter<Events>();

const removeListener0 = ee.on(Event.SomeEvent0, payload => {}); // payload type === string
const removeListener1 = ee.once(Event.SomeEvent1, payload => {}); // payload type === number
const removeListener2 = ee.prependListener(Event.SomeEvent_StringKey, payload => {}); // payload type === { name: string; age: number }
const removeListener3 = ee.prependOnceListener(Event.SomeEvent_StringKey, payload => {}); // payload type === { name: string; age: number }

ee.emit(Event.SomeEvent0, "string"); // ok
// ee.emit(Event.SomeEvent0, 1); // wrong type - TS error

ee.emit(Event.SomeEvent1, 666); // ok
// ee.emit(Event.SomeEvent1, "aaa"); // wrong type - TS error

ee.emit(Event.SomeEvent_StringKey, { name: "Tomek", age: 123 }); // ok
// ee.emit(Event.SomeEvent_StringKey, {}); // wrong type - TS error

removeListener0(); // pretty self explanatory :)
removeListener1();
removeListener2();
removeListener3();
22 changes: 22 additions & 0 deletions src/examples/fromEventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TypedEventEmitter } from "../TypedEventEmitter";

// Process extends from EventEmitter
global.process.on("uncaughtException", ex => {
console.log("Caught from eventEmitter", ex.message);
});

enum ProcessEvent {
UncaughtException = "uncaughtException"
}

type ProcessEvents = {
[ProcessEvent.UncaughtException]: Error;
};

const typedEventEmitter = TypedEventEmitter.fromEventEmitter<ProcessEvents>(global.process);

typedEventEmitter.on(ProcessEvent.UncaughtException, ex => {
console.log("Caught from typedEventEmitter", ex.message); // type ex === Error
});

throw new Error("Hehuheuheueh");
Loading

0 comments on commit 5fc59e6

Please sign in to comment.