diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index cbcfb34393..485a947b8b 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -45,8 +45,7 @@ "dependencies": { "@launchdarkly/js-client-sdk-common": "1.3.0", "@react-native-async-storage/async-storage": "^1.21.0", - "base64-js": "^1.5.1", - "event-target-shim": "^6.0.2" + "base64-js": "^1.5.1" }, "devDependencies": { "@launchdarkly/private-js-mocks": "0.0.1", diff --git a/packages/sdk/react-native/src/index.ts b/packages/sdk/react-native/src/index.ts index bcfda9922e..170a41314c 100644 --- a/packages/sdk/react-native/src/index.ts +++ b/packages/sdk/react-native/src/index.ts @@ -5,12 +5,9 @@ * * @packageDocumentation */ -import { setupPolyfill } from './polyfills'; import ReactNativeLDClient from './ReactNativeLDClient'; import RNOptions from './RNOptions'; -setupPolyfill(); - export * from '@launchdarkly/js-client-sdk-common'; export * from './hooks'; diff --git a/packages/sdk/react-native/src/polyfills/CustomEvent.ts b/packages/sdk/react-native/src/polyfills/CustomEvent.ts deleted file mode 100644 index 351b91af37..0000000000 --- a/packages/sdk/react-native/src/polyfills/CustomEvent.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Ripped from: - * https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Events/CustomEvent.js#L21 - */ -import { Event } from 'event-target-shim/es5'; - -type CustomEventOptions = { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - detail?: any; -}; - -export default class CustomEvent extends Event { - detail: any; - - constructor(typeArg: string, options: CustomEventOptions) { - const { bubbles, cancelable, composed, detail } = options; - super(typeArg, { bubbles, cancelable, composed }); - - this.detail = detail; // this would correspond to `NativeEvent` in SyntheticEvent - } -} diff --git a/packages/sdk/react-native/src/polyfills/index.ts b/packages/sdk/react-native/src/polyfills/index.ts index 03e1b11fe4..a65734323e 100644 --- a/packages/sdk/react-native/src/polyfills/index.ts +++ b/packages/sdk/react-native/src/polyfills/index.ts @@ -1,13 +1,4 @@ -import EventTarget from 'event-target-shim'; - import { type Hasher, sha256 } from '../fromExternal/js-sha256'; import { base64FromByteArray, btoa } from './btoa'; -import CustomEvent from './CustomEvent'; -function setupPolyfill() { - Object.assign(global, { - EventTarget, - CustomEvent, - }); -} -export { base64FromByteArray, btoa, type Hasher, setupPolyfill, sha256 }; +export { base64FromByteArray, btoa, type Hasher, sha256 }; diff --git a/packages/shared/sdk-client/src/api/LDEmitter.test.ts b/packages/shared/sdk-client/src/api/LDEmitter.test.ts index eff966efee..def8355a39 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.test.ts +++ b/packages/shared/sdk-client/src/api/LDEmitter.test.ts @@ -1,14 +1,21 @@ -import { LDContext } from '@launchdarkly/js-sdk-common'; +import { LDContext, LDLogger } from '@launchdarkly/js-sdk-common'; import LDEmitter from './LDEmitter'; describe('LDEmitter', () => { const error = { type: 'network', message: 'unreachable' }; let emitter: LDEmitter; + let logger: LDLogger; beforeEach(() => { jest.resetAllMocks(); - emitter = new LDEmitter(); + logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + emitter = new LDEmitter(logger); }); test('subscribe and handle', () => { @@ -77,12 +84,13 @@ describe('LDEmitter', () => { test('on listener with arguments', () => { const context = { kind: 'user', key: 'test-user-1' }; - const onListener = jest.fn((c: LDContext) => c); + const arg2 = { test: 'test' }; + const onListener = jest.fn((c: LDContext, a2: any) => [c, a2]); emitter.on('change', onListener); - emitter.emit('change', context); + emitter.emit('change', context, arg2); - expect(onListener).toBeCalledWith(context); + expect(onListener).toBeCalledWith(context, arg2); }); test('unsubscribe one of many listeners', () => { @@ -131,4 +139,15 @@ describe('LDEmitter', () => { expect(errorHandler1).not.toBeCalled(); expect(errorHandler2).not.toBeCalled(); }); + + it('handles errors generated by the callback', () => { + emitter.on('error', () => { + throw new Error('toast'); + }); + // Should not have an uncaught exception. + emitter.emit('error'); + expect(logger.error).toHaveBeenCalledWith( + 'Encountered error invoking handler for "error", detail: "Error: toast"', + ); + }); }); diff --git a/packages/shared/sdk-client/src/api/LDEmitter.ts b/packages/shared/sdk-client/src/api/LDEmitter.ts index 18e4bdab14..1b00b5a8fd 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.ts +++ b/packages/shared/sdk-client/src/api/LDEmitter.ts @@ -1,35 +1,13 @@ +import { LDLogger } from '@launchdarkly/js-sdk-common'; + export type EventName = 'error' | 'change'; -type CustomEventListeners = { - original: Function; - custom: Function; -}; -/** - * Native api usage: EventTarget. - * - * This is an event emitter using the standard built-in EventTarget web api. - * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget - * - * In react-native use event-target-shim to polyfill EventTarget. This is safe - * because the react-native repo uses it too. - * https://github.com/mysticatea/event-target-shim - */ export default class LDEmitter { - private et: EventTarget = new EventTarget(); + private listeners: Map = new Map(); - private listeners: Map = new Map(); + constructor(private logger?: LDLogger) {} - /** - * Cache all listeners in a Map so we can remove them later - * @param name string event name - * @param originalListener pointer to the original function as specified by - * the consumer - * @param customListener pointer to the custom function based on original - * listener. This is needed to allow for CustomEvents. - * @private - */ - private saveListener(name: EventName, originalListener: Function, customListener: Function) { - const listener = { original: originalListener, custom: customListener }; + on(name: EventName, listener: Function) { if (!this.listeners.has(name)) { this.listeners.set(name, [listener]); } else { @@ -37,17 +15,6 @@ export default class LDEmitter { } } - on(name: EventName, listener: Function) { - const customListener = (e: Event) => { - const { detail } = e as CustomEvent; - - // invoke listener with args from CustomEvent - listener(...detail); - }; - this.saveListener(name, listener, customListener); - this.et.addEventListener(name, customListener); - } - /** * Unsubscribe one or all events. * @@ -61,11 +28,8 @@ export default class LDEmitter { } if (listener) { - const toBeRemoved = existingListeners.find((c) => c.original === listener); - this.et.removeEventListener(name, toBeRemoved?.custom as any); - // remove from internal cache - const updated = existingListeners.filter((l) => l.original !== listener); + const updated = existingListeners.filter((fn) => fn !== listener); if (updated.length === 0) { this.listeners.delete(name); } else { @@ -74,15 +38,21 @@ export default class LDEmitter { return; } - // remove all listeners - existingListeners.forEach((l) => { - this.et.removeEventListener(name, l.custom as any); - }); + // listener was not specified, so remove them all for that event this.listeners.delete(name); } + private invokeListener(listener: Function, name: EventName, ...detail: any[]) { + try { + listener(...detail); + } catch (err) { + this.logger?.error(`Encountered error invoking handler for "${name}", detail: "${err}"`); + } + } + emit(name: EventName, ...detail: any[]) { - this.et.dispatchEvent(new CustomEvent(name, { detail })); + const listeners = this.listeners.get(name); + listeners?.forEach((listener) => this.invokeListener(listener, name, ...detail)); } eventNames(): string[] {