Warning, /firebird/firebird-ng/src/app/utils/config-property.ts is written in an unsupported language. File is not indexed.
0001 import {BehaviorSubject, Observable} from 'rxjs';
0002
0003
0004 /**
0005 * Storage general interface for storing ConfigProperty-ies.
0006 * ConfigProperty uses the storage to save and load values.
0007 *
0008 * Time-based Configuration System:
0009 * - Each configuration value is stored with an associated timestamp
0010 * - Timestamps are stored in parallel variables with ".time" suffix (e.g., "myConfig" value has "myConfig.time" timestamp)
0011 * - When setting a value with a specific timestamp, it only updates if the stored timestamp is older
0012 * - If no timestamp is provided when setting a value, the current time ("now") is used
0013 * - This allows for conflict resolution when multiple sources might update the same configuration
0014 */
0015 interface PersistentPropertyStorage {
0016 getItem(key: string): string | null;
0017 setItem(key: string, value: string): void;
0018 }
0019
0020 /**
0021 * Use local storage to save load ConfigProperty
0022 */
0023 class PersistentPropertyLocalStorage implements PersistentPropertyStorage {
0024 getItem(key: string): string | null {
0025 return localStorage.getItem(key);
0026 }
0027
0028 setItem(key: string, value: string): void {
0029 localStorage.setItem(key, value);
0030 }
0031 }
0032
0033 /**
0034 * Manages an individual configuration property. Provides reactive updates to subscribers,
0035 * persistence to localStorage, and optional value validation.
0036 *
0037 * Includes time-based update logic to handle concurrent modifications:
0038 * - Each value is stored with a timestamp
0039 * - Updates can specify a timestamp to enable conflict resolution
0040 * - Only updates with newer timestamps will overwrite existing values
0041 *
0042 * @template T The type of the configuration value.
0043 */
0044 export class ConfigProperty<T> {
0045
0046 private valueType: string;
0047
0048 public subject: BehaviorSubject<T>;
0049
0050 /** Observable for subscribers to react to changes in the property value. */
0051 public changes$: Observable<T>;
0052
0053 /**
0054 * Creates an instance of ConfigProperty.
0055 *
0056 * @param {string} _key The localStorage key under which the property value is stored.
0057 * @param {T} defaultValue The default value of the property if not previously stored.
0058 * @param storage
0059 * @param {() => void} saveCallback The callback to execute after setting a new value.
0060 * @param {(value: T) => boolean} [validator] Optional validator function to validate the property value.
0061 */
0062 constructor(
0063 private _key: string,
0064 private defaultValue: T,
0065 private saveCallback?: () => void,
0066 private validator?: (value: T) => boolean,
0067 private storage: PersistentPropertyStorage = new PersistentPropertyLocalStorage(),
0068 ) {
0069 const value = this.loadValue();
0070 this.subject = new BehaviorSubject<T>(value);
0071 this.changes$ = this.subject.asObservable();
0072 this.valueType = typeof value;
0073 }
0074
0075
0076 /**
0077 * Loads the property value from localStorage or returns the default value if not found or invalid.
0078 *
0079 * @returns {T} The loaded or default value of the property.
0080 */
0081 private loadValue(): T {
0082 let storedValue: string|null = null;
0083 let parsedValue: any = undefined;
0084 try {
0085 storedValue = this.storage.getItem(this._key);
0086
0087 if (storedValue !== null) {
0088 parsedValue = (typeof this.defaultValue) !== 'string' ? JSON.parse(storedValue) : storedValue;
0089 } else {
0090 parsedValue = this.defaultValue;
0091 }
0092 return this.validator && !this.validator(parsedValue) ? this.defaultValue : parsedValue;
0093 } catch (error) {
0094 console.error(`Error at ConfigProperty.loadValue, key='${this._key}'`);
0095 console.log(' storedValue', storedValue);
0096 console.log(' parsedValue', parsedValue);
0097 console.log(' Default value will be used: ', this.defaultValue);
0098 console.log(error);
0099
0100 return this.defaultValue;
0101 }
0102 }
0103
0104 /**
0105 * Gets the timestamp of when the current value was stored.
0106 *
0107 * @returns {number | null} The timestamp in milliseconds, or null if not found or invalid.
0108 */
0109 private getStoredTime(): number | null {
0110 try {
0111 const timeKey = `${this._key}.time`;
0112 const storedTime = this.storage.getItem(timeKey);
0113 if (!storedTime) {
0114 return null;
0115 }
0116 const parsedTime = parseInt(storedTime, 10);
0117 // Return null if the timestamp is invalid (NaN)
0118 return isNaN(parsedTime) ? null : parsedTime;
0119 } catch (error) {
0120 console.error(`Error loading timestamp for key='${this._key}'`, error);
0121 return null;
0122 }
0123 }
0124
0125 /**
0126 * Saves the timestamp for when the value was stored.
0127 *
0128 * @param {number} timestamp The timestamp in milliseconds.
0129 */
0130 private saveTime(timestamp: number): void {
0131 const timeKey = `${this._key}.time`;
0132 this.storage.setItem(timeKey, timestamp.toString());
0133 }
0134
0135 /**
0136 * Sets the property value with optional timestamp-based conflict resolution.
0137 * If a timestamp is provided, the value is only updated if the stored timestamp is older.
0138 * If no timestamp is provided, the current time is used.
0139 *
0140 * @param {T} value The new value to set for the property.
0141 * @param {number} [time] Optional timestamp in milliseconds. If not provided, Date.now() is used.
0142 * @param {boolean} [ignoreTime=false] If true, bypasses timestamp-based conflict resolution.
0143 */
0144 setValue(value: T, time?: number, ignoreTime: boolean = false): void {
0145 if (this.validator && !this.validator(value)) {
0146 console.error('Validation failed for:', value);
0147 return;
0148 }
0149
0150 // If no explicit time provided, use Date.now() but ensure it's unique
0151 let updateTime: number;
0152 if (time !== undefined) {
0153 updateTime = time;
0154 } else {
0155 updateTime = Date.now();
0156 }
0157
0158 const storedTime = this.getStoredTime();
0159
0160 // Only update if no stored time exists, if the update time is newer, or if ignoreTime is true
0161 // (!) There was a lot of thought on >=, it is considered the less of all complexities:
0162 // What we want with these configs, is to not overwrite current configs with stale configs.
0163 // >= is good for this. If one overwrites config several times (e.g. in tests) we don't care
0164 if (ignoreTime || storedTime === null || updateTime >= storedTime) {
0165 this.storage.setItem(this._key, typeof value !== 'string' ? JSON.stringify(value) : value);
0166 this.saveTime(updateTime);
0167
0168 if(this.saveCallback) {
0169 this.saveCallback();
0170 }
0171
0172 this.subject.next(value);
0173 } else {
0174 console.log(`Skipping update for key='${this._key}': stored time (${storedTime}) is newer than update time (${updateTime})`);
0175 }
0176 }
0177
0178 /**
0179 * Sets the property value after validation. If the value is valid, it updates the property and calls the save callback.
0180 * Uses the current timestamp for the update.
0181 *
0182 * @param {T} value The new value to set for the property.
0183 */
0184 set value(value: T) {
0185 this.setValue(value);
0186 }
0187
0188 /**
0189 * Gets the current value of the property.
0190 *
0191 * @returns {T} The current value of the property.
0192 */
0193 get value(): T {
0194 return this.subject.value;
0195 }
0196
0197 get key(): string {
0198 return this._key;
0199 }
0200
0201
0202 /**
0203 * Resets value to its default given at Config construction.
0204 * This also updates the storage and timestamp.
0205 */
0206 public setDefault() {
0207 this.setValue(this.defaultValue);
0208 }
0209
0210 /**
0211 * Gets the timestamp of the current stored value.
0212 * @returns The timestamp in milliseconds, or null if not found.
0213 */
0214 public getTimestamp(): number | null {
0215 return this.getStoredTime();
0216 }
0217 }