• 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  /**
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}