Skip to content

Commit f0e2aa6

Browse files
authored
fix: missing events bundled dep (#660)
Fixes: #659 This PR fixes an issue created with [this change](#650), which removed the `events` polyfill package from `@openfeature/core` where is wasn't always needed (see that issue for details). The problem was that we still imported `events` in the `@openfeature/core` module, but can't use the `events` bundled in the `@openfeature/web-sdk` since the bundled package there isn't accessible from imports in `@openfeature/core`. This PR _**removes all imports of `events`**_ from `@openfeature/core`, and instead only imports types. Imports of `events` only now occur in the web-sdk (where it's bundled) and server-sdk (where it's made available by the node runtime), not in the common module. Unfortunately this issue was a bit tough to track down, because `events` is VERY common, and lots of bundlers, etc will add it, so it's frequently available "accidentally". Thanks to @juanparadox for the report. --------- Signed-off-by: Todd Baert <[email protected]>
1 parent d9b922b commit f0e2aa6

23 files changed

+144
-72
lines changed

packages/client/src/client/open-feature-client.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
JsonValue,
1212
Logger,
1313
OpenFeatureError,
14-
OpenFeatureEventEmitter,
1514
ProviderEvents,
1615
ProviderStatus,
1716
ResolutionDetails,
@@ -21,6 +20,7 @@ import {
2120
import { FlagEvaluationOptions } from '../evaluation';
2221
import { OpenFeature } from '../open-feature';
2322
import { Provider } from '../provider';
23+
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
2424
import { Client } from './client';
2525

2626
type OpenFeatureClientOptions = {
@@ -36,7 +36,7 @@ export class OpenFeatureClient implements Client {
3636
// functions are passed here to make sure that these values are always up to date,
3737
// and so we don't have to make these public properties on the API class.
3838
private readonly providerAccessor: () => Provider,
39-
private readonly emitterAccessor: () => OpenFeatureEventEmitter,
39+
private readonly emitterAccessor: () => InternalEventEmitter,
4040
private readonly globalLogger: () => Logger,
4141
private readonly options: OpenFeatureClientOptions
4242
) {}

packages/client/src/events/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './open-feature-event-emitter';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
2+
3+
/**
4+
* The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
5+
* OpenFeatureEventEmitter to include additional properties that can be included
6+
* in the event details.
7+
*/
8+
export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { GenericEventEmitter } from '@openfeature/core';
2+
import EventEmitter from 'events';
3+
4+
/**
5+
* The OpenFeatureEventEmitter can be used by provider developers to emit
6+
* events at various parts of the provider lifecycle.
7+
*
8+
* NOTE: Ready and error events are automatically emitted by the SDK based on
9+
* the result of the initialize method.
10+
*/
11+
export class OpenFeatureEventEmitter extends GenericEventEmitter {
12+
protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
13+
14+
constructor() {
15+
super();
16+
this.eventEmitter.on('error', (err) => {
17+
this._logger?.error('Error running event handler:', err);
18+
});
19+
}
20+
}

packages/client/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import 'events'; // we need this to trigger the bundling of the browser events module: https://www.npmjs.com/package/events
21
export * from './client';
32
export * from './provider';
43
export * from './evaluation';
54
export * from './open-feature';
5+
export * from './events';
66
export * from '@openfeature/core';

packages/client/src/open-feature.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { EvaluationContext, ManageContext, OpenFeatureCommonAPI } from '@openfeature/core';
22
import { Client, OpenFeatureClient } from './client';
33
import { NOOP_PROVIDER, Provider } from './provider';
4+
import { OpenFeatureEventEmitter } from './events';
45

56
// use a symbol as a key for the global singleton
67
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@@ -11,7 +12,9 @@ type OpenFeatureGlobal = {
1112
const _globalThis = globalThis as OpenFeatureGlobal;
1213

1314
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements ManageContext<Promise<void>> {
15+
protected _events = new OpenFeatureEventEmitter();
1416
protected _defaultProvider: Provider = NOOP_PROVIDER;
17+
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
1518

1619
// eslint-disable-next-line @typescript-eslint/no-empty-function
1720
private constructor() {

packages/client/test/events.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ProviderMetadata,
1010
ProviderStatus,
1111
ResolutionDetails,
12-
StaleEvent,
12+
StaleEvent
1313
} from '../src';
1414

1515
const TIMEOUT = 1000;

packages/client/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
// "rootDir": "./", /* Specify the root folder within your source files. */
2929
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
3030
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31-
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
31+
"paths": {
32+
"@openfeature/core": [ "../shared/src" ]
33+
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
3234
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
3335
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
3436
// "types": [], /* Specify type package names to be included without being referenced in a source file. */

packages/server/src/client/open-feature-client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
FlagValueType,
99
Hook,
1010
HookContext,
11-
InternalEventEmitter,
1211
JsonValue,
1312
Logger,
1413
ManageContext,
@@ -23,6 +22,7 @@ import { FlagEvaluationOptions } from '../evaluation';
2322
import { OpenFeature } from '../open-feature';
2423
import { Provider } from '../provider';
2524
import { Client } from './client';
25+
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
2626

2727
type OpenFeatureClientOptions = {
2828
name?: string;

packages/server/src/events/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './open-feature-event-emitter';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
2+
3+
/**
4+
* The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
5+
* OpenFeatureEventEmitter to include additional properties that can be included
6+
* in the event details.
7+
*/
8+
export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { GenericEventEmitter } from '@openfeature/core';
2+
import EventEmitter from 'events';
3+
4+
/**
5+
* The OpenFeatureEventEmitter can be used by provider developers to emit
6+
* events at various parts of the provider lifecycle.
7+
*
8+
* NOTE: Ready and error events are automatically emitted by the SDK based on
9+
* the result of the initialize method.
10+
*/
11+
export class OpenFeatureEventEmitter extends GenericEventEmitter {
12+
protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
13+
14+
constructor() {
15+
super();
16+
this.eventEmitter.on('error', (err) => {
17+
this._logger?.error('Error running event handler:', err);
18+
});
19+
}
20+
};

packages/server/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './provider';
33
export * from './evaluation';
44
export * from './open-feature';
55
export * from './transaction-context';
6+
export * from './events';
67
export * from '@openfeature/core';

packages/server/src/open-feature.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
TransactionContextPropagator,
1414
} from './transaction-context';
1515
import { Client, OpenFeatureClient } from './client';
16+
import { OpenFeatureEventEmitter } from './events';
1617

1718
// use a symbol as a key for the global singleton
1819
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js-sdk/api');
@@ -26,8 +27,11 @@ export class OpenFeatureAPI
2627
extends OpenFeatureCommonAPI<Provider>
2728
implements ManageContext<OpenFeatureAPI>, ManageTransactionContextPropagator<OpenFeatureCommonAPI<Provider>>
2829
{
29-
private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
30+
protected _events = new OpenFeatureEventEmitter();
3031
protected _defaultProvider: Provider = NOOP_PROVIDER;
32+
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
33+
34+
private _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
3135

3236
private constructor() {
3337
super('server');

packages/server/src/provider/in-memory-provider/in-memory-provider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
JsonValue,
77
Logger,
88
OpenFeatureError,
9-
OpenFeatureEventEmitter,
109
ProviderEvents,
1110
ResolutionDetails,
1211
StandardResolutionReasons,
@@ -15,6 +14,7 @@ import {
1514
import { Provider } from '../provider';
1615
import { Flag, FlagConfiguration } from './flag-configuration';
1716
import { VariantFoundError } from './variant-not-found-error';
17+
import { OpenFeatureEventEmitter } from '../..';
1818

1919
/**
2020
* A simple OpenFeature provider intended for demos and as a test stub.

packages/server/test/events.spec.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
ProviderMetadata,
1010
ProviderStatus,
1111
ResolutionDetails,
12-
StaleEvent,
12+
StaleEvent
1313
} from '../src';
1414

1515
const TIMEOUT = 1000;
@@ -184,7 +184,7 @@ describe('Events', () => {
184184
OpenFeature.addHandler(ProviderEvents.Error, () => {
185185
resolve();
186186
});
187-
})
187+
}),
188188
]).then(() => {
189189
done();
190190
});
@@ -312,7 +312,11 @@ describe('Events', () => {
312312
});
313313

314314
it('handler added while while provider initializing runs', (done) => {
315-
const provider = new MockProvider({ name: 'race', initialStatus: ProviderStatus.NOT_READY, initDelay: TIMEOUT / 2 });
315+
const provider = new MockProvider({
316+
name: 'race',
317+
initialStatus: ProviderStatus.NOT_READY,
318+
initDelay: TIMEOUT / 2,
319+
});
316320

317321
// set the default provider
318322
OpenFeature.setProvider(provider);
@@ -505,12 +509,12 @@ describe('Events', () => {
505509
describe('API', () => {
506510
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
507511
const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
508-
512+
509513
OpenFeature.setProvider(clientId, provider);
510514
expect(provider.initialize).not.toHaveBeenCalled();
511-
515+
512516
OpenFeature.addHandler(ProviderEvents.Error, () => {
513-
done();
517+
done();
514518
});
515519
});
516520
});
@@ -519,14 +523,14 @@ describe('Events', () => {
519523
it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
520524
const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
521525
const client = OpenFeature.getClient(clientId);
522-
526+
523527
OpenFeature.setProvider(clientId, provider);
524528
expect(provider.initialize).not.toHaveBeenCalled();
525-
529+
526530
client.addHandler(ProviderEvents.Ready, () => {
527-
done();
531+
done();
528532
});
529533
});
530-
});
534+
});
531535
});
532536
});

packages/server/test/in-memory-provider.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ErrorCode, FlagNotFoundError, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
1+
import { FlagNotFoundError, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
22
import { InMemoryProvider } from '../src';
33
import { VariantFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
44

packages/server/tsconfig.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
// "rootDir": "./", /* Specify the root folder within your source files. */
2828
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
2929
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
30-
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
30+
"paths": {
31+
"@openfeature/core": [ "../shared/src" ]
32+
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
3133
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
3234
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
3335
// "types": [], /* Specify type package names to be included without being referenced in a source file. */

packages/shared/src/events/open-feature-event-emitter.ts renamed to packages/shared/src/events/generic-event-emitter.ts

+12-28
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import EventEmitter from 'events';
21
import { Logger, ManageLogger, SafeLogger } from '../logger';
3-
import { CommonEventDetails, EventContext, EventDetails, EventHandler } from './eventing';
2+
import { EventContext, EventDetails, EventHandler } from './eventing';
43
import { ProviderEvents } from './events';
54

6-
abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
5+
/**
6+
* The GenericEventEmitter should only be used within the SDK. It supports additional properties that can be included
7+
* in the event details.
8+
*/
9+
export abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
710
implements ManageLogger<GenericEventEmitter<AdditionalContext>>
811
{
12+
protected abstract readonly eventEmitter: NodeJS.EventEmitter;
13+
914
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1015
private readonly _handlers = new WeakMap<EventHandler<any>, EventHandler<any>>();
11-
private readonly eventEmitter = new EventEmitter({ captureRejections: true });
1216
private _eventLogger?: Logger;
1317

14-
constructor(private readonly globalLogger?: () => Logger) {
15-
this.eventEmitter.on('error', (err) => {
16-
this._logger?.error('Error running event handler:', err);
17-
});
18-
}
18+
constructor(private readonly globalLogger?: () => Logger) {}
1919

2020
emit<T extends ProviderEvents>(eventType: T, context?: EventContext<T, AdditionalContext>): void {
2121
this.eventEmitter.emit(eventType, context);
@@ -25,7 +25,7 @@ abstract class GenericEventEmitter<AdditionalContext extends Record<string, unkn
2525
// The handlers have to be wrapped with an async function because if a synchronous functions throws an error,
2626
// the other handlers will not run.
2727
const asyncHandler = async (details?: EventDetails<T>) => {
28-
await handler(details);
28+
await handler(details);
2929
};
3030
// The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
3131
this._handlers.set(handler, asyncHandler);
@@ -61,23 +61,7 @@ abstract class GenericEventEmitter<AdditionalContext extends Record<string, unkn
6161
return this;
6262
}
6363

64-
private get _logger() {
64+
protected get _logger() {
6565
return this._eventLogger ?? this.globalLogger?.();
6666
}
67-
}
68-
69-
/**
70-
* The OpenFeatureEventEmitter can be used by provider developers to emit
71-
* events at various parts of the provider lifecycle.
72-
*
73-
* NOTE: Ready and error events are automatically emitted by the SDK based on
74-
* the result of the initialize method.
75-
*/
76-
export class OpenFeatureEventEmitter extends GenericEventEmitter {};
77-
78-
/**
79-
* The InternalEventEmitter should only be used within the SDK. It extends the
80-
* OpenFeatureEventEmitter to include additional properties that can be included
81-
* in the event details.
82-
*/
83-
export class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
67+
}

packages/shared/src/events/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from './event-utils';
22
export * from './eventing';
33
export * from './events';
4-
export * from './open-feature-event-emitter';
4+
export * from './generic-event-emitter';

0 commit comments

Comments
 (0)