• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-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 *  * ViewPU - View for Partial Update
16 *
17* all definitions in this file are framework internal
18*/
19
20type DFXCommand = { what: string, viewId: number, isRecursive: boolean };
21type RecycleUpdateFunc = (elmtId: number, isFirstRender: boolean, recycleNode: ViewPU) => void;
22
23abstract class ViewPU extends PUV2ViewBase
24  implements IViewPropertiesChangeSubscriber, IView {
25
26  // flag for initial rendering or re-render on-going.
27  private isRenderInProgress: boolean = false;
28
29  // flag for initial rendering being done
30  private isInitialRenderDone: boolean = false;
31
32  private runReuse_: boolean = false;
33
34  private paramsGenerator_: () => Object;
35
36  private watchedProps: Map<string, (propName: string) => void> = new Map<string, (propName: string) => void>();
37
38  private recycleManager_: RecycleManager = undefined;
39
40  // Internal variable to keep track is component recycled or not.
41  private hasBeenRecycled_: boolean = false;
42
43  private preventRecursiveRecycle_: boolean = false;
44
45  private delayRecycleNodeRerender: boolean = false;
46
47  private delayRecycleNodeRerenderDeep: boolean = false;
48
49  // @Provide'd variables by this class and its ancestors
50  protected providedVars_: Map<string, ObservedPropertyAbstractPU<any>> = new Map<string, ObservedPropertyAbstractPU<any>>();
51
52  // my LocalStorage instance, shared with ancestor Views.
53  // create a default instance on demand if none is initialized
54  protected localStoragebackStore_: LocalStorage = undefined;
55
56  private ownObservedPropertiesStore__?: Set<ObservedPropertyAbstractPU<any>>;
57
58  private get ownObservedPropertiesStore_() {
59    if (!this.ownObservedPropertiesStore__) {
60      // lazy init
61      this.ownObservedPropertiesStore__ = new Set<ObservedPropertyAbstractPU<any>>();
62      this.obtainOwnObservedProperties();
63    }
64    return this.ownObservedPropertiesStore__;
65  }
66
67  protected obtainOwnObservedProperties(): void {
68    let usesStateMgmtVersion = 0;
69    Object.getOwnPropertyNames(this)
70      .filter((propName) => {
71        // do not include backing store, and ObserveV2/MonitorV2/ComputedV2 meta data objects
72        return (propName.startsWith('__') &&
73          !propName.startsWith(ObserveV2.OB_PREFIX) &&
74          !propName.startsWith(MonitorV2.WATCH_PREFIX) &&
75          !propName.startsWith(ComputedV2.COMPUTED_PREFIX));
76      })
77      .forEach((propName) => {
78        const stateVar = Reflect.get(this, propName) as Object;
79        if (stateVar && typeof stateVar === 'object' && 'notifyPropertyHasChangedPU' in stateVar) {
80          stateMgmtConsole.debug(`... add state variable ${propName} to ${stateVar}`);
81          this.ownObservedPropertiesStore_.add(stateVar as unknown as ObservedPropertyAbstractPU<any>);
82          usesStateMgmtVersion = 2;
83        } else {
84          stateMgmtConsole.debug(`${this.debugInfo__()} ${propName} application may use an unregular naming style, or stateVar may be Non-Object.`);
85        }
86      });
87
88    if (this.isViewV3 === true) {
89      if (usesStateMgmtVersion === 2) {
90        const error = `${this.debugInfo__()}: mixed use of stateMgmt V2 and V3 variable decorators. Application error!`;
91        stateMgmtConsole.applicationError(error);
92        throw new Error(error);
93      }
94    }
95    stateMgmtConsole.debug(`${this.debugInfo__()}: uses stateMgmt version ${this.isViewV3 === true ? 3 : 2}`);
96  }
97
98  public get localStorage_(): LocalStorage {
99    if (!this.localStoragebackStore_ && this.getParent()) {
100      stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: get localStorage_ : Using LocalStorage instance of the parent View.`);
101      this.localStoragebackStore_ = this.getParent().localStorage_;
102    }
103
104    if (!this.localStoragebackStore_) {
105      stateMgmtConsole.info(`${this.debugInfo__()}: constructor: is accessing LocalStorage without being provided an instance. Creating a default instance.`);
106      this.localStoragebackStore_ = new LocalStorage({ /* empty */ });
107    }
108    return this.localStoragebackStore_;
109  }
110
111  public set localStorage_(instance: LocalStorage) {
112    if (!instance) {
113      // setting to undefined not allowed
114      return;
115    }
116    if (this.localStoragebackStore_) {
117      stateMgmtConsole.applicationError(`${this.debugInfo__()}: constructor: is setting LocalStorage instance twice. Application error.`);
118    }
119    this.localStoragebackStore_ = instance;
120  }
121
122  // FIXME
123  // indicate if this is  V2 or a V3 component
124  // V2 by default, changed to V3 by the first V3 decorated variable
125  // when splitting ViewPU and ViewV3
126  // use instanceOf. Until then, this is a workaround.
127  // @state, @track, etc V3 decorator functions modify isViewV3 to return true
128  // (decorator can modify functions in prototype)
129  // FIXME
130  private get isViewV3(): boolean {
131    return false;
132  }
133
134  /**
135   * Create a View
136   *
137   * 1. option: top level View, specify
138   *    - compilerAssignedUniqueChildId must specify
139   *    - parent=undefined
140   *    - localStorage  must provide if @LocalSTorageLink/Prop variables are used
141   *      in this View or descendant Views.
142   *
143   * 2. option: not a top level View
144   *    - compilerAssignedUniqueChildId must specify
145   *    - parent must specify
146   *    - localStorage do not specify, will inherit from parent View.
147   *
148  */
149  constructor(parent: IView, localStorage: LocalStorage, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) {
150    super(parent, elmtId, extraInfo);
151    // if set use the elmtId also as the ViewPU object's subscribable id.
152    // these matching is requirement for updateChildViewById(elmtId) being able to
153    // find the child ViewPU object by given elmtId
154    //this.id_ = elmtId == UINodeRegisterProxy.notRecordingDependencies ? SubscriberManager.MakeId() : elmtId;
155
156    this.localStoragebackStore_ = undefined;
157    stateMgmtConsole.debug(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`);
158
159    PUV2ViewBase.arkThemeScopeManager?.onViewPUCreate(this)
160
161    if (localStorage) {
162      this.localStorage_ = localStorage;
163      stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: Using LocalStorage instance provided via @Entry or view instance creation.`);
164    }
165
166    SubscriberManager.Add(this);
167    stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: done`);
168  }
169
170  onGlobalThemeChanged(): void {
171    this.onWillApplyThemeInternally();
172    this.forceCompleteRerender(false)
173    this.childrenWeakrefMap_.forEach((weakRefChild) => {
174      const child = weakRefChild.deref();
175      if (child) {
176        child.onGlobalThemeChanged();
177      }
178    });
179  }
180
181  // inform the subscribed property
182  // that the View and thereby all properties
183  // are about to be deleted
184  abstract aboutToBeDeleted(): void;
185
186  aboutToReuse(params: Object): void { }
187
188  aboutToRecycle(): void { }
189
190  private onWillApplyThemeInternally(): void {
191    const theme = PUV2ViewBase.arkThemeScopeManager?.getFinalTheme(this.id__())
192    if (theme) {
193        this.onWillApplyTheme(theme)
194    }
195  }
196
197  onWillApplyTheme(theme: Theme): void {}
198  // super class will call this function from
199  // its aboutToBeDeleted implementation
200  protected aboutToBeDeletedInternal(): void {
201    stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`);
202    // if this isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary
203    // to set and recursively set its children any more
204    if (!this.isDeleting_) {
205      this.isDeleting_ = true;
206      this.setDeleteStatusRecursively();
207    }
208    // tell UINodeRegisterProxy that all elmtIds under
209    // this ViewPU should be treated as already unregistered
210
211    stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `);
212
213    // purge the elmtIds owned by this viewPU from the updateFuncByElmtId and also the state variable dependent elmtIds
214    Array.from(this.updateFuncByElmtId.keys()).forEach((elmtId: number) => {
215      this.purgeDeleteElmtId(elmtId);
216    })
217    if (this.hasRecycleManager()) {
218      this.getRecycleManager().purgeAllCachedRecycleNode();
219    }
220
221    // un-registration of ElementIDs
222    stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`);
223
224    // it will unregister removed elmtIds from all ViewPu, equals purgeDeletedElmtIdsRecursively
225    this.purgeDeletedElmtIds();
226    // unregisters its own id once its children are unregistered above
227    UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs([this.id__()]);
228
229    stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID  - DONE`);
230
231    // in case this ViewPU is currently frozen
232    PUV2ViewBase.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`);
233
234    this.updateFuncByElmtId.clear();
235    this.watchedProps.clear();
236    this.providedVars_.clear();
237    if (this.ownObservedPropertiesStore__) {
238      this.ownObservedPropertiesStore__.clear();
239    }
240    if (this.getParent()) {
241      this.getParent().removeChild(this);
242    }
243    PUV2ViewBase.arkThemeScopeManager?.onViewPUDelete(this);
244    this.localStoragebackStore_ = undefined;
245  }
246
247  public purgeDeleteElmtId(rmElmtId: number): boolean {
248    stateMgmtConsole.debug(`${this.debugInfo__()} purgeDeleteElmtId (PU) is purging the rmElmtId:${rmElmtId}`);
249    const result = this.updateFuncByElmtId.delete(rmElmtId);
250    if (result) {
251      this.purgeVariableDependenciesOnElmtIdOwnFunc(rmElmtId);
252      // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_  does not need to keep it
253      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId);
254    }
255    return result;
256  }
257
258  protected purgeVariableDependenciesOnElmtIdOwnFunc(elmtId: number): void {
259    this.ownObservedPropertiesStore_.forEach((stateVar: ObservedPropertyAbstractPU<any>) => {
260      stateVar.purgeDependencyOnElmtId(elmtId);
261    });
262  }
263  protected debugInfoStateVars(): string {
264    let result: string = `|--${this.constructor.name}[${this.id__()}]`;
265    Object.getOwnPropertyNames(this)
266      .filter((varName: string) => varName.startsWith('__') && !varName.startsWith(ObserveV2.OB_PREFIX))
267      .forEach((varName) => {
268        const prop: any = Reflect.get(this, varName);
269        if ('debugInfoDecorator' in prop) {
270          const observedProp = prop as ObservedPropertyAbstractPU<any>;
271          result += `\n  ${observedProp.debugInfoDecorator()} '${observedProp.info()}'[${observedProp.id__()}]`;
272          result += `\n  ${observedProp.debugInfoSubscribers()}`;
273          result += `\n  ${observedProp.debugInfoSyncPeers()}`;
274          result += `\n  ${observedProp.debugInfoDependentElmtIds()}`;
275          result += `\n  ${observedProp.debugInfoDependentComponents()}`;
276        }
277      });
278    return result;
279  }
280
281   /**
282   * Indicate if this @Component is allowed to freeze by calling with freezeState=true
283   * Called with value of the @Component decorator 'freezeWhenInactive' parameter
284   * or depending how UI compiler works also with 'undefined'
285   * @param freezeState only value 'true' will be used, otherwise inherits from parent
286   * if not parent, set to false.
287   */
288   protected initAllowComponentFreeze(freezeState: boolean | undefined): void {
289    // set to true if freeze parameter set for this @Component to true
290    // otherwise inherit from parent @Component (if it exists).
291    this.isCompFreezeAllowed_ = freezeState || this.isCompFreezeAllowed_;
292    stateMgmtConsole.debug(`${this.debugInfo__()}: @Component freezeWhenInactive state is set to ${this.isCompFreezeAllowed()}`);
293  }
294
295  /**
296 * ArkUI engine will call this function when the corresponding CustomNode's active status change.
297 * ArkUI engine will not recurse children nodes to inform the stateMgmt for the performance reason.
298 * So the stateMgmt needs to recurse the children although the isCompFreezeAllowed is false because the children nodes
299 * may enable the freezeWhenInActive.
300 * @param active true for active, false for inactive
301 */
302  public setActiveInternal(active: boolean): void {
303    stateMgmtProfiler.begin('ViewPU.setActive');
304    if (this.isCompFreezeAllowed()) {
305      this.isActive_ = active;
306      if (this.isActive_) {
307        this.onActiveInternal();
308      } else {
309        this.onInactiveInternal();
310      }
311    }
312    for (const child of this.childrenWeakrefMap_.values()) {
313      const childView: IView | undefined = child.deref();
314      if (childView) {
315        childView.setActiveInternal(active);
316      }
317    }
318    stateMgmtProfiler.end();
319  }
320
321  private onActiveInternal(): void {
322    if (!this.isActive_) {
323      return;
324    }
325
326    stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`);
327    this.performDelayedUpdate();
328    // Remove the active component from the Map for Dfx
329    ViewPU.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`);
330  }
331
332
333  private onInactiveInternal(): void {
334    if (this.isActive_) {
335      return;
336    }
337
338    stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`);
339    for (const stateLinkProp of this.ownObservedPropertiesStore_) {
340      stateLinkProp.enableDelayedNotification();
341    }
342    // Add the inactive Components to Map for Dfx listing
343    ViewPU.inactiveComponents_.add(`${this.constructor.name}[${this.id__()}]`);
344  }
345
346  // abstract functions to be implemented by application defined class / transpiled code
347  protected abstract purgeVariableDependenciesOnElmtId(removedElmtId: number);
348  protected abstract initialRender(): void;
349  protected abstract rerender(): void;
350
351  public abstract updateRecycleElmtId(oldElmtId: number, newElmtId: number): void;
352  public abstract updateStateVars(params: Object);
353
354  public initialRenderView(): void {
355    stateMgmtProfiler.begin('ViewPU.initialRenderView');
356    this.onWillApplyThemeInternally();
357    this.obtainOwnObservedProperties();
358    this.isRenderInProgress = true;
359    this.initialRender();
360    this.isRenderInProgress = false;
361    this.isInitialRenderDone = true;
362    stateMgmtProfiler.end();
363  }
364
365  public UpdateElement(elmtId: number): void {
366    stateMgmtProfiler.begin('ViewPU.UpdateElement');
367    if (elmtId === this.id__()) {
368      // do not attempt to update itself.
369      // a @Prop can add a dependency of the ViewPU onto itself. Ignore it.
370      stateMgmtProfiler.end();
371      return;
372    }
373
374    // do not process an Element that has been marked to be deleted
375    const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
376    const updateFunc = entry ? entry.getUpdateFunc() : undefined;
377
378    if (typeof updateFunc !== 'function') {
379      stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`);
380    } else {
381      stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${entry.getComponentName()} elmtId ${elmtId} start ...`);
382      this.isRenderInProgress = true;
383      stateMgmtProfiler.begin('ViewPU.updateFunc');
384      updateFunc(elmtId, /* isFirstRender */ false);
385      stateMgmtProfiler.end();
386      stateMgmtProfiler.begin('ViewPU.finishUpdateFunc (native)');
387      this.finishUpdateFunc(elmtId);
388      stateMgmtProfiler.end();
389      this.isRenderInProgress = false;
390      stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${entry.getComponentName()} elmtId ${elmtId} - DONE`);
391    }
392    stateMgmtProfiler.end();
393  }
394
395  public delayCompleteRerender(deep: boolean = false): void {
396    this.delayRecycleNodeRerender = true;
397    this.delayRecycleNodeRerenderDeep = deep;
398  }
399
400  public flushDelayCompleteRerender(): void {
401    this.forceCompleteRerender(this.delayRecycleNodeRerenderDeep);
402    this.delayRecycleNodeRerender = false;
403  }
404
405  /**
406   * force a complete rerender / update on specific node by executing update function.
407   *
408   * @param elmtId which node needs to update.
409   *
410   * framework internal functions, apps must not call
411   */
412  public forceRerenderNode(elmtId: number): void {
413    stateMgmtProfiler.begin('ViewPU/V2.forceRerenderNode');
414    // see which elmtIds are managed by this View
415    // and clean up all book keeping for them
416    this.purgeDeletedElmtIds();
417    this.UpdateElement(elmtId);
418
419    // remove elemtId from dirtDescendantElementIds.
420    this.dirtDescendantElementIds_.delete(elmtId);
421    stateMgmtProfiler.end();
422  }
423
424  // implements IMultiPropertiesChangeSubscriber
425  viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void {
426    stateMgmtProfiler.begin('ViewPU.viewPropertyHasChanged');
427    aceTrace.begin('ViewPU.viewPropertyHasChanged', this.constructor.name, varName, dependentElmtIds.size);
428    if (this.isRenderInProgress) {
429      stateMgmtConsole.applicationError(`${this.debugInfo__()}: State variable '${varName}' has changed during render! It's illegal to change @Component state while build (initial render or re-render) is on-going. Application error!`);
430    }
431
432    this.syncInstanceId();
433
434    if (dependentElmtIds.size && !this.isFirstRender()) {
435      if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
436        // mark ComposedElement dirty when first elmtIds are added
437        // do not need to do this every time
438        this.markNeedUpdate();
439      }
440      stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`);
441      for (const elmtId of dependentElmtIds) {
442        if (this.hasRecycleManager()) {
443          this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId));
444        } else {
445          this.dirtDescendantElementIds_.add(elmtId);
446        }
447      }
448      stateMgmtConsole.debug(`   ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`);
449    } else {
450      stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`);
451      stateMgmtConsole.debug(`   ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`);
452    }
453
454    let cb = this.watchedProps.get(varName);
455    if (cb && typeof cb === 'function') {
456      stateMgmtConsole.debug(`   ... calling @Watch function`);
457      cb.call(this, varName);
458    }
459
460    this.restoreInstanceId();
461    aceTrace.end();
462    stateMgmtProfiler.end();
463  }
464
465
466  /**
467 *  inform that UINode with given elmtId needs rerender
468 *  does NOT exec @Watch function.
469 *  only used on V3 code path from ObserveV2.fireChange.
470 *
471 * FIXME will still use in the future?
472 */
473  public uiNodeNeedUpdateV3(elmtId: number): void {
474    if (this.isFirstRender()) {
475      return;
476    }
477
478    stateMgmtProfiler.begin(`ViewPU.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`);
479
480    if (!this.dirtDescendantElementIds_.size) { //  && !this runReuse_) {
481      // mark ComposedElement dirty when first elmtIds are added
482      // do not need to do this every time
483      this.syncInstanceId();
484      this.markNeedUpdate();
485      this.restoreInstanceId();
486    }
487    if (this.hasRecycleManager()) {
488      this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId));
489    } else {
490      this.dirtDescendantElementIds_.add(elmtId);
491    }
492    stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`);
493
494    stateMgmtProfiler.end();
495  }
496
497  private performDelayedUpdate(): void {
498    if (!this.ownObservedPropertiesStore_.size && !this.elmtIdsDelayedUpdate.size) {
499      return;
500    }
501    stateMgmtProfiler.begin('ViewPU.performDelayedUpdate');
502    aceTrace.begin('ViewPU.performDelayedUpdate', this.constructor.name);
503    stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate start ...`);
504    this.syncInstanceId();
505
506    for (const stateLinkPropVar of this.ownObservedPropertiesStore_) {
507      const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate();
508      if (changedElmtIds) {
509        const varName = stateLinkPropVar.info();
510        if (changedElmtIds.size && !this.isFirstRender()) {
511          for (const elmtId of changedElmtIds) {
512            this.dirtDescendantElementIds_.add(elmtId);
513          }
514        }
515
516        stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate: all elmtIds that need re-render [${Array.from(this.dirtDescendantElementIds_).toString()}].`);
517
518        const cb = this.watchedProps.get(varName);
519        if (cb) {
520          stateMgmtConsole.debug(`   ... calling @Watch function`);
521          cb.call(this, varName);
522        }
523      }
524    } // for all ownStateLinkProps_
525
526    for (let elementId of this.elmtIdsDelayedUpdate) {
527      this.dirtDescendantElementIds_.add(elementId);
528    }
529    this.elmtIdsDelayedUpdate.clear();
530
531    this.restoreInstanceId();
532
533    if (this.dirtDescendantElementIds_.size) {
534      this.markNeedUpdate();
535    }
536    aceTrace.end();
537    stateMgmtProfiler.end();
538  }
539
540  /**
541   * Function to be called from the constructor of the sub component
542   * to register a @Watch variable
543   * @param propStr name of the variable. Note from @Provide and @Consume this is
544   *      the variable name and not the alias!
545   * @param callback application defined member function of sub-class
546   */
547  protected declareWatch(propStr: string, callback: (propName: string) => void): void {
548    this.watchedProps.set(propStr, callback);
549  }
550
551  /**
552   * This View @Provide's a variable under given name
553   * Call this function from the constructor of the sub class
554   * @param providedPropName either the variable name or the alias defined as
555   *        decorator param
556   * @param store the backing store object for this variable (not the get/set variable!)
557   */
558  protected addProvidedVar<T>(providedPropName: string, store: ObservedPropertyAbstractPU<T>, allowOverride: boolean = false) {
559    if (!allowOverride && this.findProvidePU(providedPropName)) {
560      throw new ReferenceError(`${this.constructor.name}: duplicate @Provide property with name ${providedPropName}. Property with this name is provided by one of the ancestor Views already. @Provide override not allowed.`);
561    }
562    store.setDecoratorInfo('@Provide');
563    this.providedVars_.set(providedPropName, store);
564  }
565
566  /*
567    findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component:
568    if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU.
569  */
570  public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined {
571    return this.providedVars_.get(providedPropName) || (this.parent_ && this.parent_.findProvidePU(providedPropName));
572  }
573
574  /**
575   * Method for the sub-class to call from its constructor for resolving
576   *       a @Consume variable and initializing its backing store
577   *       with the SyncedPropertyTwoWay<T> object created from the
578   *       @Provide variable's backing store.
579   * @param providedPropName the name of the @Provide'd variable.
580   *     This is either the @Consume decorator parameter, or variable name.
581   * @param consumeVarName the @Consume variable name (not the
582   *            @Consume decorator parameter)
583   * @returns initializing value of the @Consume backing store
584   */
585  protected initializeConsume<T>(providedPropName: string,
586    consumeVarName: string): ObservedPropertyAbstractPU<T> {
587    let providedVarStore: ObservedPropertyAbstractPU<any> = this.findProvidePU(providedPropName);
588    if (providedVarStore === undefined) {
589      throw new ReferenceError(`${this.debugInfo__()} missing @Provide property with name ${providedPropName}.
590          Fail to resolve @Consume(${providedPropName}).`);
591    }
592
593    const factory = <T>(source: ObservedPropertyAbstract<T>) => {
594      const result: ObservedPropertyAbstractPU<T> = new SynchedPropertyTwoWayPU<T>(source, this, consumeVarName);
595      result.setDecoratorInfo('@Consume');
596      stateMgmtConsole.debug(`The @Consume is instance of ${result.constructor.name}`);
597      return result;
598    };
599    return providedVarStore.createSync(factory) as ObservedPropertyAbstractPU<T>;
600  }
601
602
603  /**
604   * given the elmtId of a child or child of child within this custom component
605   * remember this component needs a partial update
606   * @param elmtId
607   */
608  public markElemenDirtyById(elmtId: number): void {
609    // TODO ace-ets2bundle, framework, compiled apps need to update together
610    // this function will be removed after a short transition period
611    stateMgmtConsole.applicationError(`${this.debugInfo__()}: markElemenDirtyById no longer supported.
612        Please update your ace-ets2bundle and recompile your application. Application error!`);
613  }
614
615  /**
616   * For each recorded dirty Element in this custom component
617   * run its update function
618   *
619   */
620  public updateDirtyElements(): void {
621    stateMgmtProfiler.begin('ViewPU.updateDirtyElements');
622    do {
623      stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber)}, starting ....`);
624
625      // see which elmtIds are managed by this View
626      // and clean up all book keeping for them
627      this.purgeDeletedElmtIds();
628
629      // process all elmtIds marked as needing update in ascending order.
630      // ascending order ensures parent nodes will be updated before their children
631      // prior cleanup ensure no already deleted Elements have their update func executed
632      const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber);
633      // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added
634      // to newly created this.dirtDescendantElementIds_ Set
635      dirtElmtIdsFromRootNode.forEach(elmtId => {
636        if (this.hasRecycleManager()) {
637          this.UpdateElement(this.recycleManager_.proxyNodeId(elmtId));
638        } else {
639          this.UpdateElement(elmtId);
640        }
641        this.dirtDescendantElementIds_.delete(elmtId);
642      });
643
644      if (this.dirtDescendantElementIds_.size) {
645        stateMgmtConsole.applicationError(`${this.debugInfo__()}: New UINode objects added to update queue while re-render! - Likely caused by @Component state change during build phase, not allowed. Application error!`);
646      }
647    } while (this.dirtDescendantElementIds_.size);
648    stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE, dump of ViewPU in next lines`);
649    stateMgmtProfiler.end();
650  }
651
652  // executed on first render only
653  // kept for backward compatibility with old ace-ets2bundle
654  public observeComponentCreation(compilerAssignedUpdateFunc: UpdateFunc): void {
655    if (this.isDeleting_) {
656      stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation `);
657      return;
658    }
659    const updateFunc = (elmtId: number, isFirstRender: boolean): void => {
660      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
661      this.currentlyRenderedElmtIdStack_.push(elmtId);
662      compilerAssignedUpdateFunc(elmtId, isFirstRender);
663      this.currentlyRenderedElmtIdStack_.pop();
664      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
665    }
666
667    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
668    // in observeComponentCreation function we do not get info about the component name, in
669    // observeComponentCreation2 we do.
670    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc });
671    // add element id -> owning ViewPU
672    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
673    try {
674      updateFunc(elmtId, /* is first render */ true);
675    } catch (error) {
676      // avoid the incompatible change that move set function before updateFunc.
677      this.updateFuncByElmtId.delete(elmtId);
678      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
679      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
680      throw error;
681    }
682  }
683
684  public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: UIClassObject): void {
685    if (this.isDeleting_) {
686      stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `);
687      return;
688    }
689    const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode';
690    if (_componentName === '__Recycle__') {
691      return;
692    }
693    const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { };
694    const updateFunc = (elmtId: number, isFirstRender: boolean): void => {
695      this.syncInstanceId();
696      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] ${!this.isViewV3 ? '(enable PU state observe) ' : ''} ${ConfigureStateMgmt.instance.needsV2Observe() ? '(enabled V2 state observe) ' : ''} - start ....`);
697
698      PUV2ViewBase.arkThemeScopeManager?.onComponentCreateEnter(_componentName, elmtId, isFirstRender, this)
699
700      ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
701
702      if (!this.isViewV3) {
703        // Enable PU state tracking only in PU @Components
704        this.currentlyRenderedElmtIdStack_.push(elmtId);
705        stateMgmtDFX.inRenderingElementId.push(elmtId);
706      }
707
708      // if V2 @Observed/@Track used anywhere in the app (there is no more fine grained criteria),
709      // enable V2 object deep observation
710      // FIXME: A @Component should only use PU or V2 state, but ReactNative dynamic viewer uses both.
711      if (this.isViewV3 || ConfigureStateMgmt.instance.needsV2Observe()) {
712        // FIXME: like in V2 setting bindId_ in ObserveV2 does not work with 'stacked'
713        // update + initial render calls, like in if and ForEach case, convert to stack as well
714        ObserveV2.getObserve().startRecordDependencies(this, elmtId);
715      }
716
717      compilerAssignedUpdateFunc(elmtId, isFirstRender);
718      if (!isFirstRender) {
719        _popFunc();
720      }
721      let node = this.getNodeById(elmtId);
722      if (node !== undefined) {
723        (node as ArkComponent).cleanStageValue();
724      }
725
726      if (this.isViewV3 || ConfigureStateMgmt.instance.needsV2Observe()) {
727        ObserveV2.getObserve().stopRecordDependencies();
728      }
729      if (!this.isViewV3) {
730        this.currentlyRenderedElmtIdStack_.pop();
731        stateMgmtDFX.inRenderingElementId.pop();
732      }
733      ViewStackProcessor.StopGetAccessRecording();
734
735      PUV2ViewBase.arkThemeScopeManager?.onComponentCreateExit(elmtId)
736
737      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`}  ${_componentName}[${elmtId}] - DONE ....`);
738      this.restoreInstanceId();
739    };
740
741    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
742    // needs to move set before updateFunc.
743    // make sure the key and object value exist since it will add node in attributeModifier during updateFunc.
744    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject });
745    // add element id -> owning ViewPU
746    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
747    try {
748      updateFunc(elmtId, /* is first render */ true);
749    } catch (error) {
750      // avoid the incompatible change that move set function before updateFunc.
751      this.updateFuncByElmtId.delete(elmtId);
752      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
753      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
754      throw error;
755    }
756    stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`);
757  }
758
759
760  getOrCreateRecycleManager(): RecycleManager {
761    if (!this.recycleManager_) {
762      this.recycleManager_ = new RecycleManager;
763    }
764    return this.recycleManager_;
765  }
766
767  getRecycleManager(): RecycleManager {
768    return this.recycleManager_;
769  }
770
771  hasRecycleManager(): boolean {
772    return !(this.recycleManager_ === undefined);
773  }
774
775  initRecycleManager(): void {
776    if (this.recycleManager_) {
777      stateMgmtConsole.error(`${this.debugInfo__()}: init recycleManager multiple times. Internal error.`);
778      return;
779    }
780    this.recycleManager_ = new RecycleManager;
781  }
782  rebuildUpdateFunc(elmtId, compilerAssignedUpdateFunc): void {
783    const updateFunc = (elmtId, isFirstRender): void => {
784      this.currentlyRenderedElmtIdStack_.push(elmtId);
785      compilerAssignedUpdateFunc(elmtId, isFirstRender);
786      this.currentlyRenderedElmtIdStack_.pop();
787    };
788    if (this.updateFuncByElmtId.has(elmtId)) {
789      this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc });
790    }
791  }
792
793  /**
794   * @function observeRecycleComponentCreation
795   * @description custom node recycle creation
796   * @param name custom node name
797   * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function
798   * @return void
799   */
800  public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void {
801    // convert recycle update func to update func
802    const compilerAssignedUpdateFunc: UpdateFunc = (element, isFirstRender) => {
803      recycleUpdateFunc(element, isFirstRender, undefined);
804    };
805    let node: ViewPU;
806    // if there is no suitable recycle node, run a normal creation function.
807    if (!this.hasRecycleManager() || !(node = this.getRecycleManager().popRecycleNode(name))) {
808      stateMgmtConsole.debug(`${this.constructor.name}[${this.id__()}]: cannot init node by recycle, crate new node`);
809      this.observeComponentCreation(compilerAssignedUpdateFunc);
810      return;
811    }
812
813    // if there is a suitable recycle node, run a recycle update function.
814    const newElmtId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
815    const oldElmtId: number = node.id__();
816    this.recycleManager_.updateNodeId(oldElmtId, newElmtId);
817    node.hasBeenRecycled_ = false;
818    this.rebuildUpdateFunc(oldElmtId, compilerAssignedUpdateFunc);
819    recycleUpdateFunc(oldElmtId, /* is first render */ true, node);
820  }
821
822  // param is used by BuilderNode
823  aboutToReuseInternal(param?: Object) {
824    this.runReuse_ = true;
825    stateMgmtTrace.scopedTrace(() => {
826      if (this.paramsGenerator_ && typeof this.paramsGenerator_ === 'function') {
827        const params = param ? param : this.paramsGenerator_();
828        this.updateStateVars(params);
829        this.aboutToReuse(params);
830      }
831    }, 'aboutToReuse', this.constructor.name);
832
833    for (const stateLinkPropVar of this.ownObservedPropertiesStore_) {
834      const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate(true);
835      if (changedElmtIds) {
836        if (changedElmtIds.size && !this.isFirstRender()) {
837          for (const elmtId of changedElmtIds) {
838            this.dirtDescendantElementIds_.add(elmtId);
839          }
840        }
841      }
842    }
843    if (!this.delayRecycleNodeRerender) {
844      this.updateDirtyElements();
845    } else {
846      this.flushDelayCompleteRerender();
847    }
848    this.childrenWeakrefMap_.forEach((weakRefChild) => {
849      const child = weakRefChild.deref();
850      if (child) {
851        if (child instanceof ViewPU) {
852          if (!child.hasBeenRecycled_) {
853            child.aboutToReuseInternal();
854          }
855        } else {
856          // FIXME fix for mixed V2 - V3 Hierarchies
857          throw new Error('aboutToReuseInternal: Recycle not implemented for ViewV2, yet');
858        }
859      } // if child
860    });
861    this.runReuse_ = false;
862  }
863
864  stopRecursiveRecycle() {
865    this.preventRecursiveRecycle_ = true;
866  }
867
868  aboutToRecycleInternal() {
869    this.runReuse_ = true;
870    stateMgmtTrace.scopedTrace(() => {
871      this.aboutToRecycle();
872    }, 'aboutToRecycle', this.constructor.name);
873    if (this.preventRecursiveRecycle_) {
874      this.preventRecursiveRecycle_ = false;
875      return;
876    }
877    this.childrenWeakrefMap_.forEach((weakRefChild) => {
878      const child = weakRefChild.deref();
879      if (child) {
880        if (child instanceof ViewPU) {
881          if (!child.hasBeenRecycled_) {
882            child.aboutToRecycleInternal();
883          }
884        } else {
885          // FIXME fix for mixed V2 - V3 Hierarchies
886          throw new Error('aboutToRecycleInternal: Recycle not yet implemented for ViewV2');
887        }
888      } // if child
889    });
890    this.runReuse_ = false;
891  }
892
893  // add current JS object to it's parent recycle manager
894  public recycleSelf(name: string): void {
895
896    if (this.getParent() && this.getParent() instanceof ViewPU && !(this.getParent() as ViewPU).isDeleting_) {
897      const parentPU : ViewPU = this.getParent() as ViewPU;
898      parentPU.getOrCreateRecycleManager().pushRecycleNode(name, this);
899      this.hasBeenRecycled_ = true;
900    } else {
901      this.resetRecycleCustomNode();
902    }
903  }
904
905  public isRecycled() : boolean {
906    return this.hasBeenRecycled_;
907  }
908
909  public UpdateLazyForEachElements(elmtIds: Array<number>): void {
910    if (!Array.isArray(elmtIds)) {
911      return;
912    }
913    Array.from(elmtIds).sort(ViewPU.compareNumber).forEach((elmtId: number) => {
914      const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
915      const updateFunc: UpdateFunc = entry ? entry.getUpdateFunc() : undefined;
916      if (typeof updateFunc !== 'function') {
917        stateMgmtConsole.debug(`${this.debugInfo__()}: update function of elmtId ${elmtId} not found, internal error!`);
918      } else {
919        this.isRenderInProgress = true;
920        updateFunc(elmtId, false);
921        this.finishUpdateFunc(elmtId);
922        this.isRenderInProgress = false;
923      }
924    })
925  }
926
927  /**
928     * CreateStorageLink and CreateStorageLinkPU are used by the implementation of @StorageLink and
929     * @LocalStotrageLink in full update and partial update solution respectively.
930     * These are not part of the public AppStorage API , apps should not use.
931     * @param storagePropName - key in LocalStorage
932     * @param defaultValue - value to use when creating a new prop in the LocalStotage
933     * @param owningView - the View/ViewPU owning the @StorageLink/@LocalStorageLink variable
934     * @param viewVariableName -  @StorageLink/@LocalStorageLink variable name
935     * @returns SynchedPropertySimple/ObjectTwoWay/PU
936     */
937  public createStorageLink<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
938    const appStorageLink = AppStorage.__createSync<T>(storagePropName, defaultValue,
939      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
940        ? undefined
941        : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName)
942    ) as ObservedPropertyAbstractPU<T>;
943    appStorageLink?.setDecoratorInfo('@StorageLink');
944    return appStorageLink;
945  }
946
947  public createStorageProp<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
948    const appStorageProp = AppStorage.__createSync<T>(storagePropName, defaultValue,
949      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
950        ? undefined
951        : new SynchedPropertyOneWayPU<T>(source, this, viewVariableName)
952    ) as ObservedPropertyAbstractPU<T>;
953    appStorageProp?.setDecoratorInfo('@StorageProp');
954    return appStorageProp;
955  }
956
957  public createLocalStorageLink<T>(storagePropName: string, defaultValue: T,
958    viewVariableName: string): ObservedPropertyAbstractPU<T> {
959    const localStorageLink = this.localStorage_.__createSync<T>(storagePropName, defaultValue,
960      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
961        ? undefined
962        : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName)
963    ) as ObservedPropertyAbstractPU<T>;
964    localStorageLink?.setDecoratorInfo('@LocalStorageLink');
965    return localStorageLink;
966  }
967
968  public createLocalStorageProp<T>(storagePropName: string, defaultValue: T,
969    viewVariableName: string): ObservedPropertyAbstractPU<T> {
970    const localStorageProp = this.localStorage_.__createSync<T>(storagePropName, defaultValue,
971      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
972        ? undefined
973        : new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
974    ) as ObservedPropertyAbstractPU<T>;
975    localStorageProp?.setDecoratorInfo('@LocalStorageProp');
976    return localStorageProp;
977  }
978
979  /**
980   * onDumpInfo is used to process commands delivered by the hidumper process
981   * @param commands -  list of commands provided in the shell
982   * @returns void
983   */
984  protected onDumpInfo(commands: string[]): void {
985
986    let dfxCommands: DFXCommand[] = this.processOnDumpCommands(commands);
987
988    dfxCommands.forEach((command) => {
989      let view: ViewPU = undefined;
990      if (command.viewId) {
991        view = this.findViewPUInHierarchy(command.viewId);
992        if (!view) {
993          DumpLog.print(0, `\nTarget view: ${command.viewId} not found for command: ${command.what}\n`);
994          return;
995        }
996      } else {
997        view = this;
998        command.viewId = view.id__();
999      }
1000      switch (command.what) {
1001        case '-dumpAll':
1002          view.printDFXHeader('ViewPU Info', command);
1003          DumpLog.print(0, view.debugInfoView(command.isRecursive));
1004          break;
1005        case '-viewHierarchy':
1006          view.printDFXHeader('ViewPU Hierarchy', command);
1007          DumpLog.print(0, view.debugInfoViewHierarchy(command.isRecursive));
1008          break;
1009        case '-stateVariables':
1010          view.printDFXHeader('ViewPU State Variables', command);
1011          DumpLog.print(0, view.debugInfoStateVars());
1012          break;
1013        case '-registeredElementIds':
1014          view.printDFXHeader('ViewPU Registered Element IDs', command);
1015          DumpLog.print(0, view.debugInfoUpdateFuncByElmtId(command.isRecursive));
1016          break;
1017        case '-dirtyElementIds':
1018          view.printDFXHeader('ViewPU Dirty Registered Element IDs', command);
1019          DumpLog.print(0, view.debugInfoDirtDescendantElementIds(command.isRecursive));
1020          break;
1021        case '-inactiveComponents':
1022          view.printDFXHeader('List of Inactive Components', command);
1023          DumpLog.print(0, view.debugInfoInactiveComponents());
1024          break;
1025        case '-profiler':
1026          view.printDFXHeader('Profiler Info', command);
1027          view.dumpReport();
1028          this.sendStateInfo('{}');
1029          break;
1030        default:
1031          DumpLog.print(0, `\nUnsupported JS DFX dump command: [${command.what}, viewId=${command.viewId}, isRecursive=${command.isRecursive}]\n`);
1032      }
1033    })
1034  }
1035
1036  private printDFXHeader(header: string, command: DFXCommand): void {
1037    let length: number = 50;
1038    let remainder: number = length - header.length < 0 ? 0 : length - header.length;
1039    DumpLog.print(0, `\n${'-'.repeat(remainder / 2)}${header}${'-'.repeat(remainder / 2)}`);
1040    DumpLog.print(0, `[${command.what}, viewId=${command.viewId}, isRecursive=${command.isRecursive}]\n`);
1041  }
1042
1043  private processOnDumpCommands(commands: string[]): DFXCommand[] {
1044    let isFlag: Function = (param: string): boolean => {
1045      return '-r'.match(param) != null || param.startsWith('-viewId=');
1046    }
1047
1048    let dfxCommands: DFXCommand[] = [];
1049
1050    for (var i: number = 0; i < commands.length; i++) {
1051      let command = commands[i];
1052      if (isFlag(command)) {
1053        if (command.startsWith('-viewId=')) {
1054          let dfxCommand: DFXCommand = dfxCommands[dfxCommands.length - 1];
1055          if (dfxCommand) {
1056            let input: string[] = command.split('=');
1057            if (input[1]) {
1058              let viewId: number = Number.parseInt(input[1]);
1059              dfxCommand.viewId = Number.isNaN(viewId) ? UINodeRegisterProxy.notRecordingDependencies : viewId;
1060            }
1061          }
1062        } else if (command.match('-r')) {
1063          let dfxCommand: DFXCommand = dfxCommands[dfxCommands.length - 1];
1064          if (dfxCommand) {
1065            dfxCommand.isRecursive = true;
1066          }
1067        }
1068      } else {
1069        dfxCommands.push({
1070          what: command,
1071          viewId: undefined,
1072          isRecursive: false,
1073        });
1074      }
1075    }
1076    return dfxCommands;
1077  }
1078
1079  public findViewPUInHierarchy(id: number): ViewPU {
1080    let weakChild = this.childrenWeakrefMap_.get(id);
1081    if (weakChild) {
1082      const child = weakChild.deref();
1083      // found child with id, is it a ViewPU?
1084      return (child instanceof ViewPU) ? child : undefined;
1085    }
1086
1087    // did not find, continue searching
1088    let retVal: ViewPU = undefined;
1089    for (const [key, value] of this.childrenWeakrefMap_.entries()) {
1090      retVal = value.deref().findViewPUInHierarchy(id);
1091      if (retVal) {
1092        break;
1093      }
1094    }
1095    return retVal;
1096  }
1097
1098  private debugInfoView(recursive: boolean = false): string {
1099    return this.debugInfoViewInternal(recursive);
1100  }
1101
1102  private debugInfoViewInternal(recursive: boolean = false): string {
1103    let retVal: string = `@Component\n${this.constructor.name}[${this.id__()}]`;
1104    retVal += `\n\nView Hierarchy:\n${this.debugInfoViewHierarchy(recursive)}`;
1105    retVal += `\n\nState variables:\n${this.debugInfoStateVars()}`;
1106    retVal += `\n\nRegistered Element IDs:\n${this.debugInfoUpdateFuncByElmtId(recursive)}`;
1107    retVal += `\n\nDirty Registered Element IDs:\n${this.debugInfoDirtDescendantElementIds(recursive)}`;
1108    return retVal;
1109  }
1110
1111  private debugInfoDirtDescendantElementIds(recursive: boolean = false): string {
1112    return this.debugInfoDirtDescendantElementIdsInternal(0, recursive, { total: 0 });
1113  }
1114
1115  public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string {
1116    let retVaL: string = `\n${'  '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`;
1117    this.dirtDescendantElementIds_.forEach((value) => {
1118      retVaL += `${value}, `;
1119    });
1120    counter.total += this.dirtDescendantElementIds_.size;
1121    retVaL += `\n${'  '.repeat(depth + 1)}}[${this.dirtDescendantElementIds_.size}]`;
1122    if (recursive) {
1123      this.childrenWeakrefMap_.forEach((value, key, map) => {
1124        retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter);
1125      })
1126    }
1127
1128    if (recursive && depth == 0) {
1129      retVaL += `\nTotal: ${counter.total}`;
1130    }
1131    return retVaL;
1132  }
1133
1134  /**
1135    * onDumpInspector is invoked by native side to create Inspector tree including state variables
1136    * @returns dump info
1137    */
1138  protected onDumpInspector(): string {
1139    let res: DumpInfo = new DumpInfo();
1140    res.viewInfo = { componentName: this.constructor.name, id: this.id__() };
1141    Object.getOwnPropertyNames(this)
1142      .filter((varName: string) => varName.startsWith('__') && !varName.startsWith(ObserveV2.OB_PREFIX))
1143      .forEach((varName) => {
1144        const prop: any = Reflect.get(this, varName);
1145        if (typeof prop === 'object' && 'debugInfoDecorator' in prop) {
1146          const observedProp: ObservedPropertyAbstractPU<any> = prop as ObservedPropertyAbstractPU<any>;
1147          res.observedPropertiesInfo.push(stateMgmtDFX.getObservedPropertyInfo(observedProp, false));
1148        }
1149      });
1150    let resInfo: string = '';
1151    try {
1152      resInfo = JSON.stringify(res);
1153    } catch (error) {
1154      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in getInspector: ${(error as Error).message}`);
1155    }
1156    return resInfo;
1157  }
1158
1159
1160
1161  /**
1162   * on first render create a new Instance of Repeat
1163   * on re-render connect to existing instance
1164   * @param arr
1165   * @returns
1166   */
1167  public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => {
1168    // factory is for future extensions, currently always return the same
1169    const elmtId = this.getCurrentlyRenderedElmtId();
1170    let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>;
1171    if (!repeat) {
1172        repeat = new __Repeat<I>(this, arr);
1173        this.elmtId2Repeat_.set(elmtId, repeat);
1174    } else {
1175        repeat.updateArr(arr);
1176    }
1177
1178    return repeat;
1179  }
1180} // class ViewPU
1181
1182