• 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
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