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