• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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
16const enum PersistError {
17  Quota = 'quota',
18  Serialization = 'serialization',
19  Unknown = 'unknown'
20};
21
22type PersistErrorType = PersistError.Quota | PersistError.Serialization | PersistError.Unknown;
23type PersistErrorCallback = ((key: string, reason: PersistErrorType, message: string) => void) | undefined;
24type StorageDefaultCreator<T> = () => T;
25
26interface TypeConstructorWithArgs<T> {
27  new(...args: any): T;
28}
29
30class StorageHelper {
31  protected static readonly INVALID_DEFAULT_VALUE: string = 'The default creator should be function when first connect';
32  protected static readonly DELETE_NOT_EXIST_KEY: string = 'The key to be deleted does not exist';
33  protected static readonly INVALID_TYPE: string = 'The type should have function constructor signature when use storage';
34  protected static readonly EMPTY_STRING_KEY: string = 'Cannot use empty string as the key';
35  protected static readonly INVALID_LENGTH_KEY: string = 'Cannot use the key! The length of key should be 2 to 255';
36  protected static readonly INVALID_CHARACTER_KEY: string = 'Cannot use the key! The value of key can only consist of letters, digits and underscores';
37  protected static readonly NULL_OR_UNDEFINED_KEY: string = `The parameter cannot be null or undefined`;
38
39  // sotre old type name to check the type matches or not
40  protected oldTypeValues_: Map<string, string>;
41
42  constructor() {
43    this.oldTypeValues_ = new Map<string, string>();
44  }
45
46  protected getConnectedKey<T>(type: TypeConstructorWithArgs<T>,
47    keyOrDefaultCreator?: string | StorageDefaultCreator<T>): string | undefined {
48    if (keyOrDefaultCreator === null || keyOrDefaultCreator === undefined) {
49      stateMgmtConsole.applicationWarn(StorageHelper.NULL_OR_UNDEFINED_KEY +  ', try to use the type name as key');
50    }
51
52    if (typeof keyOrDefaultCreator === 'string') {
53      return keyOrDefaultCreator;
54    }
55
56    return this.getTypeName(type);
57  }
58
59  protected getKeyOrTypeName<T>(keyOrType: string | TypeConstructorWithArgs<T>): string | undefined {
60    if (typeof keyOrType === 'function') {
61      keyOrType = this.getTypeName(keyOrType);
62    }
63    return keyOrType;
64  }
65
66  protected checkTypeByName<T>(key: string, type: TypeConstructorWithArgs<T>, oldType: string): void {
67    if (this.getTypeName(type) !== oldType) {
68      throw new Error(`The type mismatches when use the key '${key}' in storage`);
69    }
70  }
71
72  protected checkTypeByInstanceOf<T>(key: string, type: TypeConstructorWithArgs<T>, ins: T): void {
73    this.checkTypeIsFunc(type);
74
75    if (!(ins instanceof type)) {
76      throw new Error(`The type mismatches when use the key '${key}' in storage`);
77    }
78  }
79
80  protected getTypeName<T>(type: TypeConstructorWithArgs<T>): string | undefined {
81    this.checkTypeIsFunc(type);
82
83    let name: string | undefined = type.name;
84    while (name === undefined) {
85      type = Object.getPrototypeOf(type);
86      if (!type) {
87        break;
88      }
89      name = type.name;
90    }
91    return name;
92  }
93
94  protected isKeyValid(key: string | null | undefined): boolean {
95    if (typeof key !== 'string') {
96      throw new Error(StorageHelper.INVALID_TYPE);
97    }
98
99    // The key string is empty
100    if (key === '') {
101      stateMgmtConsole.applicationError(StorageHelper.EMPTY_STRING_KEY);
102      return false;
103    }
104
105    const len: number = key.length;
106    // The key string length should shorter than 1024
107    if (len >= 1024) {
108      stateMgmtConsole.applicationError(StorageHelper.INVALID_LENGTH_KEY);
109      return false;
110    }
111
112    if (len < 2 || len > 255) {
113      stateMgmtConsole.applicationWarn(StorageHelper.INVALID_LENGTH_KEY);
114    }
115
116    if (!/^[0-9a-zA-Z_]+$/.test(key)) {
117      stateMgmtConsole.applicationWarn(StorageHelper.INVALID_CHARACTER_KEY);
118    }
119
120    return true;
121  }
122
123  private checkTypeIsFunc<T>(type: TypeConstructorWithArgs<T>): void {
124    if (typeof type !== 'function') {
125      throw new Error(StorageHelper.INVALID_TYPE);
126    }
127  }
128}
129
130class AppStorageV2Impl extends StorageHelper {
131  private static instance_: AppStorageV2Impl = undefined;
132
133  private memorizedValues_: Map<string, any>;
134
135  constructor() {
136    super();
137    this.memorizedValues_ = new Map<string, any>();
138  }
139
140  public static instance(): AppStorageV2Impl {
141    if (AppStorageV2Impl.instance_) {
142      return AppStorageV2Impl.instance_;
143    }
144    AppStorageV2Impl.instance_ = new AppStorageV2Impl();
145    return AppStorageV2Impl.instance_;
146  }
147
148  public connect<T extends object>(type: TypeConstructorWithArgs<T>, keyOrDefaultCreator?: string | StorageDefaultCreator<T>,
149    defaultCreator?: StorageDefaultCreator<T>): T | undefined {
150    const key: string = this.getConnectedKey(type, keyOrDefaultCreator);
151
152    if (!this.isKeyValid(key)) {
153      return undefined;
154    }
155
156    if (typeof keyOrDefaultCreator === 'function') {
157      defaultCreator = keyOrDefaultCreator;
158    }
159
160    if (!this.memorizedValues_.has(key)) {
161      if (typeof defaultCreator !== 'function') {
162        throw new Error(AppStorageV2Impl.INVALID_DEFAULT_VALUE);
163      }
164
165      const defaultValue: T = defaultCreator();
166
167      this.checkTypeByInstanceOf(key, type, defaultValue);
168
169      this.memorizedValues_.set(key, defaultValue);
170      this.oldTypeValues_.set(key, this.getTypeName(type));
171      return defaultValue;
172    }
173
174    this.checkTypeByName(key, type, this.oldTypeValues_.get(key));
175
176    const existedValue: T = this.memorizedValues_.get(key);
177    return existedValue;
178  }
179
180  public remove<T>(keyOrType: string | TypeConstructorWithArgs<T>): void {
181    if (keyOrType === null || keyOrType === undefined) {
182      stateMgmtConsole.applicationWarn(AppStorageV2Impl.NULL_OR_UNDEFINED_KEY);
183      return;
184    }
185
186    const key: string = this.getKeyOrTypeName(keyOrType);
187
188    if (!this.isKeyValid(key)) {
189      return;
190    }
191
192    this.removeFromMemory(key);
193  }
194
195  public getMemoryKeys(): Array<string> {
196    return Array.from(this.memorizedValues_.keys());
197  }
198
199  private removeFromMemory(key: string): void {
200    const isDeleted: boolean = this.memorizedValues_.delete(key);
201
202    if (!isDeleted) {
203      stateMgmtConsole.applicationWarn(AppStorageV2Impl.DELETE_NOT_EXIST_KEY);
204    } else {
205      this.oldTypeValues_.delete(key);
206    }
207  }
208}
209
210class PersistenceV2Impl extends StorageHelper {
211  public static readonly MIN_PERSISTENCE_ID = 0x1010000000000;
212  public static nextPersistId_ = PersistenceV2Impl.MIN_PERSISTENCE_ID;
213
214  private static readonly NOT_SUPPORT_TYPE_MESSAGE_: string = 'Not support! Can only use the class object in Persistence';
215  private static readonly KEYS_ARR_: string = '___keys_arr';
216  private static readonly MAX_DATA_LENGTH_: number = 8000;
217  private static readonly NOT_SUPPORT_TYPES_: Array<any> =
218    [Array, Set, Map, WeakSet, WeakMap, Date, Boolean, Number, String, Symbol, BigInt, RegExp, Function, Promise, ArrayBuffer];
219  private static storage_: IStorage;
220  private static instance_: PersistenceV2Impl = undefined;
221
222  // the map is used to store the persisted value in memory, can reuse when re-connect if the key exists
223  private map_: Map<string, any>;
224  private keysArr_: Set<string>;
225  private cb_: PersistErrorCallback = undefined;
226  private idToKey_: Map<number, string>;
227
228  constructor() {
229    super();
230    this.map_ = new Proxy(new Map<string, any>(), new SetMapProxyHandler());
231    this.keysArr_ = new Set<string>();
232    this.idToKey_ = new Map<number, string>();
233  }
234
235  public static instance(): PersistenceV2Impl {
236    if (PersistenceV2Impl.instance_) {
237      return PersistenceV2Impl.instance_;
238    }
239    PersistenceV2Impl.instance_ = new PersistenceV2Impl();
240    return PersistenceV2Impl.instance_;
241  }
242
243  public static configureBackend(storage: IStorage): void {
244    PersistenceV2Impl.storage_ = storage;
245  }
246
247  public connect<T extends object>(type: TypeConstructorWithArgs<T>, keyOrDefaultCreator?: string | StorageDefaultCreator<T>,
248    defaultCreator?: StorageDefaultCreator<T>): T | undefined {
249    this.checkTypeIsClassObject(type);
250
251    const key: string | undefined = this.getRightKey(type, keyOrDefaultCreator);
252    if (!this.isKeyValid(key)) {
253      return undefined;
254    }
255
256    if (typeof keyOrDefaultCreator === 'function') {
257      defaultCreator = keyOrDefaultCreator;
258    }
259
260    // In memory
261    if (this.map_.has(key)) {
262      const existedValue: T = this.map_.get(key);
263      this.checkTypeByName(key, type, this.oldTypeValues_.get(key));
264      return existedValue;
265    }
266
267    const observedValue: T | undefined = this.getConnectDefaultValue(key, type, defaultCreator);
268    if (!observedValue) {
269      return undefined;
270    }
271
272    const id: number = ++PersistenceV2Impl.nextPersistId_;
273    this.idToKey_.set(id, key);
274
275    // Not in memory, but in disk
276    if (PersistenceV2Impl.storage_.has(key)) {
277      return this.getValueFromDisk(key, id, observedValue, type);
278    }
279
280    // Neither in memory or in disk
281    return this.setValueToDisk(key, id, observedValue, type);
282  }
283
284  public keys(): Array<string> {
285    try {
286      if (!this.keysArr_.size) {
287        this.keysArr_ = this.getKeysArrFromStorage();
288      }
289    } catch (err) {
290      if (this.cb_ && typeof this.cb_ === 'function') {
291        this.cb_('', PersistError.Unknown, `fail to get all persisted keys`);
292        return;
293      }
294      throw new Error(err);
295    }
296
297    return Array.from(this.keysArr_);
298  }
299
300  public remove<T>(keyOrType: string | TypeConstructorWithArgs<T>): void {
301    if (keyOrType === null || keyOrType === undefined) {
302      stateMgmtConsole.applicationWarn(PersistenceV2Impl.NULL_OR_UNDEFINED_KEY);
303      return;
304    }
305
306    this.checkTypeIsClassObject(keyOrType);
307
308    const key: string = this.getKeyOrTypeName(keyOrType);
309
310    if (!this.isKeyValid(key)) {
311      return;
312    }
313
314    this.disconnectValue(key);
315  }
316
317  public save<T>(keyOrType: string | TypeConstructorWithArgs<T>): void {
318    if (keyOrType === null || keyOrType === undefined) {
319      stateMgmtConsole.applicationWarn(PersistenceV2Impl.NULL_OR_UNDEFINED_KEY);
320      return;
321    }
322
323    this.checkTypeIsClassObject(keyOrType);
324
325    const key: string = this.getKeyOrTypeName(keyOrType);
326
327    if (!this.isKeyValid(key)) {
328      return;
329    }
330
331    if (!this.map_.has(key)) {
332      stateMgmtConsole.applicationWarn(`Cannot save the key '${key}'! The key is disconnected`);
333      return;
334    }
335
336    try {
337      PersistenceV2Impl.storage_.set(key, JSONCoder.stringify(this.map_.get(key)));
338    } catch (err) {
339      this.errorHelper(key, PersistError.Serialization, err);
340    }
341  }
342
343  public notifyOnError(cb: PersistErrorCallback): void {
344    this.cb_ = cb;
345  }
346
347  public onChangeObserved(persistKeys: Array<number>): void {
348    this.writeAllChangedToFile(persistKeys);
349  }
350
351  private connectNewValue(key: string, newValue: any, typeName: string): void {
352    this.map_.set(key, newValue);
353    this.oldTypeValues_.set(key, typeName);
354
355    this.storeKeyToPersistenceV2(key);
356  }
357
358  private disconnectValue(key: string): void {
359    this.map_.delete(key);
360    this.oldTypeValues_.delete(key);
361
362    this.removeFromPersistenceV2(key);
363  }
364
365  private checkTypeIsClassObject<T>(keyOrType: string | TypeConstructorWithArgs<T>) {
366    if ((typeof keyOrType !== 'string' && typeof keyOrType !== 'function') ||
367      PersistenceV2Impl.NOT_SUPPORT_TYPES_.includes(keyOrType as any)) {
368      throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_);
369    }
370  }
371
372  private getRightKey<T extends object>(type: TypeConstructorWithArgs<T>,
373    keyOrDefaultCreator?: string | StorageDefaultCreator<T>) {
374    const key: string = this.getConnectedKey(type, keyOrDefaultCreator);
375
376    if (key === undefined) {
377      throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_);
378    }
379
380    if (key === PersistenceV2Impl.KEYS_ARR_) {
381      this.errorHelper(key, PersistError.Quota, `The key '${key}' cannot be used`);
382      return undefined;
383    }
384
385    return key;
386  }
387
388  private getConnectDefaultValue<T extends object>(key: string, type: TypeConstructorWithArgs<T>,
389    defaultCreator?: StorageDefaultCreator<T>): T | undefined {
390    if (!PersistenceV2Impl.storage_) {
391      this.errorHelper(key, PersistError.Unknown, `The storage is null`);
392      return undefined;
393    }
394
395    if (typeof defaultCreator !== 'function') {
396      throw new Error(PersistenceV2Impl.INVALID_DEFAULT_VALUE);
397    }
398
399    const observedValue: T = defaultCreator();
400
401    this.checkTypeByInstanceOf(key, type, observedValue);
402
403    if (this.isNotClassObject(observedValue)) {
404      throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_);
405    }
406
407    return observedValue;
408  }
409
410  private getValueFromDisk<T extends object>(key: string, id: number, observedValue: T,
411    type: TypeConstructorWithArgs<T>): T | undefined {
412    let newObservedValue: T;
413
414    try {
415      const json: string = PersistenceV2Impl.storage_.get(key);
416
417      // Adding ref for persistence
418      ObserveV2.getObserve().startRecordDependencies(this, id);
419      newObservedValue = JSONCoder.parseTo(observedValue, json) as T;
420      ObserveV2.getObserve().stopRecordDependencies();
421    } catch (err) {
422      this.errorHelper(key, PersistError.Serialization, err);
423    }
424
425    this.checkTypeByInstanceOf(key, type, newObservedValue);
426
427    this.connectNewValue(key, newObservedValue, this.getTypeName(type));
428    return newObservedValue;
429  }
430
431  private setValueToDisk<T extends object>(key: string, id: number, observedValue: T,
432    type: TypeConstructorWithArgs<T>): T | undefined {
433    ObserveV2.getObserve().startRecordDependencies(this, id);
434    // Map is a proxy object. When it is connected for the first time, map.has is used to add dependencies,
435    // and map.set is used to trigger writing to disk.
436    const hasKey: boolean = this.map_.has(key);
437    ObserveV2.getObserve().stopRecordDependencies();
438
439    // Writing to persistence by ref
440    if (!hasKey) {
441      this.connectNewValue(key, observedValue, this.getTypeName(type));
442    }
443    return observedValue;
444  }
445
446  private writeAllChangedToFile(persistKeys: Array<number>): void {
447    for (let i = 0; i < persistKeys.length; ++i) {
448      const id: number = persistKeys[i];
449      const key: string = this.idToKey_.get(id);
450
451      try {
452        const hasKey: boolean = this.map_.has(key);
453
454        if (hasKey) {
455          const value: object = this.map_.get(key);
456
457          ObserveV2.getObserve().startRecordDependencies(this, id);
458          const json: string = JSONCoder.stringify(value);
459          ObserveV2.getObserve().stopRecordDependencies();
460
461          if (this.isOverSizeLimit(json)) {
462            stateMgmtConsole.applicationError(
463              `Cannot store the key '${key}'! The length of data must be less than ${PersistenceV2Impl.MAX_DATA_LENGTH_}`);
464          } else {
465            PersistenceV2Impl.storage_.set(key, json);
466          }
467        }
468      } catch (err) {
469        if (this.cb_ && typeof this.cb_ === 'function') {
470          this.cb_(key, PersistError.Serialization, err);
471          continue;
472        }
473
474        stateMgmtConsole.applicationError(`For '${key}' key: ` + err);
475      }
476    }
477  }
478
479  private isOverSizeLimit(json: string): boolean {
480    if (typeof json !== 'string') {
481      return false;
482    }
483
484    return json.length >= PersistenceV2Impl.MAX_DATA_LENGTH_;
485  }
486
487  private isNotClassObject(value: object): boolean {
488    return Array.isArray(value) || this.isNotSupportType(value);
489  }
490
491  private isNotSupportType(value: object): boolean {
492    for (let i = 0; i < PersistenceV2Impl.NOT_SUPPORT_TYPES_.length; ++i) {
493      if (value instanceof PersistenceV2Impl.NOT_SUPPORT_TYPES_[i]) {
494        return true;
495      }
496    }
497    return false;
498  }
499
500  private storeKeyToPersistenceV2(key: string) {
501    try {
502      if (this.keysArr_.has(key)) {
503        return;
504      }
505
506      // Initializing the keys arr in memory
507      if (!this.keysArr_.size) {
508        this.keysArr_ = this.getKeysArrFromStorage();
509      }
510
511      this.keysArr_.add(key);
512
513      // Updating the keys arr in disk
514      this.storeKeysArrToStorage(this.keysArr_);
515    } catch (err) {
516      this.errorHelper(key, PersistError.Unknown, `fail to store the key '${key}'`);
517    }
518  }
519
520  private removeFromPersistenceV2(key: string): void {
521    try {
522      if (!PersistenceV2Impl.storage_.has(key)) {
523        stateMgmtConsole.applicationWarn(PersistenceV2Impl.DELETE_NOT_EXIST_KEY);
524        return;
525      }
526
527      PersistenceV2Impl.storage_.delete(key);
528
529      // The first call to remove
530      if (!this.keysArr_.has(key)) {
531        this.keysArr_ = this.getKeysArrFromStorage();
532      }
533
534      this.keysArr_.delete(key);
535      this.storeKeysArrToStorage(this.keysArr_);
536    } catch (err) {
537      this.errorHelper(key, PersistError.Unknown, `fail to remove the key '${key}'`);
538    }
539  }
540
541  private getKeysArrFromStorage(): Set<string> {
542    if (!PersistenceV2Impl.storage_.has(PersistenceV2Impl.KEYS_ARR_)) {
543      return this.keysArr_;
544    }
545    const jsonKeysArr: string = PersistenceV2Impl.storage_.get(PersistenceV2Impl.KEYS_ARR_);
546    return new Set(JSON.parse(jsonKeysArr));
547  }
548
549  private storeKeysArrToStorage(keysArr: Set<string>) {
550    PersistenceV2Impl.storage_.set(PersistenceV2Impl.KEYS_ARR_, JSON.stringify(Array.from(keysArr)));
551  }
552
553  private errorHelper(key: string, reason: PersistError, message: string) {
554    if (this.cb_ && typeof this.cb_ === 'function') {
555      this.cb_(key, reason, message);
556      return;
557    }
558
559    if (!key) {
560      key = 'unknown';
561    }
562    throw new Error(`For '${key}' key: ` + message);
563  }
564}
565