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 public id: number; 34 private dirty: boolean; 35 // indicate the value is accessible or not 36 // only used for AddMonitor 37 private isAccessible: boolean; 38 39 constructor(path: string, id?: number) { 40 this.path = path; 41 this.dirty = false; 42 this.props = path.split('.'); 43 this.id = id ? id : -1; 44 this.isAccessible = false; 45 } 46 47 setValue(isInit: boolean, newValue: T): boolean { 48 this.now = newValue; 49 50 if (isInit) { 51 this.before = this.now; 52 this.isAccessible = true; 53 // just init the before and now value not dirty in init 54 return false; 55 } 56 57 if (this.id < MonitorV2.MIN_WATCH_FROM_API_ID) { 58 // @Monitor 59 // @Monitor does not care if the property is accessible or not, so ignore to set isAccessible 60 this.dirty = this.before !== this.now; 61 } else { 62 // AddMonitor 63 // consider value dirty if it wasn't accessible before setting the new value 64 this.dirty = (!this.isAccessible) || (this.before !== this.now); 65 this.isAccessible = true; 66 } 67 return this.dirty; 68 } 69 70 setNotFound(isInit: boolean): boolean { 71 if (!isInit && this.isAccessible) { 72 this.dirty = true; 73 } 74 this.isAccessible = false; 75 this.now = undefined; 76 return this.dirty; 77 } 78 79 // mv newValue to oldValue, set dirty to false 80 reset(): void { 81 this.before = this.now; 82 this.dirty = false; 83 } 84 85 isDirty(): boolean { 86 return this.dirty; 87 } 88} 89 90/** 91 * MonitorV2 92 * one MonitorV2 object per @Monitor function 93 * watchId - similar to elmtId, identify one MonitorV2 in Observe.idToCmp Map 94 * observeObjectAccess = get each object on the 'path' to create dependency and add them with Observe.addRef 95 * fireChange - exec @Monitor function and re-new dependencies with observeObjectAccess 96 */ 97 98 99class MonitorV2 { 100 public static readonly WATCH_PREFIX = '___watch_'; 101 public static readonly WATCH_INSTANCE_PREFIX = '___watch__obj_'; 102 103 // start with high number to avoid same id as elmtId for components/Computed. 104 // 0 --- 0x1000000000 ----- 0x1000000000000 --------- 0x1010000000000 -------- 0x1015000000000 ---- 0x1020000000000 ---- 105 // elementId computedId @Monitor MonitorAPI MonitorAPIForSync PersistenceV2 106 public static readonly MIN_WATCH_ID = 0x1000000000000; 107 public static readonly MIN_WATCH_FROM_API_ID = 0x1010000000000; 108 public static readonly MIN_SYNC_WATCH_FROM_API_ID = 0x1015000000000; 109 public static nextWatchId_ = MonitorV2.MIN_WATCH_ID; 110 public static nextWatchApiId_ = MonitorV2.MIN_WATCH_FROM_API_ID; 111 public static nextSyncWatchApiId_ = MonitorV2.MIN_SYNC_WATCH_FROM_API_ID; 112 113 private values_: Map<string, MonitorValueV2<unknown>> = new Map<string, MonitorValueV2<unknown>>(); 114 private target_: object; // @Monitor function 'this': data object or ViewV2 115 private monitorFunction: (m: IMonitor) => void; 116 private watchId_: number; // unique id, similar to elmtId but identifies this object 117 private isSync_: boolean = false; 118 private isDecorator_: boolean = true; 119 120 constructor(target: object, pathsString: string, func: (m: IMonitor) => void, isDecorator: boolean, isSync: boolean = false) { 121 this.target_ = target; 122 this.monitorFunction = func; 123 this.isDecorator_ = isDecorator; 124 this.isSync_ = isSync; 125 // split space separated array of paths 126 let paths = pathsString.split(/\s+/g); 127 128 if (this.isDecorator_) { 129 this.watchId_ = ++MonitorV2.nextWatchId_; 130 paths.forEach(path => { 131 this.values_.set(path, new MonitorValueV2<unknown>(path)); 132 }); 133 this.monitorFunction = func; 134 } else { 135 this.watchId_ = this.isSync_ ? ++MonitorV2.nextSyncWatchApiId_ : ++MonitorV2.nextWatchApiId_; 136 paths.forEach(path => { 137 this.values_.set(path, new MonitorValueV2<unknown>(path, this.isSync_ ? ++MonitorV2.nextSyncWatchApiId_ : 138 ++MonitorV2.nextWatchApiId_)); 139 }); 140 } 141 // add watchId to owning ViewV2 or view model data object 142 // ViewV2 uses to call clearBinding(id) 143 // FIXME data object leave data inside ObservedV2, because they can not 144 // call clearBinding(id) before they get deleted. 145 const meta = target[MonitorV2.WATCH_INSTANCE_PREFIX] ??= {}; 146 meta[pathsString] = this.watchId_; 147 } 148 149 public getTarget(): Object { 150 return this.target_; 151 } 152 153 public isSync(): boolean { 154 return this.isSync_; 155 } 156 157 public addPath(path: string): boolean { 158 if (this.values_.has(path)) { 159 stateMgmtConsole.applicationError(`AddMonitor ${this.getMonitorFuncName()} failed when adding path ${path} because duplicate key`); 160 return false; 161 } 162 this.values_.set(path, new MonitorValueV2<unknown>(path, this.isSync_ ? ++MonitorV2.nextSyncWatchApiId_ : 163 ++MonitorV2.nextWatchApiId_)); 164 return false; 165 } 166 167 public removePath(path: string): boolean { 168 const monitorValue = this.values_.get(path); 169 if (monitorValue) { 170 ObserveV2.getObserve().clearBinding(monitorValue.id); 171 delete ObserveV2.getObserve().id2Others_[monitorValue.id]; 172 this.values_.delete(path); 173 return true; 174 } 175 return false; 176 } 177 178 public getWatchId(): number { 179 return this.watchId_; 180 } 181 182 public getMonitorFuncName(): string { 183 return this.monitorFunction.name; 184 } 185 186 public getValues(): Map<string, MonitorValueV2<unknown>> { 187 return this.values_; 188 } 189 190 /** 191 Return array of those monitored paths 192 that changed since previous invocation 193 */ 194 public get dirty(): Array<string> { 195 let ret = new Array<string>(); 196 this.values_.forEach(monitorValue => { 197 if (monitorValue.isDirty()) { 198 ret.push(monitorValue.path); 199 } 200 }); 201 return ret; 202 } 203 204 /** 205 * return IMonitorValue for given path 206 * or if no path is specified any dirty (changed) monitor value 207 */ 208 public value<T>(path?: string): IMonitorValue<T> { 209 if (path) { 210 return this.values_.get(path) as IMonitorValue<T>; 211 } 212 for (let monitorValue of this.values_.values()) { 213 if (monitorValue.isDirty()) { 214 return monitorValue as MonitorValueV2<T> as IMonitorValue<T>; 215 } 216 } 217 return undefined; 218 } 219 220 InitRun(): MonitorV2 { 221 // if @Monitor, run the bindRun which is the same logic as before 222 if (this.isDecorator_) { 223 this.bindRun(true); 224 return this; 225 } 226 227 // AddMonitor, record dependencies for all path 228 ObserveV2.getObserve().registerMonitor(this, this.watchId_); 229 this.values_.forEach((item: MonitorValueV2<unknown>) => { 230 // each path has its own id, and will be push into the stack 231 // the state value will only collect the path id not the whole MonitorV2 id like the @Monitor does 232 ObserveV2.getObserve().startRecordDependencies(this, item.id); 233 const [success, value] = this.analysisProp(true, item); 234 ObserveV2.getObserve().stopRecordDependencies(); 235 if (!success) { 236 stateMgmtConsole.debug(`AddMonitor input path no longer valid.`); 237 item.setNotFound(true); 238 return; 239 } 240 item.setValue(true, value); 241 }) 242 return this; 243 } 244 245 // Called by ObserveV2 once if any monitored path was dirty. 246 // Executes the monitor function. 247 public runMonitorFunction(): void { 248 stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); 249 if (this.dirty.length === 0) { 250 stateMgmtConsole.debug(`No dirty values! not firing!`); 251 return; 252 } 253 try { 254 // exec @Monitor/AddMonitor function 255 this.monitorFunction.call(this.target_, this); 256 } catch (e) { 257 stateMgmtConsole.applicationError(`AddMonitor exception caught for ${this.monitorFunction.name}`, e.toString()); 258 throw e; 259 } finally { 260 this.resetMonitor(); 261 } 262 } 263 264 public notifyChange(): void { 265 if (this.bindRun(/* is init / first run */ false)) { 266 stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); 267 this.runMonitorFunction(); 268 } 269 } 270 271 // Only used for MonitorAPI: AddMonitor 272 // Called by ObserveV2 when a monitor path has changed. 273 // Analyze the changed path and return this Monitor's 274 // watchId if the path was dirty and the monitor function needs to be executed later. 275public notifyChangeForEachPath(pathId: number): number { 276 for (const item of this.values_.values()) { 277 if (item.id === pathId) { 278 return this.recordDependenciesForProp(item) ? this.watchId_ : -1; 279 } 280 } 281 return -1; 282} 283 // Only used for MonitorAPI: AddMonitor 284 // record dependencies for given MonitorValue, when any monitored path 285 // has changed and notifyChange is called 286 private recordDependenciesForProp(monitoredValue: MonitorValueV2<unknown>): boolean { 287 ObserveV2.getObserve().startRecordDependencies(this, monitoredValue.id); 288 const [success, value] = this.analysisProp(false, monitoredValue); 289 ObserveV2.getObserve().stopRecordDependencies(); 290 if (!success) { 291 stateMgmtConsole.debug(`AddMonitor input path no longer valid.`); 292 return monitoredValue.setNotFound(false); 293 } 294 return monitoredValue.setValue(false, value); // dirty? 295 } 296 297 public notifyChangeOnReuse(): void { 298 this.bindRun(true); 299 } 300 301 // called after @Monitor function call 302 private resetMonitor(): void { 303 this.values_.forEach(item => item.reset()); 304 } 305 306 // analysisProp for each monitored path 307 private bindRun(isInit: boolean = false): boolean { 308 ObserveV2.getObserve().startRecordDependencies(this, this.watchId_); 309 let ret = false; 310 this.values_.forEach((item) => { 311 const [success, value] = this.analysisProp(isInit, item); 312 if (!success) { 313 // cannot invoke setNotFound there, bindRun is only run in @Monitor 314 stateMgmtConsole.debug(`@Monitor path no longer valid.`); 315 return; 316 } 317 let dirty = item.setValue(isInit, value); 318 ret = ret || dirty; 319 }); 320 321 ObserveV2.getObserve().stopRecordDependencies(); 322 return ret; 323 } 324 325 // record / update object dependencies by reading each object along the path 326 // return the value, i.e. the value of the last path item 327 private analysisProp<T>(isInit: boolean, monitoredValue: MonitorValueV2<T>): [success: boolean, value: T] { 328 let obj = this.target_; 329 for (let prop of monitoredValue.props) { 330 if (obj && typeof obj === 'object' && Reflect.has(obj, prop)) { 331 obj = obj[prop]; 332 } else { 333 isInit && stateMgmtConsole.warn(`watch prop ${monitoredValue.path} initialize not found, make sure it exists!`); 334 return [false, undefined]; 335 } 336 } 337 return [true, obj as unknown as T]; 338 } 339 340 public static clearWatchesFromTarget(target: Object): void { 341 let meta: Object; 342 if (!target || typeof target !== 'object' || 343 !(meta = target[MonitorV2.WATCH_INSTANCE_PREFIX]) || typeof meta !== 'object') { 344 return; 345 } 346 347 stateMgmtConsole.debug(`MonitorV2: clearWatchesFromTarget: from target ${target.constructor?.name} watchIds to clear ${JSON.stringify(Array.from(Object.values(meta)))}`); 348 Array.from(Object.values(meta)).forEach((watchId) => ObserveV2.getObserve().clearWatch(watchId)); 349 } 350} 351 352 353// Performance Improvement 354class AsyncAddMonitorV2 { 355 static watches: any[] = []; 356 357 static addMonitor(target: any, name: string): void { 358 if (AsyncAddMonitorV2.watches.length === 0) { 359 Promise.resolve(true) 360 .then(AsyncAddMonitorV2.run) 361 .catch(error => { 362 stateMgmtConsole.applicationError(`Exception caught in @Monitor function ${name}`, error); 363 _arkUIUncaughtPromiseError(error); 364 }); 365 } 366 AsyncAddMonitorV2.watches.push([target, name]); 367 } 368 369 static run(): void { 370 for (let item of AsyncAddMonitorV2.watches) { 371 ObserveV2.getObserve().constructMonitor(item[0], item[1]); 372 } 373 AsyncAddMonitorV2.watches = []; 374 } 375} 376