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