78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
import { Observable, ReplaySubject, Subject, firstValueFrom, map, scan, startWith } from 'rxjs';
|
|
|
|
import { ExtensionsLog, log } from '../logs/log';
|
|
import { deepFreeze } from '../utils';
|
|
|
|
export const MSG_CANNOT_REGISTER_READ_ONLY = 'Cannot register to a read-only registry';
|
|
|
|
export type PluginExtensionConfigs<T> = {
|
|
pluginId: string;
|
|
configs: T[];
|
|
};
|
|
|
|
export type RegistryType<T> = Record<string | symbol, T>;
|
|
|
|
// This is the base-class used by the separate specific registries.
|
|
export abstract class Registry<TRegistryValue, TMapType> {
|
|
// Used in cases when we would like to pass a read-only registry to plugin.
|
|
// In these cases we are passing in the `registrySubject` to the constructor.
|
|
// (If TRUE `initialState` is ignored.)
|
|
private isReadOnly: boolean;
|
|
// This is the subject that receives extension configs for a loaded plugin.
|
|
private resultSubject: Subject<PluginExtensionConfigs<TMapType>>;
|
|
protected logger: ExtensionsLog;
|
|
// This is the subject that we expose.
|
|
// (It will buffer the last value on the stream - the registry - and emit it to new subscribers immediately.)
|
|
protected registrySubject: ReplaySubject<RegistryType<TRegistryValue>>;
|
|
|
|
constructor(options: {
|
|
registrySubject?: ReplaySubject<RegistryType<TRegistryValue>>;
|
|
initialState?: RegistryType<TRegistryValue>;
|
|
log?: ExtensionsLog;
|
|
}) {
|
|
this.resultSubject = new Subject<PluginExtensionConfigs<TMapType>>();
|
|
this.logger = options.log ?? log;
|
|
this.isReadOnly = false;
|
|
|
|
// If the registry subject (observable) is provided, it means that all the registry updates are taken care of outside of this class -> it is read-only.
|
|
if (options.registrySubject) {
|
|
this.registrySubject = options.registrySubject;
|
|
this.isReadOnly = true;
|
|
|
|
return;
|
|
}
|
|
|
|
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
|
|
this.resultSubject
|
|
.pipe(
|
|
scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
|
|
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
|
|
startWith(options.initialState ?? {}),
|
|
map((registry) => deepFreeze(registry))
|
|
)
|
|
// Emitting the new registry to `this.registrySubject`
|
|
.subscribe(this.registrySubject);
|
|
}
|
|
|
|
abstract mapToRegistry(
|
|
registry: RegistryType<TRegistryValue>,
|
|
item: PluginExtensionConfigs<TMapType>
|
|
): RegistryType<TRegistryValue>;
|
|
|
|
register(result: PluginExtensionConfigs<TMapType>): void {
|
|
if (this.isReadOnly) {
|
|
throw new Error(MSG_CANNOT_REGISTER_READ_ONLY);
|
|
}
|
|
|
|
this.resultSubject.next(result);
|
|
}
|
|
|
|
asObservable(): Observable<RegistryType<TRegistryValue>> {
|
|
return this.registrySubject.asObservable();
|
|
}
|
|
|
|
getState(): Promise<RegistryType<TRegistryValue>> {
|
|
return firstValueFrom(this.asObservable());
|
|
}
|
|
}
|