diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/mutableStateMeta.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/mutableStateMeta.ts index 480c83190ddc821e861b326b0bd7a8f328dc3b2a..d2404e2950a244b90f4d233f9c985eaa1be079fd 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/mutableStateMeta.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/mutableStateMeta.ts @@ -68,7 +68,8 @@ export class MutableStateMeta extends MutableStateMetaBase implements IMutableSt public addRef(): void { if ( ObserveSingleton.instance.renderingComponent === ObserveSingleton.RenderingMonitor || - ObserveSingleton.instance.renderingComponent === ObserveSingleton.RenderingComputed + ObserveSingleton.instance.renderingComponent === ObserveSingleton.RenderingComputed || + ObserveSingleton.instance.renderingComponent === ObserveSingleton.RenderingPersistentStorage ) { this.bindingRefs_.add(ObserveSingleton.instance.renderingComponentRef!.weakThis); ObserveSingleton.instance.renderingComponentRef!.reverseBindings.add(this.weakThis); diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/observeSingleton.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/observeSingleton.ts index 3d3e24d7f6dce05d0edaa8e5808d664f7d41f829..32e0964fc528fe33cfaa16546a32a31f05c94243 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/observeSingleton.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/base/observeSingleton.ts @@ -23,6 +23,7 @@ import { StateManagerImpl } from '@koalaui/runtime'; import { StateMgmtConsole } from '../tools/stateMgmtDFX'; import { MonitorFunctionDecorator, MonitorValueInternal } from '../decoratorImpl/decoratorMonitor'; import { ComputedDecoratedVariable, IComputedDecoratorRef } from '../decoratorImpl/decoratorComputed'; +import { PersistenceV2Impl } from '../storage/persistenceV2'; type TaskType = () => T; @@ -40,6 +41,7 @@ export class ObserveSingleton implements IObserve { public static readonly RenderingComponentV2: number = 2; public static readonly RenderingMonitor: number = 3; public static readonly RenderingComputed: number = 4; + public static readonly RenderingPersistentStorage: number = 5; public _renderingComponent: number = ObserveSingleton.RenderingComponent; private mutateMutableStateMode_: NotifyMutableStateMode = NotifyMutableStateMode.normal; @@ -47,6 +49,7 @@ export class ObserveSingleton implements IObserve { private monitorPathRefsChanged_ = new Set>(); private computedPropRefsChanged_ = new Set>(); private queuedMutableStateChanges_ = new Set>(); + private persistencePropRefsChanged_ = new Set>(); private finalizationRegistry = new FinalizationRegistry>( this.finalizeComputedAndMonitorPath ); @@ -115,6 +118,10 @@ export class ObserveSingleton implements IObserve { } public addDirtyRef(trackedRef: ITrackedDecoratorRef): void { + if (trackedRef.id >= PersistenceV2Impl.MIN_PERSISTENCE_ID) { + this.persistencePropRefsChanged_.add(trackedRef.weakThis); + return; + } if (trackedRef.id >= MonitorFunctionDecorator.MIN_MONITOR_ID) { this.monitorPathRefsChanged_.add(trackedRef.weakThis); } else if (trackedRef.id >= ComputedDecoratedVariable.MIN_COMPUTED_ID) { @@ -144,6 +151,11 @@ export class ObserveSingleton implements IObserve { this.computedPropRefsChanged_ = new Set>(); this.updateDirtyComputedProps(computedProps); } + if (this.persistencePropRefsChanged_.size) { + const persistenceProps = this.persistencePropRefsChanged_; + this.persistencePropRefsChanged_ = new Set>(); + PersistenceV2Impl.instance().onChangeObserved(persistenceProps); + } if (this.monitorPathRefsChanged_.size > 0) { const monitors = this.monitorPathRefsChanged_; this.monitorPathRefsChanged_ = new Set>(); @@ -154,7 +166,8 @@ export class ObserveSingleton implements IObserve { }); } } - } while (this.monitorPathRefsChanged_.size + this.computedPropRefsChanged_.size > 0); + } while (this.monitorPathRefsChanged_.size + this.computedPropRefsChanged_.size + + this.persistencePropRefsChanged_.size > 0); } private updateDirtyComputedProps(computedProps: Set>): void { diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/index.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/index.ts index 43a5a4a784398b9387affb5c9a1f4ec29f776ad2..cc7c180f70ee6e6dd77ba50db4c02a6104360e14 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/index.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/index.ts @@ -22,7 +22,9 @@ export * from './decoratorImpl/decoratorBase'; export * from './decoratorImpl/decoratorComputed'; export * from './decoratorImpl/decoratorMonitor'; export * from './storage/appStorage'; +export * from './storage/appStorageV2'; export * from './storage/localStorage'; +export * from './storage/persistenceV2'; export * from './base/types'; export * from './decoratorImpl/decoratorWatch'; export * from './decoratorImpl/decoratorInteropWatch'; diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/appStorageV2.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/appStorageV2.ts new file mode 100644 index 0000000000000000000000000000000000000000..36afb85db2a3715db389d8064b9f772f167f34b9 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/appStorageV2.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UIUtils } from '../utils'; +import { StorageHelper } from './persistenceV2' +import { StorageDefaultCreator } from './persistenceV2' +import { StateMgmtConsole } from '../tools/stateMgmtDFX'; + +export class AppStorageV2 { + public static connect( + ttype: Type, + key: string, + defaultCreator?: StorageDefaultCreator): T | undefined { + return AppStorageV2Impl.instance().connect(ttype, key, defaultCreator) + } + + public static connect( + ttype: Type, + defaultCreator?: StorageDefaultCreator): T | undefined { + return AppStorageV2Impl.instance().connect(ttype, defaultCreator); + } + + public static remove(keyOrType: string | Type): void { + AppStorageV2Impl.instance().remove(keyOrType); + } + + public static keys(): Array { + return AppStorageV2Impl.instance().keys(); + } +} + +export class AppStorageV2Impl { + private static instance_: AppStorageV2Impl | undefined = undefined; + private memorizedValues_: Map; // IObservedObject? + + constructor() { + super(); + this.memorizedValues_ = new Map(); + } + + public static instance(): AppStorageV2Impl { + if (AppStorageV2Impl.instance_) { + return AppStorageV2Impl.instance_!; + } + AppStorageV2Impl.instance_ = new AppStorageV2Impl(); + return AppStorageV2Impl.instance_!; + } + + public connect( + ttype: Type, + key: string, + defaultCreator?: StorageDefaultCreator): T | undefined { + + if (ttype.isPrimitive()) { + throw new Error(StorageHelper.INVALID_DEFAULT_VALUE_PRIMITIVE); + } + if (!StorageHelper.isKeyValid(key)) { + return undefined; + } + + if (!this.memorizedValues_.has(key!)) { + if (defaultCreator === undefined) { + throw new Error(StorageHelper.INVALID_DEFAULT_VALUE_CREATOR); + } + let defaultValue = defaultCreator!(); + StorageHelper.checkTypeByInstanceOf(key!, ttype, defaultValue); + let observedValue = UIUtils.makeObserved(defaultValue); + + this.memorizedValues_.set(key!, observedValue); + return observedValue; + } + + let obj = this.memorizedValues_.get(key!); + StorageHelper.checkTypeByType(key!, ttype, Type.of(obj)); + return obj as T; + } + + public connect( + ttype: Type, + defaultCreator?: StorageDefaultCreator): T | undefined { + return this.connect(ttype, ttype.getName(), defaultCreator); + } + + public remove(keyOrType: string | Type): void { + const key = StorageHelper.getKeyOrTypeNameWithChecks(keyOrType); + if (!key) { + return; + } + this.removeFromMemory(key!); + } + + public keys(): Array { + return Array.from(this.memorizedValues_.keys()); + } + + private removeFromMemory(key: string): void { + const isDeleted: boolean = this.memorizedValues_.delete(key); + if (!isDeleted) { + StateMgmtConsole.log(StorageHelper.DELETE_NOT_EXIST_KEY); + } + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistenceV2.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistenceV2.ts new file mode 100644 index 0000000000000000000000000000000000000000..64fe48880f8a4a37a45a8c65c112c9b59f973676 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistenceV2.ts @@ -0,0 +1,925 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { StateMgmtConsole } from '../tools/stateMgmtDFX'; +import { DecoratedVariableBase } from '../decoratorImpl/decoratorBase'; +import { IBackingValue } from '../base/iBackingValue'; +import { FactoryInternal } from '../base/iFactoryInternal'; +import { ObserveSingleton } from '../base/observeSingleton'; +import { IBindingSource, ITrackedDecoratorRef } from '../base/mutableStateMeta'; +import { RenderIdType } from '../decorator'; + +import { StateMgmtTool } from '#stateMgmtTool'; +import { UIUtils } from '../utils'; +import { IAniStorage, AniStorage, AreaMode } from './persistentStorage' + +export type StorageDefaultCreator = () => T; + +type StringOrUndefinedType = String | undefined +type FixedStringArrayType = FixedArray; + +export type ToJSONType = (value: T) => jsonx.JsonElement; +export type FromJSONType = (element: jsonx.JsonElement) => T; + +export interface IConnectOptions { + ttype: Type; + key?: string; + defaultCreator?: StorageDefaultCreator; + areaMode?: AreaMode; +} + +const enum PersistError { + Quota = 'quota', + Serialization = 'serialization', + Unknown = 'unknown' +}; + +type PersistErrorCallback = ((key: string, reason: PersistError, message: string) => void) | undefined; + +const enum MapType { + NOT_IN_MAP = -1, + MODULE_MAP = 0, + GLOBAL_MAP = 1 +} + +interface IStorageKey { + key: string; +} + +export class PersistenceV2 { + public static configureBackend(storage: IAniStorage): void { + PersistenceV2Impl.instance().configureBackend(storage); + } + + public static connect( + ttype: Type, + toJson: ToJSONType, + fromJson: FromJSONType, + defaultCreator?: StorageDefaultCreator + ): T | undefined { + return PersistenceV2Impl.instance().connect(ttype, ttype.getName(), toJson, fromJson, defaultCreator); + } + + public static connect( + ttype: Type, + key: string, + toJson: ToJSONType, + fromJson: FromJSONType, + defaultCreator?: StorageDefaultCreator, + ): T | undefined { + return PersistenceV2Impl.instance().connect(ttype, key, toJson, fromJson, defaultCreator); + } + + public static globalConnect( + connectOptions: IConnectOptions, + toJson: ToJSONType, + fromJson: FromJSONType): T | undefined { + return PersistenceV2Impl.instance().globalConnect(connectOptions, toJson, fromJson); + } + + public static keys(): Array { + return PersistenceV2Impl.instance().keys(); + } + + public static remove(keyOrType: string | Type): boolean { + return PersistenceV2Impl.instance().remove(keyOrType); + } + + public static save(keyOrType: string | Type): boolean { + return PersistenceV2Impl.instance().save(keyOrType); + } +} + +class StoragePropertyV2 + extends DecoratedVariableBase + implements ITrackedDecoratorRef, IStorageKey { + private backing_: IBackingValue | T | undefined; + public id: RenderIdType; // We keep ID only for sorting purposes, but sorting not really needed + public weakThis: WeakRef; + public reverseBindings: Set>; + public key: string; + public observable: boolean; + + constructor(key: string, initValue: T) { + super("", null, ""); + this.id = ++PersistenceV2Impl.nextPersistId_; + this.weakThis = new WeakRef(this as ITrackedDecoratorRef); + this.reverseBindings = new Set>; + ObserveSingleton.instance.addToTrackedRegistry(this, this.reverseBindings); + if (StateMgmtTool.isIObservedObject(initValue) || this.isObservedInterface(initValue)) { + this.backing_ = FactoryInternal.mkDecoratorValue("StoragePropertyV2", initValue); + this.observable = true; + } else { + this.backing_ = initValue; + this.observable = false; + } + this.key = key; + } + + private isObservedInterface(value: T): boolean { + if (typeof value !== "object") { + return false; + } + try { + const handler = StateMgmtTool.tryGetHandler(value as Object); + return handler !== undefined || handler !== null; + } catch (err) { + // Not proxied + } + return false; + } + + constructor(key: string) { + super("", null, ""); + this.id = ++PersistenceV2Impl.nextPersistId_; + this.weakThis = new WeakRef(this as ITrackedDecoratorRef); + this.reverseBindings = new Set>; + ObserveSingleton.instance.addToTrackedRegistry(this, this.reverseBindings); + this.backing_ = undefined; + this.key = key; + this.observable = false; + } + + clearReverseBindings(): void { + this.reverseBindings.forEach((dep: WeakRef) => { + let ref = dep!.deref() + if (ref) { + ref.clearBindingRefs(this.weakThis) + } else { + this.reverseBindings.delete(dep) + } + }) + } + + public get(): T | undefined { + if (this.backing_ === undefined) { + return undefined + } + if (this.observable) { + return (this.backing_! as IBackingValue).get(this.shouldAddRef()); + } + return this.backing_ as T; + } + + public set(newValue: T): void { + if (this.backing_ !== undefined) { + if (this.observable) { + (this.backing_! as IBackingValue).set(newValue); + } + else { + this.backing_ = newValue; + } + return; + } + + if (StateMgmtTool.isIObservedObject(newValue) || this.isObservedInterface(newValue)) { + this.backing_ = FactoryInternal.mkDecoratorValue("StoragePropertyV2", newValue); + this.observable = true; + } else { + this.backing_ = newValue; + this.observable = false; + } + } +} + +export class StorageHelper { + public static readonly INVALID_DEFAULT_VALUE_CREATOR: string = 'The default creator should be function when first connect'; + public static readonly DELETE_NOT_EXIST_KEY: string = 'The key to be deleted does not exist'; + public static readonly INVALID_TYPE: string = 'The type should have function constructor signature when use storage'; + public static readonly EMPTY_STRING_KEY: string = 'Cannot use empty string as the key'; + public static readonly INVALID_LENGTH_KEY: string = 'Cannot use the key! The length of key should be 2 to 255'; + public static readonly INVALID_CHARACTER_KEY: string = 'Cannot use the key! The value of key can only consist of letters, digits and underscores'; + public static readonly NULL_OR_UNDEFINED_KEY: string = `The parameter cannot be null or undefined`; + public static readonly ERROR_NOT_IN_THE_STORE: string = `The parameter is not in the store`; + public static readonly INVALID_DEFAULT_VALUE_PRIMITIVE: string = 'Can not store primitive data types'; + + public static getKeyOrTypeNameWithChecks(keyOrType: string | Type): string | undefined { + if (typeof keyOrType === 'string') { + const key = keyOrType as string; + return StorageHelper.isKeyValid(key) ? key : undefined; + } + return (keyOrType as Type).getName(); + } + + public static checkTypeByType(key: string, newType: Type, oldType: Type): void { + if (!newType.assignableFrom(oldType)) { + throw new Error(`The ** type mismatches when use the key '${key}' in storage`); + } + } + + public static checkTypeByName(key: string, ttype: Type, typeName: string): void { + if (ttype.getName() !== typeName) { + throw new Error(`The type mismatches when use the key '${key}' in storage, '${ttype.getName()}' vs. '${typeName}'`); + } + } + + public static checkTypeByInstanceOf(key: string, ttype: Type, obj: T): void { + if (!ttype.assignableFrom(Type.of(obj))) { + throw new Error(`The type mismatches when use the key '${key}' in storage`); + } + } + + public static isKeyValid(key: string): boolean { + // The key string is empty + if (key === '') { + StateMgmtConsole.log(StorageHelper.EMPTY_STRING_KEY); + return false; + } + + const len: number = key.length; + // The key string length should shorter than 1024, error + if (len >= 1024) { + StateMgmtConsole.log(StorageHelper.INVALID_LENGTH_KEY); + return false; + } + + // Warnings only + if (len < 2 || len > 255) { + StateMgmtConsole.log(StorageHelper.INVALID_LENGTH_KEY); + } + if (!(new RegExp("^[0-9a-zA-Z_]+$")).test(key)) { + StateMgmtConsole.log(StorageHelper.INVALID_CHARACTER_KEY); + } + + return true; + } +} + +export class PersistenceV2Impl { + private static readonly NOT_SUPPORTED_TYPES_: Array = + [ + Type.from>(), + Type.from>(), + Type.from>(), + Type.from>(), + Type.from>(), + Type.from(), + Type.from(), + Type.from(), + Type.from(), + Type.from(), + Type.from(), + Type.from(), + Type.from>(), + Type.from() + ]; + + public static readonly MIN_PERSISTENCE_ID = 0x30000000; + public static nextPersistId_ = PersistenceV2Impl.MIN_PERSISTENCE_ID; + + private storageBackend_: IAniStorage | undefined = undefined; + private static instance_: PersistenceV2Impl | undefined = undefined; + + private static readonly NOT_SUPPORT_TYPE_MESSAGE_: string = 'Type is not supported! Can only use the class object in Persistence'; + private static readonly KEYS_DUPLICATE_: string = 'ERROR, Duplicate key used when connect'; + private static readonly NOT_SUPPORT_AREAMODE_MESSAGE_: string = 'AreaMode Value Error! value range can only in EL1-EL5'; + private static readonly KEYS_ARR_: string = '___keys_arr'; + private static readonly MAX_DATA_LENGTH_: number = 8000; + + private entriesMap_: Map; + private globalEntriesMap_: Map; + private globalMapAreaMode_: Map; + private keysSet_: Set; + private globalKeysArr_: Array>; + private propertyWriters_: Map void>; + private errorCB_: PersistErrorCallback = undefined; + private typeMap_: Map; + private observationInProgress_: boolean = false; + public static backendUpdateCountForTesting: int = 0; + + constructor() { + super(); + this.entriesMap_ = new Map; + this.globalEntriesMap_ = new Map; + this.globalMapAreaMode_ = new Map(); + this.keysSet_ = new Set(); + this.globalKeysArr_ = [new Set(), new Set(), new Set(), new Set(), new Set()]; + this.typeMap_ = new Map(); + this.propertyWriters_ = new Map void>(); + this.storageBackend_ = new AniStorage(); + } + + public static instance(): PersistenceV2Impl { + if (PersistenceV2Impl.instance_ !== undefined) { + return PersistenceV2Impl.instance_!; + } + PersistenceV2Impl.instance_ = new PersistenceV2Impl(); + return PersistenceV2Impl.instance_!; + } + + // Test helper + public static instanceReset() { + PersistenceV2Impl.instance_ = undefined; + } + + // Test helper + public static instanceExists(): boolean { + return PersistenceV2Impl.instance_ !== undefined; + } + + public configureBackend(storage: IAniStorage): void { + this.storageBackend_ = storage; + } + + public connect( + ttype: Type, + toJson: ToJSONType, + fromJson: FromJSONType, + defaultCreator?: StorageDefaultCreator + ): T | undefined { + return this.connect(ttype, ttype.getName(), toJson, fromJson, defaultCreator); + } + + public connect( + ttype: Type, + key: string, + toJson: ToJSONType, + fromJson: FromJSONType, + defaultCreator?: StorageDefaultCreator, + ): T | undefined { + if (this.storageBackend_ === undefined) { + this.errorHelper(key, PersistError.Unknown, `The storage is null`); + return undefined; + } + + this.checkTypeIsValidClassObject(ttype); + + if (ttype.isPrimitive()) { + throw new Error(StorageHelper.INVALID_DEFAULT_VALUE_CREATOR); + } + + if (!this.isPersistentKeyValid(key)) { + return undefined; + } + + // In memory + if (this.globalEntriesMap_.has(key)) { + throw new Error(PersistenceV2Impl.KEYS_DUPLICATE_); + } + + if (this.entriesMap_.has(key)) { + StorageHelper.checkTypeByType(key, ttype, this.typeMap_.get(key)!); + const existingValue = this.entriesMap_.get(key) as StoragePropertyV2; + return existingValue.get(); + } + + // Not in memory (not connected), but exists on the disk + if (this.storageBackend_!.has(key)) { + return this.readValueFromDisk(key, ttype, toJson, fromJson); + } + + // Key is neither in the memory nor in the storage/disk + // Create default value and check correctness + const storageProperty = this.createDefaultValue(key, ttype, defaultCreator); + if (!storageProperty) { + return undefined; + } + + let observedValue = storageProperty!.get(); + this.connectNewValue(key, storageProperty, ttype, toJson); + return observedValue; + } + + public globalConnect( + connectOptions: IConnectOptions, + toJson: ToJSONType, + fromJson: FromJSONType): T | undefined { + return this.doGlobalConnect(connectOptions, toJson, fromJson); + } + + private doGlobalConnect(connectOptions: IConnectOptions, + toJson: ToJSONType, fromJson: FromJSONType): T | undefined { + + this.checkTypeIsValidClassObject(connectOptions.ttype); + + const key = this.getPersistentKeyOrTypeNameWithChecks(connectOptions); + if (!key) { + return undefined; + } + + if (this.storageBackend_ === undefined) { + this.errorHelper(key, PersistError.Unknown, `The storage is null`); + return undefined; + } + + // In memory, do duplicate key check + if (this.entriesMap_.has(key)) { + throw new Error(PersistenceV2Impl.KEYS_DUPLICATE_); + } + + // In memory, return if globalEntriesMap_ exist + if (this.globalEntriesMap_.has(key)) { + StorageHelper.checkTypeByType(key, connectOptions.ttype, this.typeMap_.get(key)!); + const existingValue = this.globalEntriesMap_.get(key) as StoragePropertyV2; + return existingValue!.get() as T; + } + + // Not in memory, but on disk + const areaMode: AreaMode = this.getAreaMode(connectOptions.areaMode); + this.globalMapAreaMode_.set(key, areaMode); + if (this.storageBackend_!.has(key, areaMode)) { + return this.readValueFromDisk(key, connectOptions.ttype, toJson, fromJson, areaMode); + } + + // Neither in memory or in disk, create new entry + let storageProperty = this.createDefaultValue(key, connectOptions.ttype, connectOptions.defaultCreator); + if (!storageProperty) { + return undefined; + } + let observedValue = storageProperty!.get(); + + if (!observedValue) { + return undefined; + } + + this.connectNewValue(key, storageProperty, connectOptions.ttype, toJson, true, areaMode); + return observedValue; + } + + public keys(): Array { + const allKeys: Array = new Array(); + try { + // add module path key + if (!this.keysSet_.size) { + this.keysSet_ = this.getKeysFromStorage(); + } + for (const key of this.keysSet_) { + allKeys.push(key); + } + // add global path key + for (let i = 0; i < this.globalKeysArr_.length; i++) { + if (!this.globalKeysArr_[i].size) { + this.globalKeysArr_[i] = this.getKeysFromStorage(i as AreaMode); + } + for (const key of this.globalKeysArr_[i]) { + allKeys.push(key); + } + } + } catch (err) { + if (this.errorCB_) { + this.errorCB_!('', PersistError.Unknown, `fail to get all persisted keys`); + return []; + } + throw err; + } + return allKeys; + } + + public remove(keyOrType: string | Type): boolean { + let key = StorageHelper.getKeyOrTypeNameWithChecks(keyOrType); + if (!key) { + return false; + } + this.disconnectValue(key); + return true; + } + + public save(keyOrType: string | Type): boolean { + let key = StorageHelper.getKeyOrTypeNameWithChecks(keyOrType); + if (!key) { + return false; + } + + let obj = this.globalEntriesMap_.has(key) + ? this.globalEntriesMap_.get(key) + : (this.entriesMap_.has(key) ? this.entriesMap_.get(key) : undefined); + + if (obj === undefined) { + StateMgmtConsole.log(`Cannot save the key '${key}'! The key is disconnected`); + return false; + } + let status = true; + try { + let writer = this.propertyWriters_.get(key); + if (writer) { + writer(); + } + } catch (err) { + this.errorHelper(key, PersistError.Serialization, JSON.stringify(err)); + status = false; + } + return status; + } + + public notifyOnError(callback: PersistErrorCallback): void { + this.errorCB_ = callback; + } + + public onChangeObserved(persistRefs: Set>): void { + this.writeAllChangedToDisk(persistRefs); + } + + private startObservation(property: StoragePropertyV2) { + if (!property.observable) { + return; + } + ObserveSingleton.instance.renderingComponent = ObserveSingleton.RenderingPersistentStorage + ObserveSingleton.instance.renderingComponentRef = property; + this.observationInProgress_ = true; + } + + private stopObservation() { + if (!this.observationInProgress_) { + return; + } + ObserveSingleton.instance.renderingComponent = ObserveSingleton.RenderingComponent; + ObserveSingleton.instance.renderingComponentRef = undefined; + this.observationInProgress_ = false; + } + + private propertyWriter(key: string, toJson: ToJSONType) { + const keyType: MapType = this.getKeyMapType(key!); + if (keyType === MapType.NOT_IN_MAP) { + return; + } + + const property = + (keyType === MapType.GLOBAL_MAP ? this.globalEntriesMap_.get(key!)! : this.entriesMap_.get(key!)) + as StoragePropertyV2; + + const ttype = this.typeMap_.get(key!); + + this.startObservation(property); + const jsonElement = PersistenceV2Impl.toJsonWithType(toJson, property!.get() as T); + let jsonString = JSON.stringifyJsonElement(jsonElement); + this.stopObservation(); + + if (this.isOverSizeLimit(jsonString)) { + StateMgmtConsole.log( + `Cannot store the key '${key}'! The length of data must be less than ${PersistenceV2Impl.MAX_DATA_LENGTH_}`); + return; + } + const areaMode = (keyType === MapType.GLOBAL_MAP) ? this.globalMapAreaMode_.get(key!) : undefined; + // Write to backend + StateMgmtConsole.log("### propertyWriter set to backend " + jsonString); + this.storageBackend_!.set(key!, jsonString, areaMode); + PersistenceV2Impl.backendUpdateCountForTesting++; + } + + private connectNewValue( + key: string, + newValue: StoragePropertyV2, + ttype: Type, + toJson: ToJSONType, + writeFlag: boolean = true, + areaMode?: AreaMode): void { + this.propertyWriters_.set( + key, + () => { this.propertyWriter(key, toJson) } as() => void + ); + + if (areaMode !== undefined) { + this.globalEntriesMap_.set(key, newValue); + } else { + this.entriesMap_.set(key, newValue); + } + this.typeMap_.set(key, ttype); + + if (writeFlag) { + this.storeKeyToPersistenceV2(key, areaMode); + // Schedule writing to back storage + ObserveSingleton.instance.addDirtyRef(newValue) + } + } + + private disconnectValue(key: string): void { + const keyType: MapType = this.getKeyMapType(key); + let areaMode: AreaMode | undefined; + if (keyType === MapType.GLOBAL_MAP) { + this.globalEntriesMap_.delete(key); + areaMode = this.globalMapAreaMode_.get(key); + this.globalMapAreaMode_.delete(key); + } else if (keyType === MapType.MODULE_MAP) { + this.entriesMap_.delete(key); + } + + this.typeMap_.delete(key); + this.propertyWriters_.delete(key); + this.removeFromPersistenceV2(key, areaMode); + } + + private checkTypeIsValidClassObject(ttype: Type) { + PersistenceV2Impl.NOT_SUPPORTED_TYPES_.forEach((wrong_ttype) => { + if (wrong_ttype.equals(ttype)) { + throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_); + } + }) + } + + private getAreaMode(areaMode?: AreaMode): AreaMode { + if (areaMode === undefined) { + return AreaMode.EL2; + } + if (areaMode >= AreaMode.EL1 && areaMode <= AreaMode.EL5) { + return areaMode; + } + throw new Error(PersistenceV2Impl.NOT_SUPPORT_AREAMODE_MESSAGE_); + } + + private getKeyMapType(key: string): MapType { + if (this.globalEntriesMap_.has(key)) { + return MapType.GLOBAL_MAP; + } + if (this.entriesMap_.has(key)) { + return MapType.MODULE_MAP; + } + return MapType.NOT_IN_MAP; + } + + private createDefaultValue(key: string, ttype: Type, + defaultCreator?: StorageDefaultCreator): StoragePropertyV2 | undefined { + if (!defaultCreator) { + this.errorHelper(key, PersistError.Unknown, `Can not create default value for '${key}'`); + return undefined; + } + const value: T = defaultCreator!(); + StorageHelper.checkTypeByInstanceOf(key, ttype, value); + if (PersistenceV2Impl.isNotAValidClassObject(value)) { + throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_); + } + + return new StoragePropertyV2(key, UIUtils.makeObserved(value)); + } + + private static getTargetClassName(value: T): string { + try { + let maybeTarget = StateMgmtTool.tryGetTarget(value); + let target = maybeTarget ? maybeTarget as T : value; + return Type.of(target).getName(); + } catch (err) { + // not proxied + } + return Type.of(value).getName(); + } + + private static fromJsonWithType(fromJson: FromJSONType, record: jsonx.JsonElement) + : [string, T] { + let recordArray = record.asArray(); + let typeString = recordArray[0].asString(); + let value = fromJson(recordArray[1]); + return [typeString, value]; + } + + private static toJsonWithType(toJson: ToJSONType, obj: T): jsonx.JsonElement { + let topArray = new Array(); + let classname = PersistenceV2Impl.getTargetClassName(obj); + let el = jsonx.JsonElement.createString(classname); + topArray.push(el); + topArray.push(toJson(obj)); + let ret = jsonx.JsonElement.createArray(topArray); + return ret; + } + + private readValueFromDisk( + key: string, + ttype: Type, + toJson: ToJSONType, + fromJson: FromJSONType, + areaMode?: AreaMode): T | undefined { + try { + const jsonString: string = this.storageBackend_!.get(key, areaMode)!; + if (!jsonString) { + this.errorHelper(key, PersistError.Serialization, StorageHelper.ERROR_NOT_IN_THE_STORE); + return undefined; + } + let property = new StoragePropertyV2(key); + const jsonElement = JSON.parseJsonElement(jsonString); + let newValueTuple = PersistenceV2Impl.fromJsonWithType(fromJson, jsonElement); + let typeName = newValueTuple[0] + let newValue = newValueTuple[1]; + if (newValue === undefined) { + throw new Error("unable to deserialize object for the key: " + key); + } + let newObservedValue = UIUtils.makeObserved(newValue!); + property.set(newObservedValue); + + // Collect dependencies + this.startObservation(property); + PersistenceV2Impl.toJsonWithType(toJson, newObservedValue); + this.stopObservation(); + + // Exception if type mismatch + StorageHelper.checkTypeByName(key, ttype, typeName); + StorageHelper.checkTypeByInstanceOf(key, ttype, newValue); + // Adds to one of the maps, do not store key on disk + this.connectNewValue(key, property, ttype, toJson, false, areaMode); + return newObservedValue; + } catch (err) { + this.stopObservation(); + this.errorHelper(key, PersistError.Serialization, JSON.stringify(err)); + } + return undefined; + } + + private writeAllChangedToDisk(refs: Set>): void { + refs.forEach((item) => { + let property = item.deref(); + if (property) { + const key = (property as IStorageKey).key; + + try { + if (this.propertyWriters_.has(key!)) { + this.propertyWriters_.get(key!)!(); + } + } catch (err) { + if (this.errorCB_) { + this.errorCB_!(key!, PersistError.Serialization, JSON.stringify(err)); + } + StateMgmtConsole.log(`Exception writing for '${key}' key: ` + err); + } + } + }) + } + + private isOverSizeLimit(json: string): boolean { + return json.length >= PersistenceV2Impl.MAX_DATA_LENGTH_; + } + + private static isNotAValidClassObject(value: object): boolean { + const wrongType = + Type.of(value).isPrimitive() || + Array.isArray(value) || + value instanceof Array || + value instanceof Set || + value instanceof Map || + value instanceof WeakSet || + value instanceof WeakMap || + value instanceof Date || + value instanceof Boolean || + value instanceof Number || + value instanceof String || + value instanceof BigInt || + value instanceof RegExp || + value instanceof Function || + value instanceof Promise || + value instanceof ArrayBuffer; + return wrongType; + } + + private storeKeyToPersistenceV2(key: string, areaMode?: AreaMode): void { + try { + if (areaMode !== undefined) { + if (this.globalKeysArr_[areaMode].has(key)) { + return; + } + // Initializing the keys arr in memory + if (!this.globalKeysArr_[areaMode].size) { + this.globalKeysArr_[areaMode] = this.getKeysFromStorage(areaMode); + } + this.globalKeysArr_[areaMode].add(key); + // Updating the keys arr in disk + this.storeKeysToStorage(this.globalKeysArr_[areaMode], areaMode); + } else { + if (this.keysSet_.has(key)) { + return; + } + + // Initializing the keys arr in memory + if (!this.keysSet_.size) { + this.keysSet_ = this.getKeysFromStorage(); + } + this.keysSet_.add(key); + // Updating the keys arr in disk + this.storeKeysToStorage(this.keysSet_); + } + } catch (err) { + this.errorHelper(key, PersistError.Unknown, `fail to store the key '${key}'`); + } + } + + private removeForModulePath(key: string): void { + this.storageBackend_!.delete(key); + // The first call for module path + if (!this.keysSet_.has(key)) { + this.keysSet_ = this.getKeysFromStorage(); + } + this.keysSet_.delete(key); + this.storeKeysToStorage(this.keysSet_); + } + + private removeFlagForGlobalPath(key: string): boolean { + let removeFlag = false; + // first call for global path + for (let i = 0; i < this.globalKeysArr_.length; i++) { + if (this.storageBackend_!.has(key, i as AreaMode)) { + removeFlag = true; + this.storageBackend_!.delete(key, i as AreaMode); + this.globalKeysArr_[i] = this.getKeysFromStorage(i as AreaMode); + this.globalKeysArr_[i].delete(key); + this.storeKeysToStorage(this.globalKeysArr_[i], i as AreaMode); + } + } + return removeFlag; + } + + private removeFromPersistenceV2(key: string, areaMode?: AreaMode): void { + StateMgmtConsole.log("### removeFromPersistenceV2 start"); + try { + // check for global path + if (areaMode !== undefined) { + StateMgmtConsole.log("### removeFromPersistenceV2 YES aremode"); + this.storageBackend_!.delete(key, areaMode); + this.globalKeysArr_[areaMode].delete(key); + this.storeKeysToStorage(this.globalKeysArr_[areaMode], areaMode); + } else { + StateMgmtConsole.log("### removeFromPersistenceV2 NO areamode"); + let removeFlag: boolean = false; + // check for module path + if (this.storageBackend_!.has(key)) { + StateMgmtConsole.log("### removeFromPersistenceV2 NO areamode 01"); + removeFlag = true; + this.removeForModulePath(key); + } else { + StateMgmtConsole.log("### removeFromPersistenceV2 NO key in the storage NO areamode else 03"); + removeFlag = this.removeFlagForGlobalPath(key); + } + if (!removeFlag) { + StateMgmtConsole.log("### removeFromPersistenceV2 NO areamode 04"); + StateMgmtConsole.log(StorageHelper.DELETE_NOT_EXIST_KEY + `keys:${key}`); + } + } + } catch (err) { + StateMgmtConsole.log("### removeFromPersistenceV2 fails to remove " + err); + this.errorHelper(key, PersistError.Unknown, `fail to remove the key '${key}'`); + } + } + + private getKeysFromStorage(areaMode?: AreaMode): Set { + if (areaMode !== undefined && !this.storageBackend_!.has(PersistenceV2Impl.KEYS_ARR_, areaMode!)) { + return this.globalKeysArr_[areaMode]; + } + else if (areaMode === undefined && !this.storageBackend_!.has(PersistenceV2Impl.KEYS_ARR_)) { + return this.keysSet_; + } + + const jsonKeysArr: string | undefined = this.storageBackend_!.get(PersistenceV2Impl.KEYS_ARR_, areaMode); + + let returnSet = new Set(); + if (jsonKeysArr === undefined) { + return returnSet; + } + + const arrayForTypeDetection: FixedArray = new StringOrUndefinedType[2]; + let keysArray = JSON.parse(jsonKeysArr, Type.of(arrayForTypeDetection)); + if (keysArray === undefined) { + return returnSet; + } + for (let idx = 0; idx < keysArray!.length; idx++) { + returnSet.add(keysArray![idx] as string); + } + return returnSet; + } + + private storeKeysToStorage(keysSet: Set, areaMode?: AreaMode | undefined): void { + //this.storageBackend_!.set(PersistenceV2Impl.KEYS_ARR_, JSON.stringify(Array.from(keysSet)), areaMode); + //return; + let keysArray: FixedStringArrayType = new StringOrUndefinedType[keysSet.size]; + let idx: int = 0; + keysSet.forEach((key) => { keysArray[idx++] = key; }) + this.storageBackend_!.set(PersistenceV2Impl.KEYS_ARR_, JSON.stringify(keysArray), areaMode); + } + + private isPersistentKeyValid(key: string): string | undefined { + if (key === PersistenceV2Impl.KEYS_ARR_) { + this.errorHelper(key, PersistError.Quota, `The key '${key}' cannot be used`); + return undefined; + } + return StorageHelper.isKeyValid(key!) ? key : undefined; + } + + private getPersistentKeyOrTypeNameWithChecks(options: IConnectOptions): string | undefined { + let key = options.key; + if (!options.key) { + StateMgmtConsole.log(StorageHelper.NULL_OR_UNDEFINED_KEY + ', try to use the type name as key'); + key = options.ttype.getName(); + } + + if (key === undefined) { + throw new Error(PersistenceV2Impl.NOT_SUPPORT_TYPE_MESSAGE_); + } + return this.isPersistentKeyValid(key); + } + + private errorHelper(key: string, reason: PersistError, message: string) { + if (this.errorCB_ !== undefined) { + this.errorCB_!(key, reason, message); + return; + } + if (!key) { + key = 'unknown'; + } + throw new Error(`For '${key}' key: ` + message); + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistentStorage.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistentStorage.ts index 324f4273d01f3c0fc0e2e643839b97d4f2776cb1..e361bd96cde9b7f62ab8012a760da6ae56f17153 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistentStorage.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/storage/persistentStorage.ts @@ -19,12 +19,20 @@ import { AppStorage } from './appStorage'; import { ArkUIAniModule } from 'arkui.ani'; import { StateMgmtConsole } from '../tools/stateMgmtDFX'; -interface IAniStorage { - get(key: string): string | undefined; - set(key: string, val: string): void; - has(key: string): boolean; +export const enum AreaMode { + EL1 = 0, + EL2 = 1, + EL3 = 2, + EL4 = 3, + EL5 = 4 +} + +export interface IAniStorage { + get(key: string, areaMode: AreaMode = AreaMode.EL2): string | undefined; + set(key: string, val: string, areaMode: AreaMode = AreaMode.EL2): void; + has(key: string, areaMode: AreaMode = AreaMode.EL2): boolean; clear(): void; - delete(key: string): void; + delete(key: string, areaMode: AreaMode = AreaMode.EL2): void; } // class JsonElement{} @@ -90,20 +98,20 @@ class TypedMap { } } -class AniStorage implements IAniStorage { - get(key: string): string | undefined { +export class AniStorage implements IAniStorage { + get(key: string, areaMode: AreaMode = AreaMode.EL2): string | undefined { return ArkUIAniModule._PersistentStorage_Get(key); } - set(key: string, val: string): void { + set(key: string, val: string, areaMode: AreaMode = AreaMode.EL2): void { ArkUIAniModule._PersistentStorage_Set(key, val); } - has(key: string): boolean { + has(key: string, areaMode: AreaMode = AreaMode.EL2): boolean { return ArkUIAniModule._PersistentStorage_Has(key); } clear(): void { ArkUIAniModule._PersistentStorage_Clear(); } - delete(key: string): void { + delete(key: string, areaMode: AreaMode = AreaMode.EL2): void { ArkUIAniModule._PersistentStorage_Delete(key); } } diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/arkts/stateMgmtTool.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/arkts/stateMgmtTool.ts index 43613a40a289f8217266094ec5446c581a411c50..de0941b9330d41cceaab5977d6ff2cab13327e17 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/arkts/stateMgmtTool.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/arkts/stateMgmtTool.ts @@ -82,6 +82,12 @@ export class StateMgmtTool { ? (proxy.Proxy.tryGetHandler(value) as NullableObject) // a very slow call so need to judge proxy first : undefined; } + static tryGetTarget(value: Object): NullableObject { + const objType = Type.of(value); + return objType instanceof ClassType && (objType as ClassType).getName().endsWith('@Proxy') + ? (proxy.Proxy.tryGetTarget(value as Object) as NullableObject) + : undefined; + } static createProxy(value: T, allowDeep: boolean = false): T { return proxy.Proxy.create(value, new InterfaceProxyHandler(allowDeep)) as T; } diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/ts/stateMgmtTool.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/ts/stateMgmtTool.ts index 752fe932db02cc3caa419d046e6c179d8aed6cf8..46643053fe9d74177c947d4ab13cf06dfd0cc9c9 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/ts/stateMgmtTool.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/ts/stateMgmtTool.ts @@ -71,6 +71,9 @@ export class StateMgmtTool { static tryGetHandler(value: NullableObject): NullableObject { return value; } + static tryGetTarget(value: Object): NullableObject { + return value as NullableObject; + } static createProxy(value: T): T { return value; } diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni index 8068ee394165e7297e7a4fc4f54d762a93a71114..24891932e49f382513484f60c4995cebe84daa1c 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/components.gni @@ -497,8 +497,10 @@ arkui_files = [ "arkui-preprocessed/arkui/stateManagement/remember.ets", "arkui-preprocessed/arkui/stateManagement/runtime/index.ets", "arkui-preprocessed/arkui/stateManagement/storage/appStorage.ets", + "arkui-preprocessed/arkui/stateManagement/storage/appStorageV2.ets", "arkui-preprocessed/arkui/stateManagement/storage/environment.ets", "arkui-preprocessed/arkui/stateManagement/storage/localStorage.ets", + "arkui-preprocessed/arkui/stateManagement/storage/persistenceV2.ets", "arkui-preprocessed/arkui/stateManagement/storage/persistentStorage.ets", "arkui-preprocessed/arkui/stateManagement/storage/storageBase.ets", "arkui-preprocessed/arkui/stateManagement/storage/storageProperty.ets",