Skip to content

Commit

Permalink
♻️ [Emitten] Change library from an object to a Map (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
beefchimi authored Feb 5, 2023
1 parent 5b4ec69 commit ad57620
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-bottles-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'emitten': patch
---

Internally, switch the library values from an object to a Map.
4 changes: 1 addition & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
},
"rules": {
"prettier/prettier": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/method-signature-style": "off",
"@typescript-eslint/no-dynamic-delete": "off"
"@typescript-eslint/explicit-function-return-type": "off"
}
}
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ import {Emitten} from 'emitten';
// Both the `eventName` and the `args` from the `listener` are
// captured by TypeScript to assert type-safety!
type EventMap = {
change(value: string): void;
count(value?: number): void;
collect(...values: boolean[]): void;
// Method signature style could also look like:
change: (value: string) => void;
count: (value?: number) => void;
collect: (...values: boolean[]) => void;
other: (required: string, ...optional: string[]) => void;
};

Expand Down
36 changes: 16 additions & 20 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,23 @@ For this use case, you most likely want to use `Emitten` instead of `EmittenProt
// There is an important distinction... using `type`, you will:
// 1. Not need to `extend` from `EmittenMap`.
// 2. Automatically receive type-safety for `event` names.

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type EventMap = {
change(value: string): void;
count(value?: number): void;
collect(values: boolean[]): void;
rest(required: string, ...optional: string[]): void;
nothing(): void;
change: (value: string) => void;
count: (value?: number) => void;
collect: (values: boolean[]) => void;
rest: (required: string, ...optional: string[]) => void;
nothing: () => void;
};

// If you do prefer using `interface`, just know that:
// 1. You MUST `extend` from `EmittenMap`.
// 2. You will NOT receive type-safety for `event` names.
export interface AltMap extends EmittenMap {
change(value: string): void;
count(value?: number): void;
collect(values: boolean[]): void;
rest(required: string, ...optional: string[]): void;
nothing(): void;
change: (value: string) => void;
count: (value?: number) => void;
collect: (values: boolean[]) => void;
rest: (required: string, ...optional: string[]) => void;
nothing: () => void;
}

// Instantiate a new instance, passing the `EventMap`
Expand Down Expand Up @@ -133,10 +131,9 @@ myEvents.empty();
Since “derived classes” have access to the `protected` members of their “base class”, you can utilize `EmittenProtected` to both utilize `protected` members while also keeping them `protected` when instantiating your new `class`.

```ts
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type ExtendedEventMap = {
custom(value: string): void;
other(value: number): void;
custom: (value: string) => void;
other: (value: number) => void;
};

class ExtendedEmitten extends EmittenProtected<ExtendedEventMap> {
Expand Down Expand Up @@ -193,12 +190,11 @@ extended.empty();
We can of course create classes that do not extend `Emitten`, and instead create a `private` instance of `Emitten` to perform event actions on.

```ts
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type AnotherMap = {
change(value: string): void;
count(value?: number): void;
names(...values: string[]): void;
diverse(first: string, second: number, ...last: boolean[]): void;
change: (value: string) => void;
count: (value?: number) => void;
names: (...values: string[]) => void;
diverse: (first: string, second: number, ...last: boolean[]) => void;
};

class AnotherExample {
Expand Down
67 changes: 32 additions & 35 deletions src/EmittenProtected.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type {EmittenMap, EmittenLibraryPartial} from './types';
import type {EmittenMap, EmittenLibrary} from './types';

export class EmittenProtected<T extends EmittenMap> {
#multiLibrary: EmittenLibraryPartial<T> = {};
#singleLibrary: EmittenLibraryPartial<T> = {};
#multiLibrary: EmittenLibrary<T> = new Map();
#singleLibrary: EmittenLibrary<T> = new Map();

protected get activeEvents() {
// This redundant getter + method are required
Expand All @@ -11,77 +11,74 @@ export class EmittenProtected<T extends EmittenMap> {
}

protected getActiveEvents() {
const multiKeys = Object.keys(this.#multiLibrary);
const singleKeys = Object.keys(this.#singleLibrary);
const multiKeys = this.#multiLibrary.keys();
const singleKeys = this.#singleLibrary.keys();

const dedupedKeys = new Set([...multiKeys, ...singleKeys]);
const result: Array<keyof T> = [...dedupedKeys];

return result;
return [...dedupedKeys];
}

protected off<K extends keyof T>(eventName: K, listener: T[K]) {
const multiSet = this.#multiLibrary[eventName];
const singleSet = this.#singleLibrary[eventName];
this.#multiLibrary.get(eventName)?.delete(listener);

if (multiSet != null) {
multiSet.delete(listener);
if (multiSet.size === 0) delete this.#multiLibrary[eventName];
if (this.#multiLibrary.get(eventName)?.size === 0) {
this.#multiLibrary.delete(eventName);
}

if (singleSet != null) {
singleSet.delete(listener);
if (singleSet.size === 0) delete this.#singleLibrary[eventName];
this.#singleLibrary.get(eventName)?.delete(listener);

if (this.#singleLibrary.get(eventName)?.size === 0) {
this.#singleLibrary.delete(eventName);
}
}

protected on<K extends keyof T>(eventName: K, listener: T[K]) {
if (this.#multiLibrary[eventName] == null) {
this.#multiLibrary[eventName] = new Set();
if (!this.#multiLibrary.has(eventName)) {
this.#multiLibrary.set(eventName, new Set());
}

this.#multiLibrary[eventName]?.add(listener);
// TypeScript doesn't understand that the above
// condition results in this `.get()` being defined.
this.#multiLibrary.get(eventName)?.add(listener);

return () => {
this.off(eventName, listener);
};
}

protected once<K extends keyof T>(eventName: K, listener: T[K]) {
if (this.#singleLibrary[eventName] == null) {
this.#singleLibrary[eventName] = new Set();
if (!this.#singleLibrary.has(eventName)) {
this.#singleLibrary.set(eventName, new Set());
}

this.#singleLibrary[eventName]?.add(listener);
// TypeScript doesn't understand that the above
// condition results in this `.get()` being defined.
this.#singleLibrary.get(eventName)?.add(listener);
}

protected emit<K extends keyof T>(eventName: K, ...values: Parameters<T[K]>) {
const multiSet = this.#multiLibrary[eventName];
const singleSet = this.#singleLibrary[eventName];

multiSet?.forEach((listener) => {
this.#multiLibrary.get(eventName)?.forEach((listener) => {
listener(...values);
});

singleSet?.forEach((listener) => {
this.#singleLibrary.get(eventName)?.forEach((listener) => {
listener(...values);
});

delete this.#singleLibrary[eventName];
this.#singleLibrary.delete(eventName);
}

protected empty() {
this.#every(this.#multiLibrary);
this.#every(this.#singleLibrary);
}

#every = (library: EmittenLibraryPartial<T>) => {
for (const eventName in library) {
if (Object.hasOwn(library, eventName)) {
library[eventName]?.forEach((listener) => {
this.off(eventName, listener);
});
}
}
#every = (library: EmittenLibrary<T>) => {
library.forEach((collection, eventName) => {
collection.forEach((listener) => {
this.off(eventName, listener);
});
});
};
}
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ export type {
EmittenListener,
EmittenMap,
EmittenLibrary,
EmittenLibraryPartial,
} from './types';
8 changes: 4 additions & 4 deletions src/tests/Emitten.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {Emitten} from '../Emitten';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type MockEventMap = {
foo(value: string): void;
bar(value?: number): void;
baz(...values: boolean[]): void;
qux(required: string, ...optional: string[]): void;
foo: (value: string) => void;
bar: (value?: number) => void;
baz: (...values: boolean[]) => void;
qux: (required: string, ...optional: string[]) => void;
};

describe('Emitten full public members', () => {
Expand Down
4 changes: 1 addition & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@ export type EmittenListener<V extends readonly unknown[] = any[]> = (
) => void;

export type EmittenMap = Record<EmittenKey, EmittenListener>;

export type EmittenLibrary<T> = Record<keyof T, Set<T[keyof T]>>;
export type EmittenLibraryPartial<T> = Partial<EmittenLibrary<T>>;
export type EmittenLibrary<T> = Map<keyof T, Set<T[keyof T]>>;
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
Expand Down

0 comments on commit ad57620

Please sign in to comment.