You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
12 KiB
386 lines
12 KiB
import { Emitter } from '@rocket.chat/emitter';
|
|
import _ from 'underscore';
|
|
import type { ISetting, SettingValue } from '@rocket.chat/core-typings';
|
|
|
|
import { SystemLogger } from '../../../server/lib/logger/system';
|
|
|
|
const warn = process.env.NODE_ENV === 'development' || process.env.TEST_MODE;
|
|
|
|
type SettingsConfig = {
|
|
debounce: number;
|
|
};
|
|
|
|
type OverCustomSettingsConfig = Partial<SettingsConfig>;
|
|
|
|
export interface ICachedSettings {
|
|
/*
|
|
* @description: The settings object as ready
|
|
*/
|
|
initialized(): void;
|
|
|
|
has(_id: ISetting['_id']): boolean;
|
|
|
|
getSetting(_id: ISetting['_id']): ISetting | undefined;
|
|
|
|
get<T extends SettingValue = SettingValue>(_id: ISetting['_id']): T;
|
|
|
|
getByRegexp<T extends SettingValue = SettingValue>(_id: RegExp): [string, T][];
|
|
|
|
watchMultiple<T extends SettingValue = SettingValue>(_id: ISetting['_id'][], callback: (settings: T[]) => void): () => void;
|
|
|
|
watch<T extends SettingValue = SettingValue>(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void;
|
|
|
|
watchOnce<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
cb: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void;
|
|
|
|
change<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
callback: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void;
|
|
|
|
changeMultiple<T extends SettingValue = SettingValue>(
|
|
_ids: ISetting['_id'][],
|
|
callback: (settings: T[]) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void;
|
|
|
|
changeOnce<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
callback: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void;
|
|
|
|
set(record: ISetting): void;
|
|
|
|
getConfig(config?: OverCustomSettingsConfig): SettingsConfig;
|
|
|
|
watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void;
|
|
|
|
changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void;
|
|
|
|
onReady(cb: () => void): void;
|
|
}
|
|
|
|
/**
|
|
* Class responsible for setting up the settings, cache and propagation changes
|
|
* Should be agnostic to the actual settings implementation, running on meteor or standalone
|
|
*
|
|
* You should not instantiate this class directly, only for testing purposes
|
|
*
|
|
* @extends Emitter
|
|
* @alpha
|
|
*/
|
|
export class CachedSettings
|
|
extends Emitter<
|
|
{
|
|
'*': [string, SettingValue];
|
|
} & {
|
|
ready: undefined;
|
|
[k: string]: SettingValue;
|
|
}
|
|
>
|
|
implements ICachedSettings
|
|
{
|
|
ready = false;
|
|
|
|
store = new Map<string, ISetting>();
|
|
|
|
/**
|
|
* The settings object as ready
|
|
*/
|
|
initialized(): void {
|
|
if (this.ready) {
|
|
return;
|
|
}
|
|
this.ready = true;
|
|
this.emit('ready');
|
|
SystemLogger.debug('Settings initialized');
|
|
}
|
|
|
|
/**
|
|
* returns if the setting is defined
|
|
* @param _id - The setting id
|
|
* @returns {boolean}
|
|
*/
|
|
public has(_id: ISetting['_id']): boolean {
|
|
if (!this.ready && warn) {
|
|
SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`);
|
|
}
|
|
return this.store.has(_id);
|
|
}
|
|
|
|
/**
|
|
* Gets the current Object of the setting
|
|
* @param _id - The setting id
|
|
* @returns {ISetting} - The current Object of the setting
|
|
*/
|
|
public getSetting(_id: ISetting['_id']): ISetting | undefined {
|
|
if (!this.ready && warn) {
|
|
SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`);
|
|
}
|
|
return this.store.get(_id);
|
|
}
|
|
|
|
/**
|
|
* Gets the current value of the setting
|
|
* - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that
|
|
* - The setting's value will be cached in memory so it won't call the DB every time you fetch a particular setting
|
|
* @param _id - The setting id
|
|
* @returns {SettingValue} - The current value of the setting
|
|
*/
|
|
public get<T extends SettingValue = SettingValue>(_id: ISetting['_id']): T {
|
|
if (!this.ready && warn) {
|
|
SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`);
|
|
}
|
|
return this.store.get(_id)?.value as T;
|
|
}
|
|
|
|
/**
|
|
* Gets the current value of the setting
|
|
* - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that
|
|
* @deprecated
|
|
* @param _id - The setting id
|
|
* @returns {SettingValue} - The current value of the setting
|
|
*/
|
|
public getByRegexp<T extends SettingValue = SettingValue>(_id: RegExp): [string, T][] {
|
|
if (!this.ready && warn) {
|
|
SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`);
|
|
}
|
|
|
|
return [...this.store.entries()].filter(([key]) => _id.test(key)).map(([key, setting]) => [key, setting.value]) as [string, T][];
|
|
}
|
|
|
|
/**
|
|
* Get the current value of the settings, and keep track of changes
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the settings got initialized
|
|
* @param _ids - Array of setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public watchMultiple<T extends SettingValue = SettingValue>(_id: ISetting['_id'][], callback: (settings: T[]) => void): () => void {
|
|
if (!this.ready) {
|
|
const cancel = new Set<() => void>();
|
|
|
|
cancel.add(
|
|
this.once('ready', (): void => {
|
|
cancel.clear();
|
|
cancel.add(this.watchMultiple(_id, callback));
|
|
}),
|
|
);
|
|
return (): void => {
|
|
cancel.forEach((fn) => fn());
|
|
};
|
|
}
|
|
|
|
if (_id.every((id) => this.store.has(id))) {
|
|
const settings = _id.map((id) => this.store.get(id)?.value);
|
|
callback(settings as T[]);
|
|
}
|
|
const mergeFunction = _.debounce((): void => {
|
|
callback(_id.map((id) => this.store.get(id)?.value) as T[]);
|
|
}, 100);
|
|
|
|
const fns = _id.map((id) => this.on(id, mergeFunction));
|
|
return (): void => {
|
|
fns.forEach((fn) => fn());
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the current value of the setting, and keep track of changes
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the settings got initialized
|
|
* @param _id - The setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public watch<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
cb: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void {
|
|
if (!this.ready) {
|
|
const cancel = new Set<() => void>();
|
|
cancel.add(
|
|
this.once('ready', (): void => {
|
|
cancel.clear();
|
|
cancel.add(this.watch(_id, cb, config));
|
|
}),
|
|
);
|
|
return (): void => {
|
|
cancel.forEach((fn) => fn());
|
|
};
|
|
}
|
|
|
|
this.store.has(_id) && cb(this.store.get(_id)?.value as T);
|
|
return this.change(_id, cb, config);
|
|
}
|
|
|
|
/**
|
|
* Get the current value of the setting, or wait until the initialized
|
|
* - This is a one time run
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the settings got initialized
|
|
* @param _id - The setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public watchOnce<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
cb: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void {
|
|
if (this.store.has(_id)) {
|
|
cb(this.store.get(_id)?.value as T);
|
|
return (): void => undefined;
|
|
}
|
|
return this.changeOnce(_id, cb, config);
|
|
}
|
|
|
|
/**
|
|
* Observes the given setting by id and keep track of changes
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the setting is changed
|
|
* - The callback is not fire until all the settings get initialized
|
|
* @param _id - The setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public change<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
callback: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void {
|
|
const { debounce } = this.getConfig(config);
|
|
return this.on(_id, _.debounce(callback, debounce) as any);
|
|
}
|
|
|
|
/**
|
|
* Observes multiple settings and keep track of changes
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the setting is changed
|
|
* - The callback is not fire until all the settings get initialized
|
|
* @param _ids - Array of setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public changeMultiple<T extends SettingValue = SettingValue>(
|
|
_ids: ISetting['_id'][],
|
|
callback: (settings: T[]) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void {
|
|
const fns = _ids.map((id) =>
|
|
this.change(
|
|
id,
|
|
(): void => {
|
|
callback(_ids.map((id) => this.store.get(id)?.value) as T[]);
|
|
},
|
|
config,
|
|
),
|
|
);
|
|
return (): void => {
|
|
fns.forEach((fn) => fn());
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Observes the setting and fires only if there is a change. Runs only once
|
|
* - This is a one time run
|
|
* - This callback is debounced
|
|
* - The callback is not fire until the setting is changed
|
|
* - The callback is not fire until all the settings get initialized
|
|
* @param _id - The setting id
|
|
* @param callback - The callback to run
|
|
* @returns {() => void} - A function that can be used to cancel the observe
|
|
*/
|
|
public changeOnce<T extends SettingValue = SettingValue>(
|
|
_id: ISetting['_id'],
|
|
callback: (args: T) => void,
|
|
config?: OverCustomSettingsConfig,
|
|
): () => void {
|
|
const { debounce } = this.getConfig(config);
|
|
return this.once(_id, _.debounce(callback, debounce) as any);
|
|
}
|
|
|
|
/**
|
|
* Sets the value of the setting
|
|
* - if the value set is the same as the current value, the change will not be fired
|
|
* - if the value is set before the initialization, the emit will be queued and will be fired after initialization
|
|
* @param _id - The setting id
|
|
* @param value - The value to set
|
|
* @returns {void}
|
|
*/
|
|
public set(record: ISetting): void {
|
|
if (this.store.has(record._id) && this.store.get(record._id)?.value === record.value) {
|
|
return;
|
|
}
|
|
|
|
this.store.set(record._id, record);
|
|
if (!this.ready) {
|
|
this.once('ready', () => {
|
|
this.emit(record._id, this.store.get(record._id)?.value);
|
|
this.emit('*', [record._id, this.store.get(record._id)?.value]);
|
|
});
|
|
return;
|
|
}
|
|
this.emit(record._id, this.store.get(record._id)?.value);
|
|
this.emit('*', [record._id, this.store.get(record._id)?.value]);
|
|
}
|
|
|
|
public getConfig = (config?: OverCustomSettingsConfig): SettingsConfig => ({
|
|
debounce: 500,
|
|
...config,
|
|
});
|
|
|
|
/** @deprecated */
|
|
public watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void {
|
|
if (!this.ready) {
|
|
const cancel = new Set<() => void>();
|
|
cancel.add(
|
|
this.once('ready', (): void => {
|
|
cancel.clear();
|
|
cancel.add(this.watchByRegex(regex, cb, config));
|
|
}),
|
|
);
|
|
return (): void => {
|
|
cancel.forEach((fn) => fn());
|
|
};
|
|
}
|
|
[...this.store.entries()].forEach(([key, setting]) => {
|
|
if (regex.test(key)) {
|
|
cb(key, setting.value);
|
|
}
|
|
});
|
|
|
|
return this.changeByRegex(regex, cb, config);
|
|
}
|
|
|
|
/** @deprecated */
|
|
public changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void {
|
|
const store: Map<string, (...args: [string, SettingValue]) => void> = new Map();
|
|
return this.on('*', ([_id, value]) => {
|
|
if (regex.test(_id)) {
|
|
const { debounce } = this.getConfig(config);
|
|
const cb = store.get(_id) || _.debounce(callback, debounce);
|
|
cb(_id, value);
|
|
store.set(_id, cb);
|
|
}
|
|
regex.lastIndex = 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait until the settings get ready then run the callback
|
|
*/
|
|
public onReady(cb: () => void): void {
|
|
if (this.ready) {
|
|
return cb();
|
|
}
|
|
this.once('ready', cb);
|
|
}
|
|
}
|
|
|