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 ObservedV2, 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 public getMonitorFuncName(): string { 107 return this.monitorFunction.name; 108 } 109 110 /** 111 Return array of those monitored paths 112 that changed since previous invocation 113 */ 114 public get dirty() : Array<string> { 115 let ret = new Array<string>(); 116 this.values_.forEach(monitorValue => { 117 if (monitorValue.isDirty()) { 118 ret.push(monitorValue.path); 119 } 120 }); 121 return ret; 122 } 123 124 /** 125 * return IMonitorValue for given path 126 * or if no path is specified any dirty (changed) monitor value 127 */ 128 public value<T>(path?: String): IMonitorValue<T> { 129 for (let monitorValue of this.values_) { 130 if ((path === undefined && monitorValue.isDirty()) || monitorValue.path === path) { 131 return monitorValue as MonitorValueV2<T> as IMonitorValue<T>; 132 } 133 } 134 return undefined; 135 } 136 137 InitRun(): MonitorV2 { 138 this.bindRun(true); 139 return this; 140 } 141 142 public notifyChange(): void { 143 if (this.bindRun(/* is init / first run */ false)) { 144 stateMgmtConsole.debug(`@Monitor function '${this.monitorFunction.name}' exec ...`); 145 146 try { 147 // exec @Monitor function 148 this.monitorFunction.call(this.target_, this); 149 } catch(e) { 150 stateMgmtConsole.applicationError(`@Monitor exception caught for ${this.monitorFunction.name}`, e.toString()); 151 throw e; 152 } finally { 153 this.resetMonitor(); 154 } 155 } 156 } 157 158 public notifyChangeOnReuse(): void { 159 this.bindRun(true); 160 } 161 162 // called after @Monitor function call 163 private resetMonitor(): void { 164 this.values_.forEach(item => item.reset()); 165 } 166 167 // analysisProp for each monitored path 168 private bindRun(isInit: boolean = false): boolean { 169 ObserveV2.getObserve().startRecordDependencies(this, this.watchId_); 170 let ret = false; 171 this.values_.forEach((item) => { 172 const [success, value] = this.analysisProp(isInit, item); 173 if (!success ) { 174 stateMgmtConsole.debug(`@Monitor path no longer valid.`); 175 return; 176 } 177 let dirty = item.setValue(isInit, value); 178 ret = ret || dirty; 179 }); 180 181 ObserveV2.getObserve().stopRecordDependencies(); 182 return ret; 183 } 184 185 // record / update object dependencies by reading each object along the path 186 // return the value, i.e. the value of the last path item 187 private analysisProp<T>(isInit: boolean, monitoredValue: MonitorValueV2<T>): [ success: boolean, value : T ] { 188 let obj = this.target_; 189 for (let prop of monitoredValue.props) { 190 if (obj && typeof obj === 'object' && Reflect.has(obj, prop)) { 191 obj = obj[prop]; 192 } else { 193 isInit && stateMgmtConsole.warn(`watch prop ${monitoredValue.path} initialize not found, make sure it exists!`); 194 return [false, undefined]; 195 } 196 } 197 return [true, obj as unknown as T]; 198 } 199 200 public static clearWatchesFromTarget(target: Object): void { 201 let meta: Object; 202 if (!target || typeof target !== 'object' || 203 !(meta = target[MonitorV2.WATCH_INSTANCE_PREFIX]) || typeof meta !== 'object') { 204 return; 205 } 206 207 stateMgmtConsole.debug(`MonitorV2: clearWatchesFromTarget: from target ${target.constructor?.name} watchIds to clear ${JSON.stringify(Array.from(Object.values(meta)))}`); 208 Array.from(Object.values(meta)).forEach((watchId) => ObserveV2.getObserve().clearWatch(watchId)); 209 } 210} 211 212 213// Performance Improvement 214class AsyncAddMonitorV2 { 215 static watches: any[] = []; 216 217 static addMonitor(target: any, name: string): void { 218 if (AsyncAddMonitorV2.watches.length === 0) { 219 Promise.resolve(true) 220 .then(AsyncAddMonitorV2.run) 221 .catch(error => { 222 stateMgmtConsole.applicationError(`Exception caught in @Monitor function ${name}`, error); 223 _arkUIUncaughtPromiseError(error); 224 }); 225 } 226 AsyncAddMonitorV2.watches.push([target, name]); 227 } 228 229 static run(): void { 230 for (let item of AsyncAddMonitorV2.watches) { 231 ObserveV2.getObserve().constructMonitor(item[0], item[1]); 232 } 233 AsyncAddMonitorV2.watches = []; 234 } 235} 236