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