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