feat: use EvenEmitter3 for web-sdk (#847)

<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

Fixes an issue where the `events` node polyfill does not comply to the
`node:events` types.
When trying to use the web OpenFeatureEventEmitter the following error
message comes up, describing that the `events` polyfill's EventEmitter
is incompatible to `node:events` EventEmitter.

```
✘ [ERROR] TS2416: Property 'eventEmitter' in type 'OpenFeatureEventEmitter' is not assignable to the same property in base type 'GenericEventEmitter<ProviderEmittableEvents, Record<string, unknown>>'.
  Type 'EventEmitter' is not assignable to type 'PlatformEventEmitter'.
    Types of property 'addListener' are incompatible.
      Type '(type: string | number, listener: Listener) => EventEmitter' is not assignable to type '(eventName: string | symbol, listener: (...args: any[]) => void) => PlatformEventEmitter'.
        Types of parameters 'type' and 'eventName' are incompatible.
          Type 'string | symbol' is not assignable to type 'string | number'.
            Type 'symbol' is not assignable to type 'string | number'.
```

This PR fixes that issue by not using the `events` anymore and instead
using https://www.npmjs.com/package/eventemitter3

cc @toddbaert 

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

Fixes #845

---------

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Lukas Reining 2024-03-05 23:45:49 +01:00 committed by GitHub
parent 1461074f20
commit 861cf83782
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 27 additions and 38 deletions

30
package-lock.json generated
View File

@ -17,7 +17,6 @@
],
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@types/events": "^3.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.16",
"@types/react": "^18.2.55",
@ -31,7 +30,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jsdoc": "^48.0.6",
"events": "^3.3.0",
"eventemitter3": "^5.0.1",
"jest": "^29.7.0",
"jest-config": "^29.7.0",
"jest-cucumber": "^3.0.1",
@ -2154,12 +2153,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/events": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@ -4787,14 +4780,11 @@
"node": ">= 0.6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"engines": {
"node": ">=0.8.x"
}
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
},
"node_modules/exec-sh": {
"version": "0.3.6",
@ -13743,7 +13733,7 @@
},
"packages/client": {
"name": "@openfeature/web-sdk",
"version": "0.4.13",
"version": "0.4.14",
"license": "Apache-2.0",
"devDependencies": {
"@openfeature/core": "0.0.27"
@ -13754,7 +13744,7 @@
},
"packages/nest": {
"name": "@openfeature/nestjs-sdk",
"version": "0.1.0-experimental",
"version": "0.1.1-experimental",
"license": "Apache-2.0",
"devDependencies": {
"@nestjs/common": "^10.2.10",
@ -13775,7 +13765,7 @@
},
"packages/react": {
"name": "@openfeature/react-sdk",
"version": "0.2.0-experimental",
"version": "0.2.1-experimental",
"license": "Apache-2.0",
"devDependencies": {
"@openfeature/core": "*",
@ -13788,7 +13778,7 @@
},
"packages/server": {
"name": "@openfeature/server-sdk",
"version": "1.12.0",
"version": "1.13.0",
"license": "Apache-2.0",
"devDependencies": {
"@openfeature/core": "0.0.27"

View File

@ -37,7 +37,6 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@types/events": "^3.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.16",
"@types/react": "^18.2.55",
@ -51,7 +50,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jsdoc": "^48.0.6",
"events": "^3.3.0",
"eventemitter3": "^5.0.1",
"jest": "^29.7.0",
"jest-config": "^29.7.0",
"jest-cucumber": "^3.0.1",
@ -76,4 +75,4 @@
"packages/react",
"packages/nest"
]
}
}

View File

@ -1,6 +1,7 @@
import { GenericEventEmitter } from '@openfeature/core';
import { EventEmitter } from 'events';
import { EventEmitter } from 'eventemitter3';
import { ProviderEmittableEvents } from './events';
/**
* The OpenFeatureEventEmitter can be used by provider developers to emit
* events at various parts of the provider lifecycle.
@ -9,12 +10,9 @@ import { ProviderEmittableEvents } from './events';
* the result of the initialize method.
*/
export class OpenFeatureEventEmitter extends GenericEventEmitter<ProviderEmittableEvents> {
protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
protected readonly eventEmitter = new EventEmitter();
constructor() {
super();
this.eventEmitter.on('error', (err) => {
this._logger?.error('Error running event handler:', err);
});
}
}

View File

@ -7,12 +7,15 @@ import { AllProviderEvents, AnyProviderEvent } from './events';
* The GenericEventEmitter should only be used within the SDK. It supports additional properties that can be included
* in the event details.
*/
export abstract class GenericEventEmitter<E extends AnyProviderEvent, AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
export abstract class GenericEventEmitter<
E extends AnyProviderEvent,
AdditionalContext extends Record<string, unknown> = Record<string, unknown>,
>
implements ProviderEventEmitter<E>, ManageLogger<GenericEventEmitter<E, AdditionalContext>>
{
protected abstract readonly eventEmitter: PlatformEventEmitter;
private readonly _handlers: { [key in AnyProviderEvent]: WeakMap<EventHandler, EventHandler[]>} = {
private readonly _handlers: { [key in AnyProviderEvent]: WeakMap<EventHandler, EventHandler[]> } = {
[AllProviderEvents.ConfigurationChanged]: new WeakMap<EventHandler, EventHandler[]>(),
[AllProviderEvents.ContextChanged]: new WeakMap<EventHandler, EventHandler[]>(),
[AllProviderEvents.Ready]: new WeakMap<EventHandler, EventHandler[]>(),
@ -33,7 +36,11 @@ export abstract class GenericEventEmitter<E extends AnyProviderEvent, Additional
// The handlers have to be wrapped with an async function because if a synchronous functions throws an error,
// the other handlers will not run.
const asyncHandler = async (details?: EventDetails) => {
await handler(details);
try {
await handler(details);
} catch (err) {
this._logger?.error('Error running event handler:', err);
}
};
// The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
const existingAsyncHandlers = this._handlers[eventType].get(handler);
@ -84,7 +91,7 @@ export abstract class GenericEventEmitter<E extends AnyProviderEvent, Additional
* This is an un-exported type that corresponds to NodeJS.EventEmitter.
* We can't use that type here, because this module is used in both the browser, and the server.
* In the server, node (or whatever server runtime) provides an implementation for this.
* In the browser, we bundle in the popular 'events' package, which is a polyfill of NodeJS.EventEmitter.
* In the browser, we bundle in the popular 'EventEmitter3' package, which is a polyfill of NodeJS.EventEmitter.
*/
/* eslint-disable */
interface PlatformEventEmitter {
@ -94,13 +101,8 @@ interface PlatformEventEmitter {
removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this;
off(eventName: string | symbol, listener: (...args: any[]) => void): this;
removeAllListeners(event?: string | symbol): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(eventName: string | symbol): Function[];
rawListeners(eventName: string | symbol): Function[];
emit(eventName: string | symbol, ...args: any[]): boolean;
listenerCount(eventName: string | symbol, listener?: Function): number;
prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this;
prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this;
eventNames(): Array<string | symbol>;
}
}