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