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