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