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