Skip to content

Commit

Permalink
feat(core): Registry listeners (#1191)
Browse files Browse the repository at this point in the history
* feat(core): registry listeners + hook

* remove hook

* tests

* equivalent function

* name changes

* test broken listeners
  • Loading branch information
suddjian authored and zhaoyongjie committed Nov 26, 2021
1 parent 360d4a5 commit 29df573
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ interface ItemWithLoader<T> {
loader: () => T;
}

/**
* Type of value returned from loader function when using registerLoader()
*/
type InclusiveLoaderResult<V> = V | Promise<V>;

export type RegistryValue<V, W extends InclusiveLoaderResult<V>> = V | W | undefined;

export type RegistryEntry<V, W extends InclusiveLoaderResult<V>> = {
key: string;
value: RegistryValue<V, W>;
};

/**
* A listener is called whenever a registry's entries change.
* Keys indicates which entries been affected.
*/
export type Listener = (keys: string[]) => void;

export interface RegistryConfig {
name?: string;
overwritePolicy?: OverwritePolicy;
Expand All @@ -41,12 +59,11 @@ export interface RegistryConfig {
* Can use generic to specify type of item in the registry
* @type V Type of value
* @type W Type of value returned from loader function when using registerLoader().
* W can be either V, Promise<V> or V | Promise<V>
* Set W=V when does not support asynchronous loader.
* By default W is set to V | Promise<V> to support
* both synchronous and asynchronous loaders.
*/
export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
export default class Registry<V, W extends InclusiveLoaderResult<V> = InclusiveLoaderResult<V>> {
name: string;

overwritePolicy: OverwritePolicy;
Expand All @@ -59,17 +76,23 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
[key: string]: Promise<V>;
};

listeners: Set<Listener>;

constructor(config: RegistryConfig = {}) {
const { name = '', overwritePolicy = OverwritePolicy.ALLOW } = config;
this.name = name;
this.overwritePolicy = overwritePolicy;
this.items = {};
this.promises = {};
this.listeners = new Set();
}

clear() {
const keys = this.keys();

this.items = {};
this.promises = {};
this.notifyListeners(keys);

return this;
}
Expand All @@ -95,6 +118,7 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
if (!item || willOverwrite) {
this.items[key] = { value };
delete this.promises[key];
this.notifyListeners([key]);
}

return this;
Expand All @@ -115,6 +139,7 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
if (!item || willOverwrite) {
this.items[key] = { loader };
delete this.promises[key];
this.notifyListeners([key]);
}

return this;
Expand Down Expand Up @@ -152,7 +177,7 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {

getMap() {
return this.keys().reduce<{
[key: string]: V | W | undefined;
[key: string]: RegistryValue<V, W>;
}>((prev, key) => {
const map = prev;
map[key] = this.get(key);
Expand Down Expand Up @@ -180,15 +205,15 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
return Object.keys(this.items);
}

values(): (V | W | undefined)[] {
values(): RegistryValue<V, W>[] {
return this.keys().map(key => this.get(key));
}

valuesAsPromise(): Promise<V[]> {
return Promise.all(this.keys().map(key => this.getAsPromise(key)));
}

entries(): { key: string; value: V | W | undefined }[] {
entries(): RegistryEntry<V, W>[] {
return this.keys().map(key => ({
key,
value: this.get(key),
Expand All @@ -198,7 +223,7 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
entriesAsPromise(): Promise<{ key: string; value: V }[]> {
const keys = this.keys();

return Promise.all(keys.map(key => this.getAsPromise(key))).then(values =>
return this.valuesAsPromise().then(values =>
values.map((value, i) => ({
key: keys[i],
value,
Expand All @@ -207,9 +232,32 @@ export default class Registry<V, W extends V | Promise<V> = V | Promise<V>> {
}

remove(key: string) {
const isChange = this.has(key);
delete this.items[key];
delete this.promises[key];
if (isChange) {
this.notifyListeners([key]);
}

return this;
}

addListener(listener: Listener) {
this.listeners.add(listener);
}

removeListener(listener: Listener) {
this.listeners.delete(listener);
}

private notifyListeners(keys: string[]) {
this.listeners.forEach(listener => {
try {
listener(keys);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Exception thrown from a registry listener:', e);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,83 @@ describe('Registry', () => {
});
});
});

describe('listeners', () => {
let registry = new Registry();
let listener = jest.fn();
beforeEach(() => {
registry = new Registry();
listener = jest.fn();
registry.addListener(listener);
});

it('calls the listener when a value is registered', () => {
registry.registerValue('foo', 'bar');
expect(listener).toBeCalledWith(['foo']);
});

it('calls the listener when a loader is registered', () => {
registry.registerLoader('foo', () => 'bar');
expect(listener).toBeCalledWith(['foo']);
});

it('calls the listener when a value is overriden', () => {
registry.registerValue('foo', 'bar');
listener.mockClear();
registry.registerValue('foo', 'baz');
expect(listener).toBeCalledWith(['foo']);
});

it('calls the listener when a value is removed', () => {
registry.registerValue('foo', 'bar');
listener.mockClear();
registry.remove('foo');
expect(listener).toBeCalledWith(['foo']);
});

it('does not call the listener when a value is not actually removed', () => {
registry.remove('foo');
expect(listener).not.toBeCalled();
});

it('calls the listener when registry is cleared', () => {
registry.registerValue('foo', 'bar');
registry.registerLoader('fluz', () => 'baz');
listener.mockClear();
registry.clear();
expect(listener).toBeCalledWith(['foo', 'fluz']);
});

it('removes listeners correctly', () => {
registry.removeListener(listener);
registry.registerValue('foo', 'bar');
expect(listener).not.toBeCalled();
});

describe('with a broken listener', () => {
let restoreConsole: any;
beforeEach(() => {
restoreConsole = mockConsole();
});
afterEach(() => {
restoreConsole();
});

it('keeps working', () => {
const errorListener = jest.fn().mockImplementation(() => {
throw new Error('test error');
});
const lastListener = jest.fn();

registry.addListener(errorListener);
registry.addListener(lastListener);
registry.registerValue('foo', 'bar');

expect(listener).toBeCalledWith(['foo']);
expect(errorListener).toBeCalledWith(['foo']);
expect(lastListener).toBeCalledWith(['foo']);
expect(console.error).toBeCalled();
});
});
});
});

0 comments on commit 29df573

Please sign in to comment.