Skip to content

Commit

Permalink
💥 [EmittenProtected] Everything is protected (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
beefchimi authored Jan 26, 2023
1 parent 9d58716 commit 0685229
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-squids-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'emitten': minor
---

Make all members of EmittenProtected protected.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"rules": {
"prettier/prettier": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/method-signature-style": "off",
"@typescript-eslint/no-dynamic-delete": "off"
}
}
40 changes: 28 additions & 12 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Below are some common use-cases for `Emitten` and `EmittenProtected`.

The easiest way to use `Emitten` is to simply instantiate it and begin wiring up your logic.

For this use case, you most likely want to use `Emitten` instead of `EmittenProtected`. If you instantiate using `EmittenProtected`, you will not be able to call any `protected` members, such as `.emit()` or `.empty()`.
For this use case, you most likely want to use `Emitten` instead of `EmittenProtected`. If you instantiate using `EmittenProtected`, you will not be able to call any `protected` members.

```ts
import {Emitten} from 'emitten';
Expand Down Expand Up @@ -50,7 +50,7 @@ myEvents.on('count', (value) => console.log('2nd count listener', value));
// Alternatively, you can register a listener using
// `.disposable()`, which will return the corresponding
// `.off()` method to make removal easier.
const registered = myEvents.on('count', (value) =>
const registered = myEvents.disposable('count', (value) =>
console.log('An anonymous function', value),
);

Expand Down Expand Up @@ -100,13 +100,24 @@ interface ExtendedEventMap {
}

class ExtendedEmitten extends EmittenProtected<ExtendedEventMap> {
constructor() {
// `ExtendedEmitten` now has all of the `properties`,
// `accessors`, and `methods` from `EmittenProtected`.
super();
// If required, you can selectively expose any `protected` members.
// Otherwise, if you want all members to be `public`, you can
// extend the `Emitten` class instead.

public on<TKey extends keyof ExtendedEventMap>(
eventName: TKey,
listener: EmittenListener<ExtendedEventMap[TKey]>,
) {
super.on(eventName, listener);
}

public off<TKey extends keyof ExtendedEventMap>(
eventName: TKey,
listener: EmittenListener<ExtendedEventMap[TKey]>,
) {
super.off(eventName, listener);
}

// If required, you can expose any `protected` members.
public emit<TKey extends keyof ExtendedEventMap>(
eventName: TKey,
value?: ExtendedEventMap[TKey],
Expand All @@ -121,13 +132,14 @@ class ExtendedEmitten extends EmittenProtected<ExtendedEventMap> {

const extended = new ExtendedEmitten();

extended.on('custom', (value) => console.log('value', value));
extended.report();
// Since we converted both `.on()` and `.emit()` to be `public`,
// we can safely call them on the instance.

// Since we converted `.emit()` to a `public` member,
// we can safely call this on the instance.
extended.on('custom', (value) => console.log('value', value));
extended.emit('custom', 'hello');

extended.report();

// However, we did not expose `.empty()`, so we will
// receive a TypeScript error attempting to call this.
extended.empty();
Expand All @@ -138,6 +150,10 @@ 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
function assertValue(value?: string): value is string {
return Boolean(value?.length);
}

class AnotherExample {
#id = 'DefaultId';
#counter = 0;
Expand All @@ -153,7 +169,7 @@ class AnotherExample {

constructor() {
this.#handleChange = (value) => {
this.#id = value?.length ? value : this.#id;
this.#id = assertValue(value) ? value : this.#id;
console.log('#handleChange', value);
};

Expand Down
29 changes: 0 additions & 29 deletions docs/future.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,35 +44,6 @@ function emit<TKey extends keyof T>(

This will require some experimentation.

## Private iterator

It would be nice to have a `every()` method to iterate over all `listeners` for each `event` within a `library`.

This isn’t so please though, as `.bind(this)` ends up being required when calling `.empty()`.

```ts
protected empty() {
this.#every(this.#multiLibrary, this.off.bind(this));
this.#every(this.#singleLibrary, this.off.bind(this));
}

#every(
library: EmittenLibrary<T>,
callback: <TKey extends keyof T>(
eventName: TKey,
listener: EmittenListener<T[TKey]>,
) => void,
) {
for (const eventName in library) {
if (Object.prototype.hasOwnProperty.call(library, eventName)) {
library[eventName]?.forEach((listener) =>
callback(eventName, listener),
);
}
}
}
```
## No dynamic delete

I got sloppy and used the `delete` keyword... I need to remove the `@typescript-eslint/no-dynamic-delete` override and filter that `object` properly.
Expand Down
41 changes: 39 additions & 2 deletions src/Emitten.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
import {EmittenProtected} from './EmittenProtected';
import type {EmittenListener} from './types';

export class Emitten<T> extends EmittenProtected<T> {
public emit<TKey extends keyof T>(eventName: TKey, value?: T[TKey]) {
export class Emitten<TEventMap> extends EmittenProtected<TEventMap> {
public get activeEvents() {
return super.activeEvents;
}

public off<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<TEventMap[TKey]>,
) {
super.off(eventName, listener);
}

public on<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<TEventMap[TKey]>,
) {
super.on(eventName, listener);
}

public once<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<TEventMap[TKey]>,
) {
super.once(eventName, listener);
}

public disposable<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<TEventMap[TKey]>,
) {
const result = super.disposable(eventName, listener);
return result;
}

public emit<TKey extends keyof TEventMap>(
eventName: TKey,
value?: TEventMap[TKey],
) {
super.emit(eventName, value);
}

Expand Down
55 changes: 30 additions & 25 deletions src/EmittenProtected.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {EmittenListener, EmittenLibrary} from './types';

export class EmittenProtected<T> {
#multiLibrary: EmittenLibrary<T> = {};
#singleLibrary: EmittenLibrary<T> = {};
export class EmittenProtected<TEventMap> {
#multiLibrary: EmittenLibrary<TEventMap> = {};
#singleLibrary: EmittenLibrary<TEventMap> = {};

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

Expand All @@ -13,9 +13,9 @@ export class EmittenProtected<T> {
return [...dedupedKeys];
}

off<TKey extends keyof T>(
protected off<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<T[TKey]>,
listener: EmittenListener<TEventMap[TKey]>,
) {
const multiSet = this.#multiLibrary[eventName];
const singleSet = this.#singleLibrary[eventName];
Expand All @@ -31,9 +31,9 @@ export class EmittenProtected<T> {
}
}

on<TKey extends keyof T>(
protected on<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<T[TKey]>,
listener: EmittenListener<TEventMap[TKey]>,
) {
if (this.#multiLibrary[eventName] == null) {
this.#multiLibrary[eventName] = new Set();
Expand All @@ -42,9 +42,9 @@ export class EmittenProtected<T> {
this.#multiLibrary[eventName]?.add(listener);
}

once<TKey extends keyof T>(
protected once<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<T[TKey]>,
listener: EmittenListener<TEventMap[TKey]>,
) {
if (this.#singleLibrary[eventName] == null) {
this.#singleLibrary[eventName] = new Set();
Expand All @@ -53,17 +53,21 @@ export class EmittenProtected<T> {
this.#singleLibrary[eventName]?.add(listener);
}

disposable<TKey extends keyof T>(
protected disposable<TKey extends keyof TEventMap>(
eventName: TKey,
listener: EmittenListener<T[TKey]>,
listener: EmittenListener<TEventMap[TKey]>,
) {
this.on(eventName, listener);

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

protected emit<TKey extends keyof T>(eventName: TKey, value?: T[TKey]) {
protected emit<TKey extends keyof TEventMap>(
eventName: TKey,
value?: TEventMap[TKey],
) {
const multiSet = this.#multiLibrary[eventName];
const singleSet = this.#singleLibrary[eventName];

Expand All @@ -74,21 +78,22 @@ export class EmittenProtected<T> {
singleSet?.forEach((listener) => {
listener(value);
});

delete this.#singleLibrary[eventName];
}

protected empty() {
const every = (library: EmittenLibrary<T>) => {
for (const eventName in library) {
if (Object.hasOwn(library, eventName)) {
library[eventName]?.forEach((listener) => {
this.off(eventName, listener);
});
}
}
};

every(this.#multiLibrary);
every(this.#singleLibrary);
this.#every(this.#multiLibrary);
this.#every(this.#singleLibrary);
}

#every = (library: EmittenLibrary<TEventMap>) => {
for (const eventName in library) {
if (Object.hasOwn(library, eventName)) {
library[eventName]?.forEach((listener) => {
this.off(eventName, listener);
});
}
}
};
}
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type EmittenListener<T> = (value?: T) => void;
export type EmittenListener<TValue> = (value?: TValue) => void;

export type EmittenLibrary<T> = {
[eventName in keyof T]?: Set<EmittenListener<T[eventName]>>;
export type EmittenLibrary<TEventMap> = {
[event in keyof TEventMap]?: Set<EmittenListener<TEventMap[event]>>;
};

0 comments on commit 0685229

Please sign in to comment.