Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm omitting maybeReportError from the legacy api for now. I don't understand why anyone will trust a method like maybeXXX. Secondly I don't understand why maybe reporting an error is the responsibility of an Emitter.

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();
Comment on lines +11 to +12
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I choose not to extend because we are designing this to follow EventEmitter's interface. Further, for react-native, we may need to refactor EventTarget to be a generic so we can swap it out with a polyfill. For example:

// react-native will pass in a polyfill.
// browsers will use the built-in api.
export default class LDEmitter<IEventTarget> {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we just following event emitters interface for backward compatibility, or ease of porting, or both, other?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More for ease of porting. However, I want to know your thoughts. Please expound.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it makes porting easier, then I am fine with it. I just wanted to make sure there wasn't some technical reason we specifically wanted to keep that interface.

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__"]
}