• 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        ComputedV2.clearComputedFromTarget(this);
136
137        this.updateFuncByElmtId.clear();
138        if (this.parent_) {
139            this.parent_.removeChild(this);
140        }
141    }
142
143    public initialRenderView(): void {
144        stateMgmtProfiler.begin(`ViewV2: initialRenderView`);
145        this.initialRender();
146        stateMgmtProfiler.end();
147    }
148
149    public observeComponentCreation2(compilerAssignedUpdateFunc: UpdateFunc, classObject: { prototype: Object, pop?: () => void }): void {
150        if (this.isDeleting_) {
151            stateMgmtConsole.error(`@ComponentV2 ${this.constructor.name} elmtId ${this.id__()} is already in process of destruction, will not execute observeComponentCreation2 `);
152            return;
153        }
154        const _componentName: string = (classObject && ('name' in classObject)) ? Reflect.get(classObject, 'name') as string : 'unspecified UINode';
155        const _popFunc: () => void = (classObject && 'pop' in classObject) ? classObject.pop! : (): void => { };
156        const updateFunc = (elmtId: number, isFirstRender: boolean): void => {
157            this.syncInstanceId();
158            stateMgmtConsole.debug(`@ComponentV2 ${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`} ${_componentName}[${elmtId}] - start ....`);
159
160            ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
161            ObserveV2.getObserve().startRecordDependencies(this, elmtId);
162
163            compilerAssignedUpdateFunc(elmtId, isFirstRender);
164            if (!isFirstRender) {
165                _popFunc();
166            }
167
168            let node = this.getNodeById(elmtId);
169            if (node !== undefined) {
170                (node as ArkComponent).cleanStageValue();
171            }
172
173            ObserveV2.getObserve().stopRecordDependencies();
174            ViewStackProcessor.StopGetAccessRecording();
175
176            stateMgmtConsole.debug(`${this.debugInfo__()}: ${isFirstRender ? `First render` : `Re-render/update`}  ${_componentName}[${elmtId}] - DONE ....`);
177            this.restoreInstanceId();
178        };
179
180        const elmtId = ViewStackProcessor.AllocateNewElmetIdForNextComponent();
181        // needs to move set before updateFunc.
182        // make sure the key and object value exist since it will add node in attributeModifier during updateFunc.
183        this.updateFuncByElmtId.set(elmtId, { updateFunc: updateFunc, classObject: classObject });
184        // add element id -> owning ViewV2
185        UINodeRegisterProxy.ElementIdToOwningViewPU_.set(elmtId, new WeakRef(this));
186        try {
187            updateFunc(elmtId, /* is first render */ true);
188        } catch (error) {
189            // avoid the incompatible change that move set function before updateFunc.
190            this.updateFuncByElmtId.delete(elmtId);
191            UINodeRegisterProxy.ElementIdToOwningViewPU_.delete(elmtId);
192            stateMgmtConsole.applicationError(`${this.debugInfo__()} has error in update func: ${(error as Error).message}`);
193            throw error;
194        }
195        stateMgmtConsole.debug(`${this.debugInfo__()} is initial rendering elmtId ${elmtId}, tag: ${_componentName}, and updateFuncByElmtId size :${this.updateFuncByElmtId.size}`);
196    }
197
198    /**
199   *
200   * @param paramVariableName
201   * @param @once paramVariableName
202   * @param is read only, therefore, init from parent needs to be done without
203   *        causing property setter() to be called
204   * @param newValue
205   */
206    protected initParam<Z>(paramVariableName: string, newValue: Z): void {
207        this.checkIsV1Proxy(paramVariableName, newValue);
208        VariableUtilV3.initParam<Z>(this, paramVariableName, newValue);
209    }
210    /**
211   *
212   * @param paramVariableName
213   * @param @once paramVariableName
214   * @param is read only, therefore, update from parent needs to be done without
215   *        causing property setter() to be called
216   * @param @once reject any update
217    * @param newValue
218   */
219    protected updateParam<Z>(paramVariableName: string, newValue: Z): void {
220        this.checkIsV1Proxy(paramVariableName, newValue);
221        VariableUtilV3.updateParam<Z>(this, paramVariableName, newValue);
222      }
223
224    private checkIsV1Proxy<Z>(paramVariableName: string, value: Z): void {
225        if (ObservedObject.IsObservedObject(value)) {
226            throw new Error(`Cannot assign the ComponentV1 value to the ComponentV2 for the property '${paramVariableName}'`);
227        }
228    }
229
230    /**
231   *  inform that UINode with given elmtId needs rerender
232   *  does NOT exec @Watch function.
233   *  only used on V3 code path from ObserveV2.fireChange.
234   *
235   * FIXME will still use in the future?
236   */
237    public uiNodeNeedUpdateV3(elmtId: number): void {
238        if (this.isFirstRender()) {
239            return;
240        }
241
242        stateMgmtProfiler.begin(`ViewV2.uiNodeNeedUpdate ${this.debugInfoElmtId(elmtId)}`);
243
244        if (!this.isActive_) {
245            this.scheduleDelayedUpdate(elmtId);
246            return;
247        }
248
249        if (!this.dirtDescendantElementIds_.size) { //  && !this runReuse_) {
250            // mark ComposedElement dirty when first elmtIds are added
251            // do not need to do this every time
252            this.syncInstanceId();
253            this.markNeedUpdate();
254            this.restoreInstanceId();
255        }
256        this.dirtDescendantElementIds_.add(elmtId);
257        stateMgmtConsole.debug(`${this.debugInfo__()}: uiNodeNeedUpdate: updated full list of elmtIds that need re-render [${this.debugInfoElmtIds(Array.from(this.dirtDescendantElementIds_))}].`);
258
259        stateMgmtProfiler.end();
260    }
261
262
263    /**
264 * For each recorded dirty Element in this custom component
265 * run its update function
266 *
267 */
268    public updateDirtyElements(): void {
269        stateMgmtProfiler.begin('ViewV2.updateDirtyElements');
270        do {
271            stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render): sorted dirty elmtIds: ${Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber)}, starting ....`);
272
273            // see which elmtIds are managed by this View
274            // and clean up all book keeping for them
275            this.purgeDeletedElmtIds();
276
277            // process all elmtIds marked as needing update in ascending order.
278            // ascending order ensures parent nodes will be updated before their children
279            // prior cleanup ensure no already deleted Elements have their update func executed
280            const dirtElmtIdsFromRootNode = Array.from(this.dirtDescendantElementIds_).sort(ViewV2.compareNumber);
281            // if state changed during exec update lambda inside UpdateElement, then the dirty elmtIds will be added
282            // to newly created this.dirtDescendantElementIds_ Set
283            dirtElmtIdsFromRootNode.forEach(elmtId => {
284                this.UpdateElement(elmtId);
285                this.dirtDescendantElementIds_.delete(elmtId);
286            });
287
288            if (this.dirtDescendantElementIds_.size) {
289                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!`);
290            }
291        } while (this.dirtDescendantElementIds_.size);
292        stateMgmtConsole.debug(`${this.debugInfo__()}: updateDirtyElements (re-render) - DONE`);
293        stateMgmtProfiler.end();
294    }
295
296
297    public UpdateElement(elmtId: number): void {
298
299        if (this.isDeleting_) {
300            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement(${elmtId}) (V2) returns with NO UPDATE, this @ComponentV2 is under deletion!`);
301            return;
302        }
303
304        stateMgmtProfiler.begin('ViewV2.UpdateElement');
305        if (elmtId === this.id__()) {
306            // do not attempt to update itself
307            stateMgmtProfiler.end();
308            return;
309        }
310        // do not process an Element that has been marked to be deleted
311        const entry: UpdateFuncRecord | undefined = this.updateFuncByElmtId.get(elmtId);
312        const updateFunc = entry ? entry.getUpdateFunc() : undefined;
313
314        if (typeof updateFunc !== 'function') {
315            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: update function of elmtId ${elmtId} not found, internal error!`);
316        } else {
317            const componentName = entry.getComponentName();
318            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} start ...`);
319            stateMgmtProfiler.begin('ViewV2.updateFunc');
320            try {
321                updateFunc(elmtId, /* isFirstRender */ false);
322            } catch (e) {
323                stateMgmtConsole.applicationError(`Exception caught in update function of ${componentName} for elmtId ${elmtId}`, e.toString());
324                throw e;
325            } finally {
326                stateMgmtProfiler.end();
327            }
328            stateMgmtProfiler.begin('ViewV2.finishUpdateFunc (native)');
329            this.finishUpdateFunc(elmtId);
330            stateMgmtProfiler.end();
331            stateMgmtConsole.debug(`${this.debugInfo__()}: UpdateElement: re-render of ${componentName} elmtId ${elmtId} - DONE`);
332        }
333        stateMgmtProfiler.end();
334    }
335
336    /**
337 * Retrieve child by given id
338 * @param id
339 * @returns child if child with this id exists and it is instance of ViewV2
340 */
341    public getViewV2ChildById(id: number): ViewV2 | undefined {
342        const childWeakRef = this.childrenWeakrefMap_.get(id);
343        const child = childWeakRef ? childWeakRef.deref() : undefined;
344        return (child && child instanceof ViewV2) ? child : undefined;
345    }
346
347    /**
348     * findViewPUInHierarchy function needed for @Component and @ComponentV2 mixed
349     * parent - child hierarchies. Not used by ViewV2
350     */
351    public findViewPUInHierarchy(id: number): ViewPU | undefined {
352        // this ViewV2 is not a ViewPU, continue searching amongst children
353        let retVal: ViewPU = undefined;
354        for (const [key, value] of this.childrenWeakrefMap_.entries()) {
355            retVal = value.deref().findViewPUInHierarchy(id);
356            if (retVal) {
357                break;
358            }
359        }
360        return retVal;
361    }
362
363    // WatchIds that needs to be fired later gets added to monitorIdsDelayedUpdate
364    // monitor fireChange will be triggered for all these watchIds once this view gets active
365    public addDelayedMonitorIds(watchId: number): void {
366        stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedMonitorIds called for watchId: ${watchId}`);
367        this.monitorIdsDelayedUpdate.add(watchId);
368    }
369
370    public addDelayedComputedIds(watchId: number): void {
371        stateMgmtConsole.debug(`${this.debugInfo__()} addDelayedComputedIds called for watchId: ${watchId}`);
372        this.computedIdsDelayedUpdate.add(watchId);
373    }
374
375    public setActiveInternal(newState: boolean): void {
376        stateMgmtProfiler.begin('ViewV2.setActive');
377
378        if (!this.isCompFreezeAllowed()) {
379            stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive. Component freeze state is ${this.isCompFreezeAllowed()} - ignoring`);
380            stateMgmtProfiler.end();
381            return;
382        }
383
384        stateMgmtConsole.debug(`${this.debugInfo__()}: ViewV2.setActive ${newState ? ' inActive -> active' : 'active -> inActive'}`);
385        this.isActive_ = newState;
386        if (this.isActive_) {
387          this.onActiveInternal();
388        } else {
389          this.onInactiveInternal();
390        }
391        stateMgmtProfiler.end();
392    }
393
394    private onActiveInternal(): void {
395        if (!this.isActive_) {
396          return;
397        }
398
399        stateMgmtConsole.debug(`${this.debugInfo__()}: onActiveInternal`);
400        this.performDelayedUpdate();
401
402        // Set 'isActive_' state for all descendant child Views
403        for (const child of this.childrenWeakrefMap_.values()) {
404          const childView: IView | undefined = child.deref();
405          if (childView) {
406            childView.setActiveInternal(this.isActive_);
407          }
408        }
409    }
410
411    private onInactiveInternal(): void {
412        if (this.isActive_) {
413          return;
414        }
415        stateMgmtConsole.debug(`${this.debugInfo__()}: onInactiveInternal`);
416
417        // Set 'isActive_' state for all descendant child Views
418        for (const child of this.childrenWeakrefMap_.values()) {
419          const childView: IView | undefined = child.deref();
420          if (childView) {
421            childView.setActiveInternal(this.isActive_);
422          }
423        }
424    }
425
426    private performDelayedUpdate(): void {
427        stateMgmtProfiler.begin('ViewV2: performDelayedUpdate');
428        if (this.computedIdsDelayedUpdate.size) {
429            // exec computed functions
430            ObserveV2.getObserve().updateDirtyComputedProps([...this.computedIdsDelayedUpdate]);
431        }
432        if (this.monitorIdsDelayedUpdate.size) {
433          // exec monitor functions
434          ObserveV2.getObserve().updateDirtyMonitors(this.monitorIdsDelayedUpdate);
435        }
436        if (this.elmtIdsDelayedUpdate.size) {
437          // update re-render of updated element ids once the view gets active
438          if (this.dirtDescendantElementIds_.size === 0) {
439            this.dirtDescendantElementIds_ = new Set(this.elmtIdsDelayedUpdate);
440          }
441          else {
442            this.elmtIdsDelayedUpdate.forEach((element) => {
443              this.dirtDescendantElementIds_.add(element);
444            });
445          }
446        }
447        this.markNeedUpdate();
448        this.elmtIdsDelayedUpdate.clear();
449        this.monitorIdsDelayedUpdate.clear();
450        this.computedIdsDelayedUpdate.clear();
451        stateMgmtProfiler.end();
452    }
453
454    /*
455      findProvidePU finds @Provided property recursively by traversing ViewPU's towards that of the UI tree root @Component:
456      if 'this' ViewPU has a @Provide('providedPropName') return it, otherwise ask from its parent ViewPU.
457      function needed for mixed @Component and @ComponentV2 parent child hierarchies.
458    */
459    public findProvidePU(providedPropName: string): ObservedPropertyAbstractPU<any> | undefined {
460        return this.getParent()?.findProvidePU(providedPropName);
461    }
462
463    get localStorage_(): LocalStorage {
464        // FIXME check this also works for root @ComponentV2
465        return (this.getParent()) ? this.getParent().localStorage_ : new LocalStorage({ /* empty */ });
466    }
467
468    /**
469     * @function observeRecycleComponentCreation
470     * @description custom node recycle creation not supported for V2. So a dummy function is implemented to report
471     * an error message
472     * @param name custom node name
473     * @param recycleUpdateFunc custom node recycle update which can be converted to a normal update function
474     * @return void
475     */
476    public observeRecycleComponentCreation(name: string, recycleUpdateFunc: RecycleUpdateFunc): void {
477        stateMgmtConsole.error(`${this.debugInfo__()}: Recycle not supported for ComponentV2 instances`);
478    }
479
480    public debugInfoDirtDescendantElementIdsInternal(depth: number = 0, recursive: boolean = false, counter: ProfileRecursionCounter): string {
481        let retVaL: string = `\n${'  '.repeat(depth)}|--${this.constructor.name}[${this.id__()}]: {`;
482        retVaL += `ViewV2 keeps no info about dirty elmtIds`;
483        if (recursive) {
484            this.childrenWeakrefMap_.forEach((value, key, map) => {
485                retVaL += value.deref()?.debugInfoDirtDescendantElementIdsInternal(depth + 1, recursive, counter);
486            });
487        }
488
489        if (recursive && depth === 0) {
490            retVaL += `\nTotal: ${counter.total}`;
491        }
492        return retVaL;
493    }
494
495
496    protected debugInfoStateVars(): string {
497        return ''; // TODO DFX, read out META
498    }
499
500    /**
501   * on first render create a new Instance of Repeat
502   * on re-render connect to existing instance
503   * @param arr
504   * @returns
505   */
506    public __mkRepeatAPI: <I>(arr: Array<I>) => RepeatAPI<I> = <I>(arr: Array<I>): RepeatAPI<I> => {
507        // factory is for future extensions, currently always return the same
508        const elmtId = ObserveV2.getCurrentRecordedId();
509        let repeat = this.elmtId2Repeat_.get(elmtId) as __Repeat<I>;
510        if (!repeat) {
511            repeat = new __Repeat<I>(this, arr);
512            this.elmtId2Repeat_.set(elmtId, repeat);
513        } else {
514            repeat.updateArr(arr);
515        }
516        return repeat;
517    };
518}
519