• 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 *
22 * Helper class for handling V2 decorated variables
23 */
24class VariableUtilV3 {
25    /**
26       * setReadOnlyAttr - helper function used to update @param
27       * from parent @Component. Not allowed for @param @once .
28       * @param target  - the object, usually the ViewV2
29       * @param attrName - @param variable name
30       * @param newValue - update to new value
31       */
32    public static initParam<Z>(target: object, attrName: string, newValue: Z): void {
33      const meta = target[ObserveV2.V2_DECO_META]?.[attrName];
34      if (!meta || meta.deco !== '@param') {
35        const error = `Use initParam(${attrName}) only to init @param. Internal error!`;
36        stateMgmtConsole.error(error);
37        throw new Error(error);
38      }
39      // prevent update for @param @once
40      const storeProp = ObserveV2.OB_PREFIX + attrName;
41      stateMgmtConsole.propertyAccess(`initParam '@param ${attrName}' - setting backing store`);
42      target[storeProp] = newValue;
43      ObserveV2.getObserve().addRef(target, attrName);
44    }
45
46      /**
47       * setReadOnlyAttr - helper function used to update @param
48       * from parent @Component. Not allowed for @param @once .
49       * @param target  - the object, usually the ViewV2
50       * @param attrName - @param variable name
51       * @param newValue - update to new value
52       */
53    public static updateParam<Z>(target: object, attrName: string, newValue: Z): void {
54      // prevent update for @param @once
55      const meta = target[ObserveV2.V2_DECO_META]?.[attrName];
56      if (!meta || meta.deco !== '@param') {
57        const error = `Use updateParm(${attrName}) only to update @param. Internal error!`;
58        stateMgmtConsole.error(error);
59        throw new Error(error);
60      }
61
62      const storeProp = ObserveV2.OB_PREFIX + attrName;
63      // @observed class and @track attrName
64      if (newValue === target[storeProp]) {
65        stateMgmtConsole.propertyAccess(`updateParm '@param ${attrName}' unchanged. Doing nothing.`);
66        return;
67      }
68      if (meta.deco2 === '@once') {
69        // @param @once - init but no update
70        stateMgmtConsole.log(`updateParm: '@param @once ${attrName}' - Skip updating.`);
71      } else {
72        stateMgmtConsole.propertyAccess(`updateParm '@param ${attrName}' - updating backing store and fireChange.`);
73        target[storeProp] = newValue;
74        ObserveV2.getObserve().fireChange(target, attrName);
75      }
76    }
77  }
78
79  class ProviderConsumerUtilV2 {
80    private static readonly ALIAS_PREFIX = '___pc_alias_';
81
82    /**
83     *  meta added to the ViewV2
84     *  varName: { deco: '@Provider' | '@Consumer', aliasName: ..... }
85     *  prefix_@Provider_aliasName: {'varName': ..., 'aliasName': ...., 'deco': '@Provider' | '@Consumer'
86     */
87    private static metaAliasKey(aliasName: string, deco: '@Provider' | '@Consumer') : string {
88      return `${ProviderConsumerUtilV2.ALIAS_PREFIX}_${deco}_${aliasName}`;
89    }
90
91    /**
92     * Helper function to add meta data about @Provider and @Consumer decorators to ViewV2
93     * similar to @see addVariableDecoMeta, but adds the alias to allow search from @Consumer for @Provider counterpart
94     * @param proto prototype object of application class derived from ViewV2
95     * @param varName decorated variable
96     * @param deco '@state', '@event', etc (note '@model' gets transpiled in '@param' and '@event')
97     */
98    public static addProvideConsumeVariableDecoMeta(proto: Object, varName: string, aliasName: string, deco: '@Provider' | '@Consumer'): void {
99      // add decorator meta data to prototype
100      const meta = proto[ObserveV2.V2_DECO_META] ??= {};
101      // note: aliasName is the actual alias not the prefixed version
102      meta[varName] = { 'deco': deco, 'aliasName': aliasName };
103
104      // prefix to avoid name collisions with variable of same name as the alias!
105      const aliasProp = ProviderConsumerUtilV2.metaAliasKey(aliasName, deco);
106      meta[aliasProp] = { 'varName': varName, 'aliasName': aliasName, 'deco': deco };
107    }
108
109    /**
110    * find a @Provider'ed variable from its nearest ancestor ViewV2.
111    * @param searchingAliasName The key name to search for.
112    * @returns A tuple containing the ViewPU instance where the provider is found
113    * and the provider name
114    * If root @Component reached without finding, returns undefined.
115    */
116    public static findProvider(view: ViewV2, aliasName: string): [ViewV2, string] | undefined {
117      let checkView : IView | undefined = view?.getParent();
118      const searchingPrefixedAliasName = ProviderConsumerUtilV2.metaAliasKey(aliasName, '@Provider');
119      stateMgmtConsole.debug(`findProvider: Try to connect ${view.debugInfo__()} '@Consumer ${aliasName}' to @Provider counterpart....`);
120      while (checkView) {
121        const meta = checkView.constructor?.prototype[ObserveV2.V2_DECO_META];
122        if (checkView instanceof ViewV2 && meta && meta[searchingPrefixedAliasName]) {
123          const aliasMeta = meta[searchingPrefixedAliasName];
124          const providedVarName: string | undefined = (aliasMeta && (aliasMeta.deco === '@Provider') ? aliasMeta.varName : undefined);
125
126          if (providedVarName) {
127            stateMgmtConsole.debug(`findProvider: success: ${checkView.debugInfo__()} has matching @Provider('${aliasName}') ${providedVarName}`);
128            return [checkView, providedVarName];
129          }
130        }
131        checkView = checkView.getParent();
132      }; // while
133      stateMgmtConsole.warn(`findProvider: ${view.debugInfo__()} @Consumer('${aliasName}'), no matching @Provider found amongst ancestor @ComponentV2's!`);
134      return undefined;
135    }
136
137    /**
138    * Connects a consumer property of a view (`consumeView`) to a provider property of another view (`provideView`).
139    * This function establishes a link between the consumer and provider, allowing the consumer to access and update
140    * the provider's value directly. If the provider view is garbage collected, attempts to access the provider
141    * property will throw an error.
142    *
143    * @param consumeView - The view object that consumes data from the provider.
144    * @param consumeVarName - The name of the property in the consumer view that will be linked to the provider.
145    * @param provideView - The view object that provides the data to the consumer.
146    * @param provideVarName - The name of the property in the provider view that the consumer will access.
147    *
148    */
149    public static connectConsumer2Provider(consumeView: ViewV2, consumeVarName: string, provideView: ViewV2, provideVarName: string): any {
150      const weakView = new WeakRef<ViewV2>(provideView);
151      const provideViewName = provideView.constructor?.name;
152
153      Reflect.defineProperty(consumeView, consumeVarName, {
154        get() {
155          let view = weakView.deref();
156          stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} get`);
157          ObserveV2.getObserve().addRef(this, consumeVarName);
158          if (!view) {
159            const error = `${this.debugInfo__()}: get() on @Consumer ${consumeVarName}: providing @ComponentV2 with @Provider ${provideViewName} no longer exists. Application error.`;
160            stateMgmtConsole.error(error);
161            throw new Error(error);
162          }
163          return view[provideVarName];
164        },
165        set(val) {
166          let view = weakView.deref();
167          // If the object has not been observed, you can directly assign a value to it. This improves performance.
168          stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} set`);
169          if (!view) {
170            const error = `${this.debugInfo__()}: set() on @Consumer ${consumeVarName}: providing @ComponentV2 with @Provider ${provideViewName} no longer exists. Application error.`;
171            stateMgmtConsole.error(error);
172            throw new Error(error);
173          }
174
175          if (val !== view[provideVarName]) {
176            stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} valueChanged`);
177            view[provideVarName] = val;
178            if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance.
179              ObserveV2.getObserve().fireChange(this, consumeVarName);
180            }
181          }
182        },
183        enumerable: true
184      });
185      return provideView[provideVarName];
186    }
187
188    public static defineConsumerWithoutProvider(consumeView: ViewV2, consumeVarName: string, consumerLocalVal: any): any {
189      stateMgmtConsole.debug(`defineConsumerWithoutProvider: ${consumeView.debugInfo__()} @Consumer ${consumeVarName} does not have @Provider counter part, will use local init value`);
190
191      const storeProp = ObserveV2.OB_PREFIX + consumeVarName;
192      consumeView[storeProp] = consumerLocalVal; // use local init value, also as backing store
193      Reflect.defineProperty(consumeView, consumeVarName, {
194        get() {
195          ObserveV2.getObserve().addRef(this, consumeVarName);
196          return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + consumeVarName);
197        },
198        set(val) {
199          if (val !== this[storeProp]) {
200            this[storeProp] = val;
201            if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance.
202              ObserveV2.getObserve().fireChange(this, consumeVarName);
203            }
204          }
205        },
206        enumerable: true
207      });
208      return consumeView[storeProp];
209    }
210  }
211
212/*
213  Internal decorator for @Trace without usingV2ObservedTrack call.
214  Real @Trace decorator function is in v2_decorators.ts
215*/
216const Trace_Internal = (target: Object, propertyKey: string): void => {
217    return trackInternal(target, propertyKey);
218};
219
220/*
221  Internal decorator for @ObservedV2 without usingV2ObservedTrack call.
222  Real @ObservedV2 decorator function is in v2_decorators.ts
223*/
224function ObservedV2_Internal<T extends ConstructorV2>(BaseClass: T): T {
225    return observedV2Internal<T>(BaseClass);
226}
227
228/*
229  @ObservedV2 decorator function uses this in v2_decorators.ts
230*/
231function observedV2Internal<T extends ConstructorV2>(BaseClass: T): T {
232
233  // prevent @Track inside @observed class
234  if (BaseClass.prototype && Reflect.has(BaseClass.prototype, TrackedObject.___IS_TRACKED_OPTIMISED)) {
235    const error = `'@observed class ${BaseClass?.name}': invalid use of V2 @Track decorator inside V3 @observed class. Need to fix class definition to use @track.`;
236    stateMgmtConsole.applicationError(error);
237    throw new Error(error);
238  }
239
240  if (BaseClass.prototype && !Reflect.has(BaseClass.prototype, ObserveV2.V2_DECO_META)) {
241    // not an error, suspicious of developer oversight
242    stateMgmtConsole.warn(`'@observed class ${BaseClass?.name}': no @track property inside. Is this intended? Check our application.`);
243  }
244
245  // Use ID_REFS only if number of observed attrs is significant
246  const attrList = Object.getOwnPropertyNames(BaseClass.prototype);
247  const count = attrList.filter(attr => attr.startsWith(ObserveV2.OB_PREFIX)).length;
248  if (count > 5) {
249    stateMgmtConsole.log(`'@observed class ${BaseClass?.name}' configured to use ID_REFS optimization`);
250    BaseClass.prototype[ObserveV2.ID_REFS] = {};
251  }
252  const observedClass =  class extends BaseClass {
253    constructor(...args) {
254      super(...args);
255      AsyncAddComputedV2.addComputed(this, BaseClass.name);
256      AsyncAddMonitorV2.addMonitor(this, BaseClass.name);
257    }
258  };
259  Object.defineProperty(observedClass, 'name', { value: BaseClass.name });
260  return observedClass;
261}
262