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