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 }