Skip to content

Commit

Permalink
chore: improved LDEmitter (#207)
Browse files Browse the repository at this point in the history
* Improved emitter design
* Improved jest config
* Added unit tests
  • Loading branch information
yusinto authored Jul 18, 2023
1 parent 147c5da commit f539c7a
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 67 deletions.
1 change: 1 addition & 0 deletions packages/shared/sdk-client/jest-setupFilesAfterEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
7 changes: 0 additions & 7 deletions packages/shared/sdk-client/jest.config.js

This file was deleted.

10 changes: 10 additions & 0 deletions packages/shared/sdk-client/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"transform": { "^.+\\.ts?$": "ts-jest" },
"testMatch": ["**/*.test.ts?(x)"],
"testPathIgnorePatterns": ["node_modules", "example", "dist"],
"modulePathIgnorePatterns": ["dist"],
"testEnvironment": "jsdom",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"collectCoverageFrom": ["src/**/*.ts"],
"setupFilesAfterEnv": ["./jest-setupFilesAfterEnv.ts"]
}
27 changes: 15 additions & 12 deletions packages/shared/sdk-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,28 @@
"//": "Pinned semver because 7.5.0 introduced require('util') without the node: prefix",
"dependencies": {
"@launchdarkly/js-sdk-common": "1.0.2",
"semver": "7.4.0"
"semver": "7.5.4"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@testing-library/dom": "^9.3.1",
"@testing-library/jest-dom": "^5.16.5",
"@types/jest": "^29.5.3",
"@types/semver": "^7.5.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"eslint": "^8.43.0",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"eslint": "^8.45.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"jest-diff": "^29.5.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.1",
"jest-diff": "^29.6.1",
"jest-environment-jsdom": "^29.6.1",
"launchdarkly-js-test-helpers": "^2.2.0",
"prettier": "^2.8.8",
"ts-jest": "^29.1.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"typedoc": "0.23.26",
"typescript": "^4.6.3"
"typescript": "^5.1.6"
}
}
77 changes: 77 additions & 0 deletions packages/shared/sdk-client/src/api/LDEmitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import LDEmitter from './LDEmitter';

describe('LDEmitter', () => {
const error = { type: 'network', message: 'unreachable' };
let emitter: LDEmitter;

beforeEach(() => {
jest.resetAllMocks();
emitter = new LDEmitter();
});

test('subscribe and handle', () => {
const errorHandler1 = jest.fn();
const errorHandler2 = jest.fn();

emitter.on('error', errorHandler1);
emitter.on('error', errorHandler2);
emitter.emit('error', error);

expect(errorHandler1).toHaveBeenCalledWith(error);
expect(errorHandler2).toHaveBeenCalledWith(error);
});

test('unsubscribe and handle', () => {
const errorHandler1 = jest.fn();
const errorHandler2 = jest.fn();

emitter.on('error', errorHandler1);
emitter.on('error', errorHandler2);
emitter.off('error');
emitter.emit('error', error);

expect(errorHandler1).not.toHaveBeenCalled();
expect(errorHandler2).not.toHaveBeenCalled();
expect(emitter.listenerCount('error')).toEqual(0);
});

test('unsubscribing an event should not affect other events', () => {
const errorHandler = jest.fn();
const changeHandler = jest.fn();

emitter.on('error', errorHandler);
emitter.on('change', changeHandler);
emitter.off('error'); // unsubscribe error handler
emitter.emit('error', error);
emitter.emit('change');

// change handler should still be affective
expect(changeHandler).toHaveBeenCalled();
expect(errorHandler).not.toHaveBeenCalled();
});

test('eventNames', () => {
const errorHandler1 = jest.fn();
const changeHandler = jest.fn();
const readyHandler = jest.fn();

emitter.on('error', errorHandler1);
emitter.on('change', changeHandler);
emitter.on('ready', readyHandler);

expect(emitter.eventNames()).toEqual(['error', 'change', 'ready']);
});

test('listener count', () => {
const errorHandler1 = jest.fn();
const errorHandler2 = jest.fn();
const changeHandler = jest.fn();

emitter.on('error', errorHandler1);
emitter.on('error', errorHandler2);
emitter.on('change', changeHandler);

expect(emitter.listenerCount('error')).toEqual(2);
expect(emitter.listenerCount('change')).toEqual(1);
});
});
58 changes: 58 additions & 0 deletions packages/shared/sdk-client/src/api/LDEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export type EventName = 'change' | 'internal-change' | 'ready' | 'initialized' | 'failed' | 'error';

/**
* 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<EventName, EventListener[]> = new Map();

/**
* Cache all listeners in a Map so we can remove them later
* @param name string event name
* @param listener function to handle the event
* @private
*/
private saveListener(name: EventName, listener: EventListener) {
if (!this.listeners.has(name)) {
this.listeners.set(name, [listener]);
} else {
this.listeners.get(name)?.push(listener);
}
}

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, customListener);
this.et.addEventListener(name, customListener);
}

off(name: EventName) {
this.listeners.get(name)?.forEach((l) => {
this.et.removeEventListener(name, l);
});
this.listeners.delete(name);
}

emit(name: EventName, ...detail: any[]) {
this.et.dispatchEvent(new CustomEvent(name, { detail }));
}

eventNames(): string[] {
return [...this.listeners.keys()];
}

listenerCount(name: EventName): number {
return this.listeners.get(name)?.length ?? 0;
}
}
48 changes: 0 additions & 48 deletions packages/shared/sdk-client/src/api/LDEventTarget.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/shared/sdk-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"declarationMap": true, // enables importers to jump to source
"stripInternal": true
},
"include": ["src", "./jest-setupFilesAfterEnv.ts"],
"exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__"]
}

0 comments on commit f539c7a

Please sign in to comment.