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.createSourceDependency(sourceValue); 101 this.source_ = new ObservedPropertyObjectPU<C>(sourceValue, this, this.getPropSourceObservedPropertyFakeName()); 102 this.sourceIsOwnObject = true; 103 } 104 } 105 106 if (this.source_ != undefined) { 107 this.resetLocalValue(this.source_.get(), /* needCopyObject */ true); 108 } 109 stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done!`); 110 } 111 112 113 /* 114 like a destructor, need to call this before deleting 115 the property. 116 */ 117 aboutToBeDeleted() { 118 if (this.source_) { 119 this.source_.removeSubscriber(this); 120 if (this.sourceIsOwnObject == true && this.source_.numberOfSubscrbers() == 0) { 121 stateMgmtConsole.debug(`${this.debugInfo()}: aboutToBeDeleted. owning source_ ObservedPropertySimplePU, calling its aboutToBeDeleted`); 122 this.source_.aboutToBeDeleted(); 123 } 124 125 this.source_ = undefined; 126 } 127 super.aboutToBeDeleted(); 128 } 129 130 public debugInfoDecorator(): string { 131 return `@Prop (class SynchedPropertyOneWayPU)`; 132 } 133 134 // sync peer can be 135 // 1. the embedded ObservedPropertyPU, followed by a reset when the owning ViewPU received a local update in parent 136 // 2. a @Link or @Consume that uses this @Prop as a source. FIXME is this possible? - see the if (eventSource && this.source_ == eventSource) { 137 public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU<C>): void { 138 stateMgmtProfiler.begin("SyncedPropertyOneWayPU.syncPeerHasChanged"); 139 if (this.source_ == undefined) { 140 stateMgmtConsole.error(`${this.debugInfo()}: syncPeerHasChanged from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}. source_ undefined. Internal error.`); 141 stateMgmtProfiler.end(); 142 return; 143 } 144 145 if (eventSource && this.source_ == eventSource) { 146 // defensive programming: should always be the case! 147 const newValue = this.source_.getUnmonitored(); 148 if (this.checkIsSupportedValue(newValue)) { 149 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', local value about to change.`); 150 if (this.resetLocalValue(newValue, /* needCopyObject */ true)) { 151 this.notifyPropertyHasChangedPU(); 152 } 153 } 154 } else { 155 stateMgmtConsole.warn(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource?.debugInfo()}', Unexpected situation. syncPeerHasChanged from different sender than source_. Ignoring event.`) 156 } 157 stateMgmtProfiler.end(); 158 } 159 160 161 public syncPeerTrackedPropertyHasChanged(eventSource: ObservedPropertyAbstractPU<C>, changedPropertyName): void { 162 stateMgmtProfiler.begin("SyncedPropertyOneWayPU.syncPeerTrackedPropertyHasChanged"); 163 if (this.source_ == undefined) { 164 stateMgmtConsole.error(`${this.debugInfo()}: syncPeerTrackedPropertyHasChanged from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}. source_ undefined. Internal error.`); 165 stateMgmtProfiler.end(); 166 return; 167 } 168 169 if (eventSource && this.source_ == eventSource) { 170 // defensive programming: should always be the case! 171 const newValue = this.source_.getUnmonitored(); 172 if (this.checkIsSupportedValue(newValue)) { 173 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerTrackedPropertyHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', local value about to change.`); 174 if (this.resetLocalValue(newValue, /* needCopyObject */ true)) { 175 this.notifyTrackedObjectPropertyHasChanged(changedPropertyName); 176 } 177 } 178 } else { 179 stateMgmtConsole.warn(`${this.debugInfo()}: syncPeerTrackedPropertyHasChanged: from peer '${eventSource?.debugInfo()}', Unexpected situation. syncPeerHasChanged from different sender than source_. Ignoring event.`) 180 } 181 stateMgmtProfiler.end(); 182 } 183 184 185 public getUnmonitored(): C { 186 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: getUnmonitored.`); 187 // unmonitored get access , no call to notifyPropertyRead ! 188 return this.localCopyObservedObject_; 189 } 190 191 public get(): C { 192 stateMgmtProfiler.begin("SynchedPropertyOneWayPU.get"); 193 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get.`) 194 this.recordPropertyDependentUpdate(); 195 if (this.shouldInstallTrackedObjectReadCb) { 196 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: @Track optimised mode. Will install read cb func if value is an object`); 197 ObservedObject.registerPropertyReadCb(this.localCopyObservedObject_, this.onOptimisedObjectPropertyRead.bind(this)); 198 } else { 199 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: compatibility mode. `); 200 } 201 202 stateMgmtProfiler.end(); 203 return this.localCopyObservedObject_; 204 } 205 206 // assignment to local variable in the form of this.aProp = <object value> 207 public set(newValue: C): void { 208 if (this.localCopyObservedObject_ === newValue) { 209 stateMgmtConsole.debug(`SynchedPropertyObjectOneWayPU[${this.id__()}IP, '${this.info() || "unknown"}']: set with unchanged value - nothing to do.`); 210 return; 211 } 212 213 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to change.`); 214 const oldValue = this.localCopyObservedObject_; 215 if (this.resetLocalValue(newValue, /* needCopyObject */ false)) { 216 TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.localCopyObservedObject_, 217 this.notifyPropertyHasChangedPU.bind(this), 218 this.notifyTrackedObjectPropertyHasChanged.bind(this)); 219 } 220 } 221 222 protected onOptimisedObjectPropertyRead(readObservedObject: C, readPropertyName: string, isTracked: boolean): void { 223 stateMgmtProfiler.begin("SynchedPropertyOneWayPU.onOptimisedObjectPropertyRead"); 224 const renderingElmtId = this.getRenderingElmtId(); 225 if (renderingElmtId >= 0) { 226 if (!isTracked) { 227 stateMgmtConsole.applicationError(`${this.debugInfo()}: onOptimisedObjectPropertyRead read NOT TRACKED property '${readPropertyName}' during rendering!`); 228 throw new Error(`Illegal usage of not @Track'ed property '${readPropertyName}' on UI!`); 229 } else { 230 stateMgmtConsole.debug(`${this.debugInfo()}: onOptimisedObjectPropertyRead: ObservedObject property '@Track ${readPropertyName}' read.`); 231 if (this.getUnmonitored() === readObservedObject) { 232 this.recordTrackObjectPropertyDependencyForElmtId(renderingElmtId, readPropertyName) 233 } 234 } 235 } 236 stateMgmtProfiler.end(); 237 } 238 239 // called when updated from parent 240 // during parent ViewPU rerender, calls update lambda of child ViewPU with @Prop variable 241 // this lambda generated code calls ViewPU.updateStateVarsOfChildByElmtId, 242 // calls inside app class updateStateVars() 243 // calls reset() for each @Prop 244 public reset(sourceChangedValue: C): void { 245 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: reset (update from parent @Component).`); 246 if (this.source_ !== undefined && this.checkIsSupportedValue(sourceChangedValue)) { 247 // if this.source_.set causes an actual change, then, ObservedPropertyObject source_ will call syncPeerHasChanged method 248 this.createSourceDependency(sourceChangedValue); 249 this.source_.set(sourceChangedValue); 250 } 251 } 252 253 private createSourceDependency(sourceObject: C): void { 254 if (ObservedObject.IsObservedObject(sourceObject)) { 255 stateMgmtConsole.debug(`${this.debugInfo()} createSourceDependency: create dependency on source ObservedObject ...`); 256 const fake = (sourceObject as Object)[TrackedObject.___TRACKED_OPTI_ASSIGNMENT_FAKE_PROP_PROPERTY]; 257 } 258 } 259 260 /* 261 unsubscribe from previous wrapped ObjectObject 262 take a shallow or (TODO) deep copy 263 copied Object might already be an ObservedObject (e.g. becurse of @Observed decorator) or might be raw 264 Therefore, conditionally wrap the object, then subscribe 265 return value true only if localCopyObservedObject_ has been changed 266 */ 267 private resetLocalValue(newObservedObjectValue: C, needCopyObject: boolean): boolean { 268 // note: We can not test for newObservedObjectValue == this.localCopyObservedObject_ 269 // here because the object might still be the same, but some property of it has changed 270 // this is added for stability test: Target of target is not Object/is not callable/ 271 // InstanceOf error when target is not Callable/Can not get Prototype on non ECMA Object 272 try { 273 if (!this.checkIsSupportedValue(newObservedObjectValue)) { 274 return; 275 } 276 // unsubscribe from old local copy 277 if (this.localCopyObservedObject_ instanceof SubscribableAbstract) { 278 (this.localCopyObservedObject_ as SubscribableAbstract).removeOwningProperty(this); 279 } else { 280 ObservedObject.removeOwningProperty(this.localCopyObservedObject_, this); 281 282 // make sure the ObservedObject no longer has a read callback function 283 // assigned to it 284 ObservedObject.unregisterPropertyReadCb(this.localCopyObservedObject_); 285 } 286 } catch (error) { 287 stateMgmtConsole.error(`${this.debugInfo()}, an error occurred in resetLocalValue: ${error.message}`); 288 ArkTools.print("resetLocalValue SubscribableAbstract", SubscribableAbstract); 289 ArkTools.print("resetLocalValue ObservedObject", ObservedObject); 290 ArkTools.print("resetLocalValue this", this); 291 let a = Reflect.getPrototypeOf(this); 292 ArkTools.print("resetLocalVale getPrototypeOf", a); 293 throw error; 294 } 295 296 // shallow/deep copy value 297 // needed whenever newObservedObjectValue comes from source 298 // not needed on a local set (aka when called from set() method) 299 if (needCopyObject) { 300 ViewPU.pauseRendering(); 301 this.localCopyObservedObject_ = this.copyObject(newObservedObjectValue, this.info_); 302 ViewPU.restoreRendering(); 303 } else { 304 this.localCopyObservedObject_ = newObservedObjectValue; 305 } 306 307 if (typeof this.localCopyObservedObject_ == "object") { 308 if (this.localCopyObservedObject_ instanceof SubscribableAbstract) { 309 // deep copy will copy Set of subscribers as well. But local copy only has its own subscribers 310 // not those of its parent value. 311 (this.localCopyObservedObject_ as unknown as SubscribableAbstract).clearOwningProperties(); 312 (this.localCopyObservedObject_ as unknown as SubscribableAbstract).addOwningProperty(this); 313 } else if (ObservedObject.IsObservedObject(this.localCopyObservedObject_)) { 314 // case: new ObservedObject 315 ObservedObject.addOwningProperty(this.localCopyObservedObject_, this); 316 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.localCopyObservedObject_); 317 } else { 318 // wrap newObservedObjectValue raw object as ObservedObject and subscribe to it 319 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: Provided source object's is not proxied (is not a ObservedObject). Wrapping it inside ObservedObject.`); 320 this.localCopyObservedObject_ = ObservedObject.createNew(this.localCopyObservedObject_, this); 321 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.localCopyObservedObject_); 322 } 323 stateMgmtConsole.propertyAccess("end of reset shouldInstallTrackedObjectReadCb=" + this.shouldInstallTrackedObjectReadCb); 324 } 325 return true; 326 } 327 328 private copyObject(value: C, propName: string): C { 329 // ViewStackProcessor.getApiVersion function is not present in API9 330 // therefore shallowCopyObject will always be used in API version 9 and before 331 // but the code in this file is the same regardless of API version 332 stateMgmtConsole.debug(`${this.debugInfo()}: copyObject: Version: \ 333 ${(typeof ViewStackProcessor["getApiVersion"] == "function") ? ViewStackProcessor["getApiVersion"]() : 'unknown'}, \ 334 will use ${((typeof ViewStackProcessor["getApiVersion"] == "function") && (ViewStackProcessor["getApiVersion"]() >= 10)) ? 'deep copy' : 'shallow copy'} .`); 335 336 return ((typeof ViewStackProcessor["getApiVersion"] == "function") && 337 (ViewStackProcessor["getApiVersion"]() >= 10)) 338 ? this.deepCopyObject(value, propName) 339 : this.shallowCopyObject(value, propName); 340 } 341 342 // API 9 code path 343 private shallowCopyObject(value: C, propName: string): C { 344 let rawValue = ObservedObject.GetRawObject(value); 345 let copy: C; 346 347 if (!rawValue || typeof rawValue !== 'object') { 348 copy = rawValue; 349 } else if (typeof rawValue != "object") { 350 // FIXME would it be better to throw Exception here? 351 stateMgmtConsole.error(`${this.debugInfo()}: shallowCopyObject: request to copy non-object value, actual type is '${typeof rawValue}'. Internal error! Setting copy:=original value.`); 352 copy = rawValue; 353 } else if (rawValue instanceof Array) { 354 // case Array inside ObservedObject 355 copy = ObservedObject.createNew([...rawValue] as unknown as C, this); 356 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 357 } else if (rawValue instanceof Date) { 358 // case Date inside ObservedObject 359 let d = new Date(); 360 d.setTime((rawValue as Date).getTime()); 361 // subscribe, also Date gets wrapped / proxied by ObservedObject 362 copy = ObservedObject.createNew(d as unknown as C, this); 363 } else if (rawValue instanceof SubscribableAbstract) { 364 // case SubscribableAbstract, no wrapping inside ObservedObject 365 copy = { ...rawValue }; 366 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 367 if (copy instanceof SubscribableAbstract) { 368 // subscribe 369 (copy as unknown as SubscribableAbstract).addOwningProperty(this); 370 } 371 } else if (typeof rawValue == "object") { 372 // case Object that is not Array, not Date, not SubscribableAbstract 373 copy = ObservedObject.createNew({ ...rawValue }, this); 374 Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue)); 375 } else { 376 // TODO in PR "F": change to exception throwing: 377 stateMgmtConsole.error(`${this.debugInfo()}: shallow failed. Attempt to copy unsupported value of type '${typeof rawValue}' .`); 378 copy = rawValue; 379 } 380 381 return copy; 382 } 383 384 // API 10 code path 385 private deepCopyObject(obj: C, variable?: string): C { 386 let copy = SynchedPropertyObjectOneWayPU.deepCopyObjectInternal(obj, variable); 387 388 // this subscribe to the top level object/array of the copy 389 // same as shallowCopy does 390 if ((obj instanceof SubscribableAbstract) && 391 (copy instanceof SubscribableAbstract)) { 392 (copy as unknown as SubscribableAbstract).addOwningProperty(this); 393 } else if (ObservedObject.IsObservedObject(obj) && ObservedObject.IsObservedObject(copy)) { 394 ObservedObject.addOwningProperty(copy, this); 395 } 396 397 return copy;; 398 } 399 400 401 // do not use this function from outside unless it is for testing purposes. 402 public static deepCopyObjectInternal<C>(obj: C, variable?: string): C { 403 if (!obj || typeof obj !== 'object') { 404 return obj; 405 } 406 407 let stack = new Array<{ name: string }>(); 408 let copiedObjects = new Map<Object, Object>(); 409 410 return getDeepCopyOfObjectRecursive(obj); 411 412 function getDeepCopyOfObjectRecursive(obj: any): any { 413 if (!obj || typeof obj !== 'object') { 414 return obj; 415 } 416 417 const alreadyCopiedObject = copiedObjects.get(obj); 418 if (alreadyCopiedObject) { 419 let msg = `@Prop deepCopyObject: Found reference to already copied object: Path ${variable ? variable : 'unknown variable'}`; 420 stack.forEach(stackItem => msg += ` - ${stackItem.name}`) 421 stateMgmtConsole.debug(msg); 422 return alreadyCopiedObject; 423 } 424 425 let copy; 426 if (obj instanceof Set) { 427 copy = new Set<any>(); 428 Object.setPrototypeOf(copy, Object.getPrototypeOf(obj)); 429 copiedObjects.set(obj, copy); 430 for (const setKey of obj.keys()) { 431 stack.push({ name: setKey }); 432 copy.add(getDeepCopyOfObjectRecursive(setKey)); 433 stack.pop(); 434 } 435 } else if (obj instanceof Map) { 436 copy = new Map<any, any>(); 437 Object.setPrototypeOf(copy, Object.getPrototypeOf(obj)); 438 copiedObjects.set(obj, copy); 439 for (const mapKey of obj.keys()) { 440 stack.push({ name: mapKey }); 441 copy.set(mapKey, getDeepCopyOfObjectRecursive(obj.get(mapKey))); 442 stack.pop(); 443 } 444 } else if (obj instanceof Date) { 445 copy = new Date() 446 copy.setTime(obj.getTime()); 447 Object.setPrototypeOf(copy, Object.getPrototypeOf(obj)); 448 copiedObjects.set(obj, copy); 449 } else if (obj instanceof Object) { 450 copy = Array.isArray(obj) ? [] : {}; 451 Object.setPrototypeOf(copy, Object.getPrototypeOf(obj)); 452 copiedObjects.set(obj, copy); 453 } 454 for (const objKey of Object.keys(obj)) { 455 stack.push({ name: objKey }); 456 Reflect.set(copy, objKey, getDeepCopyOfObjectRecursive(obj[objKey])); 457 stack.pop(); 458 } 459 return ObservedObject.IsObservedObject(obj) ? ObservedObject.createNew(copy, null) : copy; 460 } 461 } 462} 463 464// class definitions for backward compatibility 465class SynchedPropertySimpleOneWayPU<T> extends SynchedPropertyOneWayPU<T> { 466 467} 468 469class SynchedPropertyObjectOneWayPU<T> extends SynchedPropertyOneWayPU<T> { 470 471} 472