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