import isEqual from 'react-fast-compare'; import type { ValueRegistry } from '../../types/registry.js'; let locked = false; export type RegistryValue = { initValue: T; context: Record; value: T; processors: { callback: SyncProcessor | AsyncProcessor; priority: number; }[]; }; export type SyncProcessor = (value: T) => T; export type AsyncProcessor = (value: T) => Promise; class Registry { values: Record>> = {}; async get( name: string, initValue: T, context?: Record, validator?: (value: T) => boolean ) { if (this.values[name]) { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && isEqual(this.values[name].context, context) && Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } } // Cache the initValue and the context this.values[name] = this.values[name] || ({} as RegistryValue); this.values[name].initValue = initValue; this.values[name].context = context; // If there is no processor, return the init value if (!this.values[name].processors) { this.values[name].value = initValue; return initValue; } const { processors } = this.values[name]; // Call the list of processors, returned value will be passed to the next processor. Start with the init value let value = initValue; for (let i = 0; i < processors.length; i += 1) { const { callback } = processors[i]; value = await callback.call(context, value); if (value === undefined) { // eslint-disable-next-line no-console console.log( `\x1b[33m⚠️ The processor for the value '${name}' is not returning anything. This may cause unexpected behavior.\x1b[0m` ); } // Validate the value if the validator is provided and it is a function if (typeof validator === 'function') { const validateResult = validator(value); if (validateResult !== true) { throw new Error(`Value ${name} is invalid: ${validateResult}`); } } } // Cache the value this.values[name].value = value; return value; } getSync( name: string, initValue: T, context?: Record, validator?: (value: T) => boolean ) { const validateFunc = (value: T) => { // Check if value is a promise if ( value !== null && typeof value === 'object' && typeof (value as unknown as Promise).then === 'function' ) { throw new Error( `The 'getSync' function does not support async processor. Please use 'get' function instead` ); } else if (typeof validator === 'function') { return validator(value); } else { return true; } }; if (this.values[name]) { // If the initValue and the context are identical, return the cached value. Skip the processors if ( isEqual(initValue, this.values[name].initValue) && isEqual(this.values[name].context, context) && Object.prototype.hasOwnProperty.call(this.values[name], 'value') ) { return this.values[name].value; } } // Cache the initValue and the context this.values[name] = this.values[name] || ({} as RegistryValue); this.values[name].initValue = initValue; this.values[name].context = context; // If there is no processor, return the init value if (!this.values[name].processors) { this.values[name].value = initValue; return initValue; } const { processors } = this.values[name]; // Call the list of processors, returned value will be passed to the next processor. Start with the init value let value = initValue; for (let i = 0; i < processors.length; i += 1) { const { callback } = processors[i]; value = callback.call(context, value); // Check if the callback function not returning anything if (value === undefined) { // eslint-disable-next-line no-console console.log( `\x1b[33m⚠️ The processor for the value '${name}' is not returning anything. This may cause unexpected behavior.\x1b[0m` ); } // Validate the value if the validator is provided and it is a function const validateResult = validateFunc(value); if (validateResult !== true) { throw new Error(`Value ${name} is invalid`); } } // Cache the value this.values[name].value = value; return value; } addProcessor( name: string, callback: SyncProcessor | AsyncProcessor, priority?: number ) { if (locked) { throw new Error( 'Registry is locked. Most likely you are trying to add a processor from a middleware. Consider using a bootstrap file to add processors' ); } if (typeof priority === 'undefined') { priority = 10; } // Throw error if priority is not a number if (typeof priority !== 'number') { throw new Error('Priority must be a number'); } // Throw error if the priority is bigger than 1000 if (priority > 1000) { throw new Error('Priority must be smaller than 1000'); } // Throw error if callback is not a function or async function if (typeof callback !== 'function') { throw new Error('Callback must be a function'); } if (!this.values[name]) { this.values[name] = { processors: [] } as Partial>; } this.values[name].processors = this.values[name].processors || []; // Add the callback to the processors, sort by priority const { processors } = this.values[name]; processors.push({ callback, priority }); processors.sort((a, b) => a.priority - b.priority); } addFinalProcessor( name: string, callback: SyncProcessor | AsyncProcessor ): void { // Check if there is already a final processor base on the priority const processors = this.values[name]?.processors || []; if (processors.find((p) => p.priority === 1000)) { throw new Error( `There is already a final processor for the value ${name}` ); } this.addProcessor(name, callback, 1000); } getProcessors(name: string): { callback: SyncProcessor | AsyncProcessor; priority: number; }[] { if (!this.values[name]) { throw new Error(`The value ${name} is not registered`); } return this.values[name].processors || []; } } const registry = new Registry(); /** * Get the value from the registry * @param name - The name of the value * @param initialization - The initialization value or a function that returns the value * @param context - The context of the value * @param validator - The validator function * @returns The value from the registry */ export async function getValue( name: string, initialization: T | AsyncProcessor | SyncProcessor, context?: Record, validator?: (value: T) => boolean ): Promise { let initValue; const value = registry.values[name] || ({} as RegistryValue); // Check if the initValue is a function, then add this function to the processors as the first processor if (typeof initialization === 'function') { // Add this function to the biginning of the processors const processors = value.processors || []; processors.unshift({ callback: initialization as SyncProcessor | AsyncProcessor, priority: 0 }); registry.values[name] = { ...value, processors }; initValue = value.initValue; } else { initValue = initialization as T; } const val = await registry.get(name, initValue, context, validator); return val; } /** * Get the value from the registry * @param name - The name of the value * @param initialization - The initialization value or a function that returns the value * @param context - The context of the value * @param validator - The validator function * @returns The value from the registry */ export function getValueSync( name: string, initialization: T | SyncProcessor, context: Record, validator?: (value: T) => boolean ): T { let initValue; // Check if the initValue is a function, then add this function to the processors as the first processor if (typeof initialization === 'function') { // Add this function to the processors, add this to the biginning of the processors const processors = registry.values[name]?.processors || []; processors.unshift({ callback: initialization as SyncProcessor, priority: 0 }); registry.values[name] = registry.values[name] || ({} as RegistryValue); registry.values[name].processors = processors; initValue = registry.values[name].initValue; } else { initValue = initialization; } const val = registry.getSync(name, initValue, context, validator); return val; } /** * Add a processor for a known registry value (typed). */ export function addProcessor( name: K, callback: SyncProcessor | AsyncProcessor, priority?: number ): void; /** * Add a processor for a custom registry value. */ export function addProcessor( name: string, callback: SyncProcessor | AsyncProcessor, priority?: number ): void; export function addProcessor( name: string, callback: SyncProcessor | AsyncProcessor, priority?: number ): void { return registry.addProcessor(name, callback, priority); } /** * Add a final (priority-1000) processor for a known registry value (typed). */ export function addFinalProcessor( name: K, callback: SyncProcessor | AsyncProcessor ): void; /** * Add a final (priority-1000) processor for a custom registry value. */ export function addFinalProcessor( name: string, callback: SyncProcessor | AsyncProcessor ): void; export function addFinalProcessor( name: string, callback: SyncProcessor | AsyncProcessor ): void { return registry.addFinalProcessor(name, callback); } export function getProcessors(name: string): { callback: SyncProcessor | AsyncProcessor; priority: number; }[] { return registry.getProcessors(name); } export function lockRegistry(): void { // Reset the values cache by removing all values from all properties in the registry values Object.keys(registry.values).forEach((key) => { if (Object.prototype.hasOwnProperty.call(registry.values, key)) { delete registry.values[key].value; } }); locked = true; }