blob: 714458382490029077a3f441ffa82266cc709961 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export enum OverwritePolicy {
ALLOW = 'ALLOW',
PROHIBIT = 'PROHIBIT',
WARN = 'WARN',
}
interface ItemWithValue<T> {
value: T;
}
interface ItemWithLoader<T> {
loader: () => T;
}
export interface RegistryConfig {
name?: string;
overwritePolicy?: OverwritePolicy;
}
/**
* Registry class
*
* 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>> {
name: string;
overwritePolicy: OverwritePolicy;
items: {
[key: string]: ItemWithValue<V> | ItemWithLoader<W>;
};
promises: {
[key: string]: Promise<V>;
};
constructor(config: RegistryConfig = {}) {
const { name = '', overwritePolicy = OverwritePolicy.ALLOW } = config;
this.name = name;
this.overwritePolicy = overwritePolicy;
this.items = {};
this.promises = {};
}
clear() {
this.items = {};
this.promises = {};
return this;
}
has(key: string) {
const item = this.items[key];
return item !== null && item !== undefined;
}
registerValue(key: string, value: V) {
const item = this.items[key];
const willOverwrite =
this.has(key) && (('value' in item && item.value !== value) || 'loader' in item);
if (willOverwrite) {
if (this.overwritePolicy === OverwritePolicy.WARN) {
// eslint-disable-next-line no-console
console.warn(`Item with key "${key}" already exists. You are assigning a new value.`);
} else if (this.overwritePolicy === OverwritePolicy.PROHIBIT) {
throw new Error(`Item with key "${key}" already exists. Cannot overwrite.`);
}
}
if (!item || willOverwrite) {
this.items[key] = { value };
delete this.promises[key];
}
return this;
}
registerLoader(key: string, loader: () => W) {
const item = this.items[key];
const willOverwrite =
this.has(key) && (('loader' in item && item.loader !== loader) || 'value' in item);
if (willOverwrite) {
if (this.overwritePolicy === OverwritePolicy.WARN) {
// eslint-disable-next-line no-console
console.warn(`Item with key "${key}" already exists. You are assigning a new value.`);
} else if (this.overwritePolicy === OverwritePolicy.PROHIBIT) {
throw new Error(`Item with key "${key}" already exists. Cannot overwrite.`);
}
}
if (!item || willOverwrite) {
this.items[key] = { loader };
delete this.promises[key];
}
return this;
}
get(key: string): V | W | undefined {
const item = this.items[key];
if (item !== undefined) {
if ('loader' in item) {
return item.loader && item.loader();
}
return item.value;
}
return undefined;
}
getAsPromise(key: string): Promise<V> {
const promise = this.promises[key];
if (typeof promise !== 'undefined') {
return promise;
}
const item = this.get(key);
if (item !== undefined) {
const newPromise = Promise.resolve(item) as Promise<V>;
this.promises[key] = newPromise;
return newPromise;
}
return Promise.reject<V>(new Error(`Item with key "${key}" is not registered.`));
}
getMap() {
return this.keys().reduce<{
[key: string]: V | W | undefined;
}>((prev, key) => {
const map = prev;
map[key] = this.get(key);
return map;
}, {});
}
getMapAsPromise() {
const keys = this.keys();
return Promise.all(keys.map(key => this.getAsPromise(key))).then(values =>
values.reduce<{
[key: string]: V;
}>((prev, value, i) => {
const map = prev;
map[keys[i]] = value;
return map;
}, {}),
);
}
keys(): string[] {
return Object.keys(this.items);
}
values(): (V | W | undefined)[] {
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 }[] {
return this.keys().map(key => ({
key,
value: this.get(key),
}));
}
entriesAsPromise(): Promise<{ key: string; value: V }[]> {
const keys = this.keys();
return Promise.all(keys.map(key => this.getAsPromise(key))).then(values =>
values.map((value, i) => ({
key: keys[i],
value,
})),
);
}
remove(key: string) {
delete this.items[key];
delete this.promises[key];
return this;
}
}