Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/firebird-ng/src/app/utils/persistent-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 PersistentProperty<T> {
0045   public subject: BehaviorSubject<T>;
0046 
0047   /** Observable for subscribers to react to changes in the property value. */
0048   public changes$: Observable<T>;
0049   
0050   /** Track last automatic timestamp to ensure uniqueness */
0051   private lastAutoTimestamp: number = 0;
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 
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    */
0143   setValue(value: T, time?: number): void {
0144     if (this.validator && !this.validator(value)) {
0145       console.error('Validation failed for:', value);
0146       return;
0147     }
0148 
0149     // If no explicit time provided, use Date.now() but ensure it's unique
0150     let updateTime: number;
0151     if (time !== undefined) {
0152       updateTime = time;
0153     } else {
0154       updateTime = Date.now();
0155       // Ensure timestamp is always increasing for automatic timestamps
0156       if (updateTime <= this.lastAutoTimestamp) {
0157         updateTime = this.lastAutoTimestamp + 1;
0158       }
0159       this.lastAutoTimestamp = updateTime;
0160     }
0161     
0162     const storedTime = this.getStoredTime();
0163 
0164     // Only update if no stored time exists or if the update time is newer
0165     if (storedTime === null || updateTime > storedTime) {
0166       this.storage.setItem(this.key, typeof value !== 'string' ? JSON.stringify(value) : value);
0167       this.saveTime(updateTime);
0168 
0169       if(this.saveCallback) {
0170         this.saveCallback();
0171       }
0172 
0173       this.subject.next(value);
0174     } else {
0175       console.log(`Skipping update for key='${this.key}': stored time (${storedTime}) is newer than update time (${updateTime})`);
0176     }
0177   }
0178 
0179   /**
0180    * Sets the property value after validation. If the value is valid, it updates the property and calls the save callback.
0181    * Uses the current timestamp for the update.
0182    *
0183    * @param {T} value The new value to set for the property.
0184    */
0185   set value(value: T) {
0186     this.setValue(value);
0187   }
0188 
0189   /**
0190    * Gets the current value of the property.
0191    *
0192    * @returns {T} The current value of the property.
0193    */
0194   get value(): T {
0195     return this.subject.value;
0196   }
0197 
0198 
0199   /**
0200    * Resets value to its default given at Config construction
0201    */
0202   public setDefault() {
0203     this.subject.next(this.defaultValue);
0204   }
0205 }