1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 17/** 18 * 19 * This file includes only framework internal classes and functions 20 * non are part of SDK. Do not access from app. 21 * 22 * It includes @Monitor function decorator supporting classes MonitorV2 and AsyncMonitorV2 23 * 24 */ 25 26 27class MonitorValueV2<T> { 28 public before?: T; 29 public now?: T; 30 public path: string; 31 // properties on the path 32 public props : string[]; 33 34 private dirty : boolean; 35 36 constructor(path: string) { 37 this.path = path; 38 this.dirty = false; 39 this.props = path.split('.'); 40 } 41 42 setValue(isInit: boolean, newValue: T): boolean { 43 this.now = newValue; 44 if (isInit) { 45 this.before = this.now; 46 } 47 this.dirty = this.before !== this.now; 48 return this.dirty; 49 } 50 51 // mv newValue to oldValue, set dirty to false 52 reset(): void { 53 this.before = this.now; 54 this.dirty = false; 55 } 56 57 isDirty(): boolean { 58 return this.dirty; 59 } 60} 61 62/** 63 * MonitorV2 64 * one MonitorV2 object per @monitor function 65 * watchId - similar to elmtId, identify one MonitorV2 in Observe.idToCmp Map 66 * observeObjectAccess = get each object on the 'path' to create dependency and add them with Observe.addRef 67 * fireChange - exec @Monitor function and re-new dependencies with observeObjectAccess 68 */ 69 70 71class MonitorV2 { 72 public static readonly WATCH_PREFIX = '___watch_'; 73 public static readonly WATCH_INSTANCE_PREFIX = '___watch__obj_'; 74 75 // start with high number to avoid same id as elmtId for components. 76 public static readonly MIN_WATCH_ID = 0x1000000000000; 77 public static nextWatchId_ = MonitorV2.MIN_WATCH_ID; 78 79 80 private values_: Array<MonitorValueV2<unknown>> = new Array<MonitorValueV2<unknown>>(); 81 private target_: object; // @monitor function 'this': data object or ViewV2 82 private monitorFunction: (m: IMonitor) => void; 83 private watchId_: number; // unique id, similar to elmtId but identifies this object 84 85 constructor(target: object, pathsString: string, func: (m: IMonitor) => void) { 86 this.target_ = target; 87 this.monitorFunction = func; 88 this.watchId_ = ++MonitorV2.nextWatchId_; 89 90 // split space separated array of paths 91 let paths = pathsString.split(/\s+/g); 92 paths.forEach(path => this.values_.push(new MonitorValueV2<unknown>(path))); 93 94 // add watchId to owning ViewV2 or view model data object 95 // ViewV2 uses to call clearBinding(id) 96 // FIXME data object leave data inside ObservedV3, because they can not 97 // call clearBinding(id) before they get deleted. 98 const meta = target[MonitorV2.WATCH_INSTANCE_PREFIX] ??= {}; 99 meta[pathsString] = this.watchId_; 100 } 101 102 public getTarget() : Object { 103 return this.target_; 104 } 105 106 /** 107 Return array of those monitored paths 108 that changed since previous invocation 109 */ 110 public get dirty() : Array<string> { 111 let ret = new Array<string>(); 112 this.values_.forEach(monitorValue => { 113 if (monitorValue.isDirty()) { 114 ret.push(monitorValue.path); 115 } 116 }); 117 return ret; 118 } 119 120 /** 121 * return IMonitorValue for given path 122 * or if no path is specified any dirty (changed) monitor value 123 */ 124 public value<T>(path?: String): IMonitorValue<T> { 125 for (let monitorValue of this.values_) { 126 if ((path === undefined && monitorValue.isDirty()) || monitorValue.path === path) { 127 return monitorValue as MonitorValueV2<T> as IMonitorValue<T>; 128 } 129 } 130 return undefined; 131 } 132 133 InitRun(): MonitorV2 { 134 this.bindRun(true); 135 return this; 136 } 137 138 public notifyChange(): void { 139 if (this.bindRun(/* is init / first run */ false)) { 140 stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); 141 142 try { 143 // exec @Monitor function 144 this.monitorFunction.call(this.target_, this); 145 } catch (e) { 146 stateMgmtConsole.applicationError(`@Monitor exception caught for ${this.monitorFunction.name}`, e.toString()); 147 throw e; 148 } finally { 149 this.reset(); 150 } 151 } 152 } 153 154 // called after @Monitor function call 155 private reset(): void { 156 this.values_.forEach(item => item.reset()); 157 } 158 159 // analysisProp for each monitored path 160 private bindRun(isInit: boolean = false): boolean { 161 ObserveV2.getObserve().startRecordDependencies(this, this.watchId_); 162 let ret = false; 163 this.values_.forEach((item) => { 164 const [success, value] = this.analysisProp(isInit, item); 165 if (!success ) { 166 stateMgmtConsole.debug(`@Monitor path no longer valid.`); 167 return; 168 } 169 let dirty = item.setValue(isInit, value); 170 ret = ret || dirty; 171 }); 172 173 ObserveV2.getObserve().stopRecordDependencies(); 174 return ret; 175 } 176 177 // record / update object dependencies by reading each object along the path 178 // return the value, i.e. the value of the last path item 179 private analysisProp<T>(isInit: boolean, monitoredValue: MonitorValueV2<T>): [ success: boolean, value : T ] { 180 let obj = this.target_; 181 for (let prop of monitoredValue.props) { 182 if (typeof obj === 'object' && Reflect.has(obj, prop)) { 183 obj = obj[prop]; 184 } else { 185 isInit && stateMgmtConsole.warn(`watch prop ${monitoredValue.path} initialize not found, make sure it exists!`); 186 return [false, undefined]; 187 } 188 } 189 return [true, obj as unknown as T]; 190 } 191 192 public static clearWatchesFromTarget(target: Object): void { 193 let meta: Object; 194 if (!target || typeof target !== 'object' || 195 !(meta = target[MonitorV2.WATCH_INSTANCE_PREFIX]) || typeof meta !== 'object') { 196 return; 197 } 198 199 stateMgmtConsole.debug(`MonitorV2: clearWatchesFromTarget: from target ${target.constructor?.name} watchIds to clear ${JSON.stringify(Array.from(Object.values(meta)))}`); 200 Array.from(Object.values(meta)).forEach((watchId) => ObserveV2.getObserve().clearWatch(watchId)); 201 } 202} 203 204 205// Performance Improvement 206class AsyncAddMonitorV2 { 207 static watches: any[] = []; 208 209 static addMonitor(target: any, name: string): void { 210 if (AsyncAddMonitorV2.watches.length === 0) { 211 Promise.resolve(true) 212 .then(AsyncAddMonitorV2.run) 213 .catch(error => { 214 stateMgmtConsole.applicationError(`Exception caught in @Monitor function ${name}`, error); 215 _arkUIUncaughtPromiseError(error); 216 }); 217 } 218 AsyncAddMonitorV2.watches.push([target, name]); 219 } 220 221 static run(): void { 222 for (let item of AsyncAddMonitorV2.watches) { 223 ObserveV2.getObserve().constructMonitor(item[0], item[1]); 224 } 225 AsyncAddMonitorV2.watches = []; 226 } 227} 228