• 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, IView | 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: IView | 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  public static readonly SYMBOL_REFS = Symbol('__use_refs__');
57  public static readonly ID_REFS = Symbol('__id_refs__');
58  public static readonly MONITOR_REFS = Symbol('___monitor_refs_');
59  public static readonly COMPUTED_REFS = Symbol('___computed_refs_');
60
61  public static readonly SYMBOL_PROXY_GET_TARGET = Symbol('__proxy_get_target');
62
63  public static readonly SYMBOL_MAKE_OBSERVED = Symbol('___make_observed__');
64
65  public static readonly OB_PREFIX = '__ob_'; // OB_PREFIX + attrName => backing store attribute name
66  public static readonly OB_PREFIX_LEN = 5;
67  public static readonly NO_REUSE = -1; // mark no reuse on-going
68  // used by array Handler to create dependency on artificial 'length'
69  // property of array, mark it as changed when array has changed.
70  public static readonly OB_LENGTH = '___obj_length';
71
72  private static setMapProxy: SetMapProxyHandler = new SetMapProxyHandler();
73  private static arrayProxy: ArrayProxyHandler = new ArrayProxyHandler();
74  private static objectProxy: ObjectProxyHandler = new ObjectProxyHandler();
75
76  // see MonitorV2.observeObjectAccess: bindCmp is the MonitorV2
77  // see modified ViewV2 and ViewPU observeComponentCreation, bindCmp is the ViewV2 or ViewPU
78
79  // bindId: UINode elmtId or watchId, depending on what is being observed
80  private stackOfRenderedComponents_ : StackOfRenderedComponents = new StackOfRenderedComponents();
81
82  // Map bindId to WeakRef<ViewPU> | MonitorV2
83  private id2cmp_: { number: WeakRef<Object> } = {} as { number: WeakRef<Object> };
84
85  // Map bindId -> Set of @ObservedV2 class objects
86  // reverse dependency map for quickly removing all dependencies of a bindId
87  private id2targets_: { number: Set<WeakRef<Object>> } = {} as { number: Set<WeakRef<Object>> };
88
89  // queued up Set of bindId
90  // elmtIds of UINodes need re-render
91  // @monitor functions that need to execute
92  public elmtIdsChanged_: Set<number> = new Set();
93  private computedPropIdsChanged_: Set<number> = new Set();
94  private monitorIdsChanged_: Set<number> = new Set();
95  private persistenceChanged_: Set<number> = new Set();
96  // avoid recursive execution of updateDirty
97  // by state changes => fireChange while
98  // UINode rerender or @monitor function execution
99  private startDirty_: boolean = false;
100
101  // flag to indicate change observation is disabled
102  private disabled_: boolean = false;
103
104  // flag to indicate ComputedV2 calculation is ongoing
105  private calculatingComputedProp_: boolean = false;
106
107  // use for mark current reuse id, ObserveV2.NO_REUSE(-1) mean no reuse on-going
108  protected currentReuseId_: number = ObserveV2.NO_REUSE;
109
110  private static obsInstance_: ObserveV2;
111
112  public static getObserve(): ObserveV2 {
113    if (!this.obsInstance_) {
114      this.obsInstance_ = new ObserveV2();
115    }
116    return this.obsInstance_;
117  }
118
119  // return true given value is @ObservedV2 object
120  public static IsObservedObjectV2(value: any): boolean {
121    return (value && typeof (value) === 'object' && value[ObserveV2.V2_DECO_META]);
122  }
123
124  // return true if given value is proxied observed object, either makeObserved or autoProxyObject
125  public static IsProxiedObservedV2(value: any): boolean {
126    return (value && typeof value === 'object' && value[ObserveV2.SYMBOL_PROXY_GET_TARGET]);
127  }
128
129  // return true given value is the return value of makeObserved
130  public static IsMakeObserved(value: any): boolean {
131    return (value && typeof (value) === 'object' && value[ObserveV2.SYMBOL_MAKE_OBSERVED]);
132  }
133
134  public static getCurrentRecordedId(): number {
135    const bound = ObserveV2.getObserve().stackOfRenderedComponents_.top();
136    return bound ? bound[0] : -1;
137  }
138
139  // At the start of observeComponentCreation or
140  // MonitorV2 observeObjectAccess
141  public startRecordDependencies(cmp: IView | MonitorV2 | ComputedV2 | PersistenceV2Impl | ViewBuildNodeBase, id: number, doClearBinding: boolean = true): void {
142    if (cmp != null) {
143      doClearBinding && this.clearBinding(id);
144      this.stackOfRenderedComponents_.push(id, cmp);
145    }
146  }
147
148    // At the start of observeComponentCreation or
149  // MonitorV2 observeObjectAccess
150  public stopRecordDependencies(): void {
151    const bound = this.stackOfRenderedComponents_.pop();
152    if (bound === undefined) {
153      stateMgmtConsole.error('stopRecordDependencies finds empty stack. Internal error!');
154      return;
155    }
156    let targetsSet: Set<WeakRef<Object>>;
157    if ((targetsSet = this.id2targets_[bound[0]]) !== undefined && targetsSet.size) {
158      // only add IView | MonitorV2 | ComputedV2 if at least one dependency was
159      // recorded when rendering this ViewPU/ViewV2/Monitor/ComputedV2
160      // ViewPU is the likely case where no dependecy gets recorded
161      // for others no dependencies are unlikely to happen
162      this.id2cmp_[bound[0]] = new WeakRef<Object>(bound[1]);
163    }
164  }
165
166  // clear any previously created dependency view model object to elmtId
167  // find these view model objects with the reverse map id2targets_
168  public clearBinding(id: number): void {
169    // multiple weakRefs might point to the same target - here we get Set of unique targets
170    const targetSet = new Set<Object>();
171    this.id2targets_[id]?.forEach((weak : WeakRef<Object>) => {
172      if (weak.deref() instanceof Object) {
173        targetSet.add(weak.deref());
174      }
175    });
176
177    targetSet.forEach((target) => {
178      const idRefs: Object | undefined = target[ObserveV2.ID_REFS];
179      const symRefs: Object = target[ObserveV2.SYMBOL_REFS];
180
181      if (idRefs) {
182        idRefs[id]?.forEach(key => symRefs?.[key]?.delete(id));
183        delete idRefs[id];
184      } else {
185        for (let key in symRefs) {
186          symRefs[key]?.delete(id);
187        };
188      }
189    });
190
191    delete this.id2targets_[id];
192    delete this.id2cmp_[id];
193
194    stateMgmtConsole.propertyAccess(`clearBinding (at the end): id2cmp_ length=${Object.keys(this.id2cmp_).length}, entries=${JSON.stringify(Object.keys(this.id2cmp_))} `);
195    stateMgmtConsole.propertyAccess(`... id2targets_ length=${Object.keys(this.id2targets_).length}, entries=${JSON.stringify(Object.keys(this.id2targets_))} `);
196  }
197
198  /**
199   *
200   * this cleanUpId2CmpDeadReferences()
201   * id2cmp is a 'map' object id => WeakRef<Object> where object is ViewV2, ViewPU, MonitorV2 or ComputedV2
202   * This method iterates over the object entries and deleted all those entries whose value can no longer
203   * be deref'ed.
204   *
205   * cleanUpId2TargetsDeadReferences()
206   * is2targets is a 'map' object id => Set<WeakRef<Object>>
207   * the method traverses over the object entries and for each value of type
208   * Set<WeakRef<Object>> removes all those items from the set that can no longer be deref'ed.
209   *
210   * According to JS specifications, it is up to ArlTS runtime GC implementation when to collect unreferences objects.
211   * Parameters such as available memory, ArkTS processing load, number and size of all JS objects for GC collection
212   * can impact the time delay between an object loosing last reference and GC collecting this object.
213   *
214   * WeakRef deref() returns the object until GC has collected it.
215   * The id2cmp and is2targets cleanup herein depends on WeakRef.deref() to return undefined, i.e. it depends on GC
216   * collecting 'cmp' or 'target' objects. Only then the algorithm can remove the entry from id2cmp / from id2target.
217   * It is therefore to be expected behavior that these map objects grow and they a contain a larger number of
218   * MonitorV2, ComputedV2, and/or view model @Observed class objects that are no longer used / referenced by the application.
219   * Only after ArkTS runtime GC has collected them, this function is able to clean up the id2cmp and is2targets.
220   *
221   * This cleanUpDeadReferences() function gets called from UINodeRegisterProxy.uiNodeCleanUpIdleTask()
222   *
223   */
224  public cleanUpDeadReferences(): void {
225    this.cleanUpId2CmpDeadReferences();
226    this.cleanUpId2TargetsDeadReferences();
227  }
228
229  private cleanUpId2CmpDeadReferences(): void {
230    stateMgmtConsole.debug(`cleanUpId2CmpDeadReferences ${JSON.stringify(this.id2cmp_)} `);
231    for (const id in this.id2cmp_) {
232      stateMgmtConsole.debug('cleanUpId2CmpDeadReferences loop');
233      let weakRef: WeakRef<object> = this.id2cmp_[id];
234      if (weakRef && typeof weakRef === 'object' && 'deref' in weakRef && weakRef.deref() === undefined) {
235        stateMgmtConsole.debug('cleanUpId2CmpDeadReferences cleanup hit');
236        delete this.id2cmp_[id];
237      }
238    }
239  }
240
241  private cleanUpId2TargetsDeadReferences(): void {
242    for (const id in this.id2targets_) {
243      const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id];
244      if (targetSet && targetSet instanceof Set) {
245        for (let weakTarget of targetSet) {
246          if (weakTarget.deref() === undefined) {
247            stateMgmtConsole.debug('cleanUpId2TargetsDeadReferences cleanup hit');
248            targetSet.delete(weakTarget);
249          }
250        } // for targetSet
251      }
252    } // for id2targets_
253  }
254
255  /**
256   * counts number of WeakRef<Object> entries in id2cmp_ 'map' object
257   * @returns total count and count of WeakRefs that can be deref'ed
258   * Methods only for testing
259   */
260  public get id2CompDeRefSize(): [ totalCount: number, aliveCount: number ] {
261    let totalCount = 0;
262    let aliveCount = 0;
263    let comp: Object;
264    for (const id in this.id2cmp_) {
265      totalCount++;
266      let weakRef: WeakRef<Object> = this.id2cmp_[id];
267      if (weakRef && 'deref' in weakRef && (comp = weakRef.deref()) && comp instanceof Object) {
268        aliveCount++;
269      }
270    }
271    return [totalCount, aliveCount];
272  }
273
274  /** counts number of target WeakRef<object> entries in all the Sets inside id2targets 'map' object
275 * @returns total count and those can be dereferenced
276 * Methods only for testing
277 */
278  public get id2TargetsDerefSize(): [ totalCount: number, aliveCount: number ] {
279    let totalCount = 0;
280    let aliveCount = 0;
281    for (const id in this.id2targets_) {
282      const targetSet: Set<WeakRef<Object>> | undefined = this.id2targets_[id];
283      if (targetSet && targetSet instanceof Set) {
284        for (let weakTarget of targetSet) {
285          totalCount++;
286          if (weakTarget.deref()) {
287            aliveCount++;
288          }
289        } // for targetSet
290      }
291    } // for id2targets_
292    return [totalCount, aliveCount];
293  }
294
295  // add dependency view model object 'target' property 'attrName'
296  // to current this.bindId
297  public addRef(target: object, attrName: string): void {
298    const bound = this.stackOfRenderedComponents_.top();
299    if (!bound) {
300      return;
301    }
302    if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) {
303      const error = `${attrName}: ObserveV2.addRef: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`;
304      stateMgmtConsole.applicationError(error);
305      throw new TypeError(error);
306    }
307
308    stateMgmtConsole.propertyAccess(`ObserveV2.addRef '${attrName}' for id ${bound[0]}...`);
309    this.addRef4IdInternal(bound[0], target, attrName);
310  }
311
312  // add dependency view model object 'target' property 'attrName' to current this.bindId
313  // this variation of the addRef function is only used to record read access to V1 observed object with enableV2Compatibility enabled
314  // e.g. only from within ObservedObject proxy handler implementations.
315  public addRefV2Compatibility(target: object, attrName: string): void {
316    const bound = this.stackOfRenderedComponents_.top();
317    if (bound && bound[1]) {
318      if (!(bound[1] instanceof ViewPU)) {
319        if (bound[0] === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) {
320          const error = `${attrName}: ObserveV2.addRefV2Compatibility: trying to use V2 state '${attrName}' to init/update child V2 @Component. Application error`;
321          stateMgmtConsole.applicationError(error);
322          throw new TypeError(error);
323        }
324        stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]}...`);
325        this.addRef4IdInternal(bound[0], target, attrName);
326      } else {
327        // inside ViewPU
328        stateMgmtConsole.propertyAccess(`ObserveV2.addRefV2Compatibility '${attrName}' for id ${bound[0]} -- skip addRef because render/update is inside V1 ViewPU`);
329      }
330    }
331  }
332
333  public addRef4Id(id: number, target: object, attrName: string): void {
334    stateMgmtConsole.propertyAccess(`ObserveV2.addRef4Id '${attrName}' for id ${id} ...`);
335    this.addRef4IdInternal(id, target, attrName);
336  }
337
338  private addRef4IdInternal(id: number, target: object, attrName: string): void {
339    // Map: attribute/symbol -> dependent id
340    const symRefs = target[ObserveV2.SYMBOL_REFS] ??= {};
341    symRefs[attrName] ??= new Set();
342    symRefs[attrName].add(id);
343
344    // Map id -> attribute/symbol
345    // optimization for faster clearBinding
346    const idRefs = target[ObserveV2.ID_REFS];
347    if (idRefs) {
348      idRefs[id] ??= new Set();
349      idRefs[id].add(attrName);
350    }
351
352    const targetSet = this.id2targets_[id] ??= new Set<WeakRef<Object>>();
353    targetSet.add(new WeakRef<Object>(target));
354  }
355
356  /**
357   *
358   * @param target set tracked attribute to new value without notifying the change
359   *               !! use with caution !!
360   * @param attrName
361   * @param newValue
362   */
363  public setUnmonitored<Z>(target: object, attrName: string, newValue: Z): void {
364    const storeProp = ObserveV2.OB_PREFIX + attrName;
365    if (storeProp in target) {
366      // @Track attrName
367      stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - tracked but unchanged. Doing nothing.`);
368      target[storeProp] = newValue;
369    } else {
370      stateMgmtConsole.propertyAccess(`setUnmonitored '${attrName}' - untracked, assigning straight.`);
371      // untracked attrName
372      target[attrName] = newValue;
373    }
374  }
375
376  /**
377   * Execute given task while state change observation is disabled
378   * A state mutation caused by the task will NOT trigger UI rerender
379   * and @monitor function execution.
380   *
381   * !!! Use with Caution !!!
382   *
383   * @param task a function to execute without monitoring state changes
384   * @returns task function return value
385   */
386  public executeUnobserved<Z>(task: () => Z): Z {
387    stateMgmtConsole.propertyAccess(`executeUnobserved - start`);
388    this.disabled_ = true;
389    let ret: Z;
390    try {
391      ret = task();
392    } catch (e) {
393      stateMgmtConsole.applicationError(`executeUnobserved - task execution caused error ${e} !`);
394    }
395    this.disabled_ = false;
396    stateMgmtConsole.propertyAccess(`executeUnobserved - done`);
397    return ret;
398  }
399
400
401
402
403  /**
404  * mark view model object 'target' property 'attrName' as changed
405  * notify affected watchIds and elmtIds
406  *
407  * @param propName ObservedV2 or ViewV2 target
408  * @param attrName attrName
409  * @param ignoreOnProfile The data reported to the profiler needs to be the changed state data.
410  * If the fireChange is invoked before the data changed, it needs to be ignored on the profiler.
411  * The default value is false.
412  */
413  public fireChange(target: object, attrName: string, ignoreOnProfiler: boolean = false): void {
414    // enable to get more fine grained traces
415    // including 2 (!) .end calls.
416
417    if (!target[ObserveV2.SYMBOL_REFS] || this.disabled_) {
418      return;
419    }
420
421    const bound = this.stackOfRenderedComponents_.top();
422    if (this.calculatingComputedProp_) {
423      const prop = bound ? (bound[1] as ComputedV2).getProp() : 'unknown computed property';
424      const error = `Usage of ILLEGAL @Computed function detected for ${prop}! The @Computed function MUST NOT change the state of any observed state variable!`;
425      stateMgmtConsole.applicationError(error);
426      throw new Error(error);
427    }
428
429    // enable this trace marker for more fine grained tracing of the update pipeline
430    // note: two (!) end markers need to be enabled
431    let changedIdSet = target[ObserveV2.SYMBOL_REFS][attrName];
432    if (!changedIdSet || !(changedIdSet instanceof Set)) {
433      return;
434    }
435
436    stateMgmtConsole.propertyAccess(`ObserveV2.fireChange '${attrName}' dependent ids: ${JSON.stringify(Array.from(changedIdSet))}  ...`);
437
438    for (const id of changedIdSet) {
439      // Cannot fireChange the object that is being created.
440      if (bound && id === bound[0]) {
441        continue;
442      }
443
444      // if this is the first id to be added to any Set of changed ids,
445      // schedule an 'updateDirty' task
446      // that will run after the current call stack has unwound.
447      // purpose of check for startDirty_ is to avoid going into recursion. This could happen if
448      // exec a re-render or exec a monitor function changes some state -> calls fireChange -> ...
449      if ((this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size === 0) &&
450        /* update not already in progress */ !this.startDirty_ &&
451        /* no reuse on-going */ this.currentReuseId_ === ObserveV2.NO_REUSE) {
452        Promise.resolve()
453        .then(this.updateDirty.bind(this))
454        .catch(error => {
455          stateMgmtConsole.applicationError(`Exception occurred during the update process involving @Computed properties, @Monitor functions or UINode re-rendering`, error);
456          _arkUIUncaughtPromiseError(error);
457        });
458      }
459
460      // add bindId to the correct Set of pending changes.
461      if (id < ComputedV2.MIN_COMPUTED_ID) {
462        this.elmtIdsChanged_.add(id);
463      } else if (id < MonitorV2.MIN_WATCH_ID) {
464        this.computedPropIdsChanged_.add(id);
465      } else if (id < PersistenceV2Impl.MIN_PERSISTENCE_ID) {
466        this.monitorIdsChanged_.add(id);
467      } else {
468        this.persistenceChanged_.add(id);
469      }
470    } // for
471
472    // report the stateVar changed when recording the profiler
473    if (stateMgmtDFX.enableProfiler && !ignoreOnProfiler) {
474      stateMgmtDFX.reportStateInfoToProfilerV2(target, attrName, changedIdSet);
475    }
476  }
477
478  public updateDirty(): void {
479    this.startDirty_ = true;
480    this.updateDirty2(false);
481    this.startDirty_ = false;
482  }
483
484  /**
485   * execute /update in this order
486   * - @Computed variables
487   * - @Monitor functions
488   * - UINode re-render
489   * three nested loops, means:
490   * process @Computed until no more @Computed need update
491   * process @Monitor until no more @Computed and @Monitor
492   * process UINode update until no more @Computed and @Monitor and UINode rerender
493   *
494   * @param updateUISynchronously should be set to true if called during VSYNC only
495   *
496   */
497
498  public updateDirty2(updateUISynchronously: boolean = false, isReuse: boolean = false): void {
499    aceDebugTrace.begin('updateDirty2');
500    stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} ... `);
501    // obtain and unregister the removed elmtIds
502    UINodeRegisterProxy.obtainDeletedElmtIds();
503    UINodeRegisterProxy.unregisterElmtIdsFromIViews();
504
505    // priority order of processing:
506    // 1- update computed properties until no more need computed props update
507    // 2- update monitors until no more monitors and no more computed props
508    // 3- update UINodes until no more monitors, no more computed props, and no more UINodes
509    // FIXME prevent infinite loops
510    do {
511      do {
512        while (this.computedPropIdsChanged_.size) {
513          //  sort the ids and update in ascending order
514          // If a @Computed property depends on other @Computed properties, their
515          // ids will be smaller as they are defined first.
516          const computedProps = Array.from(this.computedPropIdsChanged_).sort((id1, id2) => id1 - id2);
517          this.computedPropIdsChanged_ = new Set<number>();
518          this.updateDirtyComputedProps(computedProps);
519        }
520
521        if (this.persistenceChanged_.size) {
522          const persistKeys: Array<number> = Array.from(this.persistenceChanged_);
523          this.persistenceChanged_ = new Set<number>();
524          PersistenceV2Impl.instance().onChangeObserved(persistKeys);
525        }
526
527        if (this.monitorIdsChanged_.size) {
528          const monitors = this.monitorIdsChanged_;
529          this.monitorIdsChanged_ = new Set<number>();
530          this.updateDirtyMonitors(monitors);
531        }
532      } while (this.monitorIdsChanged_.size + this.persistenceChanged_.size + this.computedPropIdsChanged_.size > 0);
533
534      if (this.elmtIdsChanged_.size) {
535        const elmtIds = Array.from(this.elmtIdsChanged_).sort((elmtId1, elmtId2) => elmtId1 - elmtId2);
536        this.elmtIdsChanged_ = new Set<number>();
537        updateUISynchronously ? isReuse ? this.updateUINodesForReuse(elmtIds) : this.updateUINodesSynchronously(elmtIds) : this.updateUINodes(elmtIds);
538      }
539    } while (this.elmtIdsChanged_.size + this.monitorIdsChanged_.size + this.computedPropIdsChanged_.size > 0);
540
541    stateMgmtConsole.debug(`ObservedV2.updateDirty2 updateUISynchronously=${updateUISynchronously} - DONE `);
542    aceDebugTrace.end();
543  }
544
545  public updateDirtyComputedProps(computed: Array<number>): void {
546    stateMgmtConsole.debug(`ObservedV2.updateDirtyComputedProps ${computed.length} props: ${JSON.stringify(computed)} ...`);
547    aceDebugTrace.begin(`ObservedV2.updateDirtyComputedProps ${computed.length} @Computed`);
548    computed.forEach((id) => {
549      let comp: ComputedV2 | undefined;
550      let weakComp: WeakRef<ComputedV2 | undefined> = this.id2cmp_[id];
551      if (weakComp && 'deref' in weakComp && (comp = weakComp.deref()) && comp instanceof ComputedV2) {
552        const target = comp.getTarget();
553        if (target instanceof ViewV2 && !target.isViewActive()) {
554          // add delayed ComputedIds id
555          target.addDelayedComputedIds(id);
556        } else {
557          comp.fireChange();
558        }
559      }
560    });
561    aceDebugTrace.end();
562  }
563  /**
564   * @function resetMonitorValues
565   * @description This function ensures that @Monitor function are reset and reinitialized
566   *  during the reuse cycle:
567   * - Clear and reinitialize monitor IDs and functions to prevent unintended triggers
568   * - Reset dirty states to ensure reusabiltiy
569   */
570  public resetMonitorValues(): void {
571    stateMgmtConsole.debug(`resetMonitorValues changed monitorIds count: ${this.monitorIdsChanged_.size}`);
572    if (this.monitorIdsChanged_.size) {
573      const monitors = this.monitorIdsChanged_;
574      this.monitorIdsChanged_ = new Set<number>();
575      this.updateDirtyMonitorsOnReuse(monitors);
576    }
577  }
578
579  public updateDirtyMonitorsOnReuse(monitors: Set<number>): void {
580    let weakMonitor: WeakRef<MonitorV2 | undefined>;
581    let monitor: MonitorV2 | undefined;
582    monitors.forEach((watchId) => {
583      weakMonitor = this.id2cmp_[watchId];
584      if (weakMonitor && 'deref' in weakMonitor && (monitor = weakMonitor.deref()) && monitor instanceof MonitorV2) {
585        // only update dependency and reset value, no call monitor.
586        monitor.notifyChangeOnReuse();
587      }
588    });
589  }
590
591  public updateDirtyMonitors(monitors: Set<number>): void {
592    stateMgmtConsole.debug(`ObservedV3.updateDirtyMonitors: ${Array.from(monitors).length} @monitor funcs: ${JSON.stringify(Array.from(monitors))} ...`);
593    aceDebugTrace.begin(`ObservedV3.updateDirtyMonitors: ${Array.from(monitors).length} @monitor`);
594    let weakMonitor: WeakRef<MonitorV2 | undefined>;
595    let monitor: MonitorV2 | undefined;
596    let monitorTarget: Object;
597    monitors.forEach((watchId) => {
598      weakMonitor = this.id2cmp_[watchId];
599      if (weakMonitor && 'deref' in weakMonitor && (monitor = weakMonitor.deref()) && monitor instanceof MonitorV2) {
600        if (((monitorTarget = monitor.getTarget()) instanceof ViewV2) && !monitorTarget.isViewActive()) {
601          // monitor notifyChange delayed if target is a View that is not active
602          monitorTarget.addDelayedMonitorIds(watchId);
603        } else {
604          monitor.notifyChange();
605        }
606      }
607    });
608    aceDebugTrace.end();
609  }
610
611  /**
612   * This version of UpdateUINodes does not wait for VSYNC, violates rules
613   * calls UpdateElement, thereby avoids the long and frequent code path from
614   * FlushDirtyNodesUpdate to CustomNode to ViewV2.updateDirtyElements to UpdateElement
615   * Code left here to reproduce benchmark measurements, compare with future optimisation
616   * @param elmtIds
617   *
618   */
619  private updateUINodesSynchronously(elmtIds: Array<number>): void {
620    stateMgmtConsole.debug(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtIds: ${JSON.stringify(elmtIds)} ...`);
621    aceDebugTrace.begin(`ObserveV2.updateUINodesSynchronously: ${elmtIds.length} elmtId`);
622    let view: Object;
623    let weak: any;
624    elmtIds.forEach((elmtId) => {
625      if ((weak = this.id2cmp_[elmtId]) && (typeof weak === 'object') && ('deref' in weak) &&
626        (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) {
627        if (view.isViewActive()) {
628          // FIXME need to call syncInstanceId before update?
629          view.UpdateElement(elmtId);
630        } else {
631          // schedule delayed update once the view gets active
632          view.scheduleDelayedUpdate(elmtId);
633        }
634      } // if ViewV2 or ViewPU
635    });
636    aceDebugTrace.end();
637  }
638
639  private updateUINodesForReuse(elmtIds: Array<number>): void {
640    aceDebugTrace.begin(`ObserveV2.updateUINodesForReuse: ${elmtIds.length} elmtId`);
641    let view: Object;
642    let weak: any;
643    elmtIds.forEach((elmtId) => {
644      if ((weak = this.id2cmp_[elmtId]) && weak && ('deref' in weak) &&
645        (view = weak.deref()) && ((view instanceof ViewV2) || (view instanceof ViewPU))) {
646        if (view.isViewActive()) {
647          /* update child element */ this.currentReuseId_ === view.id__() ||
648          /* update parameter */ this.currentReuseId_ === elmtId
649            ? view.UpdateElement(elmtId)
650            : view.uiNodeNeedUpdateV2(elmtId);
651        } else {
652          // schedule delayed update once the view gets active
653          view.scheduleDelayedUpdate(elmtId);
654        }
655      } // if ViewV2 or ViewPU
656    });
657    aceDebugTrace.end();
658  }
659
660  // This is the code path similar to V2, follows the rule that UI updates on VSYNC.
661  // ViewPU/ViewV2 queues the elmtId that need update, marks the CustomNode dirty in RenderContext
662  // On next VSYNC runs FlushDirtyNodesUpdate to call rerender to call UpdateElement. Much longer code path
663  // much slower
664  private updateUINodes(elmtIds: Array<number>): void {
665    stateMgmtConsole.debug(`ObserveV2.updateUINodes: ${elmtIds.length} elmtIds need rerender: ${JSON.stringify(elmtIds)} ...`);
666    aceDebugTrace.begin(`ObserveV2.updateUINodes: ${elmtIds.length} elmtId`);
667    let viewWeak: WeakRef<Object>;
668    let view: Object | undefined;
669    elmtIds.forEach((elmtId) => {
670      viewWeak = this.id2cmp_[elmtId];
671      if (viewWeak && 'deref' in viewWeak && (view = viewWeak.deref()) &&
672        ((view instanceof ViewV2) || (view instanceof ViewPU))) {
673        if (view.isViewActive()) {
674          view.uiNodeNeedUpdateV2(elmtId);
675        } else {
676          // schedule delayed update once the view gets active
677          view.scheduleDelayedUpdate(elmtId);
678        }
679      }
680    });
681    aceDebugTrace.end();
682  }
683
684  public constructMonitor(owningObject: Object, owningObjectName: string): void {
685    let watchProp = Symbol.for(MonitorV2.WATCH_PREFIX + owningObjectName);
686    if (owningObject && (typeof owningObject === 'object') && owningObject[watchProp]) {
687      Object.entries(owningObject[watchProp]).forEach(([pathString, monitorFunc]) => {
688        if (monitorFunc && pathString && typeof monitorFunc === 'function') {
689          const monitor = new MonitorV2(owningObject, pathString, monitorFunc as (m: IMonitor) => void);
690          monitor.InitRun();
691          const refs = owningObject[ObserveV2.MONITOR_REFS] ??= {};
692          // store a reference inside owningObject
693          // thereby MonitorV2 will share lifespan as owning @ComponentV2 or @ObservedV2
694          // remember: id2cmp only has a WeakRef to MonitorV2 obj
695          refs[monitorFunc.name] = monitor;
696        }
697        // FIXME Else handle error
698      });
699    } // if target[watchProp]
700  }
701
702  public constructComputed(owningObject: Object, owningObjectName: string): void {
703    const computedProp = Symbol.for(ComputedV2.COMPUTED_PREFIX + owningObjectName);
704    if (owningObject && (typeof owningObject === 'object') && owningObject[computedProp]) {
705      Object.entries(owningObject[computedProp]).forEach(([computedPropertyName, computeFunc]) => {
706        stateMgmtConsole.debug(`constructComputed: in ${owningObject?.constructor?.name} found @Computed ${computedPropertyName}`);
707        const computed = new ComputedV2(owningObject, computedPropertyName, computeFunc as unknown as () => any);
708        computed.InitRun();
709        const refs = owningObject[ObserveV2.COMPUTED_REFS] ??= {};
710        // store a reference inside owningObject
711        // thereby ComputedV2 will share lifespan as owning @ComponentV2 or @ObservedV2
712        // remember: id2cmp only has a WeakRef to ComputedV2 obj
713        refs[computedPropertyName] = computed;
714      });
715    }
716  }
717
718  public clearWatch(id: number): void {
719    this.clearBinding(id);
720  }
721
722
723
724  public static autoProxyObject(target: Object, key: string | symbol): any {
725    let val = target[key];
726    // Not an object, not a collection, no proxy required
727    if (!val || typeof (val) !== 'object' ||
728      !(Array.isArray(val) || val instanceof Set || val instanceof Map || val instanceof Date)) {
729      return val;
730    }
731
732    // Collections are the only type that require proxy observation. If they have already been observed, no further observation is needed.
733    // Prevents double-proxying: checks if the object is already proxied by either V1 or V2 (to avoid conflicts).
734    // Prevents V2 proxy creation if the developer uses makeV1Observed and also tries to wrap a V2 proxy with built-in types
735    // Handle the case where both V1 and V2 proxies exist (if V1 proxy doesn't trigger enableV2Compatibility).
736    // Currently not implemented to avoid compatibility issues with existing apps that may use both V1 and V2 proxies.
737    if (!val[ObserveV2.SYMBOL_PROXY_GET_TARGET] && !(ObservedObject.isEnableV2CompatibleInternal(val) || ObservedObject.isMakeV1Observed(val))) {
738
739      if (Array.isArray(val)) {
740        target[key] = new Proxy(val, ObserveV2.arrayProxy);
741      } else if (val instanceof Set || val instanceof Map) {
742        target[key] = new Proxy(val, ObserveV2.setMapProxy);
743      } else {
744        target[key] = new Proxy(val, ObserveV2.objectProxy);
745      }
746      val = target[key];
747    }
748
749    // If the return value is an Array, Set, Map
750    // if (this.arr[0] !== undefined, and similar for Set and Map) will not update in response /
751    // to array length/set or map size changing function without addRef on OB_LENGH
752    if (!(val instanceof Date)) {
753      if (ObservedObject.isEnableV2CompatibleInternal(val)) {
754        ObserveV2.getObserve().addRefV2Compatibility(val, ObserveV2.OB_LENGTH);
755      } else {
756        ObserveV2.getObserve().addRef(ObserveV2.IsMakeObserved(val) ? RefInfo.get(UIUtilsImpl.instance().getTarget(val)) :
757          val, ObserveV2.OB_LENGTH);
758      }
759    }
760    return val;
761  }
762
763  /**
764   * Helper function to add meta data about decorator to ViewPU or ViewV2
765   * @param proto prototype object of application class derived from  ViewPU or ViewV2
766   * @param varName decorated variable
767   * @param deco '@Local', '@Event', etc
768   */
769  public static addVariableDecoMeta(proto: Object, varName: string, deco: string): void {
770    // add decorator meta data
771    const meta = proto[ObserveV2.V2_DECO_META] ??= {};
772    meta[varName] = {};
773    meta[varName].deco = deco;
774
775    // FIXME
776    // when splitting ViewPU and ViewV2
777    // use instanceOf. Until then, this is a workaround.
778    // any @Local, @Trace, etc V2 event handles this function to return false
779    Reflect.defineProperty(proto, 'isViewV2', {
780      get() { return true; },
781      enumerable: false
782    }
783    );
784  }
785
786
787  public static addParamVariableDecoMeta(proto: Object, varName: string, deco?: string, deco2?: string): void {
788    // add decorator meta data
789    const meta = proto[ObserveV2.V2_DECO_META] ??= {};
790    meta[varName] ??= {};
791    if (deco) {
792      meta[varName].deco = deco;
793    }
794    if (deco2) {
795      meta[varName].deco2 = deco2;
796    }
797
798    // FIXME
799    // when splitting ViewPU and ViewV2
800    // use instanceOf. Until then, this is a workaround.
801    // any @Local, @Trace, etc V2 event handles this function to return false
802    Reflect.defineProperty(proto, 'isViewV2', {
803      get() { return true; },
804      enumerable: false
805    }
806    );
807  }
808
809
810  public static usesV2Variables(proto: Object): boolean {
811    return (proto && typeof proto === 'object' && proto[ObserveV2.V2_DECO_META]);
812  }
813
814  /**
815   * Get element info according to the elmtId.
816   *
817   * @param elmtId element id.
818   * @param isProfiler need to return ElementType including the id, type and isCustomNode when isProfiler is true.
819   *                   The default value is false.
820   */
821  public getElementInfoById(elmtId: number, isProfiler: boolean = false): string | ElementType {
822    let weak: WeakRef<ViewBuildNodeBase> | undefined = UINodeRegisterProxy.ElementIdToOwningViewPU_.get(elmtId);
823    let view;
824    return (weak && (view = weak.deref()) && (view instanceof PUV2ViewBase)) ? view.debugInfoElmtId(elmtId, isProfiler) : `unknown component type[${elmtId}]`;
825  }
826
827  /**
828   * Get attrName decorator info.
829   */
830  public getDecoratorInfo(target: object, attrName: string): string {
831    const meta = target[ObserveV2.V2_DECO_META];
832    if (!meta) {
833      return '';
834    }
835    const decorator = meta[attrName];
836    if (!decorator) {
837      return '';
838    }
839    let decoratorInfo: string = '';
840    if ('deco' in decorator) {
841      decoratorInfo = decorator.deco;
842    }
843    if ('aliasName' in decorator) {
844      decoratorInfo += `(${decorator.aliasName})`;
845    }
846    if ('deco2' in decorator) {
847      decoratorInfo += decorator.deco2;
848    }
849    return decoratorInfo;
850  }
851
852  public getComputedInfoById(computedId: number): string {
853    let weak = this.id2cmp_[computedId];
854    let computedV2: ComputedV2;
855    return (weak && (computedV2 = weak.deref()) && (computedV2 instanceof ComputedV2)) ? computedV2.getComputedFuncName() : '';
856  }
857
858  public getMonitorInfoById(computedId: number): string {
859    let weak = this.id2cmp_[computedId];
860    let monitorV2: MonitorV2;
861    return (weak && (monitorV2 = weak.deref()) && (monitorV2 instanceof MonitorV2)) ? monitorV2.getMonitorFuncName() : '';
862  }
863
864  public setCurrentReuseId(elmtId: number): void {
865    this.currentReuseId_ = elmtId;
866  }
867} // class ObserveV2
868
869
870const trackInternal = (
871  target: any,
872  propertyKey: string
873): void => {
874  if (typeof target === 'function' && !Reflect.has(target, propertyKey)) {
875    // dynamic track,and it not a static attribute
876    target = target.prototype;
877  }
878  const storeProp = ObserveV2.OB_PREFIX + propertyKey;
879  target[storeProp] = target[propertyKey];
880  Reflect.defineProperty(target, propertyKey, {
881    get() {
882      ObserveV2.getObserve().addRef(this, propertyKey);
883      return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + propertyKey);
884    },
885    set(val) {
886      // If the object has not been observed, you can directly assign a value to it. This improves performance.
887      if (val !== this[storeProp]) {
888        this[storeProp] = val;
889        if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance.
890          ObserveV2.getObserve().fireChange(this, propertyKey);
891        }
892      }
893    },
894    enumerable: true
895  });
896  // this marks the proto as having at least one @Trace property inside
897  // used by IsObservedObjectV2
898  target[ObserveV2.V2_DECO_META] ??= {};
899}; // trackInternal
900