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