• 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 *
23 * ObserveV2 is the singleton object for observing state variable access and
24 * change
25 */
26
27// stackOfRenderedComponentsItem[0] and stackOfRenderedComponentsItem[1] is faster than
28// the stackOfRenderedComponentsItem.id and the stackOfRenderedComponentsItem.cmp.
29// So use the array to keep id and cmp.
30type StackOfRenderedComponentsItem = [number, MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase];
31
32// in the case of ForEach, Repeat, AND If, two or more UINodes / elementIds can render at the same time
33// e.g. ForEach -> ForEach child Text, Repeat -> Nested Repeat, child Text
34// Therefore, ObserveV2 needs to keep a stack of currently rendering ids / components
35// in the same way as this is also done for PU stateMgmt with ViewPU.currentlyRenderedElmtIdStack_
36class StackOfRenderedComponents {
37  private stack_: Array<StackOfRenderedComponentsItem> = new Array<StackOfRenderedComponentsItem>();
38
39  public push(id: number, cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase): void {
40    this.stack_.push([id, cmp]);
41  }
42
43  public pop(): StackOfRenderedComponentsItem | undefined {
44    return this.stack_.pop();
45  }
46
47  public top(): StackOfRenderedComponentsItem | undefined {
48    return this.stack_.length ? this.stack_[this.stack_.length - 1] : undefined;
49  }
50}
51
52class ObserveV2 {
53  // meta data about decorated variable inside prototype
54  public static readonly V2_DECO_META = Symbol('__v2_deco_meta__');
55
56  // meta data about decorated accessor and method: @Computed and @Monitor
57  public static readonly V2_DECO_METHOD_META = Symbol('__v2_deco_method_meta__');
58
59  public static readonly SYMBOL_REFS = Symbol('__use_refs__');
60  public static readonly ID_REFS = Symbol('__id_refs__');
61  public static readonly MONITOR_REFS = Symbol('___monitor_refs_');
62  public static readonly ADD_MONITOR_REFS = Symbol('___monitor_refs_');
63  public static readonly COMPUTED_REFS = Symbol('___computed_refs_');
64
65  public static readonly SYMBOL_PROXY_GET_TARGET = Symbol('__proxy_get_target');
66
67  public static readonly SYMBOL_MAKE_OBSERVED = Symbol('___make_observed__');
68
69  public static readonly OB_PREFIX = '__ob_'; // OB_PREFIX + attrName => backing store attribute name
70  public static readonly OB_PREFIX_LEN = 5;
71  public static readonly NO_REUSE = -1; // mark no reuse on-going
72  // used by array Handler to create dependency on artificial 'length'
73  // property of array, mark it as changed when array has changed.
74  public static readonly OB_LENGTH = '___obj_length';
75
76  private static setMapProxy: SetMapProxyHandler = new SetMapProxyHandler();
77  private static arrayProxy: ArrayProxyHandler = new ArrayProxyHandler();
78  private static objectProxy: ObjectProxyHandler = new ObjectProxyHandler();
79
80  // see MonitorV2.observeObjectAccess: bindCmp is the MonitorV2
81  // see modified ViewV2 and ViewPU observeComponentCreation, bindCmp is the ViewV2 or ViewPU
82
83  // bindId: UINode elmtId or watchId, depending on what is being observed
84  private stackOfRenderedComponents_: StackOfRenderedComponents = new StackOfRenderedComponents();
85
86  // Map bindId to WeakRef<ViewBuildNodeBase>
87  public id2cmp_: { number: WeakRef<ViewBuildNodeBase> } = {} as { number: WeakRef<ViewBuildNodeBase> };
88
89  // Map bindId to WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl>
90  public id2Others_: { number: WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl> } = {} as { number: WeakRef<MonitorV2 | ComputedV2 | PersistenceV2Impl> };
91
92  // Map bindId -> Set of @ObservedV2 class objects
93  // reverse dependency map for quickly removing all dependencies of a bindId
94  private id2targets_: { number: Set<WeakRef<Object>> } = {} as { number: Set<WeakRef<Object>> };
95
96  // Queue of tasks to run in next idle period (used for optimization)
97  public idleTasks_: (Array<[(...any: any[]) => any, ...any[]]> & { first: number, end: number }) =
98    Object.assign(Array(1000).fill([]), { first: 0, end: 0 });
99  public static readonly idleTasksInitLength = 1000;
100
101  // queued up Set of bindId
102  // elmtIds of UINodes need re-render
103  // @monitor functions that need to execute
104  public elmtIdsChanged_: Set<number> = new Set();
105  // @Computed id
106  private computedPropIdsChanged_: Set<number> = new Set();
107  // AddMonitor API
108  private monitorIdsChangedForAddMonitor_: Set<number> = new Set();
109  // Sync AddMonitor API
110  private monitorSyncIdsChangedForAddMonitor_: Set<number> = new Set();
111  // @Monitor id
112  private monitorIdsChanged_: Set<number> = new Set();
113  private persistenceChanged_: Set<number> = new Set();
114  // used for Monitor API
115  // only store the MonitorV2 id, not the path id
116  // to make sure the callback function will be executed only once
117  public monitorFuncsToRun_: Set<number> = new Set();
118
119  // ViewV2s Grouped by instance id (container id)
120  private viewV2NeedUpdateMap_: Map<number, Map<ViewV2 | ViewPU, Array<number>>> = new Map();
121
122  // To avoid multiple schedules on the same container
123  private scheduledContainerIds_: Set<number> = new Set();
124
125  // avoid recursive execution of updateDirty
126  // by state changes => fireChange while
127  // UINode rerender or @monitor function execution
128  private startDirty_: boolean = false;
129
130  // flag to indicate change observation is disabled
131  private disabled_: boolean = false;
132  // flag to indicate dependency recording is disabled
133  private disableRecording_: boolean = false;
134
135  // flag to indicate ComputedV2 calculation is ongoing
136  private calculatingComputedProp_: boolean = false;
137
138  // use for mark current reuse id, ObserveV2.NO_REUSE(-1) mean no reuse on-going
139  protected currentReuseId_: number = ObserveV2.NO_REUSE;
140
141  // flag to disable nested component optimization if V1 and V2 components are involved in the nested cases.
142  public isParentChildOptimizable_ : boolean = true;
143
144  private static obsInstance_: ObserveV2;
145
146  public static getObserve(): ObserveV2 {
147    if (!this.obsInstance_) {
148      this.obsInstance_ = new ObserveV2();
149    }
150    return this.obsInstance_;
151  }
152
153  // return true given value is @ObservedV2 object
154  public static IsObservedObjectV2(value: any): boolean {
155    return (value && typeof (value) === 'object' && value[ObserveV2.V2_DECO_META]);
156  }
157
158  // return true if given value is proxied observed object, either makeObserved or autoProxyObject
159  public static IsProxiedObservedV2(value: any): boolean {
160    return (value && typeof value === 'object' && value[ObserveV2.SYMBOL_PROXY_GET_TARGET]);
161  }
162
163  // return true given value is the return value of makeObserved
164  public static IsMakeObserved(value: any): boolean {
165    return (value && typeof (value) === 'object' && value[ObserveV2.SYMBOL_MAKE_OBSERVED]);
166  }
167
168  public static getCurrentRecordedId(): number {
169    const bound = ObserveV2.getObserve().stackOfRenderedComponents_.top();
170    return bound ? bound[0] : -1;
171  }
172
173  // At the start of observeComponentCreation or
174  // MonitorV2 observeObjectAccess
175  public startRecordDependencies(cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase, id: number, doClearBinding: boolean = true): void {
176    if (cmp != null) {
177      doClearBinding && this.clearBinding(id);
178      this.stackOfRenderedComponents_.push(id, cmp);
179    }
180  }
181
182  // At the start of observeComponentCreation or
183  // MonitorV2 observeObjectAccess
184  public stopRecordDependencies(): void {
185    const bound = this.stackOfRenderedComponents_.pop();
186    if (bound === undefined) {
187      stateMgmtConsole.error('stopRecordDependencies finds empty stack. Internal error!');
188      return;
189    }
190
191    // only add IView | MonitorV2 | ComputedV2 if at least one dependency was
192    // recorded when rendering this ViewPU/ViewV2/Monitor/ComputedV2
193    // ViewPU is the likely case where no dependency gets recorded
194    // for others no dependencies are unlikely to happen
195
196    // once set, the value remains unchanged
197    let id: number = bound[0];
198    let cmp: MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase = bound[1];
199
200    // element id can be registered from id2cmp in aboutToBeDeletedInternal and unregisterElmtIdsFromIViews
201    // PersistenceV2Impl instance is Singleton
202    if (cmp instanceof ViewBuildNodeBase || cmp instanceof PersistenceV2Impl) {
203      this.id2cmp_[id] = new WeakRef<ViewBuildNodeBase | PersistenceV2Impl>(cmp);
204      return;
205    }
206    const weakRef = WeakRefPool.get(cmp);
207    // this instance, which maybe MonitorV2/ComputedV2 have been already recorded in id2Others
208    if (this.id2Others_[id] === weakRef) {
209      return;
210    }
211    this.id2Others_[id] = weakRef;
212    // register MonitorV2/ComputedV2 instance gc-callback func
213    WeakRefPool.register(cmp, id, () => {
214      delete this.id2Others_[id];
215    });
216
217  }
218
219  // clear any previously created dependency view model object to elmtId
220  // find these view model objects with the reverse map id2targets_
221  public clearBinding(id: number): void {
222    if (this.idleTasks_) {
223      if (this.idleTasks_.end - this.idleTasks_.first > ObserveV2.idleTasksInitLength) {
224        ObserveV2.getObserve().runIdleTasks();
225      }
226      this.idleTasks_[this.idleTasks_.end++] = [this.clearBindingInternal, id];
227    } else {
228      this.clearBindingInternal(id);
229    }
230  }
231
232  private clearBindingInternal(id: number): void {
233    this.id2targets_[id]?.forEach((weakRef: WeakRef<Object>) => {
234      const target = weakRef.deref();
235      const idRefs: Object | undefined = target?.[ObserveV2.ID_REFS];
236      const symRefs: Object = target?.[ObserveV2.SYMBOL_REFS];
237
238      if (idRefs) {
239        idRefs[id]?.forEach(key =>
240          symRefs?.[key]?.delete(id)
241        );
242        delete idRefs[id];
243      } else {
244        for (let key in symRefs) {
245          symRefs[key]?.delete(id);
246        };
247      }
248
249      if (target) {
250        WeakRefPool.unregister(target, id);
251      }
252    });
253
254    delete this.id2targets_[id];
255
256    stateMgmtConsole.propertyAccess(`... id2targets_ length=${Object.keys(this.id2targets_).length}, entries=${JSON.stringify(Object.keys(this.id2targets_))} `);
257  }
258
259  /**
260   *
261   * According to JS specifications, it is up to ArlTS runtime GC implementation when to collect unreferences objects.
262   * Parameters such as available memory, ArkTS processing load, number and size of all JS objects for GC collection
263   * can impact the time delay between an object loosing last reference and GC collecting this object.
264   *
265   * WeakRef deref() returns the object until GC has collected it.
266   * The id2cmp and is2targets cleanup herein depends on WeakRef.deref() to return undefined, i.e. it depends on GC
267   * collecting 'cmp' or 'target' objects. Only then the algorithm can remove the entry from id2cmp / from id2target.
268   * It is therefore to be expected behavior that these map objects grow and they a contain a larger number of
269   * MonitorV2, ComputedV2, and/or view model @Observed class objects that are no longer used / referenced by the application.
270   * Only after ArkTS runtime GC has collected them, this function is able to clean up the id2cmp and is2targets.
271   *
272   */
273
274  // runs idleTasks until empty or deadline is reached
275  public runIdleTasks(deadline: number = Infinity): void {
276    stateMgmtConsole.debug(`UINodeRegisterProxy.runIdleTasks(${deadline})`);
277
278    // fast check for early return
279    if (!this.idleTasks_ || this.idleTasks_.end === 0) {
280      return;
281    }
282
283    while (this.idleTasks_.first < this.idleTasks_.end) {
284      const [func, ...args] = this.idleTasks_[this.idleTasks_.first] || [];
285      func?.apply(this, args);
286      delete this.idleTasks_[this.idleTasks_.first];
287      this.idleTasks_.first++;
288      // ensure that there is no accumulation in idleTask leading to oom
289      if (this.idleTasks_.end - this.idleTasks_.first < ObserveV2.idleTasksInitLength &&
290        this.idleTasks_.first % 100 === 0 && Date.now() >= deadline - 1) {
291        return;
292      }
293    }
294    this.idleTasks_.first = 0;
295    this.idleTasks_.end = 0;
296    this.idleTasks_.length = ObserveV2.idleTasksInitLength;
297  }
298
299  /**
300   * counts number of WeakRef<Object> entries in id2cmp_ 'map' object
301   * @returns total count and count of WeakRefs that can be deref'ed
302   * Methods only for testing
303   */
304  public get id2CompDeRefSize(): [totalCount: number, aliveCount: number] {
305    let totalCount = 0;
306    let aliveCount = 0;
307    let comp: Object;
308    for (const id in this.id2cmp_) {
309      totalCount++;
310      let weakRef: WeakRef<Object> = this.id2cmp_[id];
311      if (weakRef && 'deref' in weakRef && (comp = weakRef.deref()) && comp instanceof Object) {
312        aliveCount++;
313      }
314    }
315    return [totalCount, aliveCount];
316  }
317
318  /**
319   * counts number of target WeakRef<object> entries in all the Sets inside id2targets 'map' object
320   * @returns total count and those can be dereferenced
321   * Methods only for testing
322   */
323  public get id2TargetsDerefSize(): [totalCount: number, aliveCount: number] {
324    let totalCount = 0;
325    let aliveCount = 0;
326    for (const id in this.id2targets_) {
327      const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id];
328      if (targetSet && targetSet instanceof Set) {
329        for (let weakTarget of targetSet) {
330          totalCount++;
331          if (weakTarget.deref()) {
332            aliveCount++;
333          }
334        } // for targetSet
335      }
336    } // for id2targets_
337    return [totalCount, aliveCount];
338  }
339
340  // add dependency view model object 'target' property 'attrName'
341  // to current this.bindId
342  public addRef(target: object, attrName: string): void {
343    const bound = this.stackOfRenderedComponents_.top();
344    if (!bound || this.disableRecording_) {
345      return;
346    }
347    if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) {
348      const error = `${attrName}: ObserveV2.addRef: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`;
349      stateMgmtConsole.applicationError(error);
350      throw new TypeError(error);
351    }
352
353    stateMgmtConsole.propertyAccess(`ObserveV2.addRef '${attrName}' for id ${bound[0]}...`);
354
355    // run in idle time or now
356    if (this.idleTasks_) {
357      this.idleTasks_[this.idleTasks_.end++] =
358        [this.addRef4IdInternal, bound[0], target, attrName];
359    } else {
360      this.addRef4IdInternal(bound[0], target, attrName);
361    }
362  }
363
364  // add dependency view model object 'target' property 'attrName' to current this.bindId
365  // this variation of the addRef function is only used to record read access to V1 observed object with enableV2Compatibility enabled
366  // e.g. only from within ObservedObject proxy handler implementations.
367  public addRefV2Compatibility(target: object, attrName: string): void {
368    const bound = this.stackOfRenderedComponents_.top();
369    if (bound && bound[1]) {
370      if (!(bound[1] instanceof ViewPU)) {
371        if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) {
372          const error = `${attrName}: ObserveV2.addRefV2Compatibility: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`;
373          stateMgmtConsole.applicationError(error);
374          throw new TypeError(error);
375        }
376        stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]}...`);
377        this.addRef4Id(bound[0], target, attrName);
378      } else {
379        // inside ViewPU
380        stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]} -- skip addRef because render/update is inside V1 ViewPU`);
381      }
382    }
383  }
384
385  public addRef4Id(id: number, target: object, attrName: string): void {
386    stateMgmtConsole.propertyAccess(`ObserveV2.addRef4Id '${attrName}' for id ${id} ...`);
387
388    // run in idle time or now
389    if (this.idleTasks_) {
390      this.idleTasks_[this.idleTasks_.end++] =
391        [this.addRef4IdInternal, id, target, attrName];
392    } else {
393      this.addRef4IdInternal(id, target, attrName);
394    }
395  }
396
397  private addRef4IdInternal(id: number, target: object, attrName: string): void {
398    // Map: attribute/symbol -> dependent id
399    const symRefs = target[ObserveV2.SYMBOL_REFS] ??= {};
400    symRefs[attrName] ??= new Set();
401    symRefs[attrName].add(id);
402
403    // Map id -> attribute/symbol
404    // optimization for faster clearBinding
405    const idRefs = target[ObserveV2.ID_REFS];
406    if (idRefs) {
407      idRefs[id] ??= new Set();
408      idRefs[id].add(attrName);
409    }
410
411    const weakRef = WeakRefPool.get(target);
412    if (this.id2targets_?.[id]?.has(weakRef)) {
413      return;
414    }
415
416    this.id2targets_[id] ??= new Set<WeakRef<Object>>();
417    this.id2targets_[id].add(weakRef);
418    WeakRefPool.register(target, id, () => {
419      if (this.id2targets_?.[id]?.delete(weakRef) && this.id2targets_[id].size === 0) {
420        delete this.id2targets_[id];
421      }
422    });
423  }
424
425  /**
426   *
427   * @param target set tracked attribute to new value without notifying the change
428   *               !! use with caution !!
429   * @param attrName
430   * @param newValue
431   */
432  public setUnmonitored<Z>(target: object, attrName: string, newValue: Z): void {
433    const storeProp = ObserveV2.OB_PREFIX + attrName;
434    if (storeProp in target) {
435      // @Track attrName
436      stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - tracked but unchanged. Doing nothing.`);
437      target[storeProp] = newValue;
438    } else {
439      stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - untracked, assigning straight.`);
440      // untracked attrName
441      target[attrName] = newValue;
442    }
443  }
444
445  /**
446   * Execute given task while state change observation is disabled
447   * A state mutation caused by the task will NOT trigger UI rerender
448   * and @monitor function execution.
449   *
450   * !!! Use with Caution !!!
451   *
452   * @param task a function to execute without monitoring state changes
453   * @returns task function return value
454   */
455  public executeUnobserved<Z>(task: () => Z): Z {
456    stateMgmtConsole.propertyAccess(`executeUnobserved - start`);
457    this.disabled_ = true;
458    let ret: Z;
459    try {
460      ret = task();
461    } catch (e) {
462      stateMgmtConsole.applicationError(`executeUnobserved - task execution caused error ${e} !`);
463    }
464    this.disabled_ = false;
465    stateMgmtConsole.propertyAccess(`executeUnobserved - done`);
466    return ret;
467  }
468
469  /**
470   * Execute given task while state dependency recording is disabled
471   * Any property read during task execution will not be recorded as
472   * a dependency
473   *
474   * !!! Use with Caution !!!
475   *
476   * @param task a function to execute without monitoring state changes
477   * @param unobserved flag to disable also change observation
478   * @returns task function return value
479   */
480  public executeUnrecorded<Z>(task: () => Z, unobserved: boolean = false): Z {
481    stateMgmtConsole.propertyAccess(`executeUnrecorded - start`);
482    this.disableRecording_ = true;
483    // store current value of disabled_ so that we can later restore it properly
484    const oldDisabledValue = this.disabled_;
485    unobserved && (this.disabled_ = true);
486    let ret: Z = task();
487    this.disableRecording_ = false;
488    unobserved && (this.disabled_ = oldDisabledValue);
489    stateMgmtConsole.propertyAccess(`executeUnrecorded - done`);
490    return ret;
491  }
492
493
494
495  /**
496  * mark view model object 'target' property 'attrName' as changed
497  * notify affected watchIds and elmtIds but exclude given elmtIds
498  *
499  * @param propName ObservedV2 or ViewV2 target
500  * @param attrName attrName
501  * @param ignoreOnProfile The data reported to the profiler needs to be the changed state data.
502  * If the fireChange is invoked before the data changed, it needs to be ignored on the profiler.
503  * The default value is false.
504  */
505  public fireChange(target: object, attrName: string, excludeElmtIds?: Set<number>,
506    ignoreOnProfiler: boolean = false): void {
507    // forcibly run idle time tasks if any
508    if (this.idleTasks_?.end) {
509      this.runIdleTasks();
510    }
511    // enable to get more fine grained traces
512    // including 2 (!) .end calls.
513
514    if (!target[ObserveV2.SYMBOL_REFS] || this.disabled_) {
515      return;
516    }
517
518    const bound = this.stackOfRenderedComponents_.top();
519    if (this.calculatingComputedProp_) {
520      const prop = bound ? (bound[1] as ComputedV2).getProp() : 'unknown computed property';
521      const error = `Usage of ILLEGAL @Computed function detected for ${prop}! The @Computed function MUST NOT change the state of any observed state variable!`;
522      stateMgmtConsole.applicationError(error);
523      throw new Error(error);
524    }
525
526    // enable this trace marker for more fine grained tracing of the update pipeline
527    // note: two (!) end markers need to be enabled
528    let changedIdSet = target[ObserveV2.SYMBOL_REFS][attrName];
529    if (changedIdSet instanceof Set === false) {
530      return;
531    }
532
533    stateMgmtConsole.propertyAccess(`ObserveV2.fireChange '${attrName}' dependent ids: ${JSON.stringify(Array.from(changedIdSet))}  ...`);
534
535    for (const id of changedIdSet) {
536      // Cannot fireChange the object that is being created.
537      if (bound && id === bound[0]) {
538        continue;
539      }
540
541      // exclude given elementIds
542      if (excludeElmtIds?.has(id)) {
543        stateMgmtConsole.propertyAccess(`... exclude id ${id}`);
544        continue;
545      }
546
547      // if this is the first id to be added to any Set of changed ids,
548      // schedule an 'updateDirty' task
549      // that will run after the current call stack has unwound.
550      // purpose of check for startDirty_ is to avoid going into recursion. This could happen if
551      // exec a re-render or exec a monitor function changes some state -> calls fireChange -> ...
552      const hasPendingChanges = (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size) > 0;
553      const isReuseInProgress = (this.currentReuseId_ !== ObserveV2.NO_REUSE);
554      const shouldUpdateDirty = (!hasPendingChanges && !this.startDirty_ && !isReuseInProgress);
555
556      if (shouldUpdateDirty) {
557        Promise.resolve().then(this.updateDirty.bind(this))
558          .catch(error => {
559            stateMgmtConsole.applicationError(`Exception occurred during the update process involving @Computed properties, @Monitor functions or UINode re-rendering`, error);
560            _arkUIUncaughtPromiseError(error);
561          });
562      }
563
564      // add bindId to the correct Set of pending changes.
565      if (id < ComputedV2.MIN_COMPUTED_ID) {
566        this.elmtIdsChanged_.add(id);
567      } else if (id < MonitorV2.MIN_WATCH_ID) {
568        this.computedPropIdsChanged_.add(id);
569      } else if (id < MonitorV2.MIN_WATCH_FROM_API_ID) {
570        this.monitorIdsChanged_.add(id);
571      } else if (id < MonitorV2.MIN_SYNC_WATCH_FROM_API_ID) {
572        this.monitorIdsChangedForAddMonitor_.add(id);
573      } else if (id < PersistenceV2Impl.MIN_PERSISTENCE_ID) {
574        this.monitorSyncIdsChangedForAddMonitor_.add(id);
575      } else {
576        this.persistenceChanged_.add(id);
577      }
578    } // for
579
580    // execute the AddMonitor synchronous function
581    while (this.monitorSyncIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0) {
582      if (this.monitorSyncIdsChangedForAddMonitor_.size) {
583        stateMgmtConsole.debug(`AddMonitor API monitorSyncIdsChangedForAddMonitor_ ${this.monitorSyncIdsChangedForAddMonitor_.size}`)
584        const monitorId: Set<number> = this.monitorSyncIdsChangedForAddMonitor_;
585        this.monitorSyncIdsChangedForAddMonitor_ = new Set<number>();
586        // update the value and dependency for each path and get the MonitorV2 id needs to be execute
587        this.updateDirtyMonitorPath(monitorId);
588      }
589      if (this.monitorFuncsToRun_.size) {
590        const monitorFuncs = this.monitorFuncsToRun_;
591        this.monitorFuncsToRun_ = new Set<number>();
592        this.runMonitorFunctionsForAddMonitor(monitorFuncs);
593      }
594    }
595
596    // report the stateVar changed when recording the profiler
597    if (stateMgmtDFX.enableProfiler && !ignoreOnProfiler) {
598      stateMgmtDFX.reportStateInfoToProfilerV2(target, attrName, changedIdSet);
599    }
600  }
601
602  /**
603   * Group elementIds by their containerId (instanceId).
604   * Make sure view.getInstanceId() is called only once for each view., not for all elmtIds!!!
605   */
606  private groupElementIdsByContainer(): void {
607    stateMgmtConsole.debug('ObserveV2.groupElementIdsByContainer');
608
609    // Create sorted array and clear set
610    const elmtIds = Array.from(this.elmtIdsChanged_, Number).sort((a, b) => a - b);
611    this.elmtIdsChanged_.clear();
612
613    // Cache for view -> instanceId
614    const viewInstanceIdCache = new Map<ViewV2 | ViewPU, number>();
615
616    for (const elmtId of elmtIds) {
617        const view = this.id2cmp_[elmtId]?.deref();
618
619        // Early continue for invalid views
620        if (!(view instanceof ViewV2 || view instanceof ViewPU)) {
621            continue;
622        }
623
624        // Get or cache instanceId
625        let instanceId = viewInstanceIdCache.get(view);
626        if (instanceId === undefined) {
627            instanceId = view.getInstanceId();
628            viewInstanceIdCache.set(view, instanceId);
629        }
630
631        // Get or create viewMap
632        let viewMap = this.viewV2NeedUpdateMap_.get(instanceId);
633        if (!viewMap) {
634          viewMap = new Map<ViewV2 | ViewPU, Array<number>>();
635          this.viewV2NeedUpdateMap_.set(instanceId, viewMap);
636        }
637
638        // Get or create view's element array
639        let elements = viewMap.get(view);
640        if (!elements) {
641            elements = [];
642            viewMap.set(view, elements);
643        }
644
645        elements.push(elmtId);
646        stateMgmtConsole.debug(`groupElementIdsByContainer: elmtId=${elmtId}, view=${view.constructor.name}, instanceId=${instanceId}`);
647    }
648}
649  public updateDirty(): void {
650    this.startDirty_ = true;
651    this.isParentChildOptimizable_ ? this.updateDirty2Optimized(): this.updateDirty2(false);
652    this.startDirty_ = false;
653  }
654
655  /**
656     * Optimized version of updateDirty2
657     * execute /update in this order
658     * - @Computed variables
659     * - @Monitor functions
660     * Request a frame update and schedule a callback to trigger on the next VSync update
661    */
662  public updateDirty2Optimized(): void {
663    stateMgmtConsole.debug(`ObservedV2.updateDirty2Optimized() start`);
664    // Calling these functions to retain the original behavior
665    // Obtain and unregister the removed elmtIds
666    UINodeRegisterProxy.obtainDeletedElmtIds();
667    UINodeRegisterProxy.unregisterElmtIdsFromIViews();
668
669    this.updateComputedAndMonitors();
670
671    if (this.elmtIdsChanged_.size === 0) {
672      stateMgmtConsole.debug(`Vsync request is unnecessary when no elements have changed - returning from updateDirty2Optimized`);
673      return;
674    }
675
676    // Group elementIds before scheduling update
677    this.groupElementIdsByContainer();
678
679    // At this point, we have the viewV2NeedUpdateMap_ populated with the ViewV2/elementIds that need update
680    // For each containerId (instance/container) in the map, schedule an update.
681    for (const containerId of this.viewV2NeedUpdateMap_.keys()) {
682      if (!this.scheduledContainerIds_.has(containerId)) {
683        stateMgmtConsole.debug(`  scheduling update for containerId: ${containerId}`);
684        this.scheduledContainerIds_.add(containerId);
685        ViewStackProcessor.scheduleUpdateOnNextVSync(this.onVSyncUpdate.bind(this), containerId);
686      }
687    }
688    stateMgmtConsole.debug(`ObservedV2.updateDirty2Optimized() end`);
689  }
690  // Callback from C++ on VSync
691  public onVSyncUpdate(containerId: number): boolean {
692    stateMgmtConsole.debug(`ObservedV2.flushDirtyViewsOnVSync containerId=${containerId} start`);
693    aceDebugTrace.begin(`ObservedV2.onVSyncUpdate`);
694    let maxFlushTimes = 3; // Refer PipelineContext::FlushDirtyNodeUpdate()
695    // Obtain and unregister the removed elmtIds
696    UINodeRegisterProxy.obtainDeletedElmtIds();
697    UINodeRegisterProxy.unregisterElmtIdsFromIViews();
698
699    // Process updates in priority order: computed properties, monitors, UI nodes
700    do {
701      this.updateComputedAndMonitors();
702      const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId);
703
704      // Clear the ViewV2 map for the current containerId
705      this.viewV2NeedUpdateMap_.delete(containerId);
706
707      if (viewV2Map?.size) {
708        // Update elements, generating new elmtIds in elmtIdsChanged_ for nested updates
709        viewV2Map.forEach((elmtIds, view) => {
710          this.updateUINodesSynchronously(elmtIds, view);
711        });
712
713        if (this.elmtIdsChanged_.size) {
714          this.groupElementIdsByContainer();
715        }
716      } else {
717        stateMgmtConsole.error(`No views to update for containerId=${containerId}`);
718        break; // Exit loop early since no updates are possible
719      }
720    } while (this.hasPendingUpdates(containerId) && --maxFlushTimes > 0);
721
722    // Check if more updates are needed
723    const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId);
724
725    if (!viewV2Map || viewV2Map.size === 0) {
726      if (viewV2Map?.size === 0) {
727        this.viewV2NeedUpdateMap_.delete(containerId);
728      }
729
730      ViewStackProcessor.scheduleUpdateOnNextVSync(null, containerId);
731
732      // After all processing, remove from scheduled set
733      this.scheduledContainerIds_.delete(containerId);
734      return false;
735    }
736    aceDebugTrace.end();
737    stateMgmtConsole.debug(`ObservedV2.onVSyncUpdate there are still views to be updated for containerId=${containerId}`);
738    return true;
739  }
740
741  public hasPendingUpdates(containerId: number): boolean {
742    const viewV2Map = this.viewV2NeedUpdateMap_.get(containerId);
743    let ret = ((viewV2Map && viewV2Map.size > 0) || this.monitorIdsChanged_.size > 0 || this.computedPropIdsChanged_.size > 0);
744    stateMgmtConsole.debug(`hasPendingUpdates() containerId: ${containerId}, viewV2Map size: ${viewV2Map?.size}, ret: ${ret}`);
745    return ret;
746  }
747
748  /**
749   * execute /update in this order
750   * - @Computed variables
751   * - @Monitor functions
752   * - UINode re-render
753   * three nested loops, means:
754   * process @Computed until no more @Computed need update
755   * process @Monitor until no more @Computed and @Monitor
756   * process UINode update until no more @Computed and @Monitor and UINode rerender
757   *
758   * @param updateUISynchronously should be set to true if called during VSYNC only
759   *
760   */
761
762  public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void {
763    aceDebugTrace.begin('updateDirty2');
764    stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `);
765    // obtain and unregister the removed elmtIds
766    UINodeRegisterProxy.obtainDeletedElmtIds();
767    UINodeRegisterProxy.unregisterElmtIdsFromIViews();
768
769    // priority order of processing:
770    // 1- update computed properties until no more need computed props update
771    // 2- update monitors until no more monitors and no more computed props
772    // 3- update UINodes until no more monitors, no more computed props, and no more UINodes
773    // FIXME prevent infinite loops
774    do {
775      this.updateComputedAndMonitors();
776
777      if (this.elmtIdsChanged_.size) {
778        const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2);
779        this.elmtIdsChanged_ = new Set<number>();
780        updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds);
781      }
782    } while (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size > 0);
783
784    aceDebugTrace.end();
785    stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `);
786  }
787
788  /**
789   * execute /update in this order
790   * - @Computed variables
791   * - @Monitor functions
792   * - UINode re-render
793   * three nested loops, means:
794   * process @Computed until no more @Computed need update
795   * process @Monitor until no more @Computed and @Monitor
796  */
797  private updateComputedAndMonitors(): void {
798    do {
799      while (this.computedPropIdsChanged_.size) {
800        //  sort the ids and update in ascending order
801        // If a @Computed property depends on other @Computed properties, their
802        // ids will be smaller as they are defined first.
803        const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2);
804        this.computedPropIdsChanged_ = new Set<number>();
805        this.updateDirtyComputedProps(computedProps);
806      }
807
808      if (this.persistenceChanged_.size) {
809        const persistKeys: Array<number> = Array.from(this.persistenceChanged_);
810        this.persistenceChanged_ = new Set<number>();
811        PersistenceV2Impl.instance().onChangeObserved(persistKeys);
812      }
813
814      if (this.monitorIdsChanged_.size) {
815        const monitors = this.monitorIdsChanged_;
816        this.monitorIdsChanged_ = new Set<number>();
817        this.updateDirtyMonitors(monitors);
818      }
819      // handle the Monitor id from API configured with asynchronous options
820      while (this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0) {
821        if (this.monitorIdsChangedForAddMonitor_.size) {
822          stateMgmtConsole.debug(`AddMonitor asynchronous ${this.monitorIdsChangedForAddMonitor_.size}`)
823          const monitorId: Set<number> = this.monitorIdsChangedForAddMonitor_;
824          this.monitorIdsChangedForAddMonitor_ = new Set<number>();
825          // update the value and dependency for each path and get the MonitorV2 id needs to be execute
826          this.updateDirtyMonitorPath(monitorId);
827        }
828        if (this.monitorFuncsToRun_.size) {
829          const monitorFuncs = this.monitorFuncsToRun_;
830          this.monitorFuncsToRun_ = new Set<number>();
831          this.runMonitorFunctionsForAddMonitor(monitorFuncs);
832        }
833      }
834    } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size +
835    this.computedPropIdsChanged_.size + this.monitorIdsChangedForAddMonitor_.size + this.monitorFuncsToRun_.size > 0);
836  }
837
838  public updateDirtyComputedProps(computed: Array<number>): void {
839    stateMgmtConsole.debug(`ObservedV2.updateDirtyComputedProps ${computed.length} props: ${JSON.stringify(computed)} ...`);
840    aceDebugTrace.begin(`ObservedV2.updateDirtyComputedProps ${computed.length} @Computed`);
841    computed.forEach((id) => {
842      const comp = this.id2Others_[id]?.deref();
843      if (comp instanceof ComputedV2) {
844        const target = comp.getTarget();
845        if (target instanceof ViewV2 && !target.isViewActive()) {
846          // add delayed ComputedIds id
847          target.addDelayedComputedIds(id);
848        } else {
849          comp.fireChange();
850        }
851      }
852    });
853    aceDebugTrace.end();
854  }
855  /**
856   * @function resetMonitorValues
857   * @description This function ensures that @Monitor function are reset and reinitialized
858   *  during the reuse cycle:
859   * - Clear and reinitialize monitor IDs and functions to prevent unintended triggers
860   * - Reset dirty states to ensure reusabiltiy
861   */
862  public resetMonitorValues(): void {
863    stateMgmtConsole.debug(`resetMonitorValues changed monitorIds count: ${this.monitorIdsChanged_.size}`);
864    if (this.monitorIdsChanged_.size) {
865      const monitors = this.monitorIdsChanged_;
866      this.monitorIdsChanged_ = new Set<number>();
867      this.updateDirtyMonitorsOnReuse(monitors);
868    }
869  }
870
871  public updateDirtyMonitorsOnReuse(monitors: Set<number>): void {
872    let monitor: MonitorV2 | undefined;
873    monitors.forEach((watchId) => {
874      monitor = this.id2Others_[watchId]?.deref();
875      if (monitor instanceof MonitorV2) {
876        // only update dependency and reset value, no call monitor.
877        monitor.notifyChangeOnReuse();
878      }
879    });
880  }
881
882  public updateDirtyMonitors(monitors: Set<number>): void {
883    stateMgmtConsole.debug(`ObservedV2.updateDirtyMonitors: ${monitors.size} @monitor funcs: ${JSON.stringify(Array.from(monitors))} ...`);
884    aceDebugTrace.begin(`ObservedV2.updateDirtyMonitors: ${monitors.size} @monitor`);
885
886    let monitor: MonitorV2 | undefined;
887    let monitorTarget: Object;
888
889    monitors.forEach((watchId) => {
890      monitor = this.id2Others_[watchId]?.deref();
891      if (monitor instanceof MonitorV2) {
892        monitorTarget = monitor.getTarget();
893        if (monitorTarget instanceof ViewV2 && !monitorTarget.isViewActive()) {
894          // monitor notifyChange delayed if target is a View that is not active
895          monitorTarget.addDelayedMonitorIds(watchId);
896        } else {
897          monitor.notifyChange();
898        }
899      }
900    });
901    aceDebugTrace.end();
902  }
903
904  public runMonitorFunctionsForAddMonitor(monitors: Set<number>): void {
905    stateMgmtConsole.debug(`ObservedV2.runMonitorFunctionsForAddMonitor: ${monitors.size}. AddMonitor funcs: ${JSON.stringify(Array.from(monitors))} ...`);
906    aceDebugTrace.begin(`ObservedV2.runMonitorFunctionsForAddMonitor: ${monitors.size}`);
907
908    let monitor: MonitorV2 | undefined;
909
910    monitors.forEach((watchId) => {
911      monitor = this.id2Others_[watchId]?.deref();
912      if (monitor instanceof MonitorV2) {
913        monitor.runMonitorFunction();
914      }
915    });
916    aceDebugTrace.end();
917  }
918
919
920  public updateDirtyMonitorPath(monitors: Set<number>): void {
921    stateMgmtConsole.debug(`ObservedV2.updateDirtyMonitorPath: ${monitors.size} addMonitor funcs: ${JSON.stringify(Array.from(monitors))} ...`);
922    aceDebugTrace.begin(`ObservedV3.updateDirtyMonitorPath: ${monitors.size} addMonitor`);
923
924    let ret: number = 0;
925    monitors.forEach((watchId) => {
926      const monitor = this.id2Others_[watchId]?.deref();
927      if (monitor instanceof MonitorV2) {
928        const monitorTarget = monitor.getTarget();
929        if (monitorTarget instanceof ViewV2 && !monitorTarget.isViewActive()) {
930          monitorTarget.addDelayedMonitorIds(watchId)
931        } else {
932          // find the path MonitorValue and record dependency again
933          // get path owning MonitorV2 id
934          ret = monitor.notifyChangeForEachPath(watchId);
935        }
936      }
937
938      // Collect AddMonitor functions that need to be executed later
939      if (ret > 0) {
940        this.monitorFuncsToRun_.add(ret);
941      }
942    });
943    aceDebugTrace.end();
944  }
945
946
947  /**
948   * This version of UpdateUINodes does not wait for VSYNC, violates rules
949   * calls UpdateElement, thereby avoids the long and frequent code path from
950   * FlushDirtyNodesUpdate to CustomNode to ViewV2.updateDirtyElements to UpdateElement
951   * Code left here to reproduce benchmark measurements, compare with future optimisation
952   * @param elmtIds
953   *
954   */
955  private updateUINodesSynchronously(elmtIds: Array<number>, inView?: ViewPU | ViewV2): void {
956    stateMgmtConsole.debug(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtIds: ${JSON.stringify(elmtIds)} ...`);
957    aceDebugTrace.begin(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtId`);
958
959    elmtIds.forEach((elmtId) => {
960      let view = inView ?? this.id2cmp_[elmtId]?.deref();
961      if ((view instanceof ViewV2) || (view instanceof ViewPU)) {
962        if (view.isViewActive()) {
963          // FIXME need to call syncInstanceId before update?
964          view.UpdateElement(elmtId);
965        } else {
966          // schedule delayed update once the view gets active
967          view.scheduleDelayedUpdate(elmtId);
968        }
969      } // if ViewV2 or ViewPU
970    });
971    aceDebugTrace.end();
972  }
973
974  private updateUINodesForReuse(elmtIds: Array<number>): void {
975    aceDebugTrace.begin(`ObserveV2.updateUINodesForReuse: ${elmtIds.length} elmtId`);
976    let view: Object;
977    let weak: any;
978    elmtIds.forEach((elmtId) => {
979      if ((weak = this.id2cmp_[elmtId]) && weak && ('deref' in weak) &&
980        (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) {
981        if (view.isViewActive()) {
982          /* update child element */ this.currentReuseId_ === view.id__() ||
983          /* update parameter */ this.currentReuseId_ === elmtId
984            ? view.UpdateElement(elmtId)
985            : view.uiNodeNeedUpdateV2(elmtId);
986        } else {
987          // schedule delayed update once the view gets active
988          view.scheduleDelayedUpdate(elmtId);
989        }
990      } // if ViewV2 or ViewPU
991    });
992    aceDebugTrace.end();
993  }
994
995  // This is the code path similar to V2, follows the rule that UI updates on VSYNC.
996  // ViewPU/ViewV2 queues the elmtId that need update, marks the CustomNode dirty in RenderContext
997  // On next VSYNC runs FlushDirtyNodesUpdate to call rerender to call UpdateElement. Much longer code path
998  // much slower
999  private updateUINodes(elmtIds: Array<number>): void {
1000    stateMgmtConsole.debug(`ObserveV2.updateUINodes: ${elmtIds.length} elmtIds need rerender: ${JSON.stringify(elmtIds)} ...`);
1001    aceDebugTrace.begin(`ObserveV2.updateUINodes: ${elmtIds.length} elmtId`);
1002
1003    elmtIds.forEach((elmtId) => {
1004      const view = this.id2cmp_[elmtId]?.deref();
1005      if ((view instanceof ViewV2) || (view instanceof ViewPU)) {
1006        if (view.isViewActive()) {
1007          view.uiNodeNeedUpdateV2(elmtId);
1008        } else {
1009          // schedule delayed update once the view gets active
1010          view.scheduleDelayedUpdate(elmtId);
1011        }
1012      }
1013    });
1014    aceDebugTrace.end();
1015  }
1016
1017  public constructMonitor(owningObject: Object, owningObjectName: string): void {
1018    let watchProp = Symbol.for(MonitorV2.WATCH_PREFIX + owningObjectName);
1019    if (owningObject && (typeof owningObject === 'object') && owningObject[watchProp]) {
1020      Object.entries(owningObject[watchProp]).forEach(([pathString, monitorFunc]) => {
1021        if (monitorFunc && pathString && typeof monitorFunc === 'function') {
1022          const monitor = new MonitorV2(owningObject, pathString, monitorFunc as (m: IMonitor) => void, true);
1023          monitor.InitRun();
1024          const refs = owningObject[ObserveV2.MONITOR_REFS] ??= {};
1025          // store a reference inside owningObject
1026          // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2
1027          // remember: id2others only has a WeakRef to MonitorV2 obj
1028          refs[monitorFunc.name] = monitor;
1029        }
1030        // FIXME Else handle error
1031      });
1032    } // if target[watchProp]
1033  }
1034
1035  public AddMonitorPath(target: object, path: string | string[], monitorFunc: MonitorCallback, options?: MonitorOptions): void {
1036    const funcName = monitorFunc.name;
1037    const refs = target[ObserveV2.ADD_MONITOR_REFS] ??= {};
1038    let monitor = refs[funcName];
1039    const pathsUniqueString = Array.isArray(path) ? path.join(' ') : path;
1040    const isSync: boolean = options ? options.isSynchronous : false;
1041    const paths = Array.isArray(path) ? path : [path];
1042    if (monitor && monitor instanceof MonitorV2) {
1043      if (isSync !== monitor.isSync()) {
1044        stateMgmtConsole.applicationError(`addMonitor failed, current function ${funcName} has already register as ${monitor.isSync()? `sync`: `async`}, cannot change to ${isSync? `sync`: `async`} anymore`);
1045        return;
1046      }
1047      paths.forEach(item => {
1048        monitor.addPath(item);
1049      });
1050      monitor.InitRun();
1051      return;
1052    }
1053
1054    monitor = new MonitorV2(target, pathsUniqueString, monitorFunc, false, isSync);
1055    monitor.InitRun();
1056    // store a reference inside target
1057    // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2 to prevent the MonitorV2 is GC
1058    // remember: id2others only has a WeakRef to MonitorV2 obj
1059    refs[funcName] = monitor;
1060  }
1061
1062  public clearMonitorPath(target: object, path: string | string[], monitorFunc?: MonitorCallback): void {
1063    const refs = target[ObserveV2.ADD_MONITOR_REFS] ??= {};
1064    const paths = Array.isArray(path) ? path : [path];
1065
1066    if (monitorFunc) {
1067      const funcName = monitorFunc.name;
1068      let monitor = refs[funcName];
1069      if (monitor && monitor instanceof MonitorV2) {
1070        paths.forEach(item => {
1071          if (!monitor.removePath(item)) {
1072            stateMgmtConsole.applicationError(
1073              `cannot clear path ${item} for ${funcName} because it was never registered with addMonitor`
1074            );
1075          }
1076        });
1077      } else {
1078        const pathsUniqueString = paths.join(' ');
1079        stateMgmtConsole.applicationError(
1080          `clearMonitor failed: cannot clear path(s) ${pathsUniqueString} for ${funcName} because no Monitor instance was found for this function`
1081        );
1082      }
1083      return;
1084    }
1085    // no monitorFunc passed: try to remove this path for all the monitorV2 instance stored in current target
1086    const monitors = Object.values(refs).filter((m): m is MonitorV2 => m instanceof MonitorV2);
1087
1088    paths.forEach(item => {
1089      let res = false;
1090      monitors.forEach(monitor => {
1091        if (monitor.removePath(item)) {
1092          res = true;
1093        }
1094      });
1095      if (!res) {
1096        stateMgmtConsole.applicationError(
1097          `cannot clear path ${item} for current target ${target.constructor.name} because no Monitor function for this path was registered`
1098        );
1099      }
1100    });
1101  }
1102
1103  public constructComputed(owningObject: Object, owningObjectName: string): void {
1104    const computedProp = Symbol.for(ComputedV2.COMPUTED_PREFIX + owningObjectName);
1105    if (owningObject && (typeof owningObject === 'object') && owningObject[computedProp]) {
1106      Object.entries(owningObject[computedProp]).forEach(([computedPropertyName, computeFunc]) => {
1107        stateMgmtConsole.debug(`constructComputed: in ${owningObject?.constructor?.name} found @Computed ${computedPropertyName}`);
1108        const computed = new ComputedV2(owningObject, computedPropertyName, computeFunc as unknown as () => any);
1109        computed.InitRun();
1110        const refs = owningObject[ObserveV2.COMPUTED_REFS] ??= {};
1111        // store a reference inside owningObject
1112        // thereby ComputedV2 will share lifespan as owning @ComponentV2 or @ObservedV2
1113        // remember: id2cmp only has a WeakRef to ComputedV2 obj
1114        const existingComputed = refs[computedPropertyName];
1115        if (existingComputed && existingComputed instanceof ComputedV2) {
1116          // current computed will be override, and will be GC soon
1117          // to avoid the Computed be triggered anymore, invalidate it
1118          this.clearBinding(existingComputed.getComputedId());
1119          stateMgmtConsole.warn(`Check compatibility, ${owningObjectName} has @Computed ${computedPropertyName} and create ${owningObject?.constructor?.name} instance`);
1120        }
1121        refs[computedPropertyName] = computed;
1122      });
1123    }
1124  }
1125
1126  public clearWatch(id: number): void {
1127    if (id < MonitorV2.MIN_WATCH_FROM_API_ID) {
1128      this.clearBinding(id);
1129      return;
1130    }
1131    const monitor: MonitorV2 = this.id2Others_[id]?.deref();
1132    if (monitor instanceof MonitorV2) {
1133      monitor.getValues().forEach((monitorValueV2: MonitorValueV2<unknown>) => {
1134        this.clearBinding(monitorValueV2.id);
1135      })
1136    }
1137    this.clearBinding(id);
1138  }
1139
1140  public registerMonitor(monitor: MonitorV2, id: number): void {
1141    const weakRef = WeakRefPool.get(monitor);
1142    // this instance, which maybe MonitorV2/ComputedV2 have been already recorded in id2Others
1143    if (this.id2Others_[id] === weakRef) {
1144      return;
1145    }
1146    this.id2Others_[id] = weakRef;
1147    // register MonitorV2/ComputedV2 instance gc-callback func
1148    WeakRefPool.register(monitor, id, () => {
1149      delete this.id2Others_[id];
1150    });
1151  }
1152
1153
1154  public static autoProxyObject(target: Object, key: string | symbol): any {
1155    let val = target[key];
1156    // Not an object, not a collection, no proxy required
1157    if (!val || typeof (val) !== 'object' ||
1158      !(Array.isArray(val) || val instanceof Set || val instanceof Map || val instanceof Date)) {
1159      return val;
1160    }
1161
1162    // Collections are the only type that require proxy observation. If they have already been observed, no further observation is needed.
1163    // Prevents double-proxying: checks if the object is already proxied by either V1 or V2 (to avoid conflicts).
1164    // Prevents V2 proxy creation if the developer uses makeV1Observed and also tries to wrap a V2 proxy with built-in types
1165    // Handle the case where both V1 and V2 proxies exist (if V1 proxy doesn't trigger enableV2Compatibility).
1166    // Currently not implemented to avoid compatibility issues with existing apps that may use both V1 and V2 proxies.
1167    if (!val[ObserveV2.SYMBOL_PROXY_GET_TARGET] && !(ObservedObject.isEnableV2CompatibleInternal(val) || ObservedObject.isMakeV1Observed(val))) {
1168
1169      if (Array.isArray(val)) {
1170        target[key] = new Proxy(val, ObserveV2.arrayProxy);
1171      } else if (val instanceof Set || val instanceof Map) {
1172        target[key] = new Proxy(val, ObserveV2.setMapProxy);
1173      } else {
1174        target[key] = new Proxy(val, ObserveV2.objectProxy);
1175      }
1176      val = target[key];
1177    }
1178
1179    // If the return value is an Array, Set, Map
1180    // if (this.arr[0] !== undefined, and similar for Set and Map) will not update in response /
1181    // to array length/set or map size changing function without addRef on OB_LENGH
1182    if (!(val instanceof Date)) {
1183      if (ObservedObject.isEnableV2CompatibleInternal(val)) {
1184        ObserveV2.getObserve().addRefV2Compatibility(val, ObserveV2.OB_LENGTH);
1185      } else {
1186        ObserveV2.getObserve().addRef(ObserveV2.IsMakeObserved(val) ? RefInfo.get(UIUtilsImpl.instance().getTarget(val)) :
1187          val, ObserveV2.OB_LENGTH);
1188      }
1189    }
1190    return val;
1191  }
1192
1193  /**
1194   * Helper function to add meta data about decorator to ViewPU or ViewV2
1195   * @param proto prototype object of application class derived from  ViewPU or ViewV2 or `@ObservedV2` class
1196   * @param varName decorated variable
1197   * @param deco '@Local', '@Event', etc
1198   *              Excludes `@Computed` and `@Monitor`
1199   */
1200  public static addVariableDecoMeta(proto: Object, varName: string, deco: string): void {
1201    // add decorator meta data
1202    const meta = proto[ObserveV2.V2_DECO_META] ??= {};
1203    meta[varName] = {};
1204    meta[varName].deco = deco;
1205
1206    // FIXME
1207    // when splitting ViewPU and ViewV2
1208    // use instanceOf. Until then, this is a workaround.
1209    // any @Local, @Trace, etc V2 event handles this function to return false
1210    Reflect.defineProperty(proto, 'isViewV2', {
1211      get() { return true; },
1212      enumerable: false
1213    });
1214  }
1215
1216    /**
1217   * Helper function to add meta data about for `@Computed` and `@Monitor`
1218   * @param proto prototype object of application class derived from  ViewPU or ViewV2 or `@ObservedV2` class
1219   * @param varName decorated variable
1220   * @param deco `@Computed` and `@Monitor`
1221   */
1222  public static addMethodDecoMeta(proto: Object, varName: string, deco: string): void {
1223    // add decorator meta data
1224    const meta = proto[ObserveV2.V2_DECO_METHOD_META] ??= {};
1225    meta[varName] = {};
1226    meta[varName].deco = deco;
1227
1228    Reflect.defineProperty(proto, 'isViewV2', {
1229      get() { return true; },
1230      enumerable: false
1231    });
1232  }
1233
1234
1235  public static addParamVariableDecoMeta(proto: Object, varName: string, deco?: string, deco2?: string): void {
1236    // add decorator meta data
1237    const meta = proto[ObserveV2.V2_DECO_META] ??= {};
1238    meta[varName] ??= {};
1239    if (deco) {
1240      meta[varName].deco = deco;
1241    }
1242    if (deco2) {
1243      meta[varName].deco2 = deco2;
1244    }
1245
1246    // FIXME
1247    // when splitting ViewPU and ViewV2
1248    // use instanceOf. Until then, this is a workaround.
1249    // any @Local, @Trace, etc V2 event handles this function to return false
1250    Reflect.defineProperty(proto, 'isViewV2', {
1251      get() { return true; },
1252      enumerable: false
1253    }
1254    );
1255  }
1256
1257
1258  public static usesV2Variables(proto: Object): boolean {
1259    return (proto && typeof proto === 'object' && proto[ObserveV2.V2_DECO_META]);
1260  }
1261
1262  /**
1263   * Get element info according to the elmtId.
1264   *
1265   * @param elmtId element id.
1266   * @param isProfiler need to return ElementType including the id, type and isCustomNode when isProfiler is true.
1267   *                   The default value is false.
1268   */
1269  public getElementInfoById(elmtId: number, isProfiler: boolean = false): string | ElementType {
1270    let weak: WeakRef<ViewBuildNodeBase> | undefined = UINodeRegisterProxy.ElementIdToOwningViewPU_.get(elmtId);
1271    let view;
1272    return (weak && (view = weak.deref()) && (view instanceof PUV2ViewBase)) ? view.debugInfoElmtId(elmtId, isProfiler) : `unknown component type[${elmtId}]`;
1273  }
1274
1275  /**
1276   * Get attrName decorator info.
1277   */
1278  public getDecoratorInfo(target: object, attrName: string): string {
1279    const meta = target[ObserveV2.V2_DECO_META];
1280    const metaMethod = target[ObserveV2.V2_DECO_METHOD_META];
1281    const decorator = meta?.[attrName] ?? metaMethod?.[attrName];
1282    return this.parseDecorator(decorator);
1283  }
1284
1285  public parseDecorator(decorator: any): string {
1286    if (!decorator) {
1287      return '';
1288    }
1289    if (typeof decorator !== 'object') {
1290      return '';
1291    }
1292    let decoratorInfo: string = '';
1293    if ('deco' in decorator) {
1294      decoratorInfo = decorator.deco;
1295    }
1296    if ('aliasName' in decorator) {
1297      decoratorInfo += `(${decorator.aliasName})`;
1298    }
1299    if ('deco2' in decorator) {
1300      decoratorInfo += decorator.deco2;
1301    }
1302    return decoratorInfo;
1303  }
1304
1305  public getComputedInfoById(computedId: number): string {
1306    let weak = this.id2Others_[computedId];
1307    let computedV2: ComputedV2;
1308    return (weak && (computedV2 = weak.deref()) && (computedV2 instanceof ComputedV2)) ? computedV2.getComputedFuncName() : '';
1309  }
1310
1311  public getMonitorInfoById(computedId: number): string {
1312    let weak = this.id2Others_[computedId];
1313    let monitorV2: MonitorV2;
1314    return (weak && (monitorV2 = weak.deref()) && (monitorV2 instanceof MonitorV2)) ? monitorV2.getMonitorFuncName() : '';
1315  }
1316
1317  public setCurrentReuseId(elmtId: number): void {
1318    this.currentReuseId_ = elmtId;
1319  }
1320} // class ObserveV2
1321
1322const trackInternal = (
1323  target: any,
1324  propertyKey: string
1325): void => {
1326  if (typeof target === 'function' && !Reflect.has(target, propertyKey)) {
1327    // dynamic track,and it not a static attribute
1328    target = target.prototype;
1329  }
1330  const storeProp = ObserveV2.OB_PREFIX + propertyKey;
1331  target[storeProp] = target[propertyKey];
1332  Reflect.defineProperty(target, propertyKey, {
1333    get() {
1334      ObserveV2.getObserve().addRef(this, propertyKey);
1335      return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + propertyKey);
1336    },
1337    set(val) {
1338      // If the object has not been observed, you can directly assign a value to it. This improves performance.
1339      if (val !== this[storeProp]) {
1340        this[storeProp] = val;
1341
1342        // the bindings <*, target, propertyKey> might not have been recorded yet (!)
1343        // fireChange will run idleTasks to record pending bindings, if any
1344        ObserveV2.getObserve().fireChange(this, propertyKey);
1345      }
1346    },
1347    enumerable: true
1348  });
1349  // this marks the proto as having at least one @Trace property inside
1350  // used by IsObservedObjectV2
1351  target[ObserveV2.V2_DECO_META] ??= {};
1352}; // trackInternal
1353
1354// used to manually mark dirty v2 before animateTo
1355function __updateDirty2Immediately_V2_Change_Observation(): void {
1356  ObserveV2.getObserve().updateDirty2();
1357}