diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decorator.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decorator.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5c51a90f03098991c9c390875be312a5dae098e --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decorator.ts @@ -0,0 +1,224 @@ +/* + * 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 { ObserveSingleton } from './base/observeSingleton'; +import { int32 } from '@koalaui/common'; +import { __StateMgmtFactoryImpl } from './base/stateMgmtFactory'; +import { ExtendableComponent } from '../component/extendableComponent'; +import { IBindingSource, ITrackedDecoratorRef } from './base/mutableStateMeta'; +import { IComputedDecoratorRef } from './decoratorImpl/decoratorComputed'; + +export interface IDecoratedVariable { + readonly varName: string; + info(): string; +} + +export interface IDecoratedV1Variable extends IDecoratedVariable { + registerWatchToSource(me: IDecoratedV1Variable): void; +} + +export interface IDecoratedImmutableVariable { + get(): T; +} + +export interface IDecoratedMutableVariable { + get(): T; + set(newValue: T): void; +} + +export interface IDecoratedUpdatableVariable { + update(newValue: T): void; +} + +export interface IStateDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IPropDecoratedVariable + extends IDecoratedMutableVariable, + IDecoratedUpdatableVariable, + IDecoratedV1Variable {} + +export interface IPropRefDecoratedVariable + extends IDecoratedMutableVariable, + IDecoratedUpdatableVariable, + IDecoratedV1Variable {} + +export interface ILinkDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IProvideDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IConsumeDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IObjectLinkDecoratedVariable + extends IDecoratedImmutableVariable, + IDecoratedUpdatableVariable, + IDecoratedV1Variable {} + +export interface IStorageLinkDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface ILocalStorageLinkDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IStoragePropRefDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface IStoragePropDecoratedVariable extends IDecoratedMutableVariable, IDecoratedV1Variable {} + +export interface ILocalStoragePropRefDecoratedVariable + extends IDecoratedMutableVariable, + IDecoratedV1Variable {} + +export type LinkSourceType = IStateDecoratedVariable | ILinkDecoratedVariable | IObjectLinkDecoratedVariable | + IPropDecoratedVariable | IPropRefDecoratedVariable | IStorageLinkDecoratedVariable | ILocalStorageLinkDecoratedVariable | + IStoragePropRefDecoratedVariable | ILocalStoragePropRefDecoratedVariable | IProvideDecoratedVariable | IConsumeDecoratedVariable; + +export interface IMutableStateMeta { + addRef(): void; + fireChange(): void; +} + +export interface IMutableKeyedStateMeta { + addRef(key: string): void; + fireChange(key: string): void; +} + +export interface IObserve { + renderingComponent: number; + renderingId: RenderIdType | undefined; + shouldAddRef(iObjectsRenderId: RenderIdType | undefined): boolean; +} + +export const OBSERVE: IObserve = ObserveSingleton.instance; + +export type RenderIdType = int32; + +export interface IObservedObject extends IWatchSubscriberRegister { + setV1RenderId(renderId: RenderIdType): void; +} + +export const STATE_MGMT_FACTORY: IStateMgmtFactory = new __StateMgmtFactoryImpl(); + +export interface IStateMgmtFactory { + makeMutableStateMeta(): IMutableStateMeta; + makeSubscribedWatches(): ISubscribedWatches; + makeState( + owningView: ExtendableComponent, + varName: string, + initValue: T, + watchFunc?: WatchFuncType + ): IStateDecoratedVariable; + makeProp( + owningView: ExtendableComponent, + varName: string, + initValue: T, + watchFunc?: WatchFuncType + ): IPropDecoratedVariable; + makePropRef( + owningView: ExtendableComponent, + varName: string, + initValue: T, + watchFunc?: WatchFuncType + ): IPropRefDecoratedVariable; + makeLink( + owningView: ExtendableComponent, + varName: string, + source: LinkSourceType, + watchFunc?: WatchFuncType + ): ILinkDecoratedVariable; + makeProvide( + owningView: ExtendableComponent, + varName: string, + provideAlias: string, + initValue: T, + allowOverride: boolean, + watchFunc?: WatchFuncType + ): IProvideDecoratedVariable; + makeConsume( + owningView: ExtendableComponent, + varName: string, + provideAlias: string, + watchFunc?: WatchFuncType + ): IConsumeDecoratedVariable; + makeObjectLink( + owningView: ExtendableComponent, + varName: string, + initValue: T, + watchFunc?: WatchFuncType + ): IObjectLinkDecoratedVariable; + makeStorageLink( + owningView: ExtendableComponent, + propName: string, + varName: string, + initValue: T, + ttype: Type, + watchFunc?: WatchFuncType + ): IStorageLinkDecoratedVariable; + makeLocalStorageLink( + owningView: ExtendableComponent, + propName: string, + varName: string, + initValue: T, + ttype: Type, + watchFunc?: WatchFuncType + ): ILocalStorageLinkDecoratedVariable; + makeStoragePropRef( + owningView: ExtendableComponent, + propName: string, + varName: string, + initValue: T, + ttype: Type, + watchFunc?: WatchFuncType + ): IStoragePropRefDecoratedVariable; + makeLocalStoragePropRef( + owningView: ExtendableComponent, + propName: string, + varName: string, + initValue: T, + ttype: Type, + watchFunc?: WatchFuncType + ): ILocalStoragePropRefDecoratedVariable; + makeComputed(computeFunction: () => T, varName: string): IComputedDecoratedVariable; + makeMonitor(pathLabmda: IMonitorPathInfo[], monitorFunction: (m: IMonitor) => void): IMonitorDecoratedVariable; +} + +export type WatchFuncType = (propertyName: string) => void; + +export type WatchIdType = int32; + +export interface IWatchSubscriberRegister { + addWatchSubscriber(watchId: WatchIdType): void; + removeWatchSubscriber(watchId: WatchIdType): boolean; +} + +export interface ISubscribedWatches extends IWatchSubscriberRegister { + executeOnSubscribingWatches(propertyName: string): void; +} + +export interface IComputedDecoratedVariable extends IComputedDecoratorRef, IDecoratedImmutableVariable {} + +export interface IMonitor { + readonly dirty: Array; + value(path?: string): IMonitorValue | undefined; +} + +export interface IMonitorDecoratedVariable { } + +export interface IMonitorPathInfo { + readonly path: string; + readonly lambda: () => Any; +} + +export interface IMonitorValue { + before: T; + now: T; + readonly path: string; +} diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decoratorImpl/decoratorMonitor.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decoratorImpl/decoratorMonitor.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ebb82a042c938bc4fde584ebc06b66dee216e1e --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/decoratorImpl/decoratorMonitor.ts @@ -0,0 +1,177 @@ +/* + * 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 { ObserveSingleton } from "../base/observeSingleton"; +import { IBindingSource } from "../base/mutableStateMeta"; +import { StateMgmtConsole } from "../tools/stateMgmtDFX"; +import { ITrackedDecoratorRef } from "../base/mutableStateMeta"; +import { RenderIdType, IMonitorValue, IMonitorDecoratedVariable, IMonitor, IMonitorPathInfo } from "../decorator"; + +export class MonitorFunctionDecorator implements IMonitorDecoratedVariable, IMonitor { + public static readonly MIN_MONITOR_ID: RenderIdType = 0x20000000; + public static nextWatchId_ = MonitorFunctionDecorator.MIN_MONITOR_ID; + + private readonly monitorFunction_: (m: IMonitor) => void; + private readonly values_: MonitorValueInternal[] = new Array(); + + constructor(pathLambda: IMonitorPathInfo[], monitorFunction: (m: IMonitor) => void) { + this.monitorFunction_ = monitorFunction; + pathLambda.forEach((info: IMonitorPathInfo) => { + this.values_.push(new MonitorValueInternal(info.path, info.lambda, this)); + }) + this.readInitialMonitorValues(); + } + + public value(path?: string): IMonitorValue | undefined { + if (path) { + for (let monitorValue of this.values_) { + if (monitorValue.dirty && monitorValue.path === path) { + return new MonitorValuePublic(monitorValue); + } + } + } else { + for (let monitorValue of this.values_) { + if (monitorValue.dirty) { + return new MonitorValuePublic(monitorValue); + } + } + } + return undefined; + } + + public notifyChangesForPath(monitorPath: ITrackedDecoratorRef): boolean { + return this.recordDependenciesForMonitorValue(false, monitorPath as MonitorValueInternal); + } + + public runMonitorFunction(): void { + if (this.dirty.length == 0) { + return; + } + try { + this.monitorFunction_(this); + } catch (e: Exception) { + StateMgmtConsole.log(`Error caught while executing @Monitor function: '${e}'`); + } finally { + this.values_.forEach((monitorValue: MonitorValueInternal) => { monitorValue.reset(); }); + } + } + + public get dirty(): string[] { + let ret = new Array(); + this.values_.forEach((monitorValue: MonitorValueInternal) => { + if (monitorValue.dirty) { + ret.push(monitorValue.path); + } + }) + return ret; + } + + private readInitialMonitorValues(): void { + this.values_.forEach((monitorValue: MonitorValueInternal) => { + this.recordDependenciesForMonitorValue(true, monitorValue); + }) + } + + /** + * Reads monitor value + * @param isFirstRun true to clear previous bindings, and read value for first time + * @param monitorValue + * @returns true if value is dirty + */ + private recordDependenciesForMonitorValue(isFirstRun: boolean, monitorValue: MonitorValueInternal): boolean { + if (!isFirstRun) { + monitorValue.clearReverseBindings(); + } + let renderingComponentBefore = ObserveSingleton.instance.renderingComponent; + let renderingComponentRefBefore = ObserveSingleton.instance.renderingComponentRef; + ObserveSingleton.instance.renderingComponent = ObserveSingleton.RenderingMonitor; + ObserveSingleton.instance.renderingComponentRef = monitorValue; + let dirty = monitorValue.readValue(isFirstRun); + ObserveSingleton.instance.renderingComponent = renderingComponentBefore; + ObserveSingleton.instance.renderingComponentRef = renderingComponentRefBefore; + return dirty; + } +} + +export class MonitorValueInternal implements IMonitorValue, ITrackedDecoratorRef { + public id: RenderIdType; + public weakThis: WeakRef; + public reverseBindings: Set> = new Set>(); + public before: Any; + public now: Any; + public path: string; + public monitor: MonitorFunctionDecorator; + + private dirty_: boolean = false; + private readonly lambda: () => Any; + + constructor(path: string, lambda: () => Any, monitor: MonitorFunctionDecorator) { + this.id = MonitorFunctionDecorator.nextWatchId_++; + this.path = path; + this.lambda = lambda; + this.weakThis = new WeakRef(this); + this.monitor = monitor; + ObserveSingleton.instance.addToTrackedRegistry(this, this.reverseBindings); + } + + public clearReverseBindings(): void { + this.reverseBindings.forEach((dep: WeakRef) => { + let ref = dep.deref(); + if (ref) { + ref.clearBindingRefs(this.weakThis); + } else { + this.reverseBindings.delete(dep); + } + }) + } + /** + * Executes lambda and check if value is dirty + * @param isFirstRun not dirty, now = before + * @return true if before !== now + */ + public readValue(isFirstRun: boolean = false): boolean { + try { + this.now = this.lambda(); + if (isFirstRun) { + this.before = this.now; + return false; + } + this.dirty_ = this.before !== this.now + return this.dirty; + } catch(e) { + StateMgmtConsole.log(`Caught exception while reading monitor path ${this.path} value: ${e}.`); + return false; + } + } + + public get dirty(): boolean { + return this.dirty_; + } + + public reset(): void { + this.before = this.now; + this.dirty_ = false; + } +} + +export class MonitorValuePublic implements IMonitorValue { + public before: T; + public now: T; + public path: string; + constructor(value: MonitorValueInternal) { + this.before = value.before as T; + this.now = value.now as T; + this.path = value.path; + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/interop/interopStorage.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/interop/interopStorage.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2c4ffc419a778f7304519dcafc57cdb175c6d14 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/interop/interopStorage.ts @@ -0,0 +1,568 @@ +/* + * 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 { DecoratedVariableBase } from '../decoratorImpl/decoratorBase'; +import { LocalStorage } from '../storage/localStorage'; +import { StorageBase } from '../storage/storageBase'; +import { AbstractProperty, IStorageProperties } from '../storage/storageProperty'; +import { StorageProperty } from '../storage/storageBase'; +import { ExtendableComponent } from '../../component/extendableComponent'; +import { WatchFuncType } from '../decorator'; +import { StorageLinkDecoratedVariable } from '../decoratorImpl/decoratorStorageLink'; +import { StateMgmtConsole } from '../tools/stateMgmtDFX'; + +/** + * Interop Storage with ArkTS1.1 + * 1) Each Storage stores the value in each map by API call; + * 2) ArkTS1.1 Storage will provide getValue, removeValue, clear for ArkTS1.2, in addition will call + * key(add/remove/clear) function provided by ArkTS1.2 to speed up key search in ArkTS1.2; + * 3) ArkTS1.2 Storage will provide getValue, removeValue, clear, getSize for ArkTS1.1, avoid to slow down + * the set operation in ArkTS1.2, it will no key speed up for ArkTS1.1 + * 4) getValue function provided by ArkTS1.1 will return ESValue(ArkTS1.1 ObservedPropertyPU), ArkTS1.2 need to create + * static StorageProperty and storage in interopStorage to speed up. + * 5) getValue function provided by ArkTS1.2 will return ESValue(rkTS1.1 ObservedPropertyPU), ArkTS1.2 need to create + * dynamic ObservedPropertyPU and storage in origin static StorageProperty. + * 6) Static StorageProperty and Dynamic ObservedPropertyPU will store same raw value and will bind each other to + * notify value change event. + */ +class InteropStorageValue { + value?: DecoratedVariableBase; +} +export class InteropStorageBase extends StorageBase { + // the Lazy key/value info of Storage in ArkTS1.1 + protected interopStorage_ = new Map(); + + protected totalKeys_ = new Map(); + + private proxy?: ESValue; + + public getProxy(): ESValue | undefined { + if (this.proxy === undefined) { + this.BindDynamicStorage(); + } + return this.proxy; + } + + public setProxy(proxy: ESValue): void { + this.proxy = proxy; + } + + // get value from Storage in ArkTS1.1 + protected getDynamicValue_: (value: string) => ESValue = (value: string): ESValue => { + throw new Error('not implement'); + }; + protected removeDynamicValue_: (value: string) => boolean = (value: string): boolean => { + throw new Error('not implement'); + }; + protected clearDynamicValue_: () => boolean = (): boolean => { + throw new Error('not implement'); + }; + + public constructor() { + super(); + } + + public BindDynamicStorage(): void { + // call ArkTS1.1 Storage to bind static Storage. + const global = ESValue.getGlobal(); + const bindFunc = global.getProperty('bindStaticLocalStorage'); + if (bindFunc.isNull() || bindFunc.isUndefined()) { + StateMgmtConsole.log('fail to find bindStaticLocalStorage'); + return; + } + // these function will call by ArkTS1.1 to speed up dynamic key search for ArkTS1.2. + const addKeyFunc = (key: string) => { + this.interopStorage_.set(key, new InteropStorageValue()); + }; + const removeKeyFunc = (key: string) => { + this.interopStorage_.delete(key); + }; + const clearKeyFunc = () => { + this.interopStorage_.clear(); + // need to clear ArkTS1.2 too + super.clear(); + }; + // used by ArkTS1.1 to interop with static storage map. + const getValue = (key: string) => { + return this.getStoragePropertyForDynamic(key); + }; + const removeValue = (key: string) => { + super.delete(key); + }; + const getSize = () => { + return super.size(); + }; + const getKeys = () => { + const keys: Set = this.keySet; + return keys; + }; + // used by ArkTS1.2 to interop with dynamic storage map. + const setGetValueFunc = (event: (value: string) => ESValue) => { + this.getDynamicValue_ = event; + }; + const setRemoveValueFunc = (event: (value: string) => boolean) => { + this.removeDynamicValue_ = event; + }; + const setClearValueFunc = (event: () => boolean) => { + this.clearDynamicValue_ = event; + }; + let proxyStorage = bindFunc.invoke( + ESValue.wrap(getValue), + ESValue.wrap(removeValue), + ESValue.wrap(getSize), + ESValue.wrap(getKeys), + ESValue.wrap(addKeyFunc), + ESValue.wrap(removeKeyFunc), + ESValue.wrap(clearKeyFunc), + ESValue.wrap(setGetValueFunc), + ESValue.wrap(setRemoveValueFunc), + ESValue.wrap(setClearValueFunc) + ); + this.setProxy(proxyStorage); + } + + // return ArkTS1.1 ObservedPropertyPU object. + public getStoragePropertyForDynamic(value: string): Any { + const storage = super.__getStoragePropUnsafe(value); + if (storage == undefined) { + return undefined; + } + const createState = ESValue.getGlobal().getProperty('createStateVariable'); + if (createState.isNull() || createState.isUndefined()) { + StateMgmtConsole.log('fail to find createStateVariable'); + return undefined; + } + const state = storage! as StorageProperty; + if (state.getProxy() === undefined) { + const setSource = ((value: Any) => { + state.set(value); + }); + const proxy = createState.invoke(ESValue.wrap(state!.get()), ESValue.wrap(setSource)); + state.setProxy(proxy); + const setProxyValue = ((value: Any) => { + proxy.invokeMethod('set', ESValue.wrap(value)); + }); + state.setProxyValue = setProxyValue; + const notifyCallback = ((propertyName: string) => { + proxy.invokeMethod('notifyPropertyHasChangedPU'); + }); + state.setNotifyCallback(notifyCallback); + } + return state.getProxy()!.unwrap(); + } + + // TODO: ArkTS1.1 -> ArkTS1.2 + public getStoragePropertyFromDynamic(value: string): StorageProperty | undefined { + throw new Error('not implement!'); + } + + public has(key: string): boolean { + const value = super.__getStoragePropUnsafe(key); + if (value != undefined) { + return true; + } + // check value in ArkTS1.1 + return this.interopStorage_.has(key); + } + + /** + * Provide names of all properties in LocalStorage + * + * @returns { Set } return (unordered) Set of keys + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public keys(): IterableIterator { + this.totalKeys_.clear(); + this.interopStorage_.forEach((value: InteropStorageValue, key: string) => { + this.totalKeys_.set(key, key); + }); + this.keySet.forEach((value: string) => { + this.totalKeys_.set(value, value); + }); + return this.totalKeys_.keys(); + } + + /** + * Returns number of properties in LocalStorage + * same as Map.prototype.size() + * + * @returns { number } return number of properties + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public size(): number { + return super.size() + this.interopStorage_.size; + } + + /** + * Returns value of given property + * return undefined if no property with this name + * or if expected ttype can not be assigned from type configured + * for this property in storage. + * + * @param { string } propName - property name + * @param {Type} ttype - data type + * @returns { T | undefined } property value if found or undefined + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public get(key: string, ttype: Type): T | undefined { + let value = super.get(key, ttype); + if (value != undefined) { + return value as T; + } + // search ArkTS1.1 Storage. + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + return undefined; + } + if (interopValue.value) { + return (interopValue.value as StorageProperty).get(); + } + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + return (interopValue.value as StorageProperty).get(); + } + + /** + * Create an AbstractProperty if property with given name already exists in storage + * and if given ttype equals the type configured for this property in storage. + * + * @param { string } propName LocalStorage property name + * @param {Type} ttype - data type + * @returns { AbstractProperty | undefined } AbstractProperty object if aforementioned conditions are + * satisfied. + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public ref(key: string, ttype: Type): AbstractProperty | undefined { + let value = super.ref(key, ttype); + if (value != undefined) { + return value; + } + // search ArkTS1.1 Storage. + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + return undefined; + } + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + const state = interopValue.value as StorageProperty; + const reference = state.mkRef(key, ttype); + state.registerWatch(reference); + return reference; + } + /** + * Update value of existing property with given name. + * update only if new value is assignable to type for this property configured in storage + * does not create a new property in storage, need to use @see setOrCreate for this purpose. + * + * @param { string } propName + * @param { T } newValue - must be of type T + * @returns { boolean } return true if key exists, and newValue can be assigned to configured type. + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public set(key: string, newValue: T): boolean { + let result = super.update(key, newValue); + if (result) { + return result; + } + // Search ArkTS1.1 + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + return false; + } + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + (interopValue.value as StorageProperty).set(newValue); + return true; + } + + /** + * case A: if property with given names does not exists in storage, yet: + * if given value can be assigned to given ttype, then + * - create new property + * - configure its type to be given ttype + * - set its initial value to given value + * otherwise do nothing, return false + * + * case B: if property with given names exists in storage already. + * call @see set() and return its return value; + * + * @param propName + * @param newValue + * @param ttype + * @returns true on 1) create new property and newtValue can be assigned to stated type, or + * 2) update existing property and newValue can be assigned to type configured for this + * property in storage. + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public setOrCreate(key: string, newValue: T, ttype: Type): boolean { + const expectedTtypeOpt = super.getType(key); + if (expectedTtypeOpt === undefined) { + // Check ArkTS1.1 + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + // create new entry, remember permissible ttype + return super.createAndSet(key, ttype, newValue); + } + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + (interopValue.value as StorageProperty).set(newValue); + return true; + } + return this.set(key, newValue); + } + + /** + * case A: if property with given name does not exists in storage, yet: + * if given defaultValue is assignable to given type, then + * - create new property with given name in storage + * - configure its type to be the given ttype + * - create a AbstractProperty that refers to this storage property + * and return it + * otherwise create no new property in storage, and return undefined. + * + * case B: if property with given name already exists in storage + * (defaultValue is not used): + * if given type equals the type configured for this property in storage + * - create a AbstractProperty that refers to this storage property. + * and return it. + * otherwise do not touch the storage property, return undefined. + * + * @param { string } propName LocalStorage property name + * @param { T } defaultValue If property does not exist in LocalStorage, + * create it with given default value. + * @param {Type} ttype - data type + * @returns { AbstractProperty } AbstractProperty object or undefined as defined above + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public setAndRef(key: string, defaultValue: T, ttype: Type): AbstractProperty | undefined { + const ttypeOpt = super.getType(key); + if (ttypeOpt === undefined) { + // search ArkTS1.1 Storage. + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + // create new entry, remember permissible ttype, set with defaultValue + if (!super.createAndSet(key, ttype, defaultValue)) { + // creation failed + return undefined; + } + const link = super.ref(key, ttype); + return link; + } + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + const state = interopValue.value as StorageProperty; + const reference = state.mkRef(key, ttype); + state.registerWatch(reference); + return reference; + } + const link = super.ref(key, ttype); + // TODO finalization reg link + return link; + } + + /** + * Delete property from StorageBase + * Use with caution: + * Before deleting a prop from LocalStorage all its subscribers need to + * unsubscribe from the property. + * This method fails and returns false if given property still has subscribers + * Another reason for failing is unknown property. + * Developer advise: + * instance of @StorageLink / @LocalStorageLink decorated variable is a subscriber of storage property, + * AbstractProperty instance created by ref, setAndRef, link, or setAndLink is also a subscriber. + * + * @param { string } propName + * @returns { boolean } false if method failed + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + delete(key: string): boolean { + let result = super.delete(key); + if (result) { + return result; + } + if (!this.interopStorage_.has(key)) { + return false; + } + result = this.removeDynamicValue_(key); + if (result) { + this.interopStorage_.delete(key); + return true; + } + return false; + } + + /** + * Delete all properties from the LocalStorage instance + * Precondition is that there are no subscribers. + * method returns false and deletes no properties if there is one or more storage properties + * that still have subscribers + * + * @returns { boolean } + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + clear(): boolean { + let result1 = super.clear(); + let result2 = this.clearDynamicValue_(); + return result1 && result2; + } + + /** + * Internal function to create a @StorageLink. Not part of the SDK + * @param owner + * @param key + * @param varName + * @param defaultValue + * @param ttype + * @param watchFunc + * @returns + */ + public __makeStorageLink( + owner: ExtendableComponent, + key: string, + varName: string, + defaultValue: T, + ttype: Type, + watchFunc?: WatchFuncType + ): StorageLinkDecoratedVariable | undefined { + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + // Use ArkTS1.2 + return super.makeStorageLink(owner, key, varName, defaultValue, ttype, watchFunc); + } + // Use ArkTS1.1 + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + const state = interopValue.value as StorageProperty; + const link = state.makeStorageLink(owner, key, varName, watchFunc); + state.registerWatch(link); + return link; + } + + /** + * Internal function to get the StorageProp for key, no type verification + * use for test code only + * not part of the SDK + * @param key + * @returns + */ + public __getStoragePropUnsafe(key: string): StorageProperty | undefined { + let value = super.__getStoragePropUnsafe(key); + if (value != undefined) { + return value; + } + // Check ArkTS1.1 + let interopValue = this.interopStorage_.get(key); + if (interopValue == undefined) { + return undefined; + } + if (!interopValue.value) { + // initialize interop value by ArkTS1.1 + interopValue.value = this.getStoragePropertyFromDynamic(key); + } + return interopValue.value as StorageProperty; + } +} + +export class InteropAppStorageBase extends InteropStorageBase { + public constructor() { + super(); + this.BindDynamicStorage(); + } + + public BindDynamicStorage(): void { + // call ArkTS1.1 Storage to bind static Storage. + const global = ESValue.getGlobal(); + const bindFunc = global.getProperty('bindStaticAppStorage'); + if (bindFunc.isNull() || bindFunc.isUndefined()) { + StateMgmtConsole.log('fail to find bindStaticAppStorage'); + return; + } + // these function will call by ArkTS1.1 to speed up dynamic key search for ArkTS1.2. + const addKeyFunc = (key: string) => { + this.interopStorage_.set(key, new InteropStorageValue()); + }; + const removeKeyFunc = (key: string) => { + this.interopStorage_.delete(key); + }; + const clearKeyFunc = () => { + this.interopStorage_.clear(); + // need to clear ArkTS1.2 too + super.clear(); + }; + // used by ArkTS1.1 to interop with static storage map. + const getValue = (key: string) => { + return this.getStoragePropertyForDynamic(key); + }; + const removeValue = (key: string) => { + super.delete(key); + }; + const getSize = () => { + return super.size(); + }; + const getKeys = () => { + const keys: Set = this.keySet; + return keys; + }; + // used by ArkTS1.2 to interop with dynamic storage map. + const setGetValueFunc = (event: (value: string) => ESValue) => { + this.getDynamicValue_ = event; + }; + const setRemoveValueFunc = (event: (value: string) => boolean) => { + this.removeDynamicValue_ = event; + }; + const setClearValueFunc = (event: () => boolean) => { + this.clearDynamicValue_ = event; + }; + bindFunc.invoke( + ESValue.wrap(getValue), + ESValue.wrap(removeValue), + ESValue.wrap(getSize), + ESValue.wrap(getKeys), + ESValue.wrap(addKeyFunc), + ESValue.wrap(removeKeyFunc), + ESValue.wrap(clearKeyFunc), + ESValue.wrap(setGetValueFunc), + ESValue.wrap(setRemoveValueFunc), + ESValue.wrap(setClearValueFunc) + ); + } +} + +export class InteropAppStorage extends LocalStorage { + constructor(){ + super(); + this.store_ = new InteropAppStorageBase(); + } +} + diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/environment.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/environment.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a34dd4dab7155585c21caa1bbe4a9de241e05fb --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/environment.ts @@ -0,0 +1,155 @@ +/* + * 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 { AppStorage } from './appStorage'; +import { ArkUIAniModule } from 'arkui.ani'; + +interface IAniEnvironment { + getAccessibilityEnabled(): boolean; + getColorMode(): number; + getFontScale(): number; + getFontWeightScale(): number; + getLayoutDirection(): string; + getLanguageCode(): string; +} + +class AniEnvironment implements IAniEnvironment { + getAccessibilityEnabled(): boolean { + return ArkUIAniModule._Env_GetAccessibilityEnabled(); + } + getColorMode(): number { + return ArkUIAniModule._Env_GetColorMode(); + } + getFontScale(): number { + return ArkUIAniModule._Env_GetFontScale(); + } + getFontWeightScale(): number { + return ArkUIAniModule._Env_GetFontWeightScale(); + } + getLayoutDirection(): string { + return ArkUIAniModule._Env_GetLayoutDirection(); + } + getLanguageCode(): string { + return ArkUIAniModule._Env_GetLanguageCode(); + } +} + +interface EnvPropsOptions { + key: string; + defaultValue: number | string | boolean; +} + +/** + * Environment + * + * Injects device properties ("environment") into AppStorage + * + */ +class Environment { + private static instance_: Environment | undefined = undefined; + private props_: Map = new Map(); + private readonly aniEnvironment: AniEnvironment = new AniEnvironment(); + private ttypeMap_: Map = new Map([ + ['accessibilityEnabled', Type.from()], + ['layoutDirection', Type.from()], + ['languageCode', Type.from()], + ['colorMode', Type.from()], + ['fontScale', Type.from()], + ['fontWeightScale', Type.from()], + ]); + + public aboutToBeDeleted(): void { + Environment.getOrCreate().props_.forEach((_, key) => { + AppStorage.delete(key); + }); + Environment.getOrCreate().props_.clear(); + } + + private static getOrCreate(): Environment { + if (Environment.instance_) { + // already initialized + return Environment.instance_!; + } + + Environment.instance_ = new Environment(); + return Environment.instance_!; + } + + public static envProp(key: string, value: T): boolean { + if (!Environment.getOrCreate().ttypeMap_.has(key)) { + return false; // Invalid key + } + const ttype = Environment.getOrCreate().ttypeMap_.get(key)!; + return Environment.getOrCreate().envPropInternal(key, value, ttype); + } + + private envPropInternal(key: string, value: T, ttype: Type): boolean { + if (AppStorage.has(key, ttype)) { + return false; + } + + let tmp: Any = undefined; + switch (key) { + case 'accessibilityEnabled': + tmp = Environment.getOrCreate().aniEnvironment.getAccessibilityEnabled(); + break; + case 'colorMode': + tmp = Environment.getOrCreate().aniEnvironment.getColorMode(); + break; + case 'fontScale': + tmp = Environment.getOrCreate().aniEnvironment.getFontScale(); + break; + case 'fontWeightScale': + tmp = Math.round(Environment.getOrCreate().aniEnvironment.getFontWeightScale() * 100) / 100; + break; + case 'layoutDirection': + tmp = Environment.getOrCreate().aniEnvironment.getLayoutDirection(); + break; + case 'languageCode': + tmp = Environment.getOrCreate().aniEnvironment.getLanguageCode(); + break; + default: + tmp = value as Any; + } + + if (tmp === undefined || tmp === -1 || tmp === '') { + tmp = value as Any; + } + + const prop = AppStorage.setAndRef(key, tmp as T, ttype); + if (!prop) { + return false; + } + + Environment.getOrCreate().props_.set(key, tmp); + return true; + } + + public static envProps(properties: EnvPropsOptions[]): void { + properties.forEach((prop) => { + const key: string = prop.key; + const defaultValue: number | string | boolean = prop.defaultValue; + const ttype = Environment.getOrCreate().ttypeMap_.get(key)!; + Environment.envProp(key, defaultValue); + }); + } + + public static keys(): Array { + return Environment.getOrCreate().keysInternal(); + } + + public keysInternal(): Array { + return Array.from(Environment.getOrCreate().props_.keys()); + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/persistentStorage.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/persistentStorage.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b824e70667b069fd6c024bd15858886bdcd6931 --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/persistentStorage.ts @@ -0,0 +1,362 @@ +/* + * 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 { IStorageProperty } from './storageBase'; +import { AbstractProperty, OnChangeType } from './storageProperty'; +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; + clear(): void; + delete(key: string): void; +} + +// class JsonElement{} +/** + * Define toJson type function. + * + * @typedef { function } ToJSONType + * @param { T } value toJson value + * @returns { JsonElement } Json stringify element object + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ +export type ToJSONType = (value: T) => jsonx.JsonElement; + +/** + * Define fromJson type function. + * + * @typedef { function } FromJSONType + * @param { JsonElement } element json element + * @returns { T } deserialization result + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ +export type FromJSONType = (element: jsonx.JsonElement) => T; + +class TypedMap { + private key2Type_ = new Map(); + private key2Value_ = new Map(); + + public add(key: string, ttype: Type, sp: IStorageProperty): boolean { + const typeOpt = this.key2Type_.get(key); + if (typeOpt !== undefined) { + if (!typeOpt!.equals(ttype)) { + return false; + } + } + this.key2Type_.set(key, ttype); + this.key2Value_.set(key, sp); + return true; + } + + public get(key: string, expectedTtype: Type): IStorageProperty | undefined { + const typeOpt = this.key2Type_.get(key); + if (typeOpt === undefined || !typeOpt!.equals(expectedTtype)) { + return undefined; + } + return this.key2Value_.get(key); + } + + public delete(key: string): boolean { + const r1 = this.key2Type_.delete(key); + const ref = this.key2Value_.get(key); + if (ref !== undefined) { + const regId = (ref as AbstractProperty).getMyTriggerFromSourceWatchId(); + AppStorage.__getStoragePropUnsafe(key)!.__unregister(regId); + } + const r2 = this.key2Value_.delete(key); + return r1 && r2; + } + + public keys(): Array { + return Array.from(this.key2Type_.keys()); + } +} + +class AniStorage implements IAniStorage { + get(key: string): string | undefined { + return ArkUIAniModule._PersistentStorage_Get(key); + } + set(key: string, val: string): void { + ArkUIAniModule._PersistentStorage_Set(key, val); + } + has(key: string): boolean { + return ArkUIAniModule._PersistentStorage_Has(key); + } + clear(): void { + ArkUIAniModule._PersistentStorage_Clear(); + } + delete(key: string): void { + ArkUIAniModule._PersistentStorage_Delete(key); + } +} + +/** + * Defines the PersistentStorage interface. + * + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ +class PersistentStorage { + private static instance_: PersistentStorage | undefined = undefined; + private map_: TypedMap = new TypedMap(); + private simpleTypeSet: Set = new Set([Type.from(), Type.from(), Type.from()]); + private readonly storage_: IAniStorage = new AniStorage(); + + private static getOrCreate(): PersistentStorage { + if (PersistentStorage.instance_) { + // already initialized + return PersistentStorage.instance_!; + } + + PersistentStorage.instance_ = new PersistentStorage(); + return PersistentStorage.instance_!; + } + + /** + * Add property 'key' to AppStorage properties whose current value will be + * persistent. + * If AppStorage does not include this property it will be added and initializes + * with given value + * + * @param { string } key - property name + * @param { T } defaultValue - If AppStorage does not include this property it will be initialized with this value + * @param { Type } ttype - type of this property. + * @param { ToJSONType } [toJson] - serialization function + * @param { FromJSONType } [fromJson] - deserialization function + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public static persistProp( + key: string, + ttype: Type, + defaultValue: T, + toJson?: ToJSONType, + fromJson?: FromJSONType + ): boolean { + return PersistentStorage.getOrCreate().persistPropInternal(key, ttype, defaultValue, toJson, fromJson); + } + + private persistPropInternal( + key: string, + ttype: Type, + defaultValue: T, + toJson?: ToJSONType, + fromJson?: FromJSONType + ): boolean { + try { + if (!this.simpleTypeSet.has(ttype) && (!toJson || !fromJson)) { + StateMgmtConsole.log( + `Object Types for key ${key} requires toJson and fromJson functions to be defined` + ); + } + const apOpt = PersistentStorage.getOrCreate().map_.get(key, ttype); + if (apOpt !== undefined) { + // persisted property already + StateMgmtConsole.log(`persistProp key ${key} persistedAlready`); + return false; + } + + // case 1: property exists in storage already and start to persist it + if (AppStorage.keySets().has(key)) { + const success = PersistentStorage.getOrCreate().__startToPersistStorageProperty(key, ttype, toJson); + if (!success) { + StateMgmtConsole.log(`Failed to start persistence for existing key ${key}`); + } + return success; + } + // case 2: Read from disk, set in AppStorage and start persistence + if ( + PersistentStorage.getOrCreate().__readFromDiskSetAndPersist( + key, + ttype, + this.simpleTypeSet.has(ttype) ? undefined : fromJson, + this.simpleTypeSet.has(ttype) ? undefined : toJson + ) + ) { + return true; + } + + // case 3: Create new property with default value and start persistence + const success = PersistentStorage.getOrCreate().__createNewAndPersist( + key, + ttype, + defaultValue, + this.simpleTypeSet.has(ttype) ? undefined : toJson, + this.simpleTypeSet.has(ttype) ? undefined : fromJson + ); + if (!success) { + StateMgmtConsole.log(`Failed to create and persist key ${key} with default value`); + } + return success; + } catch (error) { + StateMgmtConsole.log(`Unexpected error in persistProp for key ${key}: ${error}`); + return false; + } + } + + /** + * Reverse of @see persistProp + * + * @param { string } key - no longer persist the property named key + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public static deleteProp(key: string): void { + PersistentStorage.getOrCreate().deletePropInternal(key); + } + + private deletePropInternal(key: string): void { + // Remove from AniStorage + PersistentStorage.getOrCreate().storage_.delete(key); + // Remove from TypedMap and need to unregister from AppStorage + PersistentStorage.getOrCreate().map_.delete(key); + } + + // Note: persistProps can not be supported because each + // property has different T + // framework can not recover T from Array> + // must use separate persistProp calls instead + + /** + * Inform persisted AppStorage property names + * + * @returns { Array } array of AppStorage keys + * @static + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @since 20 + */ + public static keys(): Array { + return PersistentStorage.getOrCreate().keysInternal(); + } + + private keysInternal(): Array { + return PersistentStorage.getOrCreate().map_.keys(); + } + + // case 1: neither on disk nor in storage + // create with default value and start to persist + private __createNewAndPersist( + key: string, + ttype: Type, + defaultValue: T, + toJson?: ToJSONType, + fromJson?: FromJSONType + ): boolean { + if (!AppStorage.setOrCreate(key, defaultValue, ttype)) { + StateMgmtConsole.log(`__createNewAndPersist return false`); + return false; + } + return PersistentStorage.getOrCreate().__startToPersistStorageProperty(key, ttype, toJson); + } + + // case 2: not in storage + // try to read from disk (return false if not found) + // create in storage with read value and start to persist + private __readFromDiskSetAndPersist( + key: string, + ttype: Type, + fromJson?: FromJSONType, + toJson?: ToJSONType + ): boolean { + // Step 1: Read JSON string from storage + const jsonString = PersistentStorage.getOrCreate().storage_.get(key); + if (jsonString === undefined) { + return false; // Not found on disk + } + + try { + if (this.simpleTypeSet.has(ttype)) { + // Step 2: simple type just parse from disk + const value = JSON.parse(jsonString, ttype); + + // Step 3: Store the value in AppStorage + AppStorage.setOrCreate(key, value, ttype); + + // Step 4: persist the property + return PersistentStorage.getOrCreate().__startToPersistStorageProperty(key, ttype, toJson); // returns true on success + } else { + // Step 2: Parse JSON string into JsonElement + const jsonElement = JSON.parseJsonElement(jsonString); + + // Step 3: Convert JsonElement to type T using fromJson + if (fromJson === undefined) { + return false; // Cannot deserialize without fromJson + } + const value: T = fromJson(jsonElement); + + // Step 4: Store the value in AppStorage + AppStorage.setOrCreate(key, value, ttype); + + // Step 5: persist the property + return PersistentStorage.getOrCreate().__startToPersistStorageProperty(key, ttype, toJson); // returns true on success + } + } catch (error) { + if (error instanceof jsonx.JsonError) { + StateMgmtConsole.log(`JSON parsing error: ${error.message}`); + } else { + StateMgmtConsole.log(`Unexpected error: ${error}`); + } + return false; // Failure due to parsing or deserialization error + } + } + + // case 3 - used by case 1 and 2: property exists in storage (caller needs to verify) + // start to persist it + private __startToPersistStorageProperty(key: string, ttype: Type, toJson?: ToJSONType): boolean { + const ref = AppStorage.ref(key, ttype) as AbstractProperty | undefined; // Explicitly specify T + if (ref === undefined) { + StateMgmtConsole.log(`Failed to get AppStorage ref for key ${key}`); + return false; + } + PersistentStorage.getOrCreate().map_.add(key, ttype, ref); + const writeToDiskOnChange: OnChangeType = (key1: string, newValue: T) => { + if (key !== key1) { + StateMgmtConsole.log('persistProp callback will non-matching key. Ignoring. Internal error.'); + return; + } + try { + if (this.simpleTypeSet.has(ttype)) { + const jsonString = JSON.stringify(newValue); + PersistentStorage.getOrCreate().storage_.set(key, jsonString); + } else { + if (!toJson) { + StateMgmtConsole.log(`Object Types for key ${key} requires toJson functions to be defined`); + } + const jsonElement = toJson!(newValue); + // convert JsonElement to jsonString + const jsonString = JSON.stringifyJsonElement(jsonElement); + PersistentStorage.getOrCreate().storage_.set(key, jsonString); + } + } catch (error) { + if (error instanceof jsonx.JsonError) { + StateMgmtConsole.log(`JSON serialization error for key ${key}: ${error.message}`); + } else { + StateMgmtConsole.log(`Unexpected error persisting key ${key}: ${error}`); + } + } + }; + ref.onChange(writeToDiskOnChange); + PersistentStorage.getOrCreate().map_.add(key, ttype, ref); + return true; + } +} diff --git a/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/storageProperty.ts b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/storageProperty.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f39c760e3f2f46e5a64319accf69489160b521f --- /dev/null +++ b/frameworks/bridge/arkts_frontend/koala_mirror/arkoala-arkts/arkui/src/ets/stateManagement/storage/storageProperty.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2021-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 { IStorageProperty } from './storageBase'; +import { DecoratedV1VariableBase } from '../decoratorImpl/decoratorBase'; +import { WatchFunc } from '../decoratorImpl/decoratorWatch'; +import { StateMgmtConsole } from '../tools/stateMgmtDFX'; + +type GetType = () => T; +type SetType = (newVal: T) => void; +export type OnChangeType = (propName: string, newValue: T) => void; + +export interface IStorageProperties { + value: Any; + ttype: Type; +} + +export class AbstractProperty extends DecoratedV1VariableBase implements IStorageProperty { + private readonly key_: string; + private readonly ttype_: Type; + private readonly get_: GetType; + private readonly set_: SetType; + + constructor(key: string, ttype: Type, get: GetType, set: SetType) { + super('AbstractProperty', null, key); + StateMgmtConsole.log(`create new AbstractProperty for key '${key}' `); + + this.key_ = key; + this.ttype_ = ttype; + this.get_ = get; + this.set_ = set; + + const initValue: T = this.get_(); + + // @Watch + // if initial value is object, register so that property changes trigger + // @Watch function exec + this.registerWatchForObservedObjectChanges(initValue); + // registerWatch to source is done in the factory function + } + + // FIXME change to info() + // this needs renaming of info property (not function) in base classes! + public info_(): string { + return this.key_; + } + + public ttype(): Type { + return this.ttype_; + } + + public get(): T { + return this.get_(); + } + + public set(newValue: T): void { + this.set_(newValue); + } + + public onChange(onChangeCbFunc: OnChangeType | undefined): void { + if (onChangeCbFunc === undefined) { + // clear all register callbacks + this._watchFuncs.clear(); + } + if (typeof onChangeCbFunc === 'function') { + const watchFunc = (propName: string): void => { + (onChangeCbFunc as OnChangeType)(propName, this.get()); + }; + const watchFuncObj = new WatchFunc(watchFunc); + this._watchFuncs.set(watchFuncObj.id(), watchFuncObj); + } + } +} + +/** + * for backward compatibility only + * + */ +export class SubscribedAbstractProperty extends AbstractProperty { + constructor(key: string, ttype: Type, get: GetType, set: SetType) { + super(key, ttype, get, set); + } + + public aboutToBeDeleted(): void {} +} diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/interop/interopStorage.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/interop/interopStorage.ts index 77aabf992dce6b5745f17dae25c824ce06e90d4d..d1360f4ccf0b21af99a8196246e4256c7fcdd869 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/interop/interopStorage.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/interop/interopStorage.ts @@ -159,7 +159,7 @@ export class InteropStorageBase extends StorageBase { // return ArkTS1.1 ObservedPropertyPU object. public getStoragePropertyForDynamic(value: string): Any { - const storage = super.__getStoragePropUnsafe(value); + const storage = super.__getStoragePropUnsafe(value); if (storage === undefined) { return undefined; } @@ -168,14 +168,14 @@ export class InteropStorageBase extends StorageBase { StateMgmtConsole.log('fail to find createStateVariable'); return undefined; } - const state = storage! as StorageProperty; + const state = storage! as StorageProperty; if (state.getProxy() === undefined) { - const setSource = (value: NullishType): void => { + const setSource = (value: Any): void => { state.set(value); }; const proxy = createState.invoke(ESValue.wrap(state!.get()), ESValue.wrap(setSource)); state.setProxy(proxy); - const setProxyValue = (value: NullishType): void => { + const setProxyValue = (value: Any): void => { proxy.invokeMethod('set', ESValue.wrap(value)); }; state.setProxyValue = setProxyValue; diff --git a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/stateMgmtDFX.ts b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/stateMgmtDFX.ts index 3d2842409ff0d18ca75719b161ef2d837b5c195a..00b6f8404d951bc8643b154137103ca6ea608b59 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/stateMgmtDFX.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/arkoala-arkts/arkui-ohos/src/stateManagement/tools/stateMgmtDFX.ts @@ -36,7 +36,7 @@ class ViewInfo { class DecoratorInfo { decorator?: string; propertyName?: string; - value?: NullishType; + value?: Any; } export class DumpInfo { @@ -45,7 +45,7 @@ export class DumpInfo { } export class StateMgmtDFX { - static getDecoratedVariableInfo(view: NullishType, dumpInfo: DumpInfo, isV2: boolean): void { + static getDecoratedVariableInfo(view: Any, dumpInfo: DumpInfo, isV2: boolean): void { if (isV2) { StateMgmtDFX.dumpV2VariableInfo(view, dumpInfo); } else { @@ -53,7 +53,7 @@ export class StateMgmtDFX { } } - static dumpV1VariableInfo(view: NullishType, dumpInfo: DumpInfo): void { + static dumpV1VariableInfo(view: Any, dumpInfo: DumpInfo): void { if (view instanceof Object) { Object.getOwnPropertyNames(view) .filter((varName) => { @@ -72,7 +72,7 @@ export class StateMgmtDFX { } } - static dumpV2VariableInfo(view: NullishType, dumpInfo: DumpInfo): void { + static dumpV2VariableInfo(view: Any, dumpInfo: DumpInfo): void { if (view instanceof Object) { Object.getOwnPropertyNames(view) .filter((varName) => { diff --git a/frameworks/bridge/arkts_frontend/koala_projects/incremental/compat/src/arkts/observable.ts b/frameworks/bridge/arkts_frontend/koala_projects/incremental/compat/src/arkts/observable.ts index 24681f43ae860c23cacb9107cd709a5a04308df7..8928c6b5d588994fe0bf6996a77591e2275edf1d 100644 --- a/frameworks/bridge/arkts_frontend/koala_projects/incremental/compat/src/arkts/observable.ts +++ b/frameworks/bridge/arkts_frontend/koala_projects/incremental/compat/src/arkts/observable.ts @@ -286,7 +286,7 @@ class CustomArrayProxyHandler extends proxy.DefaultArrayProxyHandler { return super.set(target, index, value) } - override get(target: Array, name: string): NullishType { + override get(target: Array, name: string): Any { const observable = ObservableHandler.find(target) if (observable) { observable.onAccess() @@ -294,7 +294,7 @@ class CustomArrayProxyHandler extends proxy.DefaultArrayProxyHandler { return super.get(target, name) } - override set(target: Array, name: string, value: NullishType): boolean { + override set(target: Array, name: string, value: Any): boolean { const observable = ObservableHandler.find(target) if (observable) { observable.onModify() @@ -304,7 +304,7 @@ class CustomArrayProxyHandler extends proxy.DefaultArrayProxyHandler { return super.set(target, name, value) } - override invoke(target: Array, method: Method, args: FixedArray): NullishType { + override invoke(target: Array, method: Method, args: FixedArray): Any { const observable = ObservableHandler.find(target) if (observable) { const name = method.getName()