1/* 2 * Copyright (c) 2021 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 * 18 * LocalStorage 19 * 20 * Class implements a Map of ObservableObjectBase UI state variables. 21 * Instances can be created to manage UI state within a limited "local" 22 * access, and life cycle as defined by the app. 23 * AppStorage singleton is sub-class of LocalStorage for 24 * UI state of app-wide access and same life cycle as the app. 25 * 26 * @since 9 27 */ 28 29class LocalStorage extends NativeLocalStorage { 30 31 protected storage_: Map<string, ObservedPropertyAbstract<any>>; 32 /* 33 get access to provded LocalStorage instance thru Stake model 34 @StageModelOnly 35 @form 36 @since 10 37 */ 38 public static getShared(): LocalStorage { 39 return LocalStorage.GetShared(); 40 } 41 42 /** 43 * Construct new instance of LocalStorage 44 * initialzie with all properties and their values that Object.keys(params) returns 45 * Property values must not be undefined. 46 * @param initializingProperties Object containing keys and values. @see set() for valid values 47 * 48 * @since 9 49 */ 50 constructor(initializingProperties: Object = {}) { 51 // This is edited for the statibility issue that "construtor is false", which meaning that the super() is not callable 52 // It is just the debug log using ArkTools print. 53 try { 54 super(); 55 } catch (error) { 56 stateMgmtConsole.error(`An error occurred in the constructor of LocalStorage ${error.message}`); 57 ArkTools.print("NativeLocalStorage", NativeLocalStorage); 58 throw error; 59 } 60 stateMgmtConsole.debug(`${this.constructor.name} constructor.`) 61 this.storage_ = new Map<string, ObservedPropertyAbstract<any>>(); 62 if (Object.keys(initializingProperties).length) { 63 this.initializeProps(initializingProperties); 64 } 65 } 66 67 /** 68 * clear storage and init with given properties 69 * @param initializingProperties 70 * 71 * not a public / sdk function 72 */ 73 public initializeProps(initializingProperties: Object = {}) { 74 stateMgmtConsole.debug(`${this.constructor.name} initializing with Object keys: [${Object.keys(initializingProperties)}].`) 75 this.storage_.clear(); 76 Object.keys(initializingProperties).filter((propName) => initializingProperties[propName] != undefined).forEach((propName) => 77 this.addNewPropertyInternal(propName, initializingProperties[propName]) 78 ); 79 } 80 81 /** 82 * Use before deleting owning Ability, window, or service UI 83 * (letting it go out of scope). 84 * 85 * This method orderly closes down a LocalStorage instance by calling @see clear(). 86 * This requires that no property is left with one or more subscribers. 87 * @see clear() and @see delete() 88 * @returns true if all properties could be removed from storage 89 */ 90 public aboutToBeDeleted(): boolean { 91 return this.clear(); 92 } 93 94 /** 95 * Check if LocalStorage has a property with given name 96 * return true if prooperty with given name exists 97 * same as ES6 Map.prototype.has() 98 * @param propName searched property 99 * @returns true if property with such name exists in LocalStorage 100 * 101 * @since 9 102 */ 103 public has(propName: string): boolean { 104 return this.storage_.has(propName); 105 } 106 107 108 /** 109 * Provide names of all properties in LocalStorage 110 * same as ES6 Map.prototype.keys() 111 * @returns return a Map Iterator 112 * 113 * @since 9 114 */ 115 public keys(): IterableIterator<string> { 116 return this.storage_.keys(); 117 } 118 119 120 /** 121 * Returns number of properties in LocalStorage 122 * same as Map.prototype.size() 123 * @param propName 124 * @returns return number of properties 125 * 126 * @since 9 127 */ 128 public size(): number { 129 return this.storage_.size; 130 } 131 132 133 /** 134 * Returns value of given property 135 * return undefined if no property with this name 136 * @param propName 137 * @returns property value if found or undefined 138 * 139 * @since 9 140 */ 141 public get<T>(propName: string): T | undefined { 142 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 143 return (p) ? p.get() : undefined; 144 } 145 146 147 /** 148 * Set value of given property in LocalStorage 149 * Methosd sets nothing and returns false if property with this name does not exist 150 * or if newValue is `undefined` or `null` (`undefined`, `null` value are not allowed for state variables). 151 * @param propName 152 * @param newValue must be of type T and must not be undefined or null 153 * @returns true on success, i.e. when above conditions are satisfied, otherwise false 154 * 155 * @since 9 156 */ 157 public set<T>(propName: string, newValue: T): boolean { 158 stateMgmtProfiler.begin("LocalStorage.set"); 159 if (newValue == undefined) { 160 stateMgmtConsole.warn(`${this.constructor.name}: set('${propName}') with newValue == undefined not allowed.`); 161 stateMgmtProfiler.end(); 162 return false; 163 } 164 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 165 if (p == undefined) { 166 stateMgmtConsole.warn(`${this.constructor.name}: set: no property ${propName} error.`); 167 stateMgmtProfiler.end(); 168 return false; 169 } 170 p.set(newValue); 171 stateMgmtProfiler.end(); 172 return true; 173 } 174 175 176 /** 177 * Set value of given property, if it exists, @see set() . 178 * Add property if no property with given name and initialize with given value. 179 * Do nothing and return false if newValuue is undefined or null 180 * (undefined, null value is not allowed for state variables) 181 * @param propName 182 * @param newValue must be of type T and must not be undefined or null 183 * @returns true on success, i.e. when above conditions are satisfied, otherwise false 184 * 185 * @since 9 186 */ 187 public setOrCreate<T>(propName: string, newValue: T): boolean { 188 stateMgmtProfiler.begin("LocalStorage.setOrCreate"); 189 if (newValue == undefined) { 190 stateMgmtConsole.warn(`${this.constructor.name}: setOrCreate('${propName}') with newValue == undefined not allowed.`); 191 stateMgmtProfiler.end(); 192 return false; 193 } 194 195 var p: ObservedPropertyAbstract<T> = this.storage_.get(propName); 196 if (p) { 197 stateMgmtConsole.debug(`${this.constructor.name}.setOrCreate(${propName}, ${newValue}) update existing property`); 198 p.set(newValue); 199 } else { 200 stateMgmtConsole.debug(`${this.constructor.name}.setOrCreate(${propName}, ${newValue}) create new entry and set value`); 201 this.addNewPropertyInternal<T>(propName, newValue); 202 } 203 stateMgmtProfiler.end(); 204 return true; 205 } 206 207 208 /** 209 * Internal use helper function to create and initialize a new property. 210 * caller needs to be all the checking beforehand 211 * @param propName 212 * @param value 213 * 214 * Not a public / sdk method. 215 */ 216 private addNewPropertyInternal<T>(propName: string, value: T): ObservedPropertyAbstract<T> { 217 let newProp; 218 if (ViewStackProcessor.UsesNewPipeline()) { 219 newProp = new ObservedPropertyPU<T>(value, undefined, propName); 220 } else { 221 newProp = (typeof value === "object") ? 222 new ObservedPropertyObject<T>(value, undefined, propName) 223 : new ObservedPropertySimple<T>(value, undefined, propName); 224 } 225 this.storage_.set(propName, newProp); 226 return newProp; 227 } 228 229 /** 230 * create and return a two-way sync "(link") to named property 231 * @param propName name of source property in LocalStorage 232 * @param linkUser IPropertySubscriber to be notified when source changes, 233 * @param subscribersName optional, the linkUser (subscriber) uses this name for the property 234 * this name will be used in propertyChange(propName) callback of IMultiPropertiesChangeSubscriber 235 * @returns SynchedPropertyTwoWay{Simple|Object| object with given LocalStoage prop as its source. 236 * Apps can use SDK functions of base class SubscribedAbstractProperty<S> 237 * return undefiend if named property does not already exist in LocalStorage 238 * Apps can use SDK functions of base class SubscribedPropertyAbstract<S> 239 * return undefiend if named property does not already exist in LocalStorage 240 * 241 * @since 9 242 */ 243 public link<T>(propName: string, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> | undefined { 244 stateMgmtProfiler.begin("LocalStorage.link"); 245 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 246 if (p == undefined) { 247 stateMgmtConsole.warn(`${this.constructor.name}: link: no property ${propName} error.`); 248 stateMgmtProfiler.end(); 249 return undefined; 250 } 251 let linkResult; 252 if (ViewStackProcessor.UsesNewPipeline()) { 253 linkResult = new SynchedPropertyTwoWayPU(p, linkUser, propName); 254 } else { 255 linkResult = p.createLink(linkUser, propName); 256 } 257 linkResult.setInfo(subscribersName); 258 stateMgmtProfiler.end(); 259 return linkResult; 260 } 261 262 263 /** 264 * Like @see link(), but will create and initialize a new source property in LocalStorge if missing 265 * @param propName name of source property in LocalStorage 266 * @param defaultValue value to be used for initializing if new creating new property in LocalStorage 267 * default value must be of type S, must not be undefined or null. 268 * @param linkUser IPropertySubscriber to be notified when return 'link' changes, 269 * @param subscribersName the linkUser (subscriber) uses this name for the property 270 * this name will be used in propertyChange(propName) callback of IMultiPropertiesChangeSubscriber 271 * @returns SynchedPropertyTwoWay{Simple|Object| object with given LocalStoage prop as its source. 272 * Apps can use SDK functions of base class SubscribedAbstractProperty<S> 273 * 274 * @since 9 275 */ 276 public setAndLink<T>(propName: string, defaultValue: T, linkUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> { 277 stateMgmtProfiler.begin("LocalStorage.setAndLink"); 278 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 279 if (!p) { 280 this.setOrCreate(propName, defaultValue); 281 } 282 const link: SubscribedAbstractProperty<T> = this.link(propName, linkUser, subscribersName); 283 stateMgmtProfiler.end(); 284 return link; 285 } 286 287 288 /** 289 * create and return a one-way sync ('prop') to named property 290 * @param propName name of source property in LocalStorage 291 * @param propUser IPropertySubscriber to be notified when source changes, 292 * @param subscribersName the linkUser (subscriber) uses this name for the property 293 * this name will be used in propertyChange(propName) callback of IMultiPropertiesChangeSubscriber 294 * @returns SynchedPropertyOneWay{Simple|Object| object with given LocalStoage prop as its source. 295 * Apps can use SDK functions of base class SubscribedAbstractProperty<S> 296 * return undefiend if named property does not already exist in LocalStorage. 297 * Apps can use SDK functions of base class SubscribedPropertyAbstract<S> 298 * return undefiend if named property does not already exist in LocalStorage. 299 * @since 9 300 */ 301 public prop<T>(propName: string, propUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> | undefined { 302 stateMgmtProfiler.begin("LocalStorage.prop"); 303 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 304 if (p == undefined) { 305 stateMgmtConsole.warn(`${this.constructor.name}: prop: no property ${propName} error.`); 306 stateMgmtProfiler.end(); 307 return undefined; 308 } 309 310 let propResult; 311 if (ViewStackProcessor.UsesNewPipeline()) { 312 propResult = new SynchedPropertyOneWayPU<T>(p, propUser, propName); 313 } else { 314 propResult = p.createProp(propUser, propName); 315 } 316 propResult.setInfo(subscribersName); 317 stateMgmtProfiler.end(); 318 return propResult; 319 } 320 321 /** 322 * Like @see prop(), will create and initialize a new source property in LocalStorage if missing 323 * @param propName name of source property in LocalStorage 324 * @param defaultValue value to be used for initializing if new creating new property in LocalStorage. 325 * default value must be of type S, must not be undefined or null. 326 * @param propUser IPropertySubscriber to be notified when returned 'prop' changes, 327 * @param subscribersName the propUser (subscriber) uses this name for the property 328 * this name will be used in propertyChange(propName) callback of IMultiPropertiesChangeSubscriber 329 * @returns SynchedPropertyOneWay{Simple|Object| object with given LocalStoage prop as its source. 330 * Apps can use SDK functions of base class SubscribedAbstractProperty<S> 331 * @since 9 332 */ 333 public setAndProp<T>(propName: string, defaultValue: T, propUser?: IPropertySubscriber, subscribersName?: string): SubscribedAbstractProperty<T> { 334 stateMgmtProfiler.begin("LocalStorage.setAndProp"); 335 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 336 if (!p) { 337 this.setOrCreate(propName, defaultValue); 338 } 339 const prop: SubscribedAbstractProperty<T> = this.prop(propName, propUser, subscribersName); 340 stateMgmtProfiler.end(); 341 return prop; 342 } 343 344 /** 345 * Delete property from StorageBase 346 * Use with caution: 347 * Before deleting a prop from LocalStorage all its subscribers need to 348 * unsubscribe from the property. 349 * This method fails and returns false if given property still has subscribers 350 * Another reason for failing is unkmown property. 351 * 352 * Developer advise: 353 * Subscribers are created with @see link(), @see prop() 354 * and also via @LocalStorageLink and @LocalStorageProp state variable decorators. 355 * That means as long as their is a @Component instance that uses such decorated variable 356 * or a sync relationship with a SubscribedAbstractProperty variable the property can nit 357 * (and also should not!) be deleted from LocalStorage. 358 * 359 * @param propName 360 * @returns false if method failed 361 * 362 * @since 9 363 */ 364 public delete(propName: string): boolean { 365 stateMgmtProfiler.begin("LocalStorage.delete"); 366 var p: ObservedPropertyAbstract<any> | undefined = this.storage_.get(propName); 367 if (p) { 368 if (p.numberOfSubscrbers()) { 369 stateMgmtConsole.error(`${this.constructor.name}: Attempt to delete property ${propName} that has \ 370 ${p.numberOfSubscrbers()} subscribers. Subscribers need to unsubscribe before prop deletion.`); 371 stateMgmtProfiler.end(); 372 return false; 373 } 374 p.aboutToBeDeleted(); 375 this.storage_.delete(propName); 376 stateMgmtProfiler.end(); 377 return true; 378 } else { 379 stateMgmtConsole.warn(`${this.constructor.name}: Attempt to delete unknown property ${propName}.`); 380 stateMgmtProfiler.end(); 381 return false; 382 } 383 } 384 385 /** 386 * delete all properties from the LocalStorage instance 387 * @see delete(). 388 * precondition is that there are no subscribers. 389 * method returns false and deletes no poperties if there is any property 390 * that still has subscribers 391 * 392 * @since 9 393 */ 394 protected clear(): boolean { 395 stateMgmtProfiler.begin("LocalStorage.clean"); 396 for (let propName of this.keys()) { 397 var p: ObservedPropertyAbstract<any> = this.storage_.get(propName); 398 if (p.numberOfSubscrbers()) { 399 stateMgmtConsole.error(`${this.constructor.name}.deleteAll: Attempt to delete property ${propName} that \ 400 has ${p.numberOfSubscrbers()} subscribers. Subscribers need to unsubscribe before prop deletion. 401 Any @Component instance with a @StorageLink/Prop or @LocalStorageLink/Prop is a subscriber.`); 402 stateMgmtProfiler.end(); 403 return false; 404 } 405 } 406 for (let propName of this.keys()) { 407 var p: ObservedPropertyAbstract<any> = this.storage_.get(propName); 408 p.aboutToBeDeleted(); 409 } 410 this.storage_.clear(); 411 stateMgmtConsole.debug(`${this.constructor.name}.deleteAll: success`); 412 stateMgmtProfiler.end(); 413 return true; 414 } 415 416 /** 417 * Subscribe to value change notifications of named property 418 * Any object implementing ISinglePropertyChangeSubscriber interface 419 * and registerign itself to SubscriberManager can register 420 * Caution: do remember to unregister, otherwise the property will block 421 * cleanup, @see delete() and @see clear() 422 * 423 * @param propName property in LocalStorage to subscribe to 424 * @param subscriber object that implements ISinglePropertyChangeSubscriber interface 425 * @returns false if named property does not exist 426 * 427 * @since 9 428 */ 429 public subscribeToChangesOf<T>(propName: string, subscriber: ISinglePropertyChangeSubscriber<T>): boolean { 430 var p: ObservedPropertyAbstract<T> | undefined = this.storage_.get(propName); 431 if (p) { 432 p.addSubscriber(subscriber); 433 return true; 434 } 435 return false; 436 } 437 438 /** 439 * inverse of @see subscribeToChangesOf 440 * @param propName property in LocalStorage to subscribe to 441 * @param subscriberId id of the subscrber passed to @see subscribeToChangesOf 442 * @returns false if named property does not exist 443 * 444 * @since 9 445 */ 446 public unsubscribeFromChangesOf(propName: string, subscriberId: number): boolean { 447 var p: ObservedPropertyAbstract<any> | undefined = this.storage_.get(propName); 448 if (p) { 449 p.removeSubscriber(null, subscriberId); 450 return true; 451 } 452 return false; 453 } 454 455 public __createSync<T>(storagePropName: string, defaultValue: T, 456 factoryFunc: SynchedPropertyFactoryFunc): ObservedPropertyAbstract<T> { 457 458 let p: ObservedPropertyAbstract<T> = this.storage_.get(storagePropName); 459 if (p == undefined) { 460 // property named 'storagePropName' not yet in storage 461 // add new property to storage 462 if (defaultValue === undefined) { 463 stateMgmtConsole.error(`${this.constructor.name}.__createSync(${storagePropName}, non-existing property and undefined default value. ERROR.`); 464 return undefined; 465 } 466 467 p = this.addNewPropertyInternal<T>(storagePropName, defaultValue); 468 } 469 return factoryFunc(p); 470 } 471}