Skip to content

Commit 448ad67

Browse files
authored
feat: Remove event target shim. (#545)
Unlike the EventEmitter in the node SDK the EventTarget was not providing an interface directly on the LDClient. The LDClient has its own off/on interface which is not directly implemented via an event target. Which means removing it simplifies the implementation instead of complicating it. Second it is in the common code and node in a leaf implementation. Which means it requires a polyfill where it is not supported. Like when using hermes with React Native. Typically we dispatch using a micro-task, but EventTarget dispatches synchronously. For now this maintains the synchronous behavior. Fixes: #412
1 parent c04a9fd commit 448ad67

File tree

6 files changed

+43
-90
lines changed

6 files changed

+43
-90
lines changed

packages/sdk/react-native/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@
4545
"dependencies": {
4646
"@launchdarkly/js-client-sdk-common": "1.3.0",
4747
"@react-native-async-storage/async-storage": "^1.21.0",
48-
"base64-js": "^1.5.1",
49-
"event-target-shim": "^6.0.2"
48+
"base64-js": "^1.5.1"
5049
},
5150
"devDependencies": {
5251
"@launchdarkly/private-js-mocks": "0.0.1",

packages/sdk/react-native/src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
*
66
* @packageDocumentation
77
*/
8-
import { setupPolyfill } from './polyfills';
98
import ReactNativeLDClient from './ReactNativeLDClient';
109
import RNOptions from './RNOptions';
1110

12-
setupPolyfill();
13-
1411
export * from '@launchdarkly/js-client-sdk-common';
1512

1613
export * from './hooks';

packages/sdk/react-native/src/polyfills/CustomEvent.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
import EventTarget from 'event-target-shim';
2-
31
import { type Hasher, sha256 } from '../fromExternal/js-sha256';
42
import { base64FromByteArray, btoa } from './btoa';
5-
import CustomEvent from './CustomEvent';
63

7-
function setupPolyfill() {
8-
Object.assign(global, {
9-
EventTarget,
10-
CustomEvent,
11-
});
12-
}
13-
export { base64FromByteArray, btoa, type Hasher, setupPolyfill, sha256 };
4+
export { base64FromByteArray, btoa, type Hasher, sha256 };

packages/shared/sdk-client/src/api/LDEmitter.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
import { LDContext } from '@launchdarkly/js-sdk-common';
1+
import { LDContext, LDLogger } from '@launchdarkly/js-sdk-common';
22

33
import LDEmitter from './LDEmitter';
44

55
describe('LDEmitter', () => {
66
const error = { type: 'network', message: 'unreachable' };
77
let emitter: LDEmitter;
8+
let logger: LDLogger;
89

910
beforeEach(() => {
1011
jest.resetAllMocks();
11-
emitter = new LDEmitter();
12+
logger = {
13+
debug: jest.fn(),
14+
info: jest.fn(),
15+
warn: jest.fn(),
16+
error: jest.fn(),
17+
};
18+
emitter = new LDEmitter(logger);
1219
});
1320

1421
test('subscribe and handle', () => {
@@ -77,12 +84,13 @@ describe('LDEmitter', () => {
7784

7885
test('on listener with arguments', () => {
7986
const context = { kind: 'user', key: 'test-user-1' };
80-
const onListener = jest.fn((c: LDContext) => c);
87+
const arg2 = { test: 'test' };
88+
const onListener = jest.fn((c: LDContext, a2: any) => [c, a2]);
8189

8290
emitter.on('change', onListener);
83-
emitter.emit('change', context);
91+
emitter.emit('change', context, arg2);
8492

85-
expect(onListener).toBeCalledWith(context);
93+
expect(onListener).toBeCalledWith(context, arg2);
8694
});
8795

8896
test('unsubscribe one of many listeners', () => {
@@ -131,4 +139,15 @@ describe('LDEmitter', () => {
131139
expect(errorHandler1).not.toBeCalled();
132140
expect(errorHandler2).not.toBeCalled();
133141
});
142+
143+
it('handles errors generated by the callback', () => {
144+
emitter.on('error', () => {
145+
throw new Error('toast');
146+
});
147+
// Should not have an uncaught exception.
148+
emitter.emit('error');
149+
expect(logger.error).toHaveBeenCalledWith(
150+
'Encountered error invoking handler for "error", detail: "Error: toast"',
151+
);
152+
});
134153
});

packages/shared/sdk-client/src/api/LDEmitter.ts

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,20 @@
1+
import { LDLogger } from '@launchdarkly/js-sdk-common';
2+
13
export type EventName = 'error' | 'change';
24

3-
type CustomEventListeners = {
4-
original: Function;
5-
custom: Function;
6-
};
7-
/**
8-
* Native api usage: EventTarget.
9-
*
10-
* This is an event emitter using the standard built-in EventTarget web api.
11-
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
12-
*
13-
* In react-native use event-target-shim to polyfill EventTarget. This is safe
14-
* because the react-native repo uses it too.
15-
* https://github.com/mysticatea/event-target-shim
16-
*/
175
export default class LDEmitter {
18-
private et: EventTarget = new EventTarget();
6+
private listeners: Map<EventName, Function[]> = new Map();
197

20-
private listeners: Map<EventName, CustomEventListeners[]> = new Map();
8+
constructor(private logger?: LDLogger) {}
219

22-
/**
23-
* Cache all listeners in a Map so we can remove them later
24-
* @param name string event name
25-
* @param originalListener pointer to the original function as specified by
26-
* the consumer
27-
* @param customListener pointer to the custom function based on original
28-
* listener. This is needed to allow for CustomEvents.
29-
* @private
30-
*/
31-
private saveListener(name: EventName, originalListener: Function, customListener: Function) {
32-
const listener = { original: originalListener, custom: customListener };
10+
on(name: EventName, listener: Function) {
3311
if (!this.listeners.has(name)) {
3412
this.listeners.set(name, [listener]);
3513
} else {
3614
this.listeners.get(name)?.push(listener);
3715
}
3816
}
3917

40-
on(name: EventName, listener: Function) {
41-
const customListener = (e: Event) => {
42-
const { detail } = e as CustomEvent;
43-
44-
// invoke listener with args from CustomEvent
45-
listener(...detail);
46-
};
47-
this.saveListener(name, listener, customListener);
48-
this.et.addEventListener(name, customListener);
49-
}
50-
5118
/**
5219
* Unsubscribe one or all events.
5320
*
@@ -61,11 +28,8 @@ export default class LDEmitter {
6128
}
6229

6330
if (listener) {
64-
const toBeRemoved = existingListeners.find((c) => c.original === listener);
65-
this.et.removeEventListener(name, toBeRemoved?.custom as any);
66-
6731
// remove from internal cache
68-
const updated = existingListeners.filter((l) => l.original !== listener);
32+
const updated = existingListeners.filter((fn) => fn !== listener);
6933
if (updated.length === 0) {
7034
this.listeners.delete(name);
7135
} else {
@@ -74,15 +38,21 @@ export default class LDEmitter {
7438
return;
7539
}
7640

77-
// remove all listeners
78-
existingListeners.forEach((l) => {
79-
this.et.removeEventListener(name, l.custom as any);
80-
});
41+
// listener was not specified, so remove them all for that event
8142
this.listeners.delete(name);
8243
}
8344

45+
private invokeListener(listener: Function, name: EventName, ...detail: any[]) {
46+
try {
47+
listener(...detail);
48+
} catch (err) {
49+
this.logger?.error(`Encountered error invoking handler for "${name}", detail: "${err}"`);
50+
}
51+
}
52+
8453
emit(name: EventName, ...detail: any[]) {
85-
this.et.dispatchEvent(new CustomEvent(name, { detail }));
54+
const listeners = this.listeners.get(name);
55+
listeners?.forEach((listener) => this.invokeListener(listener, name, ...detail));
8656
}
8757

8858
eventNames(): string[] {

0 commit comments

Comments
 (0)