• 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
32type ProvidedVarsMapPU = Map<string, ObservedPropertyAbstractPU<any>>;
33
34// denotes a missing elemntId, this is the case during initial render
35const UndefinedElmtId = -1;
36
37// function type of partial update function
38type UpdateFunc = (elmtId: number, isFirstRender: boolean) => void;
39type UIClassObject = { prototype: Object, pop?: () => void };
40
41// UpdateFuncRecord: misc framework-internal info related to updating of a UINode C++ object
42// that TS side needs to know.
43// updateFunc_  lambda function to update the UINode
44// JS interface class reference (it only has static functions)
45class UpdateFuncRecord {
46  private updateFunc_: UpdateFunc;
47  private classObject_: UIClassObject;
48  private node_?: Object
49
50  constructor(params: { updateFunc: UpdateFunc, classObject?: UIClassObject, node?: Object }) {
51    this.updateFunc_ = params.updateFunc;
52    this.classObject_ = params.classObject;
53    this.node_ = params.node;
54  }
55
56  public getUpdateFunc() : UpdateFunc | undefined {
57    return this.updateFunc_;
58  }
59
60  public getComponentClass(): UIClassObject | undefined {
61    return this.classObject_;
62  }
63
64  public getComponentName(): string {
65    return (this.classObject_ && ("name" in this.classObject_)) ? Reflect.get(this.classObject_, "name") as string : "unspecified UINode";
66  }
67
68  public getPopFunc(): () => void {
69    return (this.classObject_ && "pop" in this.classObject_) ? this.classObject_.pop! : () => { };
70  }
71
72  public getNode(): Object | undefined {
73    return this.node_;
74  }
75
76  public setNode(node: Object | undefined): void{
77    this.node_ = node;
78  }
79}
80
81// function type of recycle node update function
82type RecycleUpdateFunc = (elmtId: number, isFirstRender: boolean, recycleNode: ViewPU) => void;
83
84// NativeView
85// implemented in C++  for release
86// and in utest/view_native_mock.ts for testing
87abstract class ViewPU extends NativeViewPartialUpdate
88  implements IViewPropertiesChangeSubscriber {
89
90  // Array.sort() converts array items to string to compare them, sigh!
91  static readonly compareNumber = (a: number, b: number): number => {
92    return (a < b) ? -1 : (a > b) ? 1 : 0;
93  };
94
95  private id_: number;
96
97  private parent_: ViewPU = undefined;
98  private childrenWeakrefMap_ = new Map<number, WeakRef<ViewPU>>();
99
100  // flag for initgial rendering or re-render on-going.
101  private isRenderInProgress: boolean = false;
102
103  // flag if active of inActive
104  // inActive means updates are delayed
105  private isActive_ : boolean = true;
106
107  // flag if {aboutToBeDeletedInternal} is called and the instance of ViewPU has not been GC.
108  private isDeleting_: boolean = false;
109
110  private watchedProps: Map<string, (propName: string) => void>
111    = new Map<string, (propName: string) => void>();
112
113  private recycleManager: RecycleManager = undefined;
114
115  // @Provide'd variables by this class and its ancestors
116  protected providedVars_: ProvidedVarsMapPU;
117
118  // Set of dependent elmtIds that need partial update
119  // during next re-render
120  protected dirtDescendantElementIds_: Set<number>
121    = new Set<number>();
122
123  // registry of update functions
124  // the key is the elementId of the Component/Element that's the result of this function
125  private updateFuncByElmtId = new class UpdateFuncsByElmtId {
126
127    private map_ = new Map<number, UpdateFuncRecord>();
128
129    public delete(elmtId: number): boolean {
130      return this.map_.delete(elmtId);
131    }
132
133    public set(elmtId: number, params: UpdateFunc | { updateFunc: UpdateFunc, classObject?: UIClassObject, node?: Object }): void {
134      (typeof params == "object") ?
135        this.map_.set(elmtId, new UpdateFuncRecord(params))
136        : this.map_.set(elmtId, new UpdateFuncRecord({ updateFunc: params as UpdateFunc }));
137    }
138
139    public get(elmtId: number): UpdateFuncRecord | undefined {
140      return this.map_.get(elmtId);
141    }
142
143    public keys(): IterableIterator<number> {
144      return this.map_.keys();
145    }
146
147    public clear(): void {
148      return this.map_.clear();
149    }
150
151    public get size(): number {
152      return this.map_.size;
153    }
154
155    public forEach(callbackfn: (value: UpdateFuncRecord, key: number, map: Map<number, UpdateFuncRecord>) => void) : void {
156      this.map_.forEach(callbackfn);
157    }
158
159    // dump info about known elmtIds to a string
160    // use function only for debug output and DFX.
161    public debugInfoRegisteredElmtIds(): string {
162      let result: string = "";
163      let sepa: string = "";
164      this.map_.forEach((value: UpdateFuncRecord, elmtId: number) => {
165        result += `${sepa}${value.getComponentName()}[${elmtId}]`;
166        sepa = ", ";
167      });
168      return result;
169    }
170
171    public debugInfoElmtId(elmtId: number): string {
172      const updateFuncEntry = this.map_.get(elmtId);
173      return updateFuncEntry ? `'${updateFuncEntry!.getComponentName()}[${elmtId}]'` : `'unknown component type'[${elmtId}]`;
174    }
175  }
176
177  // set of all @Local/StorageLink/Prop variables owned by this ViwPU
178  private ownStorageLinksProps_ : Set<ObservedPropertyAbstractPU<any>> = new Set<ObservedPropertyAbstractPU<any>>();
179
180  // my LocalStorage instance, shared with ancestor Views.
181  // create a default instance on demand if none is initialized
182  protected localStoragebackStore_: LocalStorage = undefined;
183
184  private ownObservedPropertiesStore__? : Set<ObservedPropertyAbstractPU<any>>;
185
186  private get ownObservedPropertiesStore_() {
187    if (!this.ownObservedPropertiesStore__) {
188      // lazy init
189      this.ownObservedPropertiesStore__ = new Set<ObservedPropertyAbstractPU<any>>();
190      this.obtainOwnObservedProperties();
191    }
192    return this.ownObservedPropertiesStore__;
193  }
194
195  protected obtainOwnObservedProperties(): void {
196    Object.getOwnPropertyNames(this)
197      .filter((propName) => {
198        return propName.startsWith("__")
199      })
200      .forEach((propName) => {
201        const stateVar = Reflect.get(this, propName) as Object;
202        if ("notifyPropertyHasChangedPU" in stateVar) {
203          stateMgmtConsole.debug(`... add state variable ${propName} to ${stateVar}`)
204          this.ownObservedPropertiesStore_.add(stateVar as unknown as ObservedPropertyAbstractPU<any>);
205        }
206      });
207  }
208
209  protected get localStorage_() {
210    if (!this.localStoragebackStore_ && this.parent_) {
211      stateMgmtConsole.debug(`${this.debugInfo()}: constructor: get localStorage_ : Using LocalStorage instance of the parent View.`);
212      this.localStoragebackStore_ = this.parent_.localStorage_;
213    }
214
215    if (!this.localStoragebackStore_) {
216      stateMgmtConsole.info(`${this.debugInfo()}: constructor: is accessing LocalStorage without being provided an instance. Creating a default instance.`);
217      this.localStoragebackStore_ = new LocalStorage({ /* empty */ });
218    }
219    return this.localStoragebackStore_;
220  }
221
222  protected set localStorage_(instance: LocalStorage) {
223    if (!instance) {
224      // setting to undefined not allowed
225      return;
226    }
227    if (this.localStoragebackStore_) {
228      stateMgmtConsole.applicationError(`${this.debugInfo()}: constructor: is setting LocalStorage instance twice. Application error.`);
229    }
230    this.localStoragebackStore_ = instance;
231  }
232
233  /**
234   * Create a View
235   *
236   * 1. option: top level View, specify
237   *    - compilerAssignedUniqueChildId must specify
238   *    - parent=undefined
239   *    - localStorage  must provide if @LocalSTorageLink/Prop variables are used
240   *      in this View or descendant Views.
241   *
242   * 2. option: not a top level View
243   *    - compilerAssignedUniqueChildId must specify
244   *    - parent must specify
245   *    - localStorage do not specify, will inherit from parent View.
246   *
247  */
248  constructor(parent: ViewPU, localStorage: LocalStorage, elmtId : number = -1) {
249    super();
250    // if set use the elmtId also as the ViewPU object's subscribable id.
251    // these matching is requiremrnt for updateChildViewById(elmtId) being able to
252    // find the child ViewPU object by given elmtId
253    this.id_= elmtId == -1 ? SubscriberManager.MakeId() : elmtId;
254    this.providedVars_ = parent ? new Map(parent.providedVars_)
255      : new Map<string, ObservedPropertyAbstractPU<any>>();
256
257    this.localStoragebackStore_ = undefined;
258    stateMgmtConsole.log(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}}'`);
259    if (parent) {
260      // this View is not a top-level View
261      this.setCardId(parent.getCardId());
262      // Call below will set this.parent_ to parent as well
263      parent.addChild(this);
264    } else if (localStorage) {
265      this.localStorage_ = localStorage;
266      stateMgmtConsole.debug(`${this.debugInfo()}: constructor: Using LocalStorage instance provided via @Entry.`);
267    }
268
269    SubscriberManager.Add(this);
270    stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done`);
271  }
272
273  // globally unique id, this is different from compilerAssignedUniqueChildId!
274  id__(): number {
275    return this.id_;
276  }
277
278  updateId(elmtId: number): void {
279    this.id_ = elmtId;
280  }
281
282  // inform the subscribed property
283  // that the View and thereby all properties
284  // are about to be deleted
285  abstract aboutToBeDeleted(): void;
286
287  // super class will call this function from
288  // its aboutToBeDeleted implementation
289  protected aboutToBeDeletedInternal(): void {
290    stateMgmtConsole.debug(`${this.debugInfo()}: aboutToBeDeletedInternal`);
291
292    // tell UINodeRegisterProxy that all elmtIds under
293    // this ViewPU should be treated as already unregistered
294
295    stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `);
296
297    // purge the elementids owning by this viewpu from the updateFuncByElmtId and also the state variable dependent elementids
298    Array.from(this.updateFuncByElmtId.keys()).forEach((elemId: number) =>{
299      this.purgeDeleteElmtId(elemId);
300    })
301
302    if (this.hasRecycleManager()) {
303      this.getRecycleManager().purgeAllCachedRecycleNode();
304    }
305
306    // unregistration of ElementIDs
307    stateMgmtConsole.debug(`${this.debugInfo()}: onUnRegElementID`);
308
309    // it will unregister removed elementids from all the viewpu, equals purgeDeletedElmtIdsRecursively
310    this.purgeDeletedElmtIds();
311
312    stateMgmtConsole.debug(`${this.debugInfo()}: onUnRegElementID  - DONE`);
313
314    this.updateFuncByElmtId.clear();
315    this.watchedProps.clear();
316    this.providedVars_.clear();
317    this.ownStorageLinksProps_.clear();
318    if (this.parent_) {
319      this.parent_.removeChild(this);
320    }
321    this.localStoragebackStore_ = undefined;
322    this.isDeleting_ = true;
323  }
324
325  public purgeDeleteElmtId(rmElmtId : number ) : boolean {
326    stateMgmtConsole.debug(`${this.debugInfo} is purging the rmElmtId:${rmElmtId}`);
327    const result = this.updateFuncByElmtId.delete(rmElmtId);
328     if (result) {
329        this.purgeVariableDependenciesOnElmtIdOwnFunc(rmElmtId);
330        // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_  does not need to keep it
331        UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId);
332       }
333    return result;
334  }
335
336  public debugInfo() : string {
337    return `@Component '${this.constructor.name}'[${this.id__()}]`;
338  }
339
340  // dump info about known elmtIds to a string
341  // use function only for debug output and DFX.
342  public debugInfoRegisteredElmtIds() : string {
343    return this.updateFuncByElmtId.debugInfoRegisteredElmtIds();
344  }
345
346  // for given elmtIds look up their component name/type and format a string out of this info
347  // use function only for debug output and DFX.
348  public debugInfoElmtIds(elmtIds : Array<number>) : string {
349    let result : string = "";
350    let sepa : string ="";
351    elmtIds.forEach((elmtId: number) => {
352      result += `${sepa}${this.debugInfoElmtId(elmtId)}`;
353      sepa=", ";
354    });
355    return result;
356  }
357
358  public debugInfoElmtId(elmtId : number) : string {
359    return this.updateFuncByElmtId.debugInfoElmtId(elmtId);
360  }
361
362  public dumpStateVars() : void {
363    stateMgmtConsole.debug(`${this.debugInfo()}:  State variables:`);
364    Object.getOwnPropertyNames(this)
365      .filter((varName: string) => varName.startsWith("__"))
366      .forEach((varName) => {
367        const prop: any = Reflect.get(this, varName);
368        const observedProp = prop as ObservedPropertyAbstractPU<any>;
369        if ("debugInfoDecorator" in prop) {
370          stateMgmtConsole.debug(`${observedProp.debugInfoDecorator()} '${observedProp.info()}'[${observedProp.id__()}] ${observedProp.debugInfoSubscribers()}`);
371          stateMgmtConsole.debug(`   ... ${observedProp.debugInfoSyncPeers()}`);
372          stateMgmtConsole.debug(`   ... ${observedProp.debugInfoDependentElmtIds()}`);
373        }
374      });
375  }
376
377  /**
378 * ArkUI engine will call this function when the corresponding CustomNode's active status change.
379 * @param active true for active, false for inactive
380 */
381  public setActiveInternal(active: boolean): void {
382    if (this.isActive_ == active) {
383      stateMgmtConsole.debug(`${this.debugInfo()}: setActive ${active} with unchanged state - ignoring`);
384      return;
385    }
386    stateMgmtConsole.debug(`${this.debugInfo()}: setActive ${active ? ' inActive -> active' : 'active -> inActive'}`);
387    this.isActive_ = active;
388    if (this.isActive_) {
389      this.onActiveInternal()
390    } else {
391      this.onInactiveInternal();
392    }
393  }
394
395  private onActiveInternal(): void {
396    if (!this.isActive_) {
397      return;
398    }
399
400    stateMgmtConsole.debug(`${this.debugInfo()}: onActiveInternal`);
401    this.performDelayedUpdate();
402    for (const child of this.childrenWeakrefMap_.values()) {
403      const childViewPU: ViewPU | undefined = child.deref();
404      if (childViewPU) {
405        childViewPU.setActiveInternal(this.isActive_);
406      }
407    }
408  }
409
410
411  private onInactiveInternal(): void {
412    if (this.isActive_) {
413      return;
414    }
415
416    stateMgmtConsole.debug(`${this.debugInfo()}: onInactiveInternal`);
417    for (const storageProp of this.ownStorageLinksProps_) {
418      storageProp.enableDelayedNotification();
419    }
420
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  }
428
429  private setParent(parent: ViewPU) {
430    if (this.parent_ && parent) {
431      stateMgmtConsole.warn(`${this.debugInfo()}: setChild: changing parent to '${parent?.debugInfo()} (unsafe operation)`);
432    }
433    this.parent_ = parent;
434  }
435
436  /**
437   * add given child and set 'this' as its parent
438   * @param child child to add
439   * @returns returns false if child with given child's id already exists
440   *
441   * framework internal function
442   * Note: Use of WeakRef ensures child and parent do not generate a cycle dependency.
443   * The add. Set<ids> is required to reliably tell what children still exist.
444   */
445  public addChild(child: ViewPU): boolean {
446    if (this.childrenWeakrefMap_.has(child.id__())) {
447      stateMgmtConsole.warn(`${this.debugInfo()}: addChild '${child?.debugInfo()}' id already exists ${child.id__()}. Internal error!`);
448      return false;
449    }
450    this.childrenWeakrefMap_.set(child.id__(), new WeakRef(child));
451    child.setParent(this);
452    return true;
453  }
454
455  /**
456   * remove given child and remove 'this' as its parent
457   * @param child child to add
458   * @returns returns false if child with given child's id does not exist
459   */
460  public removeChild(child: ViewPU): boolean {
461    const hasBeenDeleted = this.childrenWeakrefMap_.delete(child.id__());
462    if (!hasBeenDeleted) {
463      stateMgmtConsole.warn(`${this.debugInfo()}: removeChild '${child?.debugInfo()}', child id ${child.id__()} not known. Internal error!`);
464    } else {
465      child.setParent(undefined);
466    }
467    return hasBeenDeleted;
468  }
469
470  /**
471   * Retrieve child by given id
472   * @param id
473   * @returns child if in map and weak ref can still be downreferenced
474   */
475  public getChildById(id: number) {
476    const childWeakRef = this.childrenWeakrefMap_.get(id);
477    return childWeakRef ? childWeakRef.deref() : undefined;
478  }
479
480  protected abstract purgeVariableDependenciesOnElmtId(removedElmtId: number);
481  protected abstract initialRender(): void;
482  protected abstract rerender(): void;
483  protected abstract updateRecycleElmtId(oldElmtId: number, newElmtId: number): void;
484  protected updateStateVars(params: {}) : void {
485    stateMgmtConsole.error(`${this.debugInfo()}: updateStateVars unimplemented. Pls upgrade to latest eDSL transpiler version. Application error.`)
486  }
487
488  protected initialRenderView(): void {
489    this.isRenderInProgress = true;
490    this.initialRender();
491    this.isRenderInProgress = false;
492  }
493
494  private UpdateElement(elmtId: number): void {
495    if (elmtId == this.id__()) {
496      // do not attempt to update itself.
497      // a @Prop can add a dependency of the ViewPU onto itself. Ignore it.
498      return;
499    }
500    // do not process an Element that has been marked to be deleted
501    const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
502    const updateFunc = entry ? entry.getUpdateFunc() : undefined;
503
504    if ((updateFunc == undefined) || (typeof updateFunc !== "function")) {
505      stateMgmtConsole.error(`${this.debugInfo()}: update function of elmtId ${elmtId} not found, internal error!`);
506    } else {
507      const componentName = entry.getComponentName();
508      stateMgmtConsole.debug(`${this.debugInfo()}: updateDirtyElements: re-render of ${componentName} elmtId ${elmtId} start ...`);
509      this.isRenderInProgress = true;
510      updateFunc(elmtId, /* isFirstRender */ false);
511      // continue in native JSView
512      // Finish the Update in JSView::JsFinishUpdateFunc
513      // this function appends no longer used elmtIds (as receded by VSP) to the given allRmElmtIds array
514      this.finishUpdateFunc(elmtId);
515      this.isRenderInProgress = false;
516      stateMgmtConsole.debug(`${this.debugInfo()}: updateDirtyElements: re-render of ${componentName} elmtId ${elmtId} - DONE`);
517    }
518  }
519
520  /**
521   * force a complete rerender / update by executing all update functions
522   * exec a regular rerender first
523   *
524   * @param deep recurse all children as well
525   *
526   * framework internal functions, apps must not call
527   */
528  public forceCompleteRerender(deep: boolean = false): void {
529    stateMgmtConsole.warn(`${this.debugInfo()}: forceCompleteRerender - start.`);
530
531    // see which elmtIds are managed by this View
532    // and clean up all book keeping for them
533    this.purgeDeletedElmtIds();
534
535    Array.from(this.updateFuncByElmtId.keys()).sort(ViewPU.compareNumber).forEach(elmtId => this.UpdateElement(elmtId));
536
537    if (deep) {
538      this.childrenWeakrefMap_.forEach((weakRefChild: WeakRef<ViewPU>) => {
539        const child = weakRefChild.deref();
540        if (child) {
541          (child as ViewPU).forceCompleteRerender(true);
542        }
543      });
544    }
545    stateMgmtConsole.warn(`${this.debugInfo()}: forceCompleteRerender - end`);
546  }
547
548  /**
549   * force a complete rerender / update on specific node by executing update function.
550   *
551   * @param elmtId which node needs to update.
552   *
553   * framework internal functions, apps must not call
554   */
555  public forceRerenderNode(elmtId: number): void {
556    // see which elmtIds are managed by this View
557    // and clean up all book keeping for them
558    this.purgeDeletedElmtIds();
559    this.UpdateElement(elmtId);
560
561    // remove elemtId from dirtDescendantElementIds.
562    this.dirtDescendantElementIds_.delete(elmtId);
563  }
564
565  public updateStateVarsOfChildByElmtId(elmtId, params: Object) : void {
566    stateMgmtConsole.debug(`${this.debugInfo()}: updateChildViewById(${elmtId}) - start`);
567
568    if (elmtId<0) {
569      stateMgmtConsole.warn(`${this.debugInfo()}: updateChildViewById(${elmtId}) - invalid elmtId - internal error!`);
570      return ;
571    }
572    let child : ViewPU = this.getChildById(elmtId);
573    if (!child) {
574      stateMgmtConsole.warn(`${this.debugInfo()}: updateChildViewById(${elmtId}) - no child with this elmtId - internal error!`);
575      return;
576    }
577    child.updateStateVars(params);
578    stateMgmtConsole.debug(`${this.debugInfo()}: updateChildViewById(${elmtId}) - end`);
579  }
580
581  // implements IMultiPropertiesChangeSubscriber
582  viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void {
583    stateMgmtTrace.scopedTrace(() => {
584      if (this.isRenderInProgress) {
585        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!`);
586      }
587
588      this.syncInstanceId();
589
590      if (dependentElmtIds.size && !this.isFirstRender()) {
591        if (!this.dirtDescendantElementIds_.size) {
592          // mark ComposedElement dirty when first elmtIds are added
593          // do not need to do this every time
594          this.markNeedUpdate();
595        }
596        stateMgmtConsole.debug(`${this.debugInfo()}: viewPropertyHasChanged property: elmtIds that need re-render due to state variable change: ${this.debugInfoElmtIds(Array.from(dependentElmtIds))} .`)
597        for (const elmtId of dependentElmtIds) {
598          this.dirtDescendantElementIds_.add(elmtId);
599        }
600        stateMgmtConsole.debug(`   ... updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
601      } else {
602        stateMgmtConsole.debug(`${this.debugInfo()}: viewPropertyHasChanged: state variable change adds no elmtIds for re-render`);
603        stateMgmtConsole.debug(`   ... unchanged full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`)
604      }
605
606      let cb = this.watchedProps.get(varName)
607      if (cb) {
608        stateMgmtConsole.debug(`   ... calling @Watch function`);
609        cb.call(this, varName);
610      }
611
612      this.restoreInstanceId();
613    }, "ViewPU.viewPropertyHasChanged", this.constructor.name, varName, dependentElmtIds.size);
614  }
615
616
617  private performDelayedUpdate(): void {
618    stateMgmtTrace.scopedTrace(() => {
619      stateMgmtConsole.debug(`${this.debugInfo()}: performDelayedUpdate start ...`);
620    this.syncInstanceId();
621
622    for (const storageProp of this.ownStorageLinksProps_) {
623      const changedElmtIds = storageProp.moveElmtIdsForDelayedUpdate();
624      if (changedElmtIds) {
625        const varName = storageProp.info();
626        if (changedElmtIds.size && !this.isFirstRender()) {
627          for (const elmtId of changedElmtIds) {
628            this.dirtDescendantElementIds_.add(elmtId);
629          }
630        }
631
632        stateMgmtConsole.debug(`${this.debugInfo()}: performDelayedUpdate: all elmtIds that need re-render [${Array.from(this.dirtDescendantElementIds_).toString()}].`)
633
634        const cb = this.watchedProps.get(varName)
635        if (cb) {
636          stateMgmtConsole.debug(`   ... calling @Watch function`);
637          cb.call(this, varName);
638        }
639      }
640    } // for all ownStorageLinksProps_
641    this.restoreInstanceId();
642
643    if (this.dirtDescendantElementIds_.size) {
644      this.markNeedUpdate();
645    }
646
647    }, "ViewPU.performDelayedUpdate", this.constructor.name);
648  }
649
650  /**
651   * Function to be called from the constructor of the sub component
652   * to register a @Watch varibale
653   * @param propStr name of the variable. Note from @Provide and @Consume this is
654   *      the variable name and not the alias!
655   * @param callback application defined member function of sub-class
656   */
657  protected declareWatch(propStr: string, callback: (propName: string) => void): void {
658    this.watchedProps.set(propStr, callback);
659  }
660
661  /**
662   * This View @Provide's a variable under given name
663   * Call this function from the constructor of the sub class
664   * @param providedPropName either the variable name or the alias defined as
665   *        decorator param
666   * @param store the backing store object for this variable (not the get/set variable!)
667   */
668  protected addProvidedVar<T>(providedPropName: string, store: ObservedPropertyAbstractPU<T>) {
669    if (this.providedVars_.has(providedPropName)) {
670      throw new ReferenceError(`${this.constructor.name}: duplicate @Provide property with name ${providedPropName}.
671      Property with this name is provided by one of the ancestor Views already.`);
672    }
673    this.providedVars_.set(providedPropName, store);
674  }
675
676  /**
677   * Method for the sub-class to call from its constructor for resolving
678   *       a @Consume variable and initializing its backing store
679   *       with the SyncedPropertyTwoWay<T> object created from the
680   *       @Provide variable's backing store.
681   * @param providedPropName the name of the @Provide'd variable.
682   *     This is either the @Consume decorator parameter, or variable name.
683   * @param consumeVarName the @Consume variable name (not the
684   *            @Consume decorator parameter)
685   * @returns initializing value of the @Consume backing store
686   */
687  protected initializeConsume<T>(providedPropName: string,
688    consumeVarName: string): ObservedPropertyAbstractPU<T> {
689    let providedVarStore : ObservedPropertyAbstractPU<any> = this.providedVars_.get(providedPropName);
690    if (providedVarStore === undefined) {
691      throw new ReferenceError(`${this.debugInfo()} missing @Provide property with name ${providedPropName}.
692          Fail to resolve @Consume(${providedPropName}).`);
693    }
694
695    const factory = <T>(source: ObservedPropertyAbstract<T>) => {
696      const result : ObservedPropertyAbstractPU<T> = ((source instanceof ObservedPropertySimple) || (source instanceof ObservedPropertySimplePU))
697          ? new SynchedPropertyObjectTwoWayPU<T>(source, this, consumeVarName)
698          : new SynchedPropertyObjectTwoWayPU<T>(source, this, consumeVarName);
699      stateMgmtConsole.error(`${this.debugInfo()}: The @Consume is instance of ${result.constructor.name}`);
700      return result;
701    };
702    return providedVarStore.createSync(factory) as  ObservedPropertyAbstractPU<T>;
703  }
704
705
706  /**
707   * given the elmtId of a child or child of child within this custom component
708   * remember this component needs a partial update
709   * @param elmtId
710   */
711  public markElemenDirtyById(elmtId: number): void {
712    // TODO ace-ets2bundle, framework, compilated apps need to update together
713    // this function will be removed after a short transiition periode
714    stateMgmtConsole.applicationError(`${this.debugInfo()}: markElemenDirtyById no longer supported.
715        Please update your ace-ets2bundle and recompile your application. Application error!`);
716  }
717
718  /**
719   * For each recorded dirty Element in this custom component
720   * run its update function
721   *
722   */
723  public updateDirtyElements() {
724    do {
725        stateMgmtConsole.debug(`${this.debugInfo()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber)}, starting ....`);
726
727        // see which elmtIds are managed by this View
728        // and clean up all book keeping for them
729        this.purgeDeletedElmtIds();
730
731        // process all elmtIds marked as needing update in ascending order.
732        // ascending order ensures parent nodes will be updated before their children
733        // prior cleanup ensure no already deleted Elements have their update func executed
734        Array.from(this.dirtDescendantElementIds_).sort(ViewPU.compareNumber).forEach(elmtId => {
735            this.UpdateElement(elmtId);
736            this.dirtDescendantElementIds_.delete(elmtId);
737        });
738
739        if (this.dirtDescendantElementIds_.size) {
740          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!`);
741        }
742    } while(this.dirtDescendantElementIds_.size);
743    stateMgmtConsole.debug(`${this.debugInfo()}: updateDirtyElements (re-render) - DONE, dump of ViewPU in next lines`);
744    this.dumpStateVars();
745  }
746
747  // request list of all (global) elmtIds of deleted UINodes and unregister from the all viewpus
748  // this function equals purgeDeletedElmtIdsRecursively because it does unregistration for all viewpus
749  protected purgeDeletedElmtIds(): void {
750    stateMgmtConsole.debug(`purgeDeletedElmtIds @Component '${this.constructor.name}' (id: ${this.id__()}) start ...`)
751    // request list of all (global) elmtIds of deleted UINodes that need to be unregistered
752    UINodeRegisterProxy.obtainDeletedElmtIds();
753    // unregister the removed elementids requested from the cpp side for all viewpus, it will make the first viewpu slower
754    // than before, but the rest viewpu will be faster
755    UINodeRegisterProxy.unregisterElmtIdsFromViewPUs();
756    stateMgmtConsole.debug(`purgeDeletedElmtIds @Component '${this.constructor.name}' (id: ${this.id__()}) end... `)
757  }
758
759
760  protected purgeVariableDependenciesOnElmtIdOwnFunc(elmtId: number): void {
761    this.ownObservedPropertiesStore_.forEach((stateVar: ObservedPropertyAbstractPU<any>) => {
762      stateVar.purgeDependencyOnElmtId(elmtId);
763    })
764  }
765
766  // executed on first render only
767  // kept for backward compatibility with old ace-ets2bundle
768  public observeComponentCreation(compilerAssignedUpdateFunc: UpdateFunc): void {
769    const updateFunc = (elmtId: number, isFirstRender: boolean) => {
770      stateMgmtConsole.error(`${this.debugInfo()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
771      compilerAssignedUpdateFunc(elmtId, isFirstRender);
772      stateMgmtConsole.error(`${this.debugInfo()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
773    }
774
775    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
776    // in observeComponentCreation function we do not get info about the component name, in
777    // observeComponentCreation2 we do.
778    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc } );
779    // add element id -> owningviewpu
780    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
781    updateFunc(elmtId, /* is first render */ true );
782    stateMgmtConsole.error(`${this.debugInfo()}: First render for elmtId ${elmtId} - DONE.`);
783  }
784
785  // executed on first render only
786  // added July 2023, replaces observeComponentCreation
787  // classObject is the ES6 class object , mandatory to specify even the class lacks the pop function.
788  // - prototype : Object is present for every ES6 class
789  // - pop : () => void, static function present for JSXXX classes such as Column, TapGesture, etc.
790  public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype : Object, pop?: () => void }): void {
791    const _componentName : string =  (classObject && ("name" in classObject)) ? Reflect.get(classObject, "name") as string : "unspecified UINode";
792    const _popFunc : () => void = (classObject && "pop" in classObject) ? classObject.pop! : () => {};
793    const updateFunc = (elmtId: number, isFirstRender: boolean) => {
794      stateMgmtConsole.debug(`${this.debugInfo()}: ${isFirstRender ? `First render` : `Re-render/update`} start ....`);
795      ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
796      compilerAssignedUpdateFunc(elmtId, isFirstRender);
797      if (!isFirstRender) {
798        _popFunc();
799      }
800      ViewStackProcessor.StopGetAccessRecording();
801      stateMgmtConsole.debug(`${this.debugInfo()}: ${isFirstRender ? `First render` : `Re-render/update`} - DONE ....`);
802    };
803
804    const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
805
806    this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject } );
807    // add element id -> owningviewpu
808    UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId,  new WeakRef(this));
809    updateFunc(elmtId, /* is first render */ true );
810    stateMgmtConsole.debug(`${this.debugInfo()} is initial rendering elmtId ${this.id__()}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`);
811  }
812
813
814  getOrCreateRecycleManager(): RecycleManager {
815    if (!this.recycleManager) {
816      this.recycleManager = new RecycleManager
817    }
818    return this.recycleManager;
819  }
820
821  getRecycleManager(): RecycleManager {
822    return this.recycleManager;
823  }
824
825  hasRecycleManager(): boolean {
826    return !(this.recycleManager === undefined);
827  }
828
829  initRecycleManager(): void {
830    if (this.recycleManager) {
831      stateMgmtConsole.error(`${this.debugInfo()}: init recycleManager multiple times. Internal error.`);
832      return;
833    }
834    this.recycleManager = new RecycleManager;
835  }
836
837  /**
838   * @function observeRecycleComponentCreation
839   * @description custom node recycle creation
840   * @param name custom node name
841   * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function
842   * @return void
843   */
844  public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void {
845    // convert recycle update func to update func
846    const compilerAssignedUpdateFunc: UpdateFunc = (element, isFirstRender) => {
847      recycleUpdateFunc(element, isFirstRender, undefined)
848    };
849    let node: ViewPU;
850    // if there is no suitable recycle node, run a normal creation function.
851    if (!this.hasRecycleManager() || !(node = this.getRecycleManager().popRecycleNode(name))) {
852      stateMgmtConsole.debug(`${this.constructor.name}[${this.id__()}]: cannot init node by recycle, crate new node`);
853      this.observeComponentCreation(compilerAssignedUpdateFunc);
854      return;
855    }
856
857    // if there is a suitable recycle node, run a recycle update function.
858    const newElmtId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
859    const oldElmtId: number = node.id__();
860    // store the current id and origin id, used for dirty element sort in {compareNumber}
861    recycleUpdateFunc(newElmtId, /* is first render */ true, node);
862    const oldEntry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(oldElmtId);
863    this.updateFuncByElmtId.delete(oldElmtId);
864    this.updateFuncByElmtId.set(newElmtId, {
865      updateFunc: compilerAssignedUpdateFunc,
866      classObject: oldEntry && oldEntry.getComponentClass(),
867      node: oldEntry && oldEntry.getNode()
868    });
869    node.updateId(newElmtId);
870    node.updateRecycleElmtId(oldElmtId, newElmtId);
871    SubscriberManager.UpdateRecycleElmtId(oldElmtId, newElmtId);
872  }
873
874  // add current JS object to it's parent recycle manager
875  public recycleSelf(name: string): void {
876    if (this.parent_ && !this.parent_.isDeleting_) {
877      this.parent_.getOrCreateRecycleManager().pushRecycleNode(name, this);
878    } else {
879      this.resetRecycleCustomNode();
880      stateMgmtConsole.error(`${this.constructor.name}[${this.id__()}]: recycleNode must have a parent`);
881    }
882  }
883
884  // performs the update on a branch within if() { branch } else if (..) { branch } else { branch }
885  public ifElseBranchUpdateFunction(branchId : number, branchfunc : () => void ) : void {
886    const oldBranchid : number = If.getBranchId();
887
888    if (branchId == oldBranchid) {
889      stateMgmtConsole.debug(`${this.debugInfo()}: ifElseBranchUpdateFunction: IfElse branch unchanged, no work to do.`);
890      return;
891    }
892
893    // branchid identifies uniquely the if .. <1> .. else if .<2>. else .<3>.branch
894    // ifElseNode stores the most recent branch, so we can compare
895    // removedChildElmtIds will be filled with the elmtIds of all childten and their children will be deleted in response to if .. else chnage
896    let removedChildElmtIds = new Array<number>();
897    If.branchId(branchId, removedChildElmtIds);
898
899    // purging these elmtIds from state mgmt will make sure no more update function on any deleted child wi;ll be executed
900    stateMgmtConsole.debug(`ViewPU ifElseBranchUpdateFunction: elmtIds need unregister after if/else branch switch: ${JSON.stringify(removedChildElmtIds)}`);
901    this.purgeDeletedElmtIds();
902
903    branchfunc();
904  }
905
906   /**
907    Partial updates for ForEach.
908    * @param elmtId ID of element.
909    * @param itemArray Array of items for use of itemGenFunc.
910    * @param itemGenFunc Item generation function to generate new elements. If index parameter is
911    *                    given set itemGenFuncUsesIndex to true.
912    * @param idGenFunc   ID generation function to generate unique ID for each element. If index parameter is
913    *                    given set idGenFuncUsesIndex to true.
914    * @param itemGenFuncUsesIndex itemGenFunc optional index parameter is given or not.
915    * @param idGenFuncUsesIndex idGenFunc optional index parameter is given or not.
916    */
917  public forEachUpdateFunction(elmtId : number,
918    itemArray: Array<any>,
919    itemGenFunc: (item: any, index?: number) => void,
920    idGenFunc?: (item: any, index?: number) => string,
921    itemGenFuncUsesIndex: boolean = false,
922    idGenFuncUsesIndex: boolean = false) : void {
923
924      stateMgmtConsole.debug(`${this.debugInfo()}: forEachUpdateFunction (ForEach re-render) start ...`);
925
926    if (itemArray === null || itemArray === undefined) {
927      stateMgmtConsole.applicationError(`${this.debugInfo()}: forEachUpdateFunction (ForEach re-render): input array is null or undefined error. Application error!`);
928      return;
929    }
930
931    if (itemGenFunc === null || itemGenFunc === undefined) {
932      stateMgmtConsole.applicationError(`${this.debugInfo()}: forEachUpdateFunction (ForEach re-render): Item generation function missing. Application error!`);
933      return;
934    }
935
936    if (idGenFunc === undefined) {
937      stateMgmtConsole.debug(`${this.debugInfo()}: forEachUpdateFunction: providing default id gen function `);
938      idGenFuncUsesIndex = true;
939      // catch possible error caused by Stringify and re-throw an Error with a meaningful (!) error message
940      idGenFunc = (item: any, index : number) => {
941        try {
942          return `${index}__${JSON.stringify(item)}`;
943        } catch(e) {
944          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!`)
945        }
946      }
947    }
948
949    let diffIndexArray = []; // New indexes compared to old one.
950    let newIdArray = [];
951    let idDuplicates = [];
952    const arr = itemArray; // just to trigger a 'get' onto the array
953
954    // ID gen is with index.
955    if (idGenFuncUsesIndex) {
956      // Create array of new ids.
957      arr.forEach((item, indx) => {
958        newIdArray.push(idGenFunc(item, indx));
959      });
960    }
961    else {
962      // Create array of new ids.
963      arr.forEach((item, index) => {
964        newIdArray.push(`${itemGenFuncUsesIndex ? index + '_':''}` + idGenFunc(item));
965      });
966    }
967
968    // Set new array on C++ side.
969    // C++ returns array of indexes of newly added array items.
970    // these are indexes in new child list.
971    ForEach.setIdArray(elmtId, newIdArray, diffIndexArray, idDuplicates);
972
973    // Its error if there are duplicate IDs.
974    if (idDuplicates.length > 0) {
975      idDuplicates.forEach((indx) => {
976        stateMgmtConsole.error(`Error: ${newIdArray[indx]} generated for ${indx}${indx < 4 ? indx == 2 ? "nd" : "rd" : "th"} array item ${arr[indx]}.`);
977      });
978      stateMgmtConsole.applicationError(`${this.debugInfo()}: Ids generated by the ForEach id gen function must be unique. Application error!`);
979    }
980
981    stateMgmtConsole.debug(`${this.debugInfo()}: forEachUpdateFunction: diff indexes ${JSON.stringify(diffIndexArray)} . `);
982
983    // Item gen is with index.
984    stateMgmtConsole.debug(`   ... item Gen ${itemGenFuncUsesIndex ? 'with' : "without"} index`);
985    // Create new elements if any.
986    diffIndexArray.forEach((indx) => {
987      ForEach.createNewChildStart(newIdArray[indx], this);
988      if (itemGenFuncUsesIndex) {
989        itemGenFunc(arr[indx], indx);
990      } else {
991        itemGenFunc(arr[indx]);
992      }
993      ForEach.createNewChildFinish(newIdArray[indx], this);
994    });
995    stateMgmtConsole.debug(`${this.debugInfo()}: forEachUpdateFunction (ForEach re-render) - DONE.`);
996  }
997
998  /**
999     * CreateStorageLink and CreateStorageLinkPU are used by the implementation of @StorageLink and
1000     * @LocalStotrageLink in full update and partial update solution respectively.
1001     * These are not part of the public AppStorage API , apps should not use.
1002     * @param storagePropName - key in LocalStorage
1003     * @param defaultValue - value to use when creating a new prop in the LocalStotage
1004     * @param owningView - the View/ViewPU owning the @StorageLink/@LocalStorageLink variable
1005     * @param viewVariableName -  @StorageLink/@LocalStorageLink variable name
1006     * @returns SynchedPropertySimple/ObjectTwoWay/PU
1007     */
1008  public createStorageLink<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
1009    const appStorageLink = AppStorage.__createSync<T>(storagePropName, defaultValue,
1010      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1011        ? undefined
1012        : (source instanceof ObservedPropertySimple)
1013          ? new SynchedPropertyObjectTwoWayPU<T>(source, this, viewVariableName)
1014          : new SynchedPropertyObjectTwoWayPU<T>(source, this, viewVariableName)
1015    ) as ObservedPropertyAbstractPU<T>;
1016    this.ownStorageLinksProps_.add(appStorageLink);
1017    return appStorageLink;
1018  }
1019
1020  public createStorageProp<T>(storagePropName: string, defaultValue: T, viewVariableName: string): ObservedPropertyAbstractPU<T> {
1021    const appStorageProp = AppStorage.__createSync<T>(storagePropName, defaultValue,
1022      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1023        ? undefined
1024        : (source instanceof ObservedPropertySimple)
1025          ? new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
1026          : new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
1027    ) as ObservedPropertyAbstractPU<T>;
1028    this.ownStorageLinksProps_.add(appStorageProp);
1029    return appStorageProp;
1030  }
1031
1032  public createLocalStorageLink<T>(storagePropName: string, defaultValue: T,
1033    viewVariableName: string): ObservedPropertyAbstractPU<T> {
1034      const localStorageLink =  this.localStorage_.__createSync<T>(storagePropName, defaultValue,
1035      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1036        ? undefined
1037        : (source instanceof ObservedPropertySimple)
1038          ? new SynchedPropertyObjectTwoWayPU<T>(source, this, viewVariableName)
1039          : new SynchedPropertyObjectTwoWayPU<T>(source, this, viewVariableName)
1040    ) as ObservedPropertyAbstractPU<T>;
1041    this.ownStorageLinksProps_.add(localStorageLink);
1042    return localStorageLink;
1043}
1044
1045  public createLocalStorageProp<T>(storagePropName: string, defaultValue: T,
1046    viewVariableName: string): ObservedPropertyAbstractPU<T> {
1047      const localStorageProp = this.localStorage_.__createSync<T>(storagePropName, defaultValue,
1048      <T>(source: ObservedPropertyAbstract<T>) => (source === undefined)
1049        ? undefined
1050        : (source instanceof ObservedPropertySimple)
1051          ? new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
1052          : new SynchedPropertyObjectOneWayPU<T>(source, this, viewVariableName)
1053    ) as ObservedPropertyAbstractPU<T>;
1054    this.ownStorageLinksProps_.add(localStorageProp);
1055    return localStorageProp;
1056  }
1057}
1058