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