• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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