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