1/* 2 * Copyright (c) 2021-2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16/** 17 * file version 18 * 19 * To indicate the file formate 20 * 21 */ 22enum ObjectVersion { 23 NewVersion, 24 CompatibleVersion, 25 Default, 26} 27 28/** 29 * MapInfo 30 * 31 * Helper class to persist Map in Persistent storage 32 * 33 */ 34type MapItem<K, V> = { key: K, value: V }; 35class MapInfo<K, V> { 36 static readonly replacer: string = '_____map_replacer__'; 37 static readonly replacerCompatible: string = 'ace_engine_state_mgmt_map_replacer'; 38 public keys: K[]; 39 public values: V[]; 40 41 constructor( 42 public mapReplacer: string, 43 public keyToValue: MapItem<K, V>[] 44 ) { } 45 46 // Check if the given object is of type MapInfo 47 static isObject<K, V>(obj: unknown): ObjectVersion { 48 const typedObject = obj as MapInfo<K, V>; 49 if ('mapReplacer' in typedObject && typedObject.mapReplacer === MapInfo.replacer) { 50 return ObjectVersion.NewVersion; 51 } 52 if ('mapReplacer' in typedObject && typedObject.mapReplacer === MapInfo.replacerCompatible) { 53 return ObjectVersion.CompatibleVersion; 54 } 55 return ObjectVersion.Default; 56 } 57 58 // Convert Map to Object 59 static toObject<K, V>(map: Map<K, V>): MapInfo<K, V> { 60 let mapItems: MapItem<K, V>[] = []; 61 map.forEach((val: V, key: K) => { 62 mapItems.push({ key: key, value: val }) 63 }) 64 return new MapInfo(MapInfo.replacer, mapItems); 65 } 66 67 // Convert Object to Map 68 static toMap<K, V>(obj: MapInfo<K, V>): Map<K, V> { 69 return new Map<K, V>(obj.keyToValue.map((item: MapItem<K, V>) => [item.key, item.value])); 70 } 71 72 static toMapCompatible<K, V>(obj: MapInfo<K, V>): Map<K, V> { 73 return new Map<K, V>(obj.keys.map((key, i) => [key, obj.values[i]])); 74 } 75} 76 77/** 78 * SetInfo 79 * 80 * Helper class to persist Set in Persistent storage 81 * 82 */ 83class SetInfo<V> { 84 static readonly replacer: string = '_____set_replacer__'; 85 static readonly replacerCompatible: string = "ace_engine_state_mgmt_set_replacer"; 86 87 constructor( 88 public setReplacer: string, 89 public values: V[] 90 ) { } 91 92 // Check if the given object is of type SetInfo 93 static isObject<V>(obj: unknown): obj is SetInfo<V> { 94 const typedObject = obj as SetInfo<V>; 95 if ('setReplacer' in typedObject && 96 (typedObject.setReplacer === SetInfo.replacer || typedObject.setReplacer === SetInfo.replacerCompatible)) { 97 return true; 98 } 99 return false; 100 } 101 102 // Convert Set to Object 103 static toObject<V>(set: Set<V>): SetInfo<V> { 104 const values: V[] = Array.from(set.values()); 105 return new SetInfo(SetInfo.replacer, values); 106 } 107 108 // Convert Object to Set 109 static toSet<V>(obj: SetInfo<V>): Set<V> { 110 return new Set<V>(obj.values); 111 } 112} 113 114/** 115 * DateInfo 116 * 117 * Helper class to persist Date in Persistent storage 118 * 119 */ 120class DateInfo { 121 static readonly replacer: string = '_____date_replacer__'; 122 static readonly replacerCompatible: string = "ace_engine_state_mgmt_date_replacer"; 123 124 constructor( 125 public dateReplacer: string, 126 public date: string 127 ) { } 128 129 // Check if the given object is of type DateInfo 130 static isObject(obj: unknown): obj is DateInfo { 131 const typedObject = obj as DateInfo; 132 if ('dateReplacer' in typedObject && 133 (typedObject.dateReplacer === DateInfo.replacer || typedObject.dateReplacer === DateInfo.replacerCompatible)) { 134 return true; 135 } 136 return false; 137 } 138 139 // Convert Date to Object 140 static toObject(date: Date): DateInfo { 141 return new DateInfo(DateInfo.replacer, date.toISOString()); 142 } 143 144 // Convert Object to Date 145 static toDate(obj: DateInfo): Date { 146 return new Date(obj.date); 147 } 148} 149 150/** 151 * PersistentStorage 152 * 153 * Keeps current values of select AppStorage property properties persisted to file. 154 * 155 * since 9 156 */ 157 158class PersistentStorage implements IMultiPropertiesChangeSubscriber { 159 private static storage_: IStorage; 160 private static instance_: PersistentStorage = undefined; 161 162 private id_: number; 163 private links_: Map<string, SubscribedAbstractProperty<any>>; 164 165 /** 166 * 167 * @param storage method to be used by the framework to set the backend 168 * this is to be done during startup 169 * 170 * internal function, not part of the SDK 171 * 172 */ 173 public static configureBackend(storage: IStorage): void { 174 PersistentStorage.storage_ = storage; 175 } 176 177 /** 178 * private, use static functions! 179 */ 180 private static getOrCreate(): PersistentStorage { 181 if (PersistentStorage.instance_) { 182 // already initialized 183 return PersistentStorage.instance_; 184 } 185 186 PersistentStorage.instance_ = new PersistentStorage(); 187 return PersistentStorage.instance_; 188 } 189 190 /** 191 * 192 * internal function, not part of the SDK 193 */ 194 public static aboutToBeDeleted(): void { 195 if (!PersistentStorage.instance_) { 196 return; 197 } 198 199 PersistentStorage.getOrCreate().aboutToBeDeleted(); 200 PersistentStorage.instance_ = undefined; 201 } 202 203 204 /** 205 * Add property 'key' to AppStorage properties whose current value will be 206 * persistent. 207 * If AppStorage does not include this property it will be added and initializes 208 * with given value 209 * 210 * @since 10 211 * 212 * @param key property name 213 * @param defaultValue If AppStorage does not include this property it will be initialized with this value 214 * 215 */ 216 public static persistProp<T>(key: string, defaultValue: T): void { 217 PersistentStorage.getOrCreate().persistProp(key, defaultValue); 218 } 219 220 /** 221 * @see persistProp 222 * @deprecated 223 */ 224 public static PersistProp<T>(key: string, defaultValue: T): void { 225 PersistentStorage.getOrCreate().persistProp(key, defaultValue); 226 } 227 228 229 /** 230 * Reverse of @see persistProp 231 * @param key no longer persist the property named key 232 * 233 * @since 10 234 */ 235 public static deleteProp(key: string): void { 236 PersistentStorage.getOrCreate().deleteProp(key); 237 } 238 239 /** 240 * @see deleteProp 241 * @deprecated 242 */ 243 public static DeleteProp(key: string): void { 244 PersistentStorage.getOrCreate().deleteProp(key); 245 } 246 247 /** 248 * Persist given AppStorage properties with given names. 249 * If a property does not exist in AppStorage, add it and initialize it with given value 250 * works as @see persistProp for multiple properties. 251 * 252 * @param properties 253 * 254 * @since 10 255 * 256 */ 257 public static persistProps(properties: { 258 key: string, 259 defaultValue: any 260 }[]): void { 261 PersistentStorage.getOrCreate().persistProps(properties); 262 } 263 264 /** 265 * @see persistProps 266 * @deprecated 267 */ 268 public static PersistProps(properties: { 269 key: string, 270 defaultValue: any 271 }[]): void { 272 PersistentStorage.getOrCreate().persistProps(properties); 273 } 274 275 /** 276 * Inform persisted AppStorage property names 277 * @returns array of AppStorage keys 278 * 279 * @since 10 280 */ 281 public static keys(): Array<string> { 282 let result = []; 283 const it = PersistentStorage.getOrCreate().keys(); 284 let val = it.next(); 285 286 while (!val.done) { 287 result.push(val.value); 288 val = it.next(); 289 } 290 291 return result; 292 } 293 294 /** 295 * @see keys 296 * @deprecated 297 */ 298 public static Keys(): Array<string> { 299 return PersistentStorage.keys(); 300 } 301 302/** 303 * This methid offers a way to force writing the property value with given 304 * key to persistent storage. 305 * In the general case this is unnecessary as the framework observed changes 306 * and triggers writing to disk by itself. For nested objects (e.g. array of 307 * objects) however changes of a property of a property as not observed. This 308 * is the case where the application needs to signal to the framework. 309 * 310 * @param key property that has changed 311 * 312 * @since 10 313 * 314 */ 315 public static notifyHasChanged(propName: string) { 316 stateMgmtConsole.debug(`PersistentStorage: force writing '${propName}'- 317 '${PersistentStorage.getOrCreate().links_.get(propName)}' to storage`); 318 PersistentStorage.getOrCreate().writeToPersistentStorage(propName, 319 PersistentStorage.getOrCreate().links_.get(propName).get()); 320 } 321 322 /** 323 * @see notifyHasChanged 324 * @deprecated 325 */ 326 public static NotifyHasChanged(propName: string) { 327 stateMgmtConsole.debug(`PersistentStorage: force writing '${propName}'- 328 '${PersistentStorage.getOrCreate().links_.get(propName)}' to storage`); 329 PersistentStorage.getOrCreate().writeToPersistentStorage(propName, 330 PersistentStorage.getOrCreate().links_.get(propName).get()); 331 } 332 333 /** 334 * all following methods are framework internal 335 */ 336 337 private constructor() { 338 this.links_ = new Map<string, SubscribedAbstractProperty<any>>(); 339 this.id_ = SubscriberManager.MakeId(); 340 SubscriberManager.Add(this); 341 } 342 343 private keys(): IterableIterator<string> { 344 return this.links_.keys(); 345 } 346 347 private persistProp<T>(propName: string, defaultValue: T): void { 348 if (this.persistProp1(propName, defaultValue)) { 349 // persist new prop 350 stateMgmtConsole.debug(`PersistentStorage: writing '${propName}' - '${this.links_.get(propName)}' to storage`); 351 this.writeToPersistentStorage(propName, this.links_.get(propName).get()); 352 } 353 } 354 355 356 // helper function to persist a property 357 // does everything except writing prop to disk 358 private persistProp1<T>(propName: string, defaultValue: T): boolean { 359 stateMgmtConsole.debug(`PersistentStorage: persistProp1 ${propName} ${defaultValue}`); 360 if (defaultValue == null && !Utils.isApiVersionEQAbove(12)) { 361 stateMgmtConsole.error(`PersistentStorage: persistProp for ${propName} called with 'null' or 'undefined' default value!`); 362 return false; 363 } 364 365 if (this.links_.get(propName)) { 366 stateMgmtConsole.debug(`PersistentStorage: persistProp: ${propName} is already persisted`); 367 return false; 368 } 369 370 let link = AppStorage.link(propName, this); 371 if (link) { 372 stateMgmtConsole.debug(`PersistentStorage: persistProp ${propName} in AppStorage, using that`); 373 this.links_.set(propName, link); 374 } else { 375 let returnValue: T; 376 if (!PersistentStorage.storage_.has(propName)) { 377 stateMgmtConsole.debug(`PersistentStorage: no entry for ${propName}, will initialize with default value`); 378 returnValue = defaultValue; 379 } 380 else { 381 returnValue = this.readFromPersistentStorage(propName); 382 } 383 link = AppStorage.setAndLink(propName, returnValue, this); 384 if (link === undefined) { 385 stateMgmtConsole.debug(`PersistentStorage: failed to set and link app storage property ${propName}`); 386 return false; 387 } 388 this.links_.set(propName, link); 389 stateMgmtConsole.debug(`PersistentStorage: created new persistent prop for ${propName}`); 390 } 391 return true; 392 } 393 394 private persistProps(properties: { 395 key: string, 396 defaultValue: any 397 }[]): void { 398 properties.forEach(property => this.persistProp1(property.key, property.defaultValue)); 399 this.write(); 400 } 401 402 private deleteProp(propName: string): void { 403 let link = this.links_.get(propName); 404 if (link) { 405 link.aboutToBeDeleted(); 406 this.links_.delete(propName); 407 PersistentStorage.storage_.delete(propName); 408 stateMgmtConsole.debug(`PersistentStorage: deleteProp: no longer persisting '${propName}'.`); 409 } else { 410 stateMgmtConsole.warn(`PersistentStorage: '${propName}' is not a persisted property warning.`); 411 } 412 } 413 414 private write(): void { 415 this.links_.forEach((link, propName, map) => { 416 stateMgmtConsole.debug(`PersistentStorage: writing ${propName} to storage`); 417 this.writeToPersistentStorage(propName, link.get()); 418 }); 419 } 420 421 // helper function to write to the persistent storage 422 // any additional check and formatting can to be done here 423 private writeToPersistentStorage<T>(propName: string, value: T): void { 424 if (value instanceof Map) { 425 value = MapInfo.toObject(value) as unknown as T; 426 } else if (value instanceof Set) { 427 value = SetInfo.toObject(value) as unknown as T; 428 } else if (value instanceof Date) { 429 value = DateInfo.toObject(value) as unknown as T; 430 } 431 432 PersistentStorage.storage_.set(propName, value); 433 } 434 435 // helper function to read from the persistent storage 436 // any additional check and formatting can to be done here 437 private readFromPersistentStorage<T>(propName: string): T { 438 let newValue: T = PersistentStorage.storage_.get(propName); 439 if (newValue instanceof Object) { 440 if (MapInfo.isObject(newValue) === ObjectVersion.NewVersion) { 441 newValue = MapInfo.toMap(newValue as unknown as MapInfo<any, any>) as unknown as T; 442 } else if (MapInfo.isObject(newValue) === ObjectVersion.CompatibleVersion) { 443 newValue = MapInfo.toMapCompatible(newValue as unknown as MapInfo<any, any>) as unknown as T; 444 } else if (SetInfo.isObject(newValue)) { 445 newValue = SetInfo.toSet(newValue) as unknown as T; 446 } else if (DateInfo.isObject(newValue)) { 447 newValue = DateInfo.toDate(newValue) as unknown as T; 448 } 449 } 450 451 return newValue; 452 } 453 454 // FU code path method 455 public propertyHasChanged(info?: PropertyInfo): void { 456 stateMgmtConsole.debug('PersistentStorage: property changed'); 457 this.write(); 458 } 459 460 // PU code path method 461 public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU<any>) { 462 stateMgmtConsole.debug(`PersistentStorage: sync peer ${eventSource.info()} has changed`); 463 this.write(); 464 } 465 466 // public required by the interface, use the static method instead! 467 public aboutToBeDeleted(): void { 468 stateMgmtConsole.debug('PersistentStorage: about to be deleted'); 469 this.links_.forEach((val, key, map) => { 470 stateMgmtConsole.debug(`PersistentStorage: removing ${key}`); 471 val.aboutToBeDeleted(); 472 }); 473 474 this.links_.clear(); 475 SubscriberManager.Delete(this.id__()); 476 PersistentStorage.storage_.clear(); 477 } 478 479 public id__(): number { 480 return this.id_; 481 } 482}; 483 484