• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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
16/**
17 *
18 * This file includes only framework internal classes and functions
19 * non are part of SDK. Do not access from app.
20 *
21 * Implementation of @ComponentV2 is ViewV2
22 * When transpiling @ComponentV2, the transpiler generates a class that extends from ViewV2.
23 *
24 */
25
26abstract class ViewV2 extends PUV2ViewBase implements IView {
27
28    // Set of elmtIds that need re-render
29    protected dirtDescendantElementIds_: Set<number> = new Set<number>();
30
31    private monitorIdsDelayedUpdate: Set<number> = new Set();
32    private computedIdsDelayedUpdate: Set<number> = new Set();
33
34    constructor(parent: IView, elmtId: number = UINodeRegisterProxy.notRecordingDependencies, extraInfo: ExtraInfo = undefined) {
35        super(parent, elmtId, extraInfo);
36        this.setIsV2(true);
37        stateMgmtConsole.debug(`ViewV2 constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}'`);
38    }
39
40
41    /**
42     * The `freezeState` parameter determines whether this @ComponentV2 is allowed to freeze, when inactive
43     * Its called with value of the `freezeWhenInactive` parameter from the @ComponentV2 decorator,
44     * or it may be called with `undefined` depending on how the UI compiler works.
45     *
46     * @param freezeState Only the value `true` will be used to set the freeze state,
47     * otherwise it inherits from its parent instance if its freezeState is true
48     */
49    protected finalizeConstruction(freezeState?: boolean | undefined): void {
50
51        ObserveV2.getObserve().constructComputed(this, this.constructor.name);
52        ObserveV2.getObserve().constructMonitor(this, this.constructor.name);
53
54        // Always use ID_REFS in ViewV2
55        this[ObserveV2.ID_REFS] = {};
56
57        // set to true if freeze parameter set for this @ComponentV2 to true
58        // otherwise inherit from its parentComponent (if it exists).
59        this.isCompFreezeAllowed_ = freezeState || this.isCompFreezeAllowed_;
60        stateMgmtConsole.debug(`${this.debugInfo__()}: @ComponentV2 freezeWhenInactive state is set to ${this.isCompFreezeAllowed()}`);
61
62    }
63
64    public debugInfo__(): string {
65        return `@ComponentV2 '${this.constructor.name}'[${this.id__()}]`;
66    }
67
68
69    private get isViewV3(): boolean {
70        return true;
71    }
72
73    /**
74     * Virtual function implemented in ViewPU and ViewV2
75     * Unregisters and purges all child elements associated with the specified Element ID in ViewV2.
76     *
77     * @param rmElmtId - The Element ID to be purged and deleted
78     * @returns {boolean} - Returns `true` if the Element ID was successfully deleted, `false` otherwise.
79    */
80    public purgeDeleteElmtId(rmElmtId: number): boolean {
81        stateMgmtConsole.debug(`${this.debugInfo__()} purgeDeleteElmtId (V2) is purging the rmElmtId:${rmElmtId}`);
82        const result = this.updateFuncByElmtId.delete(rmElmtId);
83        if (result) {
84            const childOpt = this.getChildViewV2ForElmtId(rmElmtId);
85            if (childOpt) {
86                childOpt.setDeleting();
87                childOpt.setDeleteStatusRecursively();
88            }
89
90            // it means rmElmtId has finished all the unregistration from the js side, ElementIdToOwningViewPU_  does not need to keep it
91            UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(rmElmtId);
92        }
93
94        // Needed only for V2
95        ObserveV2.getObserve().clearBinding(rmElmtId);
96        return result;
97    }
98
99
100    // super class will call this function from
101    // its aboutToBeDeleted implementation
102    protected aboutToBeDeletedInternal(): void {
103        stateMgmtConsole.debug(`${this.debugInfo__()}: aboutToBeDeletedInternal`);
104        // if this isDeleting_ is true already, it may be set delete status recursively by its parent, so it is not necessary
105        // to set and resursively set its children any more
106        if (!this.isDeleting_) {
107            this.isDeleting_ = true;
108            this.setDeleteStatusRecursively();
109        }
110        // tell UINodeRegisterProxy that all elmtIds under
111        // this ViewV2 should be treated as already unregistered
112
113        stateMgmtConsole.debug(`${this.constructor.name}: aboutToBeDeletedInternal `);
114
115        // purge the elmtIds owned by this ViewV2 from the updateFuncByElmtId and also the state variable dependent elmtIds
116        Array.from(this.updateFuncByElmtId.keys()).forEach((elmtId: number) => {
117            // FIXME split View: enable delete  this purgeDeleteElmtId(elmtId);
118        });
119
120        // unregistration of ElementIDs
121        stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID`);
122
123        // it will unregister removed elementids from all the ViewV2, equals purgeDeletedElmtIdsRecursively
124        this.purgeDeletedElmtIds();
125
126        // unregisters its own id once its children are unregistered above
127        UINodeRegisterProxy.unregisterRemovedElmtsFromViewPUs([this.id__()]);
128
129        stateMgmtConsole.debug(`${this.debugInfo__()}: onUnRegElementID  - DONE`);
130
131        /* in case ViewPU is currently frozen
132           ViewPU inactiveComponents_ delete(`${this.constructor.name}[${this.id__()}]`);
133        */
134        MonitorV2.clearWatchesFromTarget(this);
135
136        this.updateFuncByElmtId.clear();
137        if (this.parent_) {
138            this.parent_.removeChild(this);
139        }
140    }
141
142    public initialRenderView(): void {
143        stateMgmtProfiler.begin(`ViewV2: initialRenderView`);
144        this.initialRender();
145        stateMgmtProfiler.end();
146    }
147
148    public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void {
149        if (this.isDeleting_) {
150            stateMgmtConsole.error(`@ComponentV2 ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `);
151            return;
152        }
153        const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode';
154        const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { };
155        const updateFunc = (elmtId: number, isFirstRender: boolean): void => {
156            this.syncInstanceId();
157            stateMgmtConsole.debug(`@ComponentV2 ${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - start ....`);
158
159            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
160            ObserveV2.getObserve().startRecordDependencies(this, elmtId);
161
162            compilerAssignedUpdateFunc(elmtId, isFirstRender);
163            if (!isFirstRender) {
164                _popFunc();
165            }
166
167            let node = this.getNodeById(elmtId);
168            if (node !== undefined) {
169                (node as ArkComponent).cleanStageValue();
170            }
171
172            ObserveV2.getObserve().stopRecordDependencies();
173            ViewStackProcessor.StopGetAccessRecording();
174
175            stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`}  ${_componentName}[${elmtId}] - DONE ....`);
176            this.restoreInstanceId();
177        };
178
179        const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
180        // needs to move set before updateFunc.
181        // make sure the key and object value exist since it will add node in attributeModifier during updateFunc.
182        this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject });
183        // add element id -> owning ViewV2
184        UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
185        try {
186            updateFunc(elmtId, /* is first render */ true);
187        } catch (error) {
188            // avoid the incompatible change that move set function before updateFunc.
189            this.updateFuncByElmtId.delete(elmtId);
190            UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
191            stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
192            throw error;
193        }
194        stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`);
195    }
196
197    /**
198   *
199   * @param paramVariableName
200   * @param @once paramVariableName
201   * @param is read only, therefore, init from parent needs to be done without
202   *        causing property setter() to be called
203   * @param newValue
204   */
205    protected initParam<Z>(paramVariableName: string, newValue: Z): void {
206        this.checkIsV1Proxy(paramVariableName, newValue);
207        VariableUtilV3.initParam<Z>(this, paramVariableName, newValue);
208    }
209    /**
210   *
211   * @param paramVariableName
212   * @param @once paramVariableName
213   * @param is read only, therefore, update from parent needs to be done without
214   *        causing property setter() to be called
215   * @param @once reject any update
216    * @param newValue
217   */
218    protected updateParam<Z>(paramVariableName: string, newValue: Z): void {
219        this.checkIsV1Proxy(paramVariableName, newValue);
220        VariableUtilV3.updateParam<Z>(this, paramVariableName, newValue);
221      }
222
223    private checkIsV1Proxy<Z>(paramVariableName: string, value: Z): void {
224        if (ObservedObject.IsObservedObject(value)) {
225            throw new Error(`Cannot assign the ComponentV1 value to the ComponentV2 for the property '${paramVariableName}'`);
226        }
227    }
228
229    /**
230   *  inform that UINode with given elmtId needs rerender
231   *  does NOT exec @Watch function.
232   *  only used on V3 code path from ObserveV2.fireChange.
233   *
234   * FIXME will still use in the future?
235   */
236    public uiNodeNeedUpdateV3(elmtId: number): void {
237        if (this.isFirstRender()) {
238            return;
239        }
240
241        stateMgmtProfiler.begin(`ViewV2.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`);
242
243        if (!this.isActive_) {
244            this.scheduleDelayedUpdate(elmtId);
245            return;
246        }
247
248        if (!this.dirtDescendantElementIds_.size) { //  && !this runReuse_) {
249            // mark ComposedElement dirty when first elmtIds are added
250            // do not need to do this every time
251            this.syncInstanceId();
252            this.markNeedUpdate();
253            this.restoreInstanceId();
254        }
255        this.dirtDescendantElementIds_.add(elmtId);
256        stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`);
257
258        stateMgmtProfiler.end();
259    }
260
261
262    /**
263 * For each recorded dirty Element in this custom component
264 * run its update function
265 *
266 */
267    public updateDirtyElements(): void {
268        stateMgmtProfiler.begin('ViewV2.updateDirtyElements');
269        do {
270            stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber)}, starting ....`);
271
272            // see which elmtIds are managed by this View
273            // and clean up all book keeping for them
274            this.purgeDeletedElmtIds();
275
276            // process all elmtIds marked as needing update in ascending order.
277            // ascending order ensures parent nodes will be updated before their children
278            // prior cleanup ensure no already deleted Elements have their update func executed
279            const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber);
280            // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added
281            // to newly created this.dirtDescendantElementIds_ Set
282            dirtElmtIdsFromRootNode.forEach(elmtId => {
283                this.UpdateElement(elmtId);
284                this.dirtDescendantElementIds_.delete(elmtId);
285            });
286
287            if (this.dirtDescendantElementIds_.size) {
288                stateMgmtConsole.applicationError(`${this.debugInfo__()}: New UINode objects added to update queue while re-render! - Likely caused by @Component state change during build phase, not allowed. Application error!`);
289            }
290        } while (this.dirtDescendantElementIds_.size);
291        stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE`);
292        stateMgmtProfiler.end();
293    }
294
295
296    public UpdateElement(elmtId: number): void {
297
298        if (this.isDeleting_) {
299            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement(${elmtId}) (V2) returns with NO UPDATE, this @ComponentV2 is under deletion!`);
300            return;
301        }
302
303        stateMgmtProfiler.begin('ViewV2.UpdateElement');
304        if (elmtId === this.id__()) {
305            // do not attempt to update itself
306            stateMgmtProfiler.end();
307            return;
308        }
309        // do not process an Element that has been marked to be deleted
310        const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
311        const updateFunc = entry ? entry.getUpdateFunc() : undefined;
312
313        if (typeof updateFunc !== 'function') {
314            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`);
315        } else {
316            const componentName = entry.getComponentName();
317            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} start ...`);
318            stateMgmtProfiler.begin('ViewV2.updateFunc');
319            try {
320                updateFunc(elmtId, /* isFirstRender */ false);
321            } catch (e) {
322                stateMgmtConsole.applicationError(`Exception caught in update function of ${componentName} for elmtId ${elmtId}`, e.toString());
323                throw e;
324            } finally {
325                stateMgmtProfiler.end();
326            }
327            stateMgmtProfiler.begin('ViewV2.finishUpdateFunc (native)');
328            this.finishUpdateFunc(elmtId);
329            stateMgmtProfiler.end();
330            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} - DONE`);
331        }
332        stateMgmtProfiler.end();
333    }
334
335    /**
336 * Retrieve child by given id
337 * @param id
338 * @returns child if child with this id exists and it is instance of ViewV2
339 */
340    public getViewV2ChildById(id: number): ViewV2 | undefined {
341        const childWeakRef = this.childrenWeakrefMap_.get(id);
342        const child = childWeakRef ? childWeakRef.deref() : undefined;
343        return (child && child instanceof ViewV2) ? child : undefined;
344    }
345
346    /**
347     * findViewPUInHierarchy function needed for @Component and @ComponentV2 mixed
348     * parent - child hierarchies. Not used by ViewV2
349     */
350    public findViewPUInHierarchy(id: number): ViewPU | undefined {
351        // this ViewV2 is not a ViewPU, continue searching amongst children
352        let retVal: ViewPU = undefined;
353        for (const [key, value] of this.childrenWeakrefMap_.entries()) {
354            retVal = value.deref().findViewPUInHierarchy(id);
355            if (retVal) {
356                break;
357            }
358        }
359        return retVal;
360    }
361
362    // WatchIds that needs to be fired later gets added to monitorIdsDelayedUpdate
363    // monitor fireChange will be triggered for all these watchIds once this view gets active
364    public addDelayedMonitorIds(watchId: number): void {
365        stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedMonitorIds called for watchId: ${watchId}`);
366        this.monitorIdsDelayedUpdate.add(watchId);
367    }
368
369    public addDelayedComputedIds(watchId: number): void {
370        stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedComputedIds called for watchId: ${watchId}`);
371        this.computedIdsDelayedUpdate.add(watchId);
372    }
373
374    public setActiveInternal(newState: boolean): void {
375        stateMgmtProfiler.begin('ViewV2.setActive');
376
377        if (!this.isCompFreezeAllowed()) {
378            stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive. Component freeze state is ${this.isCompFreezeAllowed()} - ignoring`);
379            stateMgmtProfiler.end();
380            return;
381        }
382
383        stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive ${newState ? ' inActive -> active' : 'active -> inActive'}`);
384        this.isActive_ = newState;
385        if (this.isActive_) {
386          this.onActiveInternal();
387        } else {
388          this.onInactiveInternal();
389        }
390        stateMgmtProfiler.end();
391    }
392
393    private onActiveInternal(): void {
394        if (!this.isActive_) {
395          return;
396        }
397
398        stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`);
399        this.performDelayedUpdate();
400
401        // Set 'isActive_' state for all descendant child Views
402        for (const child of this.childrenWeakrefMap_.values()) {
403          const childView: IView | undefined = child.deref();
404          if (childView) {
405            childView.setActiveInternal(this.isActive_);
406          }
407        }
408    }
409
410    private onInactiveInternal(): void {
411        if (this.isActive_) {
412          return;
413        }
414        stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`);
415
416        // Set 'isActive_' state for all descendant child Views
417        for (const child of this.childrenWeakrefMap_.values()) {
418          const childView: IView | undefined = child.deref();
419          if (childView) {
420            childView.setActiveInternal(this.isActive_);
421          }
422        }
423    }
424
425    private performDelayedUpdate(): void {
426        stateMgmtProfiler.begin('ViewV2: performDelayedUpdate');
427        if (this.computedIdsDelayedUpdate.size) {
428            // exec computed functions
429            ObserveV2.getObserve().updateDirtyComputedProps([...this.computedIdsDelayedUpdate]);
430        }
431        if (this.monitorIdsDelayedUpdate.size) {
432          // exec monitor functions
433          ObserveV2.getObserve().updateDirtyMonitors(this.monitorIdsDelayedUpdate);
434        }
435        if (this.elmtIdsDelayedUpdate.size) {
436          // update re-render of updated element ids once the view gets active
437          if (this.dirtDescendantElementIds_.size === 0) {
438            this.dirtDescendantElementIds_ = new Set(this.elmtIdsDelayedUpdate);
439          }
440          else {
441            this.elmtIdsDelayedUpdate.forEach((element) => {
442              this.dirtDescendantElementIds_.add(element);
443            });
444          }
445        }
446        this.markNeedUpdate();
447        this.elmtIdsDelayedUpdate.clear();
448        this.monitorIdsDelayedUpdate.clear();
449        this.computedIdsDelayedUpdate.clear();
450        stateMgmtProfiler.end();
451    }
452
453    /*
454      findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component:
455      if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU.
456      function needed for mixed @Component and @ComponentV2 parent child hierarchies.
457    */
458    public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined {
459        return this.getParent()?.findProvidePU(providedPropName);
460    }
461
462    get localStorage_(): LocalStorage {
463        // FIXME check this also works for root @ComponentV2
464        return (this.getParent()) ? this.getParent().localStorage_ : new LocalStorage({ /* empty */ });
465    }
466
467    /**
468     * @function observeRecycleComponentCreation
469     * @description custom node recycle creation not supported for V2. So a dummy function is implemented to report
470     * an error message
471     * @param name custom node name
472     * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function
473     * @return void
474     */
475    public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void {
476        stateMgmtConsole.error(`${this.debugInfo__()}: Recycle not supported for ComponentV2 instances`);
477    }
478
479    public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string {
480        let retVaL: string = `\n${'  '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`;
481        retVaL += `ViewV2 keeps no info about dirty elmtIds`;
482        if (recursive) {
483            this.childrenWeakrefMap_.forEach((value, key, map) => {
484                retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter);
485            });
486        }
487
488        if (recursive && depth === 0) {
489            retVaL += `\nTotal: ${counter.total}`;
490        }
491        return retVaL;
492    }
493
494
495    protected debugInfoStateVars(): string {
496        return ''; // TODO DFX, read out META
497    }
498
499    /**
500   * on first render create a new Instance of Repeat
501   * on re-render connect to existing instance
502   * @param arr
503   * @returns
504   */
505    public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => {
506        // factory is for future extensions, currently always return the same
507        const elmtId = ObserveV2.getCurrentRecordedId();
508        let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>;
509        if (!repeat) {
510            repeat = new __Repeat<I>(this, arr);
511            this.elmtId2Repeat_.set(elmtId, repeat);
512        } else {
513            repeat.updateArr(arr);
514        }
515        return repeat;
516    };
517}
518