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 79class 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): T { 150 const weakView = new WeakRef<ViewV2>(provideView); 151 const provideViewName = provideView.constructor?.name; 152 Reflect.defineProperty(consumeView, consumeVarName, { 153 get() { 154 let view = weakView.deref(); 155 stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} get`); 156 ObserveV2.getObserve().addRef(this, consumeVarName); 157 if (!view) { 158 const error = `${this.debugInfo__()}: get() on @Consumer ${consumeVarName}: providing @ComponentV2 with @Provider ${provideViewName} no longer exists. Application error.`; 159 stateMgmtConsole.error(error); 160 throw new Error(error); 161 } 162 return view[provideVarName]; 163 }, 164 set(val) { 165 let view = weakView.deref(); 166 // If the object has not been observed, you can directly assign a value to it. This improves performance. 167 stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} set`); 168 if (!view) { 169 const error = `${this.debugInfo__()}: set() on @Consumer ${consumeVarName}: providing @ComponentV2 with @Provider ${provideViewName} no longer exists. Application error.`; 170 stateMgmtConsole.error(error); 171 throw new Error(error); 172 } 173 174 if (val !== view[provideVarName]) { 175 stateMgmtConsole.propertyAccess(`@Consumer ${consumeVarName} valueChanged`); 176 view[provideVarName] = val; 177 if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance. 178 ObserveV2.getObserve().fireChange(this, consumeVarName); 179 } 180 } 181 }, 182 enumerable: true 183 }); 184 return provideView[provideVarName]; 185 } 186 187 public static defineConsumerWithoutProvider(consumeView: ViewV2, consumeVarName: string, consumerLocalVal: T): T { 188 stateMgmtConsole.debug(`defineConsumerWithoutProvider: ${consumeView.debugInfo__()} @Consumer ${consumeVarName} does not have @Provider counter part, will use local init value`); 189 190 const storeProp = ObserveV2.OB_PREFIX + consumeVarName; 191 consumeView[storeProp] = consumerLocalVal; // use local init value, also as backing store 192 Reflect.defineProperty(consumeView, consumeVarName, { 193 get() { 194 ObserveV2.getObserve().addRef(this, consumeVarName); 195 return ObserveV2.autoProxyObject(this, ObserveV2.OB_PREFIX + consumeVarName); 196 }, 197 set(val) { 198 if (val !== this[storeProp]) { 199 this[storeProp] = val; 200 if (this[ObserveV2.SYMBOL_REFS]) { // This condition can improve performance. 201 ObserveV2.getObserve().fireChange(this, consumeVarName); 202 } 203 } 204 }, 205 enumerable: true 206 }); 207 return consumeView[storeProp]; 208 } 209} 210 211/* 212 Internal decorator for @Trace without usingV2ObservedTrack call. 213 Real @Trace decorator function is in v2_decorators.ts 214*/ 215const Trace_Internal = (target: Object, propertyKey: string): void => { 216 return trackInternal(target, propertyKey); 217}; 218 219/* 220 Internal decorator for @ObservedV2 without usingV2ObservedTrack call. 221 Real @ObservedV2 decorator function is in v2_decorators.ts 222*/ 223function ObservedV2_Internal<T extends ConstructorV2>(BaseClass: T): T { 224 return observedV2Internal<T>(BaseClass); 225} 226 227/* 228 @ObservedV2 decorator function uses this in v2_decorators.ts 229*/ 230function observedV2Internal<T extends ConstructorV2>(BaseClass: T): T { 231 232 // prevent @Track inside @observed class 233 if (BaseClass.prototype && Reflect.has(BaseClass.prototype, TrackedObject.___IS_TRACKED_OPTIMISED)) { 234 const error = `'@observed class ${BaseClass?.name}': invalid use of V2 @Track decorator inside V3 @observed class. Need to fix class definition to use @track.`; 235 stateMgmtConsole.applicationError(error); 236 throw new Error(error); 237 } 238 239 if (BaseClass.prototype && !Reflect.has(BaseClass.prototype, ObserveV2.V2_DECO_META)) { 240 // not an error, suspicious of developer oversight 241 stateMgmtConsole.warn(`'@observed class ${BaseClass?.name}': no @track property inside. Is this intended? Check our application.`); 242 } 243 244 // Use ID_REFS only if number of observed attrs is significant 245 const attrList = Object.getOwnPropertyNames(BaseClass.prototype); 246 const count = attrList.filter(attr => attr.startsWith(ObserveV2.OB_PREFIX)).length; 247 if (count > 5) { 248 stateMgmtConsole.log(`'@observed class ${BaseClass?.name}' configured to use ID_REFS optimization`); 249 BaseClass.prototype[ObserveV2.ID_REFS] = {}; 250 } 251 const observedClass = class extends BaseClass { 252 constructor(...args) { 253 super(...args); 254 AsyncAddComputedV2.addComputed(this, BaseClass.name); 255 AsyncAddMonitorV2.addMonitor(this, BaseClass.name); 256 } 257 }; 258 Object.defineProperty(observedClass, 'name', { value: BaseClass.name }); 259 return observedClass; 260} 261