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