Merge branch 'event-emitter' into sse-and-backoff
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"import": ["@generalprotocols/cspell-dictionary/cspell.json"],
|
"import": ["@generalprotocols/cspell-dictionary/cspell.json"],
|
||||||
"words": ["nonfungible", "lockscript"],
|
"words": ["nonfungible", "lockscript", "bivariance"],
|
||||||
"ignorePaths": ["source/template/xo-template.schema.json"]
|
"ignorePaths": ["source/template/xo-template.schema.json"]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+32
-32
@@ -1105,9 +1105,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1186,9 +1186,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3503,16 +3503,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brotli-size": {
|
"node_modules/brotli-size": {
|
||||||
@@ -5292,9 +5292,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -5379,9 +5379,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -5468,9 +5468,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
@@ -5542,9 +5542,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.16",
|
"version": "1.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7129,9 +7129,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -7810,9 +7810,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.17",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -8288,9 +8288,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -8308,7 +8308,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.17",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
+61
-25
@@ -1,18 +1,29 @@
|
|||||||
export type EventMap = Record<string, unknown>;
|
export type EventMap = Record<string, unknown>;
|
||||||
|
|
||||||
type Listener<T> = (detail: T) => void;
|
type Listener<T> = (detail: Readonly<T>) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internally permits listeners for individual event payloads to be stored
|
||||||
|
* in a collection typed with the union of all event payloads.
|
||||||
|
*/
|
||||||
|
type StoredListener<T> = {
|
||||||
|
bivarianceHack(detail: Readonly<T>): void;
|
||||||
|
}['bivarianceHack'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A listener entry.
|
* A listener entry.
|
||||||
* @template T - The event type.
|
* @template T - The event payload type.
|
||||||
*/
|
*/
|
||||||
interface ListenerEntry<T> {
|
interface ListenerEntry<T> {
|
||||||
listener: Listener<T>;
|
listener: StoredListener<T>;
|
||||||
wrappedListener: Listener<T>;
|
wrappedListener: StoredListener<T>;
|
||||||
debounceTime?: number;
|
debounceTime?: number;
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback returned by {@link on} and {@link once} for removing a listener.
|
||||||
|
*/
|
||||||
export type OffCallback = () => void;
|
export type OffCallback = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +35,7 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* The listeners map.
|
* The listeners map.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private listeners: Map<keyof T, Set<ListenerEntry<T[keyof T]>>> = new Map();
|
#listeners: Map<keyof T, Set<ListenerEntry<T[keyof T]>>> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener for an event.
|
* Add a listener for an event.
|
||||||
@@ -38,19 +49,23 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
const wrappedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(listener, debounceMilliseconds) : listener;
|
const wrappedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(listener, debounceMilliseconds) : listener;
|
||||||
|
|
||||||
// If the listeners map does not have the event type, create a new set.
|
// If the listeners map does not have the event type, create a new set.
|
||||||
if (!this.listeners.has(type)) {
|
if (!this.#listeners.has(type)) {
|
||||||
this.listeners.set(type, new Set());
|
this.#listeners.set(type, new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a listener entry.
|
// Create a listener entry.
|
||||||
const listenerEntry: ListenerEntry<T[K]> = {
|
const listenerEntry: ListenerEntry<T[K]> = {
|
||||||
listener,
|
listener,
|
||||||
wrappedListener,
|
wrappedListener,
|
||||||
...(debounceMilliseconds !== undefined ? { debounceTime: debounceMilliseconds } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the debounce time if specified.
|
||||||
|
if (debounceMilliseconds && debounceMilliseconds > 0) {
|
||||||
|
listenerEntry.debounceTime = debounceMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the listener entry to the listeners map.
|
// Add the listener entry to the listeners map.
|
||||||
this.listeners.get(type)?.add(listenerEntry as ListenerEntry<T[keyof T]>);
|
this.#listeners.get(type)?.add(listenerEntry);
|
||||||
|
|
||||||
// Return an "off" callback that can be called to stop listening for events.
|
// Return an "off" callback that can be called to stop listening for events.
|
||||||
return () => this.off(type, listener);
|
return () => this.off(type, listener);
|
||||||
@@ -64,7 +79,7 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* @returns An off callback that can be called to stop listening for events.
|
* @returns An off callback that can be called to stop listening for events.
|
||||||
*/
|
*/
|
||||||
once<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds?: number): OffCallback {
|
once<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds?: number): OffCallback {
|
||||||
const wrappedListener: Listener<T[K]> = (detail: T[K]) => {
|
const wrappedListener: Listener<T[K]> = (detail: Readonly<T[K]>) => {
|
||||||
this.off(type, listener);
|
this.off(type, listener);
|
||||||
listener(detail);
|
listener(detail);
|
||||||
};
|
};
|
||||||
@@ -74,8 +89,8 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener;
|
debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener;
|
||||||
|
|
||||||
// If the listeners map does not have the event type, create a new set.
|
// If the listeners map does not have the event type, create a new set.
|
||||||
if (!this.listeners.has(type)) {
|
if (!this.#listeners.has(type)) {
|
||||||
this.listeners.set(type, new Set());
|
this.#listeners.set(type, new Set());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a listener entry.
|
// Create a listener entry.
|
||||||
@@ -83,11 +98,15 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
listener,
|
listener,
|
||||||
wrappedListener: debouncedListener,
|
wrappedListener: debouncedListener,
|
||||||
once: true,
|
once: true,
|
||||||
...(debounceMilliseconds !== undefined ? { debounceTime: debounceMilliseconds } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set the debounce time if specified.
|
||||||
|
if (debounceMilliseconds && debounceMilliseconds > 0) {
|
||||||
|
listenerEntry.debounceTime = debounceMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the listener entry to the listeners map.
|
// Add the listener entry to the listeners map.
|
||||||
this.listeners.get(type)?.add(listenerEntry as ListenerEntry<T[keyof T]>);
|
this.#listeners.get(type)?.add(listenerEntry);
|
||||||
|
|
||||||
// Return an "off" callback that can be called to stop listening for events.
|
// Return an "off" callback that can be called to stop listening for events.
|
||||||
return () => this.off(type, listener);
|
return () => this.off(type, listener);
|
||||||
@@ -98,11 +117,18 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* @param type - The event type.
|
* @param type - The event type.
|
||||||
* @param listener - The listener function.
|
* @param listener - The listener function.
|
||||||
*/
|
*/
|
||||||
off<K extends keyof T>(type: K, listener: Listener<T[K]>): void {
|
off<K extends keyof T>(type: K, listener?: Listener<T[K]>): void {
|
||||||
// Get the listeners for the event type.
|
// Get the listeners for the event type.
|
||||||
const listeners = this.listeners.get(type);
|
const listeners = this.#listeners.get(type);
|
||||||
if (!listeners) return;
|
if (!listeners) return;
|
||||||
|
|
||||||
|
// If no listener is provided, remove all listeners for the event type.
|
||||||
|
if (!listener) {
|
||||||
|
this.#listeners.delete(type);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the listener entry.
|
// Find the listener entry.
|
||||||
const listenerEntry = Array.from(listeners).find((entry) => entry.listener === listener || entry.wrappedListener === listener);
|
const listenerEntry = Array.from(listeners).find((entry) => entry.listener === listener || entry.wrappedListener === listener);
|
||||||
|
|
||||||
@@ -120,12 +146,15 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
*/
|
*/
|
||||||
emit<K extends keyof T>(type: K, payload: T[K]): boolean {
|
emit<K extends keyof T>(type: K, payload: T[K]): boolean {
|
||||||
// Get the listeners for the event type.
|
// Get the listeners for the event type.
|
||||||
const listeners = this.listeners.get(type);
|
const listeners = this.#listeners.get(type);
|
||||||
if (!listeners) return false;
|
if (!listeners) return false;
|
||||||
|
|
||||||
|
// Freeze the payload to make it readonly.
|
||||||
|
const readonlyPayload = Object.freeze(payload);
|
||||||
|
|
||||||
// Emit the event to all listeners.
|
// Emit the event to all listeners.
|
||||||
listeners.forEach((entry) => {
|
listeners.forEach((entry) => {
|
||||||
entry.wrappedListener(payload);
|
entry.wrappedListener(readonlyPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return true if there are listeners for the event, false otherwise.
|
// Return true if there are listeners for the event, false otherwise.
|
||||||
@@ -136,24 +165,29 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
* Remove all listeners.
|
* Remove all listeners.
|
||||||
*/
|
*/
|
||||||
removeAllListeners(): void {
|
removeAllListeners(): void {
|
||||||
this.listeners.clear();
|
this.#listeners.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for an event to be emitted.
|
* Wait for an event to be emitted that matches the provided predicate function's criteria.
|
||||||
* @param type - The event type.
|
* @param type - The event type.
|
||||||
* @param predicate - The predicate function.
|
* @param predicate - Predicate function to filter for whether the event payload matches the criteria.
|
||||||
* @param timeoutMs - The timeout in milliseconds.
|
* @param timeoutMs - The timeout in milliseconds.
|
||||||
* @returns The event payload.
|
* @returns The event payload.
|
||||||
*/
|
*/
|
||||||
async waitFor<K extends keyof T>(type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number): Promise<T[K]> {
|
async waitFor<K extends keyof T>(type: K, predicate: (payload: Readonly<T[K]>) => boolean, timeoutMs?: number): Promise<Readonly<T[K]>> {
|
||||||
// Create a promise to wait for the event to be emitted.
|
// Create a promise to wait for the event to be emitted.
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
// Create a listener function.
|
// Create a listener function.
|
||||||
const listener = (payload: T[K]): void => {
|
const listener = (payload: Readonly<T[K]>): void => {
|
||||||
if (predicate(payload)) {
|
try {
|
||||||
|
// If the event payload does not match the predicate condition, return.
|
||||||
|
if (!predicate(payload)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
this.off(type, listener);
|
this.off(type, listener);
|
||||||
if (timeoutId !== undefined) {
|
if (timeoutId !== undefined) {
|
||||||
@@ -161,6 +195,8 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(payload);
|
resolve(payload);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,7 +223,7 @@ export class EventEmitter<T extends EventMap> {
|
|||||||
// Create a timeout variable.
|
// Create a timeout variable.
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
return (detail: T[K]) => {
|
return (detail: Readonly<T[K]>) => {
|
||||||
// If a debounce timer is already pending, clear it before scheduling the next one.
|
// If a debounce timer is already pending, clear it before scheduling the next one.
|
||||||
if (timeout !== undefined) {
|
if (timeout !== undefined) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ const testEventEmitterOffRemovesListenerByReference = (): void => {
|
|||||||
expect(listener).not.toHaveBeenCalled();
|
expect(listener).not.toHaveBeenCalled();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that off() removes all listeners for an event type when no listener is provided.
|
||||||
|
*/
|
||||||
|
const testEventEmitterOffRemovesAllListenersForEventType = (): void => {
|
||||||
|
const emitter = new EventEmitter<TestEvents>();
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
emitter.on('message', listener);
|
||||||
|
emitter.off('message');
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
expect(emitter.emit('message', 'hello')).toBe(false);
|
||||||
|
expect(emitter.emit('count', 42)).toBe(false);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that off() does nothing when given an unknown listener reference.
|
* Tests that off() does nothing when given an unknown listener reference.
|
||||||
*/
|
*/
|
||||||
@@ -476,6 +491,23 @@ const testEventEmitterDebouncedOnceListener = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the `waitFor` method rejects if the predicate function throws
|
||||||
|
*/
|
||||||
|
const testEventEmitterWaitForRejectsOnPredicateError = async (): Promise<void> => {
|
||||||
|
const emitter = new EventEmitter<TestEvents>();
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
const waitPromise = emitter.waitFor('message', () => {
|
||||||
|
throw new Error('predicate error');
|
||||||
|
});
|
||||||
|
|
||||||
|
emitter.emit('message', 'hello');
|
||||||
|
|
||||||
|
await expect(waitPromise).rejects.toThrow('predicate error');
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
};
|
||||||
|
|
||||||
const runTests = async (): Promise<void> => {
|
const runTests = async (): Promise<void> => {
|
||||||
test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners);
|
test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners);
|
||||||
test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners);
|
test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners);
|
||||||
@@ -484,6 +516,10 @@ const runTests = async (): Promise<void> => {
|
|||||||
test('EventEmitter: returns true when emitting with listeners', testEventEmitterEmitReturnsTrueWithListeners);
|
test('EventEmitter: returns true when emitting with listeners', testEventEmitterEmitReturnsTrueWithListeners);
|
||||||
test('EventEmitter: stops calling a listener after its off callback is invoked', testEventEmitterOffCallbackRemovesListener);
|
test('EventEmitter: stops calling a listener after its off callback is invoked', testEventEmitterOffCallbackRemovesListener);
|
||||||
test('EventEmitter: removes a listener when off is called with the same reference', testEventEmitterOffRemovesListenerByReference);
|
test('EventEmitter: removes a listener when off is called with the same reference', testEventEmitterOffRemovesListenerByReference);
|
||||||
|
test(
|
||||||
|
'EventEmitter: removes all listeners for an event type when off is called with no listener',
|
||||||
|
testEventEmitterOffRemovesAllListenersForEventType,
|
||||||
|
);
|
||||||
test('EventEmitter: ignores off when the listener reference is unknown', testEventEmitterOffIgnoresUnknownListener);
|
test('EventEmitter: ignores off when the listener reference is unknown', testEventEmitterOffIgnoresUnknownListener);
|
||||||
test('EventEmitter: ignores off for an event type with no listeners', testEventEmitterOffIgnoresUnregisteredEventType);
|
test('EventEmitter: ignores off for an event type with no listeners', testEventEmitterOffIgnoresUnregisteredEventType);
|
||||||
test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce);
|
test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce);
|
||||||
@@ -500,6 +536,7 @@ const runTests = async (): Promise<void> => {
|
|||||||
test('EventEmitter: resets the debounce timer on repeated emits', testEventEmitterDebouncedTimerResetsOnRepeatedEmits);
|
test('EventEmitter: resets the debounce timer on repeated emits', testEventEmitterDebouncedTimerResetsOnRepeatedEmits);
|
||||||
test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce);
|
test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce);
|
||||||
test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener);
|
test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener);
|
||||||
|
test('EventEmitter: rejects waitFor when the predicate function throws', testEventEmitterWaitForRejectsOnPredicateError);
|
||||||
};
|
};
|
||||||
|
|
||||||
await runTests();
|
await runTests();
|
||||||
|
|||||||
Reference in New Issue
Block a user