• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}