1/* 2 * Copyright (c) 2023-2025 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/// <reference path="../../state_mgmt/src/lib/common/ifelse_native.d.ts" /> 16/// <reference path="../../state_mgmt/src/lib/puv2_common/puv2_viewstack_processor.d.ts" /> 17/// <reference path="./disposable.ts" /> 18class BuilderNode extends Disposable { 19 private _JSBuilderNode: JSBuilderNode; 20 // the name of "nodePtr_" is used in ace_engine/interfaces/native/node/native_node_napi.cpp. 21 private nodePtr_: NodePtr; 22 constructor(uiContext: UIContext, options: RenderOptions) { 23 super(); 24 let jsBuilderNode = new JSBuilderNode(uiContext, options); 25 this._JSBuilderNode = jsBuilderNode; 26 let id = Symbol('BuilderRootFrameNode'); 27 BuilderNodeFinalizationRegisterProxy.ElementIdToOwningBuilderNode_.set(id, jsBuilderNode); 28 BuilderNodeFinalizationRegisterProxy.register(this, { name: 'BuilderRootFrameNode', idOfNode: id }); 29 } 30 public update(params: Object) { 31 this._JSBuilderNode.update(params); 32 } 33 public build(builder: WrappedBuilder<Object[]>, params?: Object, options?: BuildOptions,): void { 34 this._JSBuilderNode.build(builder, params, options); 35 this.nodePtr_ = this._JSBuilderNode.getNodePtr(); 36 } 37 public getNodePtr(): NodePtr { 38 return this._JSBuilderNode.getValidNodePtr(); 39 } 40 public getFrameNode(): FrameNode { 41 return this._JSBuilderNode.getFrameNode(); 42 } 43 public getFrameNodeWithoutCheck(): FrameNode | null { 44 return this._JSBuilderNode.getFrameNodeWithoutCheck(); 45 } 46 public postTouchEvent(touchEvent: TouchEvent): boolean { 47 __JSScopeUtil__.syncInstanceId(this._JSBuilderNode.getInstanceId()); 48 let ret = this._JSBuilderNode.postTouchEvent(touchEvent); 49 __JSScopeUtil__.restoreInstanceId(); 50 return ret; 51 } 52 public postInputEvent(event: InputEventType): boolean { 53 __JSScopeUtil__.syncInstanceId(this._JSBuilderNode.getInstanceId()); 54 let ret = this._JSBuilderNode.postInputEvent(event); 55 __JSScopeUtil__.restoreInstanceId(); 56 return ret; 57 } 58 public dispose(): void { 59 super.dispose(); 60 this._JSBuilderNode.dispose(); 61 } 62 public isDisposed(): boolean { 63 return super.isDisposed() && (this._JSBuilderNode ? this._JSBuilderNode.isDisposed() : true); 64 } 65 public reuse(param?: Object): void { 66 this._JSBuilderNode.reuse(param); 67 } 68 public recycle(): void { 69 this._JSBuilderNode.recycle(); 70 } 71 public updateConfiguration(): void { 72 this._JSBuilderNode.updateConfiguration(); 73 } 74 public onReuseWithBindObject(param?: Object): void { 75 this._JSBuilderNode.onReuseWithBindObject(param); 76 } 77 public onRecycleWithBindObject(): void { 78 this._JSBuilderNode.onRecycleWithBindObject(); 79 } 80 public inheritFreezeOptions(enable: boolean): void { 81 this._JSBuilderNode.inheritFreezeOptions(enable); 82 } 83} 84 85class JSBuilderNode extends BaseNode implements IDisposable { 86 private params_: Object; 87 private uiContext_: UIContext; 88 private frameNode_: FrameNode; 89 private _nativeRef: NativeStrongRef; 90 private _supportNestingBuilder: boolean; 91 private _proxyObjectParam: Object; 92 private bindedViewOfBuilderNode:ViewPU; 93 private disposable_: Disposable; 94 private inheritFreeze: boolean; 95 private allowFreezeWhenInactive: boolean; 96 private parentallowFreeze: boolean; 97 private isFreeze: boolean; 98 public __parentViewOfBuildNode?: ViewBuildNodeBase; 99 private updateParams_: Object; 100 private activeCount_: number; 101 constructor(uiContext: UIContext, options?: RenderOptions) { 102 super(uiContext, options); 103 this.uiContext_ = uiContext; 104 this.updateFuncByElmtId = new UpdateFuncsByElmtId(); 105 this._supportNestingBuilder = false; 106 this.disposable_ = new Disposable(); 107 this.inheritFreeze = false; 108 this.allowFreezeWhenInactive = false; 109 this.parentallowFreeze = false; 110 this.isFreeze = false; 111 this.__parentViewOfBuildNode = undefined; 112 this.updateParams_ = null; 113 this.activeCount_ = 1; 114 } 115 public findProvidePU__(providePropName: string): ObservedPropertyAbstractPU<any> | undefined { 116 if (this.__enableBuilderNodeConsume__ && this.__parentViewOfBuildNode) { 117 return this.__parentViewOfBuildNode.findProvidePU__(providePropName); 118 } 119 return undefined; 120 } 121 public reuse(param: Object): void { 122 this.updateStart(); 123 try { 124 this.childrenWeakrefMap_.forEach((weakRefChild) => { 125 const child = weakRefChild.deref(); 126 if (child) { 127 if (child instanceof ViewPU) { 128 child.aboutToReuseInternal(param); 129 } 130 else { 131 // FIXME fix for mixed V2 - V3 Hierarchies 132 throw new Error('aboutToReuseInternal: Recycle not implemented for ViewV2, yet'); 133 } 134 } // if child 135 }); 136 } catch (err) { 137 this.updateEnd(); 138 throw err; 139 } 140 this.updateEnd(); 141 } 142 public recycle(): void { 143 this.childrenWeakrefMap_.forEach((weakRefChild) => { 144 const child = weakRefChild.deref(); 145 if (child) { 146 if (child instanceof ViewPU) { 147 child.aboutToRecycleInternal(); 148 } 149 else { 150 // FIXME fix for mixed V2 - V3 Hierarchies 151 throw new Error('aboutToRecycleInternal: Recycle not yet implemented for ViewV2'); 152 } 153 } // if child 154 }); 155 } 156 public onReuseWithBindObject(param?: Object): void { 157 __JSScopeUtil__.syncInstanceId(this.instanceId_); 158 super.onReuseWithBindObject(param); 159 __JSScopeUtil__.restoreInstanceId(); 160 } 161 public onRecycleWithBindObject(): void { 162 __JSScopeUtil__.syncInstanceId(this.instanceId_); 163 super.onRecycleWithBindObject(); 164 __JSScopeUtil__.restoreInstanceId(); 165 } 166 public inheritFreezeOptions(enable: boolean): void { 167 this.inheritFreeze = enable; 168 if (enable) { 169 this.setAllowFreezeWhenInactive(this.getParentAllowFreeze()); 170 } else { 171 this.setAllowFreezeWhenInactive(false); 172 } 173 } 174 public getInheritFreeze(): boolean { 175 return this.inheritFreeze; 176 } 177 public setAllowFreezeWhenInactive(enable: boolean): void { 178 this.allowFreezeWhenInactive = enable; 179 } 180 public getAllowFreezeWhenInactive(): boolean { 181 return this.allowFreezeWhenInactive; 182 } 183 public setParentAllowFreeze(enable: boolean): void { 184 this.parentallowFreeze = enable; 185 } 186 public getParentAllowFreeze(): boolean { 187 return this.parentallowFreeze; 188 } 189 public getIsFreeze(): boolean { 190 return this.isFreeze; 191 } 192 public getCardId(): number { 193 return -1; 194 } 195 private isObject(param: Object): boolean { 196 const typeName = Object.prototype.toString.call(param); 197 const objectName = `[object Object]`; 198 if (typeName === objectName) { 199 return true; 200 } else { 201 return false; 202 } 203 } 204 private buildWithNestingBuilder(builder: WrappedBuilder<Object[]>, supportLazyBuild: boolean): void { 205 if (this._supportNestingBuilder && this.isObject(this.params_)) { 206 this._proxyObjectParam = new Proxy(this.params_, { 207 set(target, property, val): boolean { 208 throw Error(`@Builder : Invalid attempt to set(write to) parameter '${property.toString()}' error!`); 209 }, 210 get: (target, property, receiver): Object => { return this.params_?.[property] } 211 }); 212 this.nodePtr_ = super.create(builder.builder?.bind(this.bindedViewOfBuilderNode ? this.bindedViewOfBuilderNode : this), 213 this._proxyObjectParam, this.updateNodeFromNative, this.updateConfiguration, supportLazyBuild); 214 } else { 215 this.nodePtr_ = super.create(builder.builder?.bind(this.bindedViewOfBuilderNode ? this.bindedViewOfBuilderNode : this), 216 this.params_, this.updateNodeFromNative, this.updateConfiguration, supportLazyBuild); 217 } 218 } 219 public build(builder: WrappedBuilder<Object[]>, params?: Object, options?: BuildOptions): void { 220 __JSScopeUtil__.syncInstanceId(this.instanceId_); 221 this._supportNestingBuilder = options?.nestingBuilderSupported ? options.nestingBuilderSupported : false; 222 const supportLazyBuild = options?.lazyBuildSupported ? options.lazyBuildSupported : false; 223 this.bindedViewOfBuilderNode = options?.bindedViewOfBuilderNode; 224 this.__enableBuilderNodeConsume__ = (options?.enableProvideConsumeCrossing)? (options?.enableProvideConsumeCrossing) : false; 225 this.params_ = params; 226 if (options?.localStorage instanceof LocalStorage) { 227 this.setShareLocalStorage(options.localStorage); 228 } 229 this.updateFuncByElmtId.clear(); 230 if(this.bindedViewOfBuilderNode){ 231 globalThis.__viewPuStack__?.push(this.bindedViewOfBuilderNode); 232 } 233 this.buildWithNestingBuilder(builder, supportLazyBuild); 234 if(this.bindedViewOfBuilderNode){ 235 globalThis.__viewPuStack__?.pop(); 236 } 237 this._nativeRef = getUINativeModule().nativeUtils.createNativeStrongRef(this.nodePtr_); 238 if (this.frameNode_ === undefined || this.frameNode_ === null) { 239 this.frameNode_ = new BuilderRootFrameNode(this.uiContext_); 240 } 241 this.frameNode_.setNodePtr(this._nativeRef, this.nodePtr_); 242 this.frameNode_.setRenderNode(this._nativeRef); 243 this.frameNode_.setBaseNode(this); 244 this.frameNode_.setBuilderNode(this); 245 let id = this.frameNode_.getUniqueId(); 246 if (this.id_ && this.id_ !== id) { 247 this.__parentViewOfBuildNode?.removeChildBuilderNode(this.id_); 248 } 249 this.id_ = id; 250 this.__parentViewOfBuildNode?.addChildBuilderNode(this); 251 FrameNodeFinalizationRegisterProxy.rootFrameNodeIdToBuilderNode_.set(this.frameNode_.getUniqueId(), new WeakRef(this.frameNode_)); 252 __JSScopeUtil__.restoreInstanceId(); 253 } 254 public update(param: Object) { 255 if (this.isFreeze) { 256 this.updateParams_ = param; 257 return; 258 } 259 __JSScopeUtil__.syncInstanceId(this.instanceId_); 260 this.updateStart(); 261 try { 262 this.purgeDeletedElmtIds(); 263 this.params_ = param; 264 Array.from(this.updateFuncByElmtId.keys()).sort((a: number, b: number): number => { 265 return (a < b) ? -1 : (a > b) ? 1 : 0; 266 }).forEach(elmtId => this.UpdateElement(elmtId)); 267 } catch (err) { 268 this.updateEnd(); 269 throw err; 270 } 271 this.updateEnd(); 272 __JSScopeUtil__.restoreInstanceId(); 273 } 274 public updateConfiguration(): void { 275 __JSScopeUtil__.syncInstanceId(this.instanceId_); 276 this.updateStart(); 277 try { 278 this.purgeDeletedElmtIds(); 279 Array.from(this.updateFuncByElmtId.keys()).sort((a: number, b: number): number => { 280 return (a < b) ? -1 : (a > b) ? 1 : 0; 281 }).forEach(elmtId => this.UpdateElement(elmtId)); 282 for (const child of this.childrenWeakrefMap_.values()) { 283 const childView = child.deref(); 284 if (childView) { 285 childView.forceCompleteRerender(true); 286 } 287 } 288 getUINativeModule().frameNode.updateConfiguration(this.getFrameNode()?.getNodePtr()); 289 } catch (err) { 290 this.updateEnd(); 291 throw err; 292 } 293 this.updateEnd(); 294 __JSScopeUtil__.restoreInstanceId(); 295 } 296 private UpdateElement(elmtId: number): void { 297 // do not process an Element that has been marked to be deleted 298 const obj: UpdateFunc | UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId); 299 const updateFunc = (typeof obj === 'object') ? obj.getUpdateFunc() : null; 300 if (typeof updateFunc === 'function') { 301 updateFunc(elmtId, /* isFirstRender */ false); 302 this.finishUpdateFunc(); 303 } 304 } 305 306 private isBuilderNodeActive(): boolean { 307 return this.activeCount_ > 0; 308 } 309 310 public setActiveInternal(active: boolean, isReuse: boolean = false): void { 311 stateMgmtProfiler.begin('BuilderNode.setActive'); 312 if (!isReuse) { 313 this.activeCount_ += active ? 1 : -1; 314 if (this.isBuilderNodeActive()) { 315 this.isFreeze = false; 316 } else { 317 this.isFreeze = this.allowFreezeWhenInactive; 318 } 319 if (this.isBuilderNodeActive() && this.updateParams_ !== null) { 320 this.update(this.updateParams_); 321 this.updateParams_ = null; 322 } 323 } 324 if (this.inheritFreeze) { 325 this.propagateToChildren(this.childrenWeakrefMap_, active, isReuse); 326 this.propagateToChildren(this.builderNodeWeakrefMap_, active, isReuse); 327 } 328 stateMgmtProfiler.end(); 329 } 330 331 public purgeDeleteElmtId(rmElmtId: number): boolean { 332 const result = this.updateFuncByElmtId.delete(rmElmtId); 333 if (result) { 334 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId); 335 } 336 return result; 337 } 338 339 public getFrameNode(): FrameNode | null { 340 if ( 341 this.frameNode_ !== undefined && 342 this.frameNode_ !== null && 343 this.frameNode_.getNodePtr() !== null 344 ) { 345 return this.frameNode_; 346 } 347 return null; 348 } 349 350 public getFrameNodeWithoutCheck(): FrameNode | null | undefined { 351 return this.frameNode_; 352 } 353 354 public observeComponentCreation(func: (arg0: number, arg1: boolean) => void) { 355 let elmId: number = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 356 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmId, new WeakRef(this)); 357 try { 358 func(elmId, true); 359 } catch (error) { 360 // avoid the incompatible change that move set function before updateFunc. 361 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmId); 362 throw error; 363 } 364 } 365 366 public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object; pop?: () => void }): void { 367 const _componentName: string = classObject && 'name' in classObject ? (Reflect.get(classObject, 'name') as string) : 'unspecified UINode'; 368 const _popFunc: () => void = 369 classObject && 'pop' in classObject ? classObject.pop! : () => { }; 370 const updateFunc = (elmtId: number, isFirstRender: boolean): void => { 371 __JSScopeUtil__.syncInstanceId(this.instanceId_); 372 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateEnter(_componentName, elmtId, isFirstRender, this); 373 ViewStackProcessor.StartGetAccessRecordingFor(elmtId); 374 // if V2 @Observed/@Track used anywhere in the app (there is no more fine grained criteria), 375 // enable V2 object deep observation 376 // FIXME: A @Component should only use PU or V2 state, but ReactNative dynamic viewer uses both. 377 if (ConfigureStateMgmt.instance.needsV2Observe()) { 378 // FIXME: like in V2 setting bindId_ in ObserveV2 does not work with 'stacked' 379 // update + initial render calls, like in if and ForEach case, convert to stack as well 380 ObserveV2.getObserve().startRecordDependencies(this, elmtId, true); 381 } 382 if (this._supportNestingBuilder) { 383 compilerAssignedUpdateFunc(elmtId, isFirstRender); 384 } else { 385 compilerAssignedUpdateFunc(elmtId, isFirstRender, this.params_); 386 } 387 if (!isFirstRender) { 388 _popFunc(); 389 } 390 if (ConfigureStateMgmt.instance.needsV2Observe()) { 391 ObserveV2.getObserve().stopRecordDependencies(); 392 } 393 ViewStackProcessor.StopGetAccessRecording(); 394 ViewBuildNodeBase.arkThemeScopeManager?.onComponentCreateExit(elmtId); 395 __JSScopeUtil__.restoreInstanceId(); 396 }; 397 398 const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent(); 399 // needs to move set before updateFunc. 400 // make sure the key and object value exist since it will add node in attributeModifier during updateFunc. 401 this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject }); 402 UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this)); 403 try { 404 updateFunc(elmtId, /* is first render */ true); 405 } catch (error) { 406 // avoid the incompatible change that move set function before updateFunc. 407 this.updateFuncByElmtId.delete(elmtId); 408 UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId); 409 throw error; 410 } 411 } 412 413 /** 414 Partial updates for ForEach. 415 * @param elmtId ID of element. 416 * @param itemArray Array of items for use of itemGenFunc. 417 * @param itemGenFunc Item generation function to generate new elements. If index parameter is 418 * given set itemGenFuncUsesIndex to true. 419 * @param idGenFunc ID generation function to generate unique ID for each element. If index parameter is 420 * given set idGenFuncUsesIndex to true. 421 * @param itemGenFuncUsesIndex itemGenFunc optional index parameter is given or not. 422 * @param idGenFuncUsesIndex idGenFunc optional index parameter is given or not. 423 */ 424 public forEachUpdateFunction( 425 elmtId: number, 426 itemArray: Array<any>, 427 itemGenFunc: (item: any, index?: number) => void, 428 idGenFunc?: (item: any, index?: number) => string, 429 itemGenFuncUsesIndex: boolean = false, 430 idGenFuncUsesIndex: boolean = false 431 ): void { 432 if (itemArray === null || itemArray === undefined) { 433 return; 434 } 435 436 if (itemGenFunc === null || itemGenFunc === undefined) { 437 return; 438 } 439 440 if (idGenFunc === undefined) { 441 idGenFuncUsesIndex = true; 442 // catch possible error caused by Stringify and re-throw an Error with a meaningful (!) error message 443 idGenFunc = (item: any, index: number): string => { 444 try { 445 return `${index}__${JSON.stringify(item)}`; 446 } catch (e) { 447 throw new Error( 448 ` 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!` 449 ); 450 } 451 }; 452 } 453 454 let diffIndexArray = []; // New indexes compared to old one. 455 let newIdArray = []; 456 let idDuplicates = []; 457 const arr = itemArray; // just to trigger a 'get' onto the array 458 459 // ID gen is with index. 460 if (idGenFuncUsesIndex) { 461 // Create array of new ids. 462 arr.forEach((item, indx) => { 463 newIdArray.push(idGenFunc(item, indx)); 464 }); 465 } else { 466 // Create array of new ids. 467 arr.forEach((item, index) => { 468 newIdArray.push( 469 `${itemGenFuncUsesIndex ? index + '_' : ''}` + idGenFunc(item) 470 ); 471 }); 472 } 473 // removedChildElmtIds will be filled with the elmtIds of all children and their children will be deleted in response to foreach change 474 let removedChildElmtIds = []; 475 // Set new array on C++ side. 476 // C++ returns array of indexes of newly added array items. 477 // these are indexes in new child list. 478 ForEach.setIdArray(elmtId, newIdArray, diffIndexArray, idDuplicates, removedChildElmtIds); 479 // Item gen is with index. 480 diffIndexArray.forEach((indx) => { 481 ForEach.createNewChildStart(newIdArray[indx], this); 482 if (itemGenFuncUsesIndex) { 483 itemGenFunc(arr[indx], indx); 484 } else { 485 itemGenFunc(arr[indx]); 486 } 487 ForEach.createNewChildFinish(newIdArray[indx], this); 488 }); 489 // un-registers the removed child elementIDs using proxy 490 UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs(removedChildElmtIds); 491 // purging these elmtIds from state mgmt will make sure no more update function on any deleted child will be executed 492 this.purgeDeletedElmtIds(); 493 } 494 public getNodePtr(): NodePtr { 495 return this.nodePtr_; 496 } 497 public getValidNodePtr(): NodePtr { 498 return this._nativeRef?.getNativeHandle(); 499 } 500 public dispose(): void { 501 if (this.nodePtr_) { 502 getUINativeModule().frameNode.fireArkUIObjectLifecycleCallback(new WeakRef(this), 'BuilderNode', this.getFrameNode()?.getNodeType() || 'BuilderNode', this.nodePtr_); 503 } 504 this.disposable_.dispose(); 505 this.frameNode_?.dispose(); 506 } 507 public isDisposed(): boolean { 508 return this.disposable_.isDisposed() && (this._nativeRef === undefined || this._nativeRef === null); 509 } 510 public disposeNode(): void { 511 super.disposeNode(); 512 this.nodePtr_ = null; 513 this._nativeRef = null; 514 this.frameNode_?.resetNodePtr(); 515 } 516 updateInstance(uiContext: UIContext): void { 517 this.uiContext_ = uiContext; 518 this.instanceId_ = uiContext.instanceId_; 519 if (this.frameNode_ !== undefined && this.frameNode_ !== null) { 520 this.frameNode_.updateInstance(uiContext); 521 } 522 } 523 524 private updateNodePtr(nodePtr: NodePtr) 525 { 526 if (nodePtr != this.nodePtr_) { 527 this.dispose(); 528 this.nodePtr_ = nodePtr; 529 this._nativeRef = getUINativeModule().nativeUtils.createNativeStrongRef(this.nodePtr_); 530 this.frameNode_.setNodePtr(this._nativeRef, this.nodePtr_); 531 } 532 } 533 534 private updateInstanceId(instanceId: number) 535 { 536 this.instanceId_ = instanceId; 537 } 538 539 protected updateNodeFromNative(instanceId: number, nodePtr: NodePtr) 540 { 541 this.updateNodePtr(nodePtr); 542 this.updateInstanceId(instanceId); 543 } 544 545 public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void { 546 throw new Error('custom component in @Builder used by BuilderNode does not support @Reusable'); 547 } 548 public ifElseBranchUpdateFunctionDirtyRetaken(): void {} 549 public forceCompleteRerender(deep: boolean): void {} 550 public forceRerenderNode(elmtId: number): void {} 551} 552