• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-2023 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
20/**
21 * WeakRef
22 * ref to an Object that does not prevent the Object from getting GC'ed
23 * current version of tsc does not know about WeakRef
24 * but Ark runtime supports it
25 *
26 */
27declare class WeakRef<T extends Object> {
28  constructor(o: T);
29  deref(): T;
30}
31
32declare class DumpLog {
33  static print(depth: number, content: string): void;
34}
35
36type DFXCommand = { what: string, viewId: number, isRecursive: boolean };
37type ProfileRecursionCounter = { total: number };
38
39type ProvidedVarsMapPU = Map<string, ObservedPropertyAbstractPU<any>>;
40
41// denotes a missing elemntId, this is the case during initial render
42const UndefinedElmtId = -1;
43
44// function type of partial update function
45type UpdateFunc = (elmtId: number, isFirstRender: boolean) => void;
46type UIClassObject = { prototype: Object, pop?: () => void };
47
48// UpdateFuncRecord: misc framework-internal info related to updating of a UINode C++ object
49// that TS side needs to know.
50// updateFunc_  lambda function to update the UINode
51// JS interface class reference (it only has static functions)
52class UpdateFuncRecord {
53  private updateFunc_: UpdateFunc;
54  private classObject_: UIClassObject;
55  private node_?: Object
56
57  constructor(params: { updateFunc: UpdateFunc, classObject?: UIClassObject, node?: Object }) {
58    this.updateFunc_ = params.updateFunc;
59    this.classObject_ = params.classObject;
60    this.node_ = params.node;
61  }
62
63  public getUpdateFunc(): UpdateFunc | undefined {
64    return this.updateFunc_;
65  }
66
67  public getComponentClass(): UIClassObject | undefined {
68    return this.classObject_;
69  }
70
71  public getComponentName(): string {
72    return (this.classObject_ && ("name" in this.classObject_)) ? Reflect.get(this.classObject_, "name") as string : "unspecified UINode";
73  }
74
75  public getPopFunc(): () => void {
76    return (this.classObject_ && "pop" in this.classObject_) ? this.classObject_.pop! : () => { };
77  }
78
79  public getNode(): Object | undefined {
80    return this.node_;
81  }
82
83  public setNode(node: Object | undefined): void {
84    this.node_ = node;
85  }
86}
87
88// function type of recycle node update function
89type RecycleUpdateFunc = (elmtId: number, isFirstRender: boolean, recycleNode: ViewPU) => void;
90
91type ExtraInfo = { page: string, line: number };
92
93// NativeView
94// implemented in C++  for release
95// and in utest/view_native_mock.ts for testing
96abstract class ViewPU extends NativeViewPartialUpdate
97  implements IViewPropertiesChangeSubscriber {
98
99  // Array.sort() converts array items to string to compare them!
100  static readonly compareNumber = (a: number, b: number): number => {
101    return (a < b) ? -1 : (a > b) ? 1 : 0;
102  };
103
104  // List of inactive components used for Dfx
105  private static readonly inactiveComponents_: Set<string> = new Set<string>();
106
107  private id_: number;
108
109  private parent_: ViewPU = undefined;
110  private childrenWeakrefMap_ = new Map<number, WeakRef<ViewPU>>();
111
112  // flag for initial rendering or re-render on-going.
113  private isRenderInProgress: boolean = false;
114
115  // flag for initial rendering being done
116  private isInitialRenderDone: boolean = false;
117
118  // indicates the currently rendered or rendered UINode's elmtIds
119  // or -1 if none is currently rendering
120  // isRenderInProgress == true always when currentlyRenderedElmtIdStack_.length >= 0
121  private currentlyRenderedElmtIdStack_: Array<number> = new Array<number>();
122
123  // static flag for paused rendering
124  // when paused, getCurrentlyRenderedElmtId() will return -1
125  private static renderingPaused: boolean = false;
126
127  // flag if active of inActive
128  // inActive means updates are delayed
129  private isActive_: boolean = true;
130
131  private runReuse_: boolean = false;
132  private hasBeenRecycled_: boolean = false;
133
134  private paramsGenerator_: () => Object;
135
136  // flag if {aboutToBeDeletedInternal} is called and the instance of ViewPU has not been GC.
137  private isDeleting_: boolean = false;
138
139  private watchedProps: Map<string, (propName: string) => void>
140    = new Map<string, (propName: string) => void>();
141
142  private recycleManager_: RecycleManager = undefined;
143
144  private isCompFreezeAllowed: boolean = false;
145
146  private extraInfo_: ExtraInfo = undefined;
147
148  // @Provide'd variables by this class and its ancestors
149  protected providedVars_: ProvidedVarsMapPU = new Map<string, ObservedPropertyAbstractPU<any>>();
150
151  // Set of dependent elmtIds that need partial update
152  // during next re-render
153  protected dirtDescendantElementIds_: Set<number>
154    = new Set<number>();
155
156  // registry of update functions
157  // the key is the elementId of the Component/Element that's the result of this function
158  private updateFuncByElmtId = new UpdateFuncsByElmtId();
159
160  // my LocalStorage instance, shared with ancestor Views.
161  // create a default instance on demand if none is initialized
162  protected localStoragebackStore_: LocalStorage = undefined;
163
164  private ownObservedPropertiesStore__?: Set<ObservedPropertyAbstractPU<any>>;
165
166  private get ownObservedPropertiesStore_() {
167    if (!this.ownObservedPropertiesStore__) {
168      // lazy init
169      this.ownObservedPropertiesStore__ = new Set<ObservedPropertyAbstractPU<any>>();
170      this.obtainOwnObservedProperties();
171    }
172    return this.ownObservedPropertiesStore__;
173  }
174
175  protected obtainOwnObservedProperties(): void {
176    Object.getOwnPropertyNames(this)
177      .filter((propName) => {
178        return propName.startsWith("__")
179      })
180      .forEach((propName) => {
181        const stateVar = Reflect.get(this, propName) as Object;
182        if (stateVar && typeof stateVar === 'object' && "notifyPropertyHasChangedPU" in stateVar) {
183          stateMgmtConsole.debug(`... add state variable ${propName} to ${stateVar}`)
184          this.ownObservedPropertiesStore_.add(stateVar as unknown as ObservedPropertyAbstractPU<any>);
185        } else {
186          stateMgmtConsole.debug(`${this.debugInfo__()} ${propName} application may use an unregular naming style, or stateVar may be Non-Object.`);
187        }
188      });
189  }
190
191  protected get localStorage_() {
192    if (!this.localStoragebackStore_ && this.parent_) {
193      stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: get localStorage_ : Using LocalStorage instance of the parent View.`);
194      this.localStoragebackStore_ = this.parent_.localStorage_;
195    }
196
197    if (!this.localStoragebackStore_) {
198      stateMgmtConsole.info(`${this.debugInfo__()}: constructor: is accessing LocalStorage without being provided an instance. Creating a default instance.`);
199      this.localStoragebackStore_ = new LocalStorage({ /* empty */ });
200    }
201    return this.localStoragebackStore_;
202  }
203
204  protected set localStorage_(instance: LocalStorage) {
205    if (!instance) {
206      // setting to undefined not allowed
207      return;
208    }
209    if (this.localStoragebackStore_) {
210      stateMgmtConsole.applicationError(`${this.debugInfo__()}: constructor: is setting LocalStorage instance twice. Application error.`);
211    }
212    this.localStoragebackStore_ = instance;
213  }
214
215  /**
216   * Create a View
217   *
218   * 1. option: top level View, specify
219   *    - compilerAssignedUniqueChildId must specify
220   *    - parent=undefined
221   *    - localStorage  must provide if @LocalSTorageLink/Prop variables are used
222   *      in this View or descendant Views.
223   *
224   * 2. option: not a top level View
225   *    - compilerAssignedUniqueChildId must specify
226   *    - parent must specify
227   *    - localStorage do not specify, will inherit from parent View.
228   *
229  */
230  constructor(parent: ViewPU, localStorage: LocalStorage, elmtId: number = -1, extraInfo: ExtraInfo = undefined) {
231    super();
232    // if set use the elmtId also as the ViewPU object's subscribable id.
233    // these matching is requirement for updateChildViewById(elmtId) being able to
234    // find the child ViewPU object by given elmtId
235    this.id_ = elmtId == -1 ? SubscriberManager.MakeId() : elmtId;
236
237    this.localStoragebackStore_ = undefined;
238    stateMgmtConsole.debug(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`);
239    if (extraInfo) {
240      this.extraInfo_ = extraInfo;
241    }
242    if (parent) {
243      // this View is not a top-level View
244      this.setCardId(parent.getCardId());
245      // Call below will set this.parent_ to parent as well
246      parent.addChild(this);
247    } else if (localStorage) {
248      this.localStorage_ = localStorage;
249      stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: Using LocalStorage instance provided via @Entry.`);
250    }
251    this.isCompFreezeAllowed = this.isCompFreezeAllowed || (this.parent_ && this.parent_.isCompFreezeAllowed);
252
253    SubscriberManager.Add(this);
254    stateMgmtConsole.debug(`${this.debugInfo__()}: constructor: done`);
255  }
256
257  // globally unique id, this is different from compilerAssignedUniqueChildId!
258  id__(): number {
259    return this.id_;
260  }
261
262  updateId(elmtId: number): void {
263    this.id_ = elmtId;
264  }
265
266  // inform the subscribed property
267  // that the View and thereby all properties
268  // are about to be deleted
269  abstract aboutToBeDeleted(): void;
270
271  aboutToReuse(params: Object): void { }
272
273  aboutToRecycle(): void { }
274
275  private setDeleteStatusRecursively(): void {
276    if (!this.childrenWeakrefMap_.size) {
277      return;
278    }
279    this.childrenWeakrefMap_.forEach((value: WeakRef<ViewPU>) => {
280      let child: ViewPU = value.deref();
281      if (child) {
282        child.isDeleting_ = true;
283        child.setDeleteStatusRecursively();
284      }
285    })
286  }
287
288  // super class will call this function from
289  // its aboutToBeDeleted implementation
290  protected aboutToBeDeletedInternal(): void {
291    stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`);
292    // if this.isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary
293    // to set and resursively set its children any more
294    if (!this.isDeleting_) {
295      this.isDeleting_ = true;
296      this.setDeleteStatusRecursively();
297    }
298    // tell UINodeRegisterProxy that all elmtIds under
299    // this ViewPU should be treated as already unregistered
300
301    stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `);
302
303    // purge the elmtIds owned by this viewPU from the updateFuncByElmtId and also the state variable dependent elmtIds
304    Array.from(this.updateFuncByElmtId.keys()).forEach((elemId: number) => {
305      this.purgeDeleteElmtId(elemId);
306    })
307
308    if (this.hasRecycleManager()) {
309      this.getRecycleManager().purgeAllCachedRecycleNode();
310    }
311
312    // unregistration of ElementIDs
313    stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`);
314
315    // it will unregister removed elementids from all the viewpu, equals purgeDeletedElmtIdsRecursively
316    this.purgeDeletedElmtIds();
317
318    stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID  - DONE`);
319
320    // in case ViewPU is currently frozen
321    ViewPU.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`);
322
323    this.updateFuncByElmtId.clear();
324    this.watchedProps.clear();
325    this.providedVars_.clear();
326    if (this.ownObservedPropertiesStore__) {
327      this.ownObservedPropertiesStore__.clear();
328    }
329    if (this.parent_) {
330      this.parent_.removeChild(this);
331    }
332    this.localStoragebackStore_ = undefined;
333  }
334
335  public purgeDeleteElmtId(rmElmtId: number): boolean {
336    stateMgmtConsole.debug(`${this.debugInfo__} is purging the rmElmtId:${rmElmtId}`);
337    const result = this.updateFuncByElmtId.delete(rmElmtId);
338    if (result) {
339      this.purgeVariableDependenciesOnElmtIdOwnFunc(rmElmtId);
340      // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_  does not need to keep it
341      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId);
342    }
343    return result;
344  }
345
346  public debugInfo__(): string {
347    return `@Component '${this.constructor.name}'[${this.id__()}]`;
348  }
349
350  public debugInfoRegisteredElmtIds() {
351    return this.updateFuncByElmtId.debugInfoRegisteredElmtIds();
352  }
353
354  // for given elmtIds look up their component name/type and format a string out of this info
355  // use function only for debug output and DFX.
356  public debugInfoElmtIds(elmtIds: Array<number>): string {
357    let result: string = "";
358    let sepa: string = "";
359    elmtIds.forEach((elmtId: number) => {
360      result += `${sepa}${this.debugInfoElmtId(elmtId)}`;
361      sepa = ", ";
362    });
363    return result;
364  }
365
366  public debugInfoElmtId(elmtId: number): string {
367    return this.updateFuncByElmtId.debugInfoElmtId(elmtId);
368  }
369
370  public dumpStateVars(): void {
371    stateMgmtConsole.debug(`${this.debugInfo__()}:  State variables:\n ${this.debugInfoStateVars()}`);
372  }
373
374  private debugInfoStateVars(): string {
375    let result: string = `|--${this.constructor.name}[${this.id__()}]`;
376    Object.getOwnPropertyNames(this)
377      .filter((varName: string) => varName.startsWith("__"))
378      .forEach((varName) => {
379        const prop: any = Reflect.get(this, varName);
380        if ("debugInfoDecorator" in prop) {
381          const observedProp = prop as ObservedPropertyAbstractPU<any>;
382          result += `\n  ${observedProp.debugInfoDecorator()} '${observedProp.info()}'[${observedProp.id__()}]`;
383          result += `\n  ${observedProp.debugInfoSubscribers()}`
384          result += `\n  ${observedProp.debugInfoSyncPeers()}`;
385          result += `\n  ${observedProp.debugInfoDependentElmtIds()}`
386        }
387      });
388    return result;
389  }
390
391  /**
392 * ArkUI engine will call this function when the corresponding CustomNode's active status change.
393 * @param active true for active, false for inactive
394 */
395  public setActiveInternal(active: boolean): void {
396    stateMgmtProfiler.begin("ViewPU.setActive");
397    if (!this.isCompFreezeAllowed) {
398      stateMgmtConsole.debug(`${this.debugInfo__()}: ViewPU.setActive. Component freeze state is ${this.isCompFreezeAllowed} - ignoring`);
399      stateMgmtProfiler.end();
400      return;
401    }
402    stateMgmtConsole.debug(`${this.debugInfo__()}: ViewPU.setActive ${active ? ' inActive -> active' : 'active -> inActive'}`);
403    this.isActive_ = active;
404    if (this.isActive_) {
405      this.onActiveInternal()
406    } else {
407      this.onInactiveInternal();
408    }
409    stateMgmtProfiler.end();
410  }
411
412  private onActiveInternal(): void {
413    if (!this.isActive_) {
414      return;
415    }
416
417    stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`);
418    this.performDelayedUpdate();
419    // Remove the active component from the Map for Dfx
420    ViewPU.inactiveComponents_.delete(`${this.constructor.name}[${this.id__()}]`);
421    for (const child of this.childrenWeakrefMap_.values()) {
422      const childViewPU: ViewPU | undefined = child.deref();
423      if (childViewPU) {
424        childViewPU.setActiveInternal(this.isActive_);
425      }
426    }
427    if (this.hasRecycleManager()) {
428      this.getRecycleManager().setActive(this.isActive_);
429    }
430  }
431
432
433  private onInactiveInternal(): void {
434    if (this.isActive_) {
435      return;
436    }
437
438    stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`);
439    for (const stateLinkProp of this.ownObservedPropertiesStore_) {
440      stateLinkProp.enableDelayedNotification();
441    }
442    // Add the inactive Components to Map for Dfx listing
443    ViewPU.inactiveComponents_.add(`${this.constructor.name}[${this.id__()}]`);
444
445    for (const child of this.childrenWeakrefMap_.values()) {
446      const childViewPU: ViewPU | undefined = child.deref();
447      if (childViewPU) {
448        childViewPU.setActiveInternal(this.isActive_);
449      }
450    }
451    if (this.hasRecycleManager()) {
452      this.getRecycleManager().setActive(this.isActive_);
453    }
454  }
455
456  private setParent(parent: ViewPU) {
457    if (this.parent_ && parent) {
458      stateMgmtConsole.warn(`${this.debugInfo__()}: setChild: changing parent to '${parent?.debugInfo__()} (unsafe operation)`);
459    }
460    this.parent_ = parent;
461  }
462
463  /**
464   * Indicate if this @Component is allowed to freeze by calling with freezeState=true
465   * Called with value of the @Component decorator 'freezeWhenInactive' parameter
466   * or depending how UI compiler works also with 'undefined'
467   * @param freezeState only value 'true' will be used, otherwise inherits from parent
468   *      if not parent, set to false.
469   */
470  protected initAllowComponentFreeze(freezeState: boolean | undefined): void {
471    // set to true if freeze parameter set for this @Component to true
472    // otherwise inherit from parent @Component (if it exists).
473    this.isCompFreezeAllowed = freezeState || this.isCompFreezeAllowed;
474    stateMgmtConsole.debug(`${this.debugInfo__()}: @Component freezeWhenInactive state is set to ${this.isCompFreezeAllowed}`);
475  }
476
477  /**
478   * add given child and set 'this' as its parent
479   * @param child child to add
480   * @returns returns false if child with given child's id already exists
481   *
482   * framework internal function
483   * Note: Use of WeakRef ensures child and parent do not generate a cycle dependency.
484   * The add. Set<ids> is required to reliably tell what children still exist.
485   */
486  public addChild(child: ViewPU): boolean {
487    if (this.childrenWeakrefMap_.has(child.id__())) {
488      stateMgmtConsole.warn(`${this.debugInfo__()}: addChild '${child?.debugInfo__()}' id already exists ${child.id__()}. Internal error!`);
489      return false;
490    }
491    this.childrenWeakrefMap_.set(child.id__(), new WeakRef(child));
492    child.setParent(this);
493    return true;
494  }
495
496  /**
497   * remove given child and remove 'this' as its parent
498   * @param child child to add
499   * @returns returns false if child with given child's id does not exist
500   */
501  public removeChild(child: ViewPU): boolean {
502    const hasBeenDeleted = this.childrenWeakrefMap_.delete(child.id__());
503    if (!hasBeenDeleted) {
504      stateMgmtConsole.warn(`${this.debugInfo__()}: removeChild '${child?.debugInfo__()}', child id ${child.id__()} not known. Internal error!`);
505    } else {
506      child.setParent(undefined);
507    }
508    return hasBeenDeleted;
509  }
510
511  /**
512   * Retrieve child by given id
513   * @param id
514   * @returns child if in map and weak ref can still be downreferenced
515   */
516  public getChildById(id: number) {
517    const childWeakRef = this.childrenWeakrefMap_.get(id);
518    return childWeakRef ? childWeakRef.deref() : undefined;
519  }
520
521  protected abstract purgeVariableDependenciesOnElmtId(removedElmtId: number);
522  protected abstract initialRender(): void;
523  protected abstract rerender(): void;
524  protected abstract updateRecycleElmtId(oldElmtId: number, newElmtId: number): void;
525  protected updateStateVars(params: {}): void {
526    stateMgmtConsole.error(`${this.debugInfo__()}: updateStateVars unimplemented. Pls upgrade to latest eDSL transpiler version. Application error.`);
527  }
528
529  protected initialRenderView(): void {
530    stateMgmtProfiler.begin("ViewPU.initialRenderView");
531    this.obtainOwnObservedProperties();
532    this.isRenderInProgress = true;
533    this.initialRender();
534    this.isRenderInProgress = false;
535    this.isInitialRenderDone = true;
536    stateMgmtProfiler.end();
537  }
538
539  private UpdateElement(elmtId: number): void {
540    stateMgmtProfiler.begin("ViewPU.UpdateElement");
541    if (elmtId == this.id__()) {
542      // do not attempt to update itself.
543      // a @Prop can add a dependency of the ViewPU onto itself. Ignore it.
544      stateMgmtProfiler.end();
545      return;
546    }
547
548    // do not process an Element that has been marked to be deleted
549    const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
550    const updateFunc = entry ? entry.getUpdateFunc() : undefined;
551
552    if (typeof updateFunc !== "function") {
553      stateMgmtConsole.debug(`${this.debugInfo__()}: update function of elmtId ${elmtId} not found, internal error!`);
554    } else {
555      const componentName = entry.getComponentName();
556      stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements: re-render of ${componentName} elmtId ${elmtId} start ...`);
557      this.isRenderInProgress = true;
558      stateMgmtProfiler.begin("ViewPU.updateFunc");
559      updateFunc(elmtId, /* isFirstRender */ false);
560      stateMgmtProfiler.end();
561      stateMgmtProfiler.begin("ViewPU.finishUpdateFunc (native)");
562      this.finishUpdateFunc(elmtId);
563      stateMgmtProfiler.end();
564      this.isRenderInProgress = false;
565      stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements: re-render of ${componentName} elmtId ${elmtId} - DONE`);
566    }
567    stateMgmtProfiler.end();
568  }
569
570  public dumpReport(): void {
571    stateMgmtConsole.warn(`Printing profiler information`);
572    stateMgmtProfiler.report();
573  }
574
575  /**
576   * force a complete rerender / update by executing all update functions
577   * exec a regular rerender first
578   *
579   * @param deep recurse all children as well
580   *
581   * framework internal functions, apps must not call
582   */
583  public forceCompleteRerender(deep: boolean = false): void {
584    stateMgmtProfiler.begin("ViewPU.forceCompleteRerender");
585    stateMgmtConsole.warn(`${this.debugInfo__()}: forceCompleteRerender - start.`);
586
587    // see which elmtIds are managed by this View
588    // and clean up all book keeping for them
589    this.purgeDeletedElmtIds();
590
591    Array.from(this.updateFuncByElmtId.keys()).sort(ViewPU.compareNumber).forEach(elmtId => this.UpdateElement(elmtId));
592
593    if (deep) {
594      this.childrenWeakrefMap_.forEach((weakRefChild: WeakRef<ViewPU>) => {
595        const child = weakRefChild.deref();
596        if (child) {
597          (child as ViewPU).forceCompleteRerender(true);
598        }
599      });
600    }
601    stateMgmtConsole.warn(`${this.debugInfo__()}: forceCompleteRerender - end`);
602    stateMgmtProfiler.end();
603  }
604
605  /**
606   * force a complete rerender / update on specific node by executing update function.
607   *
608   * @param elmtId which node needs to update.
609   *
610   * framework internal functions, apps must not call
611   */
612  public forceRerenderNode(elmtId: number): void {
613    stateMgmtProfiler.begin("ViewPU.forceRerenderNode");
614    // see which elmtIds are managed by this View
615    // and clean up all book keeping for them
616    this.purgeDeletedElmtIds();
617    this.UpdateElement(elmtId);
618
619    // remove elemtId from dirtDescendantElementIds.
620    this.dirtDescendantElementIds_.delete(elmtId);
621    stateMgmtProfiler.end();
622  }
623
624  public updateStateVarsOfChildByElmtId(elmtId, params: Object): void {
625    stateMgmtProfiler.begin("ViewPU.updateStateVarsOfChildByElmtId");
626    stateMgmtConsole.debug(`${this.debugInfo__()}: updateChildViewById(${elmtId}) - start`);
627
628    if (elmtId < 0) {
629      stateMgmtConsole.warn(`${this.debugInfo__()}: updateChildViewById(${elmtId}) - invalid elmtId - internal error!`);
630      stateMgmtProfiler.end();
631      return;
632    }
633    let child: ViewPU = this.getChildById(elmtId);
634    if (!child) {
635      stateMgmtConsole.warn(`${this.debugInfo__()}: updateChildViewById(${elmtId}) - no child with this elmtId - internal error!`);
636      stateMgmtProfiler.end();
637      return;
638    }
639    child.updateStateVars(params);
640    stateMgmtConsole.debug(`${this.debugInfo__()}: updateChildViewById(${elmtId}) - end`);
641    stateMgmtProfiler.end();
642  }
643
644  // implements IMultiPropertiesChangeSubscriber
645  viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void {
646    stateMgmtProfiler.begin("ViewPU.viewPropertyHasChanged");
647    stateMgmtTrace.scopedTrace(() => {
648      if (this.isRenderInProgress) {
649        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!`);
650      }
651
652      this.syncInstanceId();
653
654      if (dependentElmtIds.size && !this.isFirstRender()) {
655        if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
656          // mark ComposedElement dirty when first elmtIds are added
657          // do not need to do this every time
658          this.markNeedUpdate();
659        }
660        stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`)
661        for (const elmtId of dependentElmtIds) {
662          if (this.hasRecycleManager()) {
663            this.dirtDescendantElementIds_.add(this.recycleManager_.proxyNodeId(elmtId));
664          } else {
665            this.dirtDescendantElementIds_.add(elmtId);
666          }
667        }
668        stateMgmtConsole.debug(`   ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
669      } else {
670        stateMgmtConsole.debug(`${this.debugInfo__()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`);
671        stateMgmtConsole.debug(`   ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
672      }
673
674      let cb = this.watchedProps.get(varName)
675      if (cb) {
676        stateMgmtConsole.debug(`   ... calling @Watch function`);
677        cb.call(this, varName);
678      }
679
680      this.restoreInstanceId();
681    }, "ViewPU.viewPropertyHasChanged", this.constructor.name, varName, dependentElmtIds.size);
682    stateMgmtProfiler.end();
683  }
684
685
686  private performDelayedUpdate(): void {
687    if (!this.ownObservedPropertiesStore_.size) {
688      return;
689    }
690    stateMgmtProfiler.begin("ViewPU.performDelayedUpdate");
691    stateMgmtTrace.scopedTrace(() => {
692      stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate start ...`);
693      this.syncInstanceId();
694
695      for (const stateLinkPropVar of this.ownObservedPropertiesStore_) {
696        const changedElmtIds = stateLinkPropVar.moveElmtIdsForDelayedUpdate();
697        if (changedElmtIds) {
698          const varName = stateLinkPropVar.info();
699          if (changedElmtIds.size && !this.isFirstRender()) {
700            for (const elmtId of changedElmtIds) {
701              this.dirtDescendantElementIds_.add(elmtId);
702            }
703          }
704
705          stateMgmtConsole.debug(`${this.debugInfo__()}: performDelayedUpdate: all elmtIds that need re-render [${Array.from(this.dirtDescendantElementIds_).toString()}].`);
706
707          const cb = this.watchedProps.get(varName)
708          if (cb) {
709            stateMgmtConsole.debug(`   ... calling @Watch function`);
710            cb.call(this, varName);
711          }
712        }
713      } // for all ownStateLinkProps_
714      this.restoreInstanceId();
715
716      if (this.dirtDescendantElementIds_.size) {
717        this.markNeedUpdate();
718      }
719
720    }, "ViewPU.performDelayedUpdate", this.constructor.name);
721    stateMgmtProfiler.end();
722  }
723
724  /**
725   * Function to be called from the constructor of the sub component
726   * to register a @Watch varibale
727   * @param propStr name of the variable. Note from @Provide and @Consume this is
728   *      the variable name and not the alias!
729   * @param callback application defined member function of sub-class
730   */
731  protected declareWatch(propStr: string, callback: (propName: string) => void): void {
732    this.watchedProps.set(propStr, callback);
733  }
734
735  /**
736   * This View @Provide's a variable under given name
737   * Call this function from the constructor of the sub class
738   * @param providedPropName either the variable name or the alias defined as
739   *        decorator param
740   * @param store the backing store object for this variable (not the get/set variable!)
741   */
742  protected addProvidedVar<T>(providedPropName: string, store: ObservedPropertyAbstractPU<T>, allowOverride: boolean = false) {
743    if (!allowOverride && this.findProvide(providedPropName)) {
744      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.`);
745    }
746    this.providedVars_.set(providedPropName, store);
747  }
748
749  /*
750    findProvide finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component:
751    if 'this' ViewPU has a @Provide("providedPropName") return it, otherwise ask from its parent ViewPU.
752  */
753  public findProvide(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined {
754    return this.providedVars_.get(providedPropName) || (this.parent_ && this.parent_.findProvide(providedPropName));
755  }
756
757  /**
758   * Method for the sub-class to call from its constructor for resolving
759   *       a @Consume variable and initializing its backing store
760   *       with the SyncedPropertyTwoWay<T> object created from the
761   *       @Provide variable's backing store.
762   * @param providedPropName the name of the @Provide'd variable.
763   *     This is either the @Consume decorator parameter, or variable name.
764   * @param consumeVarName the @Consume variable name (not the
765   *            @Consume decorator parameter)
766   * @returns initializing value of the @Consume backing store
767   */
768  protected initializeConsume<T>(providedPropName: string,
769    consumeVarName: string): ObservedPropertyAbstractPU<T> {
770    let providedVarStore: ObservedPropertyAbstractPU<any> = this.findProvide(providedPropName);
771    if (providedVarStore === undefined) {
772      throw new ReferenceError(`${this.debugInfo__()} missing @Provide property with name ${providedPropName}.
773          Fail to resolve @Consume(${providedPropName}).`);
774    }
775
776    const factory = <T>(source: ObservedPropertyAbstract<T>) => {
777      const result: ObservedPropertyAbstractPU<T> = new SynchedPropertyTwoWayPU<T>(source, this, consumeVarName);
778      stateMgmtConsole.debug(`The @Consume is instance of ${result.constructor.name}`);
779      return result;
780    };
781    return providedVarStore.createSync(factory) as ObservedPropertyAbstractPU<T>;
782  }
783
784
785  /**
786   * given the elmtId of a child or child of child within this custom component
787   * remember this component needs a partial update
788   * @param elmtId
789   */
790  public markElemenDirtyById(elmtId: number): void {
791    // TODO ace-ets2bundle, framework, compilated apps need to update together
792    // this function will be removed after a short transiition periode
793    stateMgmtConsole.applicationError(`${this.debugInfo__()}: markElemenDirtyById no longer supported.
794        Please update your ace-ets2bundle and recompile your application. Application error!`);
795  }
796
797  /**
798   * For each recorded dirty Element in this custom component
799   * run its update function
800   *
801   */
802  public updateDirtyElements() {
803    stateMgmtProfiler.begin("ViewPU.updateDirtyElements");
804    do {
805      stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber)}, starting ....`);
806
807      // see which elmtIds are managed by this View
808      // and clean up all book keeping for them
809      this.purgeDeletedElmtIds();
810
811      // process all elmtIds marked as needing update in ascending order.
812      // ascending order ensures parent nodes will be updated before their children
813      // prior cleanup ensure no already deleted Elements have their update func executed
814      Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber).forEach(elmtId => {
815        if (this.hasRecycleManager()) {
816          this.UpdateElement(this.recycleManager_.proxyNodeId(elmtId));
817        } else {
818          this.UpdateElement(elmtId);
819        }
820        this.dirtDescendantElementIds_.delete(elmtId);
821      });
822
823      if (this.dirtDescendantElementIds_.size) {
824        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!`);
825      }
826    } while (this.dirtDescendantElementIds_.size);
827    stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE, dump of ViewPU in next lines`);
828    this.dumpStateVars();
829    stateMgmtProfiler.end();
830  }
831
832  // request list of all (global) elmtIds of deleted UINodes and unregister from the all ViewPUs
833  // this function equals purgeDeletedElmtIdsRecursively because it does unregistration for all ViewPUs
834  protected purgeDeletedElmtIds(): void {
835    stateMgmtConsole.debug(`purgeDeletedElmtIds @Component '${this.constructor.name}' (id: ${this.id__()}) start ...`)
836    // request list of all (global) elmtIds of deleted UINodes that need to be unregistered
837    UINodeRegisterProxy.obtainDeletedElmtIds();
838    // unregister the removed elementids requested from the cpp side for all viewpus, it will make the first viewpu slower
839    // than before, but the rest viewpu will be faster
840    UINodeRegisterProxy.unregisterElmtIdsFromViewPUs();
841    stateMgmtConsole.debug(`purgeDeletedElmtIds @Component '${this.constructor.name}' (id: ${this.id__()}) end... `)
842  }
843
844
845  protected purgeVariableDependenciesOnElmtIdOwnFunc(elmtId: number): void {
846    this.ownObservedPropertiesStore_.forEach((stateVar: ObservedPropertyAbstractPU<any>) => {
847      stateVar.purgeDependencyOnElmtId(elmtId);
848    })
849  }
850
851  /**
852   * return its elmtId if currently rendering or re-rendering an UINode
853   * otherwise return -1
854   * set in observeComponentCreation(2)
855   */
856  public getCurrentlyRenderedElmtId() {
857    return ViewPU.renderingPaused || this.currentlyRenderedElmtIdStack_.length == 0 ? -1 : this.currentlyRenderedElmtIdStack_.slice(-1)[0];
858  }
859
860  public static pauseRendering() {
861    ViewPU.renderingPaused = true;
862  }
863
864  public static restoreRendering() {
865    ViewPU.renderingPaused = false;
866  }
867
868  // executed on first render only
869  // kept for backward compatibility with old ace-ets2bundle
870  public observeComponentCreation(compilerAssignedUpdateFunc: UpdateFunc): void {
871    if (this.isDeleting_) {
872      stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation `);
873      return;
874    }
875    const updateFunc = (elmtId: number, isFirstRender: boolean) => {
876      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
877      this.currentlyRenderedElmtIdStack_.push(elmtId);
878      compilerAssignedUpdateFunc(elmtId, isFirstRender);
879      this.currentlyRenderedElmtIdStack_.pop();
880      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
881    }
882
883    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
884    // in observeComponentCreation function we do not get info about the component name, in
885    // observeComponentCreation2 we do.
886    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc });
887    // add element id -> owning ViewPU
888    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
889    try {
890      updateFunc(elmtId, /* is first render */ true);
891    } catch (error) {
892      // avoid the incompatible change that move set function before updateFunc.
893      this.updateFuncByElmtId.delete(elmtId);
894      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
895      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
896      throw error;
897    }
898  }
899
900  // executed on first render only
901  // added July 2023, replaces observeComponentCreation
902  // classObject is the ES6 class object , mandatory to specify even the class lacks the pop function.
903  // - prototype : Object is present for every ES6 class
904  // - pop : () => void, static function present for JSXXX classes such as Column, TapGesture, etc.
905  public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void {
906    if (this.isDeleting_) {
907      stateMgmtConsole.error(`View ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `);
908      return;
909    }
910    const _componentName: string = (classObject && ("name" in classObject)) ? Reflect.get(classObject, "name") as string : "unspecified UINode";
911    const _popFunc: () => void = (classObject && "pop" in classObject) ? classObject.pop! : () => { };
912    const updateFunc = (elmtId: number, isFirstRender: boolean) => {
913      this.syncInstanceId();
914      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
915      ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
916      this.currentlyRenderedElmtIdStack_.push(elmtId);
917      compilerAssignedUpdateFunc(elmtId, isFirstRender);
918      if (!isFirstRender) {
919        _popFunc();
920      }
921      this.currentlyRenderedElmtIdStack_.pop();
922      ViewStackProcessor.StopGetAccessRecording();
923      stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
924      this.restoreInstanceId();
925    };
926
927    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
928    // needs to move set before updateFunc.
929    // make sure the key and object value exist since it will add node in attributeModifier during updateFunc.
930    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject });
931    // add element id -> owning ViewPU
932    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
933    try {
934      updateFunc(elmtId, /* is first render */ true);
935    } catch (error) {
936      // avoid the incompatible change that move set function before updateFunc.
937      this.updateFuncByElmtId.delete(elmtId);
938      UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
939      stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`)
940      throw error;
941    }
942    stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`);
943  }
944
945  getOrCreateRecycleManager(): RecycleManager {
946    if (!this.recycleManager_) {
947      this.recycleManager_ = new RecycleManager
948    }
949    return this.recycleManager_;
950  }
951
952  getRecycleManager(): RecycleManager {
953    return this.recycleManager_;
954  }
955
956  hasRecycleManager(): boolean {
957    return !(this.recycleManager_ === undefined);
958  }
959
960  initRecycleManager(): void {
961    if (this.recycleManager_) {
962      stateMgmtConsole.error(`${this.debugInfo__()}: init recycleManager multiple times. Internal error.`);
963      return;
964    }
965    this.recycleManager_ = new RecycleManager;
966  }
967  rebuildUpdateFunc(elmtId, compilerAssignedUpdateFunc) {
968    const updateFunc = (elmtId, isFirstRender) => {
969        this.currentlyRenderedElmtIdStack_.push(elmtId);
970        compilerAssignedUpdateFunc(elmtId, isFirstRender);
971        this.currentlyRenderedElmtIdStack_.pop();
972    };
973    if (this.updateFuncByElmtId.has(elmtId)) {
974        this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc });
975    }
976  }
977
978  /**
979   * @function observeRecycleComponentCreation
980   * @description custom node recycle creation
981   * @param name custom node name
982   * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function
983   * @return void
984   */
985  public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void {
986    // convert recycle update func to update func
987    const compilerAssignedUpdateFunc: UpdateFunc = (element, isFirstRender) => {
988      recycleUpdateFunc(element, isFirstRender, undefined)
989    };
990    let node: ViewPU;
991    // if there is no suitable recycle node, run a normal creation function.
992    if (!this.hasRecycleManager() || !(node = this.getRecycleManager().popRecycleNode(name))) {
993      stateMgmtConsole.debug(`${this.constructor.name}[${this.id__()}]: cannot init node by recycle, crate new node`);
994      this.observeComponentCreation(compilerAssignedUpdateFunc);
995      return;
996    }
997
998    // if there is a suitable recycle node, run a recycle update function.
999    const newElmtId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
1000    const oldElmtId: number = node.id__();
1001    this.recycleManager_.updateNodeId(oldElmtId, newElmtId);
1002    this.hasBeenRecycled_ = true;
1003    this.rebuildUpdateFunc(oldElmtId, compilerAssignedUpdateFunc);
1004    recycleUpdateFunc(oldElmtId, /* is first render */ true, node);
1005  }
1006
1007  aboutToReuseInternal() {
1008    this.runReuse_ = true;
1009    stateMgmtTrace.scopedTrace(() => {
1010      if (this.paramsGenerator_ && typeof this.paramsGenerator_ == "function") {
1011        const params = this.paramsGenerator_();
1012        this.updateStateVars(params);
1013        this.aboutToReuse(params);
1014      }
1015    }, "aboutToReuse", this.constructor.name);
1016    this.childrenWeakrefMap_.forEach((weakRefChild) => {
1017      const child = weakRefChild.deref();
1018      if (child && !child.hasBeenRecycled_) {
1019        child.aboutToReuseInternal();
1020      }
1021    });
1022    this.updateDirtyElements();
1023    this.runReuse_ = false;
1024  }
1025
1026  aboutToRecycleInternal() {
1027    this.runReuse_ = true;
1028    stateMgmtTrace.scopedTrace(() => {
1029      this.aboutToRecycle();
1030    }, "aboutToRecycle", this.constructor.name);
1031    this.childrenWeakrefMap_.forEach((weakRefChild) => {
1032      const child = weakRefChild.deref();
1033      if (child && !child.hasBeenRecycled_) {
1034        child.aboutToRecycleInternal();
1035      }
1036    });
1037    this.runReuse_ = false;
1038  }
1039
1040  // add current JS object to it's parent recycle manager
1041  public recycleSelf(name: string): void {
1042    if (this.parent_ && !this.parent_.isDeleting_) {
1043      this.parent_.getOrCreateRecycleManager().pushRecycleNode(name, this);
1044      this.hasBeenRecycled_ = true;
1045    } else {
1046      this.resetRecycleCustomNode();
1047      stateMgmtConsole.error(`${this.constructor.name}[${this.id__()}]: recycleNode must have a parent`);
1048    }
1049  }
1050
1051  // performs the update on a branch within if() { branch } else if (..) { branch } else { branch }
1052  public ifElseBranchUpdateFunction(branchId: number, branchfunc: () => void): void {
1053    const oldBranchid: number = If.getBranchId();
1054
1055    if (branchId == oldBranchid) {
1056      stateMgmtConsole.debug(`${this.debugInfo__()}: ifElseBranchUpdateFunction: IfElse branch unchanged, no work to do.`);
1057      return;
1058    }
1059
1060    // branchid identifies uniquely the if .. <1> .. else if .<2>. else .<3>.branch
1061    // ifElseNode stores the most recent branch, so we can compare
1062    // removedChildElmtIds will be filled with the elmtIds of all children and their children will be deleted in response to if .. else chnage
1063    let removedChildElmtIds = new Array<number>();
1064    If.branchId(branchId, removedChildElmtIds);
1065
1066    //unregisters the removed child elementIDs using proxy
1067    UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs(removedChildElmtIds);
1068
1069    // purging these elmtIds from state mgmt will make sure no more update function on any deleted child wi;ll be executed
1070    stateMgmtConsole.debug(`ViewPU ifElseBranchUpdateFunction: elmtIds need unregister after if/else branch switch: ${JSON.stringify(removedChildElmtIds)}`);
1071    this.purgeDeletedElmtIds();
1072
1073    branchfunc();
1074  }
1075
1076  /**
1077   Partial updates for ForEach.
1078   * @param elmtId ID of element.
1079   * @param itemArray Array of items for use of itemGenFunc.
1080   * @param itemGenFunc Item generation function to generate new elements. If index parameter is
1081   *                    given set itemGenFuncUsesIndex to true.
1082   * @param idGenFunc   ID generation function to generate unique ID for each element. If index parameter is
1083   *                    given set idGenFuncUsesIndex to true.
1084   * @param itemGenFuncUsesIndex itemGenFunc optional index parameter is given or not.
1085   * @param idGenFuncUsesIndex idGenFunc optional index parameter is given or not.
1086   */
1087  public forEachUpdateFunction(elmtId: number,
1088    itemArray: Array<any>,
1089    itemGenFunc: (item: any, index?: number) => void,
1090    idGenFunc?: (item: any, index?: number) => string,
1091    itemGenFuncUsesIndex: boolean = false,
1092    idGenFuncUsesIndex: boolean = false): void {
1093
1094    stateMgmtProfiler.begin("ViewPU.forEachUpdateFunction");
1095    stateMgmtConsole.debug(`${this.debugInfo__()}: forEachUpdateFunction (ForEach re-render) start ...`);
1096
1097    if (itemArray === null || itemArray === undefined) {
1098      stateMgmtConsole.applicationError(`${this.debugInfo__()}: forEachUpdateFunction (ForEach re-render): input array is null or undefined error. Application error!`);
1099      stateMgmtProfiler.end();
1100      return;
1101    }
1102
1103    if (typeof itemGenFunc !== "function") {
1104      stateMgmtConsole.applicationError(`${this.debugInfo__()}: forEachUpdateFunction (ForEach re-render): Item generation function missing. Application error!`);
1105      stateMgmtProfiler.end();
1106      return;
1107    }
1108
1109    if (idGenFunc !== undefined && typeof idGenFunc !== "function") {
1110      stateMgmtConsole.applicationError(`${this.debugInfo__()}: forEachUpdateFunction (ForEach re-render): id generator is not a function. Application error!`);
1111      stateMgmtProfiler.end();
1112      return;
1113    }
1114
1115    if (idGenFunc === undefined) {
1116      stateMgmtConsole.debug(`${this.debugInfo__()}: forEachUpdateFunction: providing default id gen function `);
1117      idGenFuncUsesIndex = true;
1118      // catch possible error caused by Stringify and re-throw an Error with a meaningful (!) error message
1119      idGenFunc = (item: any, index: number) => {
1120        try {
1121          return `${index}__${JSON.stringify(item)}`;
1122        } catch (e) {
1123          throw new Error(`${this.debugInfo__()}: ForEach id ${elmtId}: use of default id generator function not possible on provided data structure. Need to specify id generator function (ForEach 3rd parameter). Application Error!`)
1124        }
1125      }
1126    }
1127
1128    let diffIndexArray = []; // New indexes compared to old one.
1129    let newIdArray = [];
1130    let idDuplicates = [];
1131    const arr = itemArray; // just to trigger a 'get' onto the array
1132
1133    // ID gen is with index.
1134    if (idGenFuncUsesIndex) {
1135      // Create array of new ids.
1136      arr.forEach((item, indx) => {
1137        newIdArray.push(idGenFunc(item, indx));
1138      });
1139    }
1140    else {
1141      // Create array of new ids.
1142      arr.forEach((item, index) => {
1143        newIdArray.push(`${itemGenFuncUsesIndex ? index + '_' : ''}` + idGenFunc(item));
1144      });
1145    }
1146
1147    // Set new array on C++ side.
1148    // C++ returns array of indexes of newly added array items.
1149    // these are indexes in new child list.
1150    ForEach.setIdArray(elmtId, newIdArray, diffIndexArray, idDuplicates);
1151
1152    // Its error if there are duplicate IDs.
1153    if (idDuplicates.length > 0) {
1154      idDuplicates.forEach((indx) => {
1155        stateMgmtConsole.error(`Error: ${newIdArray[indx]} generated for ${indx}${indx < 4 ? indx == 2 ? "nd" : "rd" : "th"} array item ${arr[indx]}.`);
1156      });
1157      stateMgmtConsole.applicationError(`${this.debugInfo__()}: Ids generated by the ForEach id gen function must be unique. Application error!`);
1158    }
1159
1160    stateMgmtConsole.debug(`${this.debugInfo__()}: forEachUpdateFunction: diff indexes ${JSON.stringify(diffIndexArray)} . `);
1161
1162    // Item gen is with index.
1163    stateMgmtConsole.debug(`   ... item Gen ${itemGenFuncUsesIndex ? 'with' : "without"} index`);
1164    // Create new elements if any.
1165    stateMgmtProfiler.begin("ViewPU.forEachUpdateFunction (native)");
1166    diffIndexArray.forEach((indx) => {
1167      ForEach.createNewChildStart(newIdArray[indx], this);
1168      if (itemGenFuncUsesIndex) {
1169        itemGenFunc(arr[indx], indx);
1170      } else {
1171        itemGenFunc(arr[indx]);
1172      }
1173      ForEach.createNewChildFinish(newIdArray[indx], this);
1174    });
1175    stateMgmtConsole.debug(`${this.debugInfo__()}: forEachUpdateFunction (ForEach re-render) - DONE.`);
1176    stateMgmtProfiler.end();
1177    stateMgmtProfiler.end();
1178  }
1179
1180  public UpdateLazyForEachElements(elmtIds: Array<number>): void {
1181    if (!Array.isArray(elmtIds)) {
1182      return;
1183    }
1184    Array.from(elmtIds).sort(ViewPU.compareNumber).forEach((elmtId: number) => {
1185      const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
1186      const updateFunc: UpdateFunc = entry ? entry.getUpdateFunc() : undefined;
1187      if (typeof updateFunc !== "function") {
1188        stateMgmtConsole.debug(`${this.debugInfo__()}: update function of elmtId ${elmtId} not found, internal error!`);
1189      } else {
1190        this.isRenderInProgress = true;
1191        updateFunc(elmtId, false);
1192        this.finishUpdateFunc(elmtId);
1193        this.isRenderInProgress = false;
1194      }
1195    })
1196  }
1197
1198  /**
1199     * CreateStorageLink and CreateStorageLinkPU are used by the implementation of @StorageLink and
1200     * @LocalStotrageLink in full update and partial update solution respectively.
1201     * These are not part of the public AppStorage API , apps should not use.
1202     * @param storagePropName - key in LocalStorage
1203     * @param defaultValue - value to use when creating a new prop in the LocalStotage
1204     * @param owningView - the View/ViewPU owning the @StorageLink/@LocalStorageLink variable
1205     * @param viewVariableName -  @StorageLink/@LocalStorageLink variable name
1206     * @returns SynchedPropertySimple/ObjectTwoWay/PU
1207     */
1208  public createStorageLink<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
1209    const appStorageLink = AppStorage.__createSync<T>(storagePropName, defaultValue,
1210      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1211        ? undefined
1212        : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName)
1213    ) as ObservedPropertyAbstractPU<T>;
1214    return appStorageLink;
1215  }
1216
1217  public createStorageProp<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
1218    const appStorageProp = AppStorage.__createSync<T>(storagePropName, defaultValue,
1219      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1220        ? undefined
1221        : new SynchedPropertyOneWayPU<T>(source, this, viewVariableName)
1222    ) as ObservedPropertyAbstractPU<T>;
1223    return appStorageProp;
1224  }
1225
1226  public createLocalStorageLink<T>(storagePropName: string, defaultValue: T,
1227    viewVariableName: string): ObservedPropertyAbstractPU<T> {
1228    const localStorageLink = this.localStorage_.__createSync<T>(storagePropName, defaultValue,
1229      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1230        ? undefined
1231        : new SynchedPropertyTwoWayPU<T>(source, this, viewVariableName)
1232    ) as ObservedPropertyAbstractPU<T>;
1233    return localStorageLink;
1234  }
1235
1236  public createLocalStorageProp<T>(storagePropName: string, defaultValue: T,
1237    viewVariableName: string): ObservedPropertyAbstractPU<T> {
1238    const localStorageProp = this.localStorage_.__createSync<T>(storagePropName, defaultValue,
1239      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1240        ? undefined
1241        : new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
1242    ) as ObservedPropertyAbstractPU<T>;
1243    return localStorageProp;
1244  }
1245
1246  public createOrGetNode(elmtId: number, builder: () => object): object {
1247    const entry = this.updateFuncByElmtId.get(elmtId);
1248    if (entry === undefined) {
1249      throw new Error(`${this.debugInfo__()} fail to create node, elmtId is illegal`);
1250    }
1251    let nodeInfo = entry.getNode();
1252    if (nodeInfo === undefined) {
1253      nodeInfo = builder();
1254      entry.setNode(nodeInfo);
1255    }
1256    return nodeInfo;
1257  }
1258
1259  /**
1260   * onDumpInfo is used to process commands delivered by the hidumper process
1261   * @param commands -  list of commands provided in the shell
1262   * @returns void
1263   */
1264  protected onDumpInfo(commands: string[]): void {
1265
1266    let dfxCommands: DFXCommand[] = this.processOnDumpCommands(commands);
1267
1268    dfxCommands.forEach((command) => {
1269      let view: ViewPU = undefined;
1270      if (command.viewId) {
1271        view = this.findViewInHierarchy(command.viewId);
1272        if (!view) {
1273          DumpLog.print(0, `\nTarget view: ${command.viewId} not found for command: ${command.what}\n`);
1274          return;
1275        }
1276      } else {
1277        view = this;
1278        command.viewId = view.id__();
1279      }
1280      switch (command.what) {
1281        case "-dumpAll":
1282          view.printDFXHeader("ViewPU Info", command);
1283          DumpLog.print(0, view.debugInfoView(command.isRecursive));
1284          break;
1285        case "-viewHierarchy":
1286          view.printDFXHeader("ViewPU Hierarchy", command);
1287          DumpLog.print(0, view.debugInfoViewHierarchy(command.isRecursive));
1288          break;
1289        case "-stateVariables":
1290          view.printDFXHeader("ViewPU State Variables", command);
1291          DumpLog.print(0, view.debugInfoStateVars());
1292          break;
1293        case "-registeredElementIds":
1294          view.printDFXHeader("ViewPU Registered Element IDs", command);
1295          DumpLog.print(0, view.debugInfoUpdateFuncByElmtId(command.isRecursive));
1296          break;
1297        case "-dirtyElementIds":
1298          view.printDFXHeader("ViewPU Dirty Registered Element IDs", command);
1299          DumpLog.print(0, view.debugInfoDirtDescendantElementIds(command.isRecursive));
1300          break;
1301        case "-inactiveComponents":
1302          view.printDFXHeader("List of Inactive Components", command);
1303          DumpLog.print(0, view.debugInfoInactiveComponents());
1304          break;
1305        case "-profiler":
1306          view.printDFXHeader("Profiler Info", command);
1307          view.dumpReport();
1308          break;
1309        default:
1310          DumpLog.print(0, `\nUnsupported JS DFX dump command: [${command.what}, viewId=${command.viewId}, isRecursive=${command.isRecursive}]\n`);
1311      }
1312    })
1313  }
1314
1315  private printDFXHeader(header: string, command: DFXCommand): void {
1316    let length: number = 50;
1317    let remainder: number = length - header.length < 0 ? 0 : length - header.length;
1318    DumpLog.print(0, `\n${'-'.repeat(remainder / 2)}${header}${'-'.repeat(remainder / 2)}`);
1319    DumpLog.print(0, `[${command.what}, viewId=${command.viewId}, isRecursive=${command.isRecursive}]\n`);
1320  }
1321
1322  private processOnDumpCommands(commands: string[]): DFXCommand[] {
1323    let isFlag: Function = (param: string): boolean => {
1324      return "-r".match(param) != null || param.startsWith("-viewId=");
1325    }
1326
1327    let dfxCommands: DFXCommand[] = [];
1328
1329    for (var i: number = 0; i < commands.length; i++) {
1330      let command = commands[i];
1331      if (isFlag(command)) {
1332        if (command.startsWith("-viewId=")) {
1333          let dfxCommand: DFXCommand = dfxCommands[dfxCommands.length - 1];
1334          if (dfxCommand) {
1335            let input: string[] = command.split('=');
1336            if (input[1]) {
1337              let viewId: number = Number.parseInt(input[1]);
1338              dfxCommand.viewId = Number.isNaN(viewId) ? -1 : viewId;
1339            }
1340          }
1341        } else if (command.match("-r")) {
1342          let dfxCommand: DFXCommand = dfxCommands[dfxCommands.length - 1];
1343          if (dfxCommand) {
1344            dfxCommand.isRecursive = true;
1345          }
1346        }
1347      } else {
1348        dfxCommands.push({
1349          what: command,
1350          viewId: undefined,
1351          isRecursive: false,
1352        })
1353      }
1354    }
1355    return dfxCommands;
1356  }
1357
1358  private findViewInHierarchy(id: number): ViewPU {
1359    let weak = this.childrenWeakrefMap_.get(id);
1360    if (weak) {
1361      return weak.deref();
1362    }
1363
1364    let retVal: ViewPU = undefined;
1365    for (const [key, value] of this.childrenWeakrefMap_.entries()) {
1366      retVal = value.deref().findViewInHierarchy(id);
1367      if (retVal)
1368        break;
1369    }
1370    return retVal;
1371  }
1372
1373  private debugInfoView(recursive: boolean = false): string {
1374    return this.debugInfoViewInternal(recursive);
1375  }
1376
1377  private debugInfoViewInternal(recursive: boolean = false): string {
1378    let retVal: string = `@Component\n${this.constructor.name}[${this.id__()}]`;
1379    retVal += `\n\nView Hierarchy:\n${this.debugInfoViewHierarchy(recursive)}`;
1380    retVal += `\n\nState variables:\n${this.debugInfoStateVars()}`;
1381    retVal += `\n\nRegistered Element IDs:\n${this.debugInfoUpdateFuncByElmtId(recursive)}`;
1382    retVal += `\n\nDirty Registered Element IDs:\n${this.debugInfoDirtDescendantElementIds(recursive)}`;
1383    return retVal;
1384  }
1385
1386  private debugInfoViewHierarchy(recursive: boolean = false): string {
1387    return this.debugInfoViewHierarchyInternal(0, recursive);
1388  }
1389
1390  private debugInfoViewHierarchyInternal(depth: number = 0, recursive: boolean = false): string {
1391    let retVaL: string = `\n${"  ".repeat(depth)}|--${this.constructor.name}[${this.id__()}]`;
1392    if (this.isCompFreezeAllowed) {
1393      retVaL += ` {freezewhenInactive : ${this.isCompFreezeAllowed}}`;
1394    }
1395
1396    if (depth < 1 || recursive) {
1397      this.childrenWeakrefMap_.forEach((value, key, map) => {
1398        retVaL += value.deref()?.debugInfoViewHierarchyInternal(depth + 1, recursive);
1399      })
1400    }
1401    return retVaL;
1402  }
1403
1404  private debugInfoUpdateFuncByElmtId(recursive: boolean = false): string {
1405    return this.debugInfoUpdateFuncByElmtIdInternal({ total: 0 }, 0, recursive);
1406  }
1407
1408  private debugInfoUpdateFuncByElmtIdInternal(counter: ProfileRecursionCounter, depth: number = 0, recursive: boolean = false): string {
1409    let retVaL: string = `\n${"  ".repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`;
1410    this.updateFuncByElmtId.forEach((value, key, map) => {
1411      retVaL += `\n${"  ".repeat(depth + 2)}${value.getComponentName()}[${key}]`
1412    })
1413    counter.total += this.updateFuncByElmtId.size;
1414    retVaL += `\n${"  ".repeat(depth + 1)}}[${this.updateFuncByElmtId.size}]`
1415    if (recursive) {
1416      this.childrenWeakrefMap_.forEach((value, key, map) => {
1417        retVaL += value.deref()?.debugInfoUpdateFuncByElmtIdInternal(counter, depth + 1, recursive);
1418      })
1419    }
1420    if (recursive && depth == 0) {
1421      retVaL += `\nTotal: ${counter.total}`
1422    }
1423    return retVaL;
1424  }
1425
1426  private debugInfoDirtDescendantElementIds(recursive: boolean = false): string {
1427    return this.debugInfoDirtDescendantElementIdsInternal(0, recursive, { total: 0 });
1428  }
1429
1430  private debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string {
1431    let retVaL: string = `\n${"  ".repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`;
1432    this.dirtDescendantElementIds_.forEach((value) => {
1433      retVaL += `${value}, `
1434    })
1435    counter.total += this.dirtDescendantElementIds_.size;
1436    retVaL += `\n${"  ".repeat(depth + 1)}}[${this.dirtDescendantElementIds_.size}]`
1437    if (recursive) {
1438      this.childrenWeakrefMap_.forEach((value, key, map) => {
1439        retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter);
1440      })
1441    }
1442
1443    if (recursive && depth == 0) {
1444      retVaL += `\nTotal: ${counter.total}`
1445    }
1446    return retVaL;
1447  }
1448
1449  private debugInfoInactiveComponents(): string {
1450    return Array.from(ViewPU.inactiveComponents_)
1451      .map((component) => `- ${component}`).join('\n');
1452  }
1453}
1454
1455class UpdateFuncsByElmtId {
1456
1457  private map_ = new Map<number, UpdateFuncRecord>();
1458
1459  public delete(elmtId: number): boolean {
1460    return this.map_.delete(elmtId);
1461  }
1462
1463  public set(elmtId: number, params: UpdateFunc | { updateFunc: UpdateFunc, classObject?: UIClassObject, node?: Object }): void {
1464    (typeof params === 'object') ?
1465      this.map_.set(elmtId, new UpdateFuncRecord(params)) :
1466      this.map_.set(elmtId, new UpdateFuncRecord({ updateFunc: params as UpdateFunc }));
1467  }
1468
1469  public get(elmtId: number): UpdateFuncRecord | undefined {
1470    return this.map_.get(elmtId);
1471  }
1472
1473  public has(elmtId: number): boolean {
1474    return this.map_.has(elmtId);
1475  }
1476
1477  public keys(): IterableIterator<number> {
1478    return this.map_.keys();
1479  }
1480
1481  public clear(): void {
1482    return this.map_.clear();
1483  }
1484
1485  public get size(): number {
1486    return this.map_.size;
1487  }
1488
1489  public forEach(callbackfn: (value: UpdateFuncRecord, key: number, map: Map<number, UpdateFuncRecord>) => void): void {
1490    this.map_.forEach(callbackfn);
1491  }
1492
1493  // dump info about known elmtIds to a string
1494  // use function only for debug output and DFX.
1495  public debugInfoRegisteredElmtIds(): string {
1496    let result: string = "";
1497    let sepa: string = "";
1498    this.map_.forEach((value: UpdateFuncRecord, elmtId: number) => {
1499      result += `${sepa}${value.getComponentName()}[${elmtId}]`;
1500      sepa = ", ";
1501    });
1502    return result;
1503  }
1504
1505  public debugInfoElmtId(elmtId: number): string {
1506    const updateFuncEntry = this.map_.get(elmtId);
1507    return updateFuncEntry ? `'${updateFuncEntry!.getComponentName()}[${elmtId}]'` : `'unknown component type'[${elmtId}]`;
1508  }
1509}
1510