UNPKG

@mcdevsl/superset-ui

Version:
264 lines (218 loc) 7.02 kB
/** * 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; } /** * 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; } /** * 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(). * 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 InclusiveLoaderResult<V> = InclusiveLoaderResult<V>> { name: string; overwritePolicy: OverwritePolicy; items: { [key: string]: ItemWithValue<V> | ItemWithLoader<W>; }; promises: { [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; } 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]; this.notifyListeners([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]; this.notifyListeners([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]: RegistryValue<V, W>; }>((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(): 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(): RegistryEntry<V, W>[] { return this.keys().map(key => ({ key, value: this.get(key), })); } entriesAsPromise(): Promise<{ key: string; value: V }[]> { const keys = this.keys(); return this.valuesAsPromise().then(values => values.map((value, i) => ({ key: keys[i], value, })), ); } 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); } }); } }