• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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