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