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