1/* 2 * Copyright (c) 2022 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 * SynchedPropertyObjectOneWayPU 18 * implementation of @Prop decorated variables of type class object 19 * 20 * all definitions in this file are framework internal 21 * 22 */ 23 24/** 25 * Initialisation scenarios: 26 * ------------------------- 27 * 28 * 1 - no local initialization, source provided (its ObservedObject value) 29 * wrap the ObservedObject into an ObservedPropertyObjectPU 30 * deep copy the ObservedObject into localCopyObservedObject_ 31 * 32 * 2 - local initialization, no source provided 33 * app transpiled code calls set 34 * leave source_ undefined 35 * no deep copy needed, but provided local init might need wrapping inside an ObservedObject to set to 36 * localCopyObservedObject_ 37 * 38 * 3 local initialization, source provided (its ObservedObject value) 39 * current app transpiled code is not optional 40 * sets source in constructor, as in case 1 41 * calls set() to set the source value, but this will not deepcopy 42 * 43 * Update scenarios: 44 * ----------------- 45 * 46 * 1- assignment of a new Object value: this.aProp = new ClassA() 47 * rhs can be ObservedObject because of @Observed decoration or now 48 * notifyPropertyHasChangedPU 49 * 50 * 2- local ObservedObject member property change 51 * objectPropertyHasChangedPU called, eventSource is the ObservedObject stored in localCopyObservedObject_ 52 * no need to copy, notifyPropertyHasChangedPU 53 * 54 * 3- Rerender of the custom component triggered from the parent 55 * reset() is called (code generated by the transpiler), set the value of source_ , if that causes a change will call syncPeerHasChanged 56 * syncPeerHasChanged need to deep copy the ObservedObject from source to localCopyObservedObject_ 57 * notifyPropertyHasChangedPU 58 * 59 * 4- source_ ObservedObject member property change 60 * objectPropertyHasChangedPU called, eventSource is the ObservedObject stored source_.getUnmonitored 61 * notifyPropertyHasChangedPU 62 */ 63 64 65class SynchedPropertyOneWayPU<C> extends ObservedPropertyAbstractPU<C> 66 implements PeerChangeEventReceiverPU<C>, ObservedObjectEventsPUReceiver<C> { 67 68 // the locally modified ObservedObject 69 private localCopyObservedObject_: C; 70 71 // reference to the source variable in parent component 72 private source_: ObservedPropertyAbstract<C>; 73 // true for @Prop code path, 74 // false for @(Local)StorageProp 75 private sourceIsOwnObject : boolean; 76 77 constructor(source: ObservedPropertyAbstract<C> | C, 78 owningChildView: IPropertySubscriber, 79 thisPropertyName: PropertyInfo) { 80 super(owningChildView, thisPropertyName); 81 82 if (source && (typeof (source) === "object") && ("subscribeMe" in source)) { 83 // code path for @(Local)StorageProp, the source is a ObservedPropertyObject<C> in a LocalStorage) 84 this.source_ = source; 85 this.sourceIsOwnObject = false; 86 87 // subscribe to receive value change updates from LocalStorage source property 88 this.source_.addSubscriber(this); 89 } else { 90 const sourceValue = source as C; 91 if (this.checkIsSupportedValue(sourceValue)) { 92 // code path for 93 // 1- source is of same type C in parent, source is its value, not the backing store ObservedPropertyObject 94 // 2- nested Object/Array inside observed another object/array in parent, source is its value 95 if (typeof sourceValue == "object" && !((sourceValue instanceof SubscribableAbstract) || ObservedObject.IsObservedObject(sourceValue))) { 96 stateMgmtConsole.applicationError(`${this.debugInfo()}: Provided source object's class is not instance of SubscribableAbstract, 97 it also lacks @Observed class decorator. Object property changes will not be observed. Application error!`); 98 } 99 stateMgmtConsole.debug(`${this.debugInfo()}: constructor: wrapping source in a new ObservedPropertyObjectPU`); 100 this.source_ = new ObservedPropertyObjectPU<C>(sourceValue, this, this.getPropSourceObservedPropertyFakeName()); 101 this.sourceIsOwnObject = true; 102 } 103 } 104 105 if (this.source_ != undefined) { 106 this.resetLocalValue(this.source_.get(), /* needCopyObject */ true); 107 } 108 stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done!`); 109 } 110 111 112 /* 113 like a destructor, need to call this before deleting 114 the property. 115 */ 116 aboutToBeDeleted() { 117 if (this.source_) { 118 this.source_.removeSubscriber(this); 119 if (this.sourceIsOwnObject == true && this.source_.numberOfSubscrbers()==0){ 120 stateMgmtConsole.debug(`${this.debugInfo()}: aboutToBeDeleted. owning source_ ObservedPropertySimplePU, calling its aboutToBeDeleted`); 121 this.source_.aboutToBeDeleted(); 122 } 123 124 this.source_ = undefined; 125 } 126 super.aboutToBeDeleted(); 127 } 128 129 public debugInfoDecorator() : string { 130 return `@Prop (class SynchedPropertyOneWayPU)`; 131 } 132 133 public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU<C>) { 134 135 if (this.source_ == undefined) { 136 stateMgmtConsole.error(`${this.debugInfo()}: syncPeerHasChanged from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}. source_ undefined. Internal error.`); 137 return; 138 } 139 140 if (eventSource && this.source_ == eventSource) { 141 // defensive programming: should always be the case! 142 const newValue = this.source_.getUnmonitored(); 143 if (this.checkIsSupportedValue(newValue)) { 144 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', local value about to change.`); 145 if (this.resetLocalValue(newValue, /* needCopyObject */ true)) { 146 this.notifyPropertyHasChangedPU(); 147 } 148 } 149 } else { 150 stateMgmtConsole.warn(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource?.debugInfo()}', Unexpected situation. syncPeerHasChanged from different sender than source_. Ignoring event.`) 151 } 152 } 153 154 /** 155 * event emited by wrapped ObservedObject, when one of its property values changes 156 * @param souceObject 157 * @param changedPropertyName 158 */ 159 public objectPropertyHasChangedPU(sourceObject: ObservedObject<C>, changedPropertyName: string) { 160 stateMgmtConsole.debug(`${this.debugInfo()}: objectPropertyHasChangedPU: property '${changedPropertyName}' of object value has changed.`); 161 this.notifyPropertyHasChangedPU(); 162 } 163 164 public objectPropertyHasBeenReadPU(sourceObject: ObservedObject<C>, changedPropertyName : string) { 165 stateMgmtConsole.debug(`${this.debugInfo()}: objectPropertyHasBeenReadPU: property '${changedPropertyName}' of object value has been read.`); 166 this.notifyPropertyHasBeenReadPU(); 167 } 168 169 public getUnmonitored(): C { 170 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: getUnmonitored.`); 171 // unmonitored get access , no call to notifyPropertyRead ! 172 return this.localCopyObservedObject_; 173 } 174 175 public get(): C { 176 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get.`) 177 this.notifyPropertyHasBeenReadPU() 178 return this.localCopyObservedObject_; 179 } 180 181 // assignment to local variable in the form of this.aProp = <object value> 182 // set 'writes through` to the ObservedObject 183 public set(newValue: C): void { 184 if (this.localCopyObservedObject_ === newValue) { 185 stateMgmtConsole.debug(`SynchedPropertyObjectOneWayPU[${this.id__()}IP, '${this.info() || "unknown"}']: set with unchanged value - nothing to do.`); 186 return; 187 } 188 189 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to change.`); 190 if (this.resetLocalValue(newValue, /* needCopyObject */ false)) { 191 this.notifyPropertyHasChangedPU(); 192 } 193 } 194 195 // called when updated from parent 196 public reset(sourceChangedValue: C): void { 197 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: reset (update from parent @Component).`); 198 if (this.source_ !== undefined && this.checkIsSupportedValue(sourceChangedValue)) { 199 // if this.source_.set causes an actual change, then, ObservedPropertyObject source_ will call syncPeerHasChanged method 200 this.source_.set(sourceChangedValue); 201 } 202 } 203 204 /* 205 unsubscribe from previous wrapped ObjectObject 206 take a shallow or (TODO) deep copy 207 copied Object might already be an ObservedObject (e.g. becurse of @Observed decorator) or might be raw 208 Therefore, conditionally wrap the object, then subscribe 209 return value true only if localCopyObservedObject_ has been changed 210 */ 211 private resetLocalValue(newObservedObjectValue: C, needCopyObject : boolean): boolean { 212 // note: We can not test for newObservedObjectValue == this.localCopyObservedObject_ 213 // here because the object might still be the same, but some property of it has changed 214 215 if(!this.checkIsSupportedValue(newObservedObjectValue)) { 216 return; 217 } 218 // unsubscribe from old local copy 219 if (this.localCopyObservedObject_ instanceof SubscribableAbstract) { 220 (this.localCopyObservedObject_ as SubscribableAbstract).removeOwningProperty(this); 221 } else { 222 ObservedObject.removeOwningProperty(this.localCopyObservedObject_, this); 223 } 224 225 // shallow/deep copy value 226 // needed whenever newObservedObjectValue comes from source 227 // not needed on a local set (aka when called from set() method) 228 this.localCopyObservedObject_ = needCopyObject ? this.copyObject(newObservedObjectValue, this.info_) : newObservedObjectValue; 229 230 if (typeof this.localCopyObservedObject_ == "object") { 231 if (this.localCopyObservedObject_ instanceof SubscribableAbstract) { 232 // deep copy will copy Set of subscribers as well. But local copy only has its own subscribers 233 // not those of its parent value. 234 (this.localCopyObservedObject_ as unknown as SubscribableAbstract).clearOwningProperties(); 235 (this.localCopyObservedObject_ as unknown as SubscribableAbstract).addOwningProperty(this); 236 } else if (ObservedObject.IsObservedObject(this.localCopyObservedObject_)) { 237 // case: new ObservedObject 238 ObservedObject.addOwningProperty(this.localCopyObservedObject_, this); 239 } else { 240 // wrap newObservedObjectValue raw object as ObservedObject and subscribe to it 241 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: Provided source object's is not proxied (is not a ObservedObject). Wrapping it inside ObservedObject.`); 242 this.localCopyObservedObject_ = ObservedObject.createNew(this.localCopyObservedObject_, this); 243 } 244 } 245 return true; 246 } 247 248 private copyObject(value: C, propName: string): C { 249 // ViewStackProcessor.getApiVersion function is not present in API9 250 // therefore shallowCopyObject will always be used in API version 9 and before 251 // but the code in this file is the same regardless of API version 252 stateMgmtConsole.debug(`${this.debugInfo()}: copyObject: Version: \ 253 ${(typeof ViewStackProcessor["getApiVersion"] == "function") ? ViewStackProcessor["getApiVersion"]() : 'unknown'}, \ 254 will use ${((typeof ViewStackProcessor["getApiVersion"] == "function") && (ViewStackProcessor["getApiVersion"]() >= 10)) ? 'deep copy' : 'shallow copy'} .`); 255 256 return ((typeof ViewStackProcessor["getApiVersion"] == "function") && 257 (ViewStackProcessor["getApiVersion"]() >= 10)) 258 ? this.deepCopyObject(value, propName) 259 : this.shallowCopyObject(value, propName); 260 } 261 262 // API 9 code path 263 private shallowCopyObject(value: C, propName: string): C { 264 let rawValue = ObservedObject.GetRawObject(value); 265 let copy: C; 266 267 if (!rawValue || typeof rawValue !== 'object') { 268 copy = rawValue; 269 } else if (typeof rawValue != "object") { 270 // FIXME would it be better to throw Exception here? 271 stateMgmtConsole.error(`${this.debugInfo()}: shallowCopyObject: request to copy non-object value, actual type is '${typeof rawValue}'. Internal error! Setting copy:=original value.`); 272 copy = rawValue; 273 } else if (rawValue instanceof Array) { 274 // case Array inside ObservedObject 275 copy = ObservedObject.createNew([...rawValue] as unknown as C, this); 276 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 277 } else if (rawValue instanceof Date) { 278 // case Date inside ObservedObject 279 let d = new Date(); 280 d.setTime((rawValue as Date).getTime()); 281 // subscribe, also Date gets wrapped / proxied by ObservedObject 282 copy = ObservedObject.createNew(d as unknown as C, this); 283 } else if (rawValue instanceof SubscribableAbstract) { 284 // case SubscribableAbstract, no wrapping inside ObservedObject 285 copy = { ...rawValue }; 286 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 287 if (copy instanceof SubscribableAbstract) { 288 // subscribe 289 (copy as unknown as SubscribableAbstract).addOwningProperty(this); 290 } 291 } else if (typeof rawValue == "object") { 292 // case Object that is not Array, not Date, not SubscribableAbstract 293 copy = ObservedObject.createNew({ ...rawValue }, this); 294 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 295 } else { 296 // TODO in PR "F": change to exception throwing: 297 stateMgmtConsole.error(`${this.debugInfo()}: shallow failed. Attempt to copy unsupported value of type '${typeof rawValue}' .`); 298 copy = rawValue; 299 } 300 301 return copy; 302 } 303 304 // API 10 code path 305 private deepCopyObject(obj: C, variable?: string): C { 306 let copy = SynchedPropertyObjectOneWayPU.deepCopyObjectInternal(obj, variable); 307 308 // this subscribe to the top level object/array of the copy 309 // same as shallowCopy does 310 if ((obj instanceof SubscribableAbstract) && 311 (copy instanceof SubscribableAbstract)) { 312 (copy as unknown as SubscribableAbstract).addOwningProperty(this); 313 } else if (ObservedObject.IsObservedObject(obj) && ObservedObject.IsObservedObject(copy)) { 314 ObservedObject.addOwningProperty(copy, this); 315 } 316 317 return copy;; 318 } 319 320 321 // do not use this function from outside unless it is for testing purposes. 322 public static deepCopyObjectInternal<C>(obj: C, variable?: string): C { 323 if (!obj || typeof obj !== 'object') { 324 return obj; 325 } 326 327 let stack = new Array<{ name: string }>(); 328 let copiedObjects = new Map<Object, Object>(); 329 330 return getDeepCopyOfObjectRecursive(obj); 331 332 function getDeepCopyOfObjectRecursive(obj: any): any { 333 if (!obj || typeof obj !== 'object') { 334 return obj; 335 } 336 337 const alreadyCopiedObject = copiedObjects.get(obj); 338 if (alreadyCopiedObject) { 339 let msg = `@Prop deepCopyObject: Found reference to already copied object: Path ${variable ? variable : 'unknown variable'}`; 340 stack.forEach(stackItem => msg += ` - ${stackItem.name}`) 341 stateMgmtConsole.debug(msg); 342 return alreadyCopiedObject; 343 } 344 345 let copy; 346 if (obj instanceof Set) { 347 copy = new Set<any>(); 348 for (const setKey of obj.keys()) { 349 stack.push({ name: setKey }); 350 copiedObjects.set(obj, copy); 351 copy.add(getDeepCopyOfObjectRecursive(setKey)); 352 stack.pop(); 353 } 354 } else if (obj instanceof Map) { 355 copy = new Map<any, any>(); 356 for (const mapKey of obj.keys()) { 357 stack.push({ name: mapKey }); 358 copiedObjects.set(obj, copy); 359 copy.set(mapKey, getDeepCopyOfObjectRecursive(obj.get(mapKey))); 360 stack.pop(); 361 } 362 } else if (obj instanceof Date) { 363 copy = new Date() 364 copy.setTime(obj.getTime()); 365 } else if (obj instanceof Object) { 366 copy = Array.isArray(obj) ? [] : {}; 367 Object.setPrototypeOf(copy, Object.getPrototypeOf(obj)); 368 for (const objKey of Object.keys(obj)) { 369 stack.push({ name: objKey }); 370 copiedObjects.set(obj, copy); 371 Reflect.set(copy, objKey, getDeepCopyOfObjectRecursive(obj[objKey])); 372 stack.pop(); 373 } 374 } 375 return ObservedObject.IsObservedObject(obj) ? ObservedObject.createNew(copy, null) : copy; 376 } 377 } 378} 379 380// class definitions for backward compatibility 381class SynchedPropertySimpleOneWayPU<T> extends SynchedPropertyOneWayPU<T> { 382 383} 384 385class SynchedPropertyObjectOneWayPU<T> extends SynchedPropertyOneWayPU<T> { 386 387} 388