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