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 * SynchedPropertyObjectTwoWayPU 18 * implementation of @Link and @Consume decorated variables of type class object 19 * 20 * all definitions in this file are framework internal 21*/ 22 23class SynchedPropertyTwoWayPU<C> extends ObservedPropertyAbstractPU<C> 24 implements PeerChangeEventReceiverPU<C>, ObservedObjectEventsPUReceiver<C> { 25 26 private source_: ObservedPropertyObjectAbstract<C>; 27 28 private fakeSourceBackup_: ObservedPropertyObjectAbstract<C>; 29 30 constructor(source: ObservedPropertyObjectAbstract<C>, 31 owningChildView: IPropertySubscriber, 32 thisPropertyName: PropertyInfo) { 33 super(owningChildView, thisPropertyName); 34 this.source_ = source; 35 if (this.source_) { 36 // register to the parent property 37 this.source_.addSubscriber(this); 38 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.source_.getUnmonitored()); 39 } else { 40 throw new SyntaxError(`${this.debugInfo()}: constructor: source variable in parent/ancestor @Component must be defined. Application error!`); 41 } 42 this.setDecoratorInfo("@Link"); 43 } 44 45 /* 46 like a destructor, need to call this before deleting 47 the property. 48 */ 49 aboutToBeDeleted() { 50 // unregister from parent of this link 51 if (this.source_) { 52 this.source_.removeSubscriber(this); 53 54 // unregister from the ObservedObject 55 ObservedObject.removeOwningProperty(this.source_.getUnmonitored(), this); 56 this.source_.__isFake_ObservedPropertyAbstract_Internal() && this.source_.aboutToBeDeleted(); 57 this.fakeSourceBackup_ && this.fakeSourceBackup_.aboutToBeDeleted(); 58 } 59 super.aboutToBeDeleted(); 60 } 61 62 private isStorageLinkProp(): boolean { 63 return (this.source_ && this.source_ instanceof ObservedPropertyAbstract && (!(this.source_ instanceof ObservedPropertyAbstractPU))); 64 } 65 66 private setObject(newValue: C): void { 67 if (!this.source_) { 68 throw new SyntaxError(`${this.debugInfo()}: setObject (assign a new value), no source variable in parent/ancestor \ 69 @Component. Application error.`); 70 } 71 72 if (this.getUnmonitored() === newValue) { 73 stateMgmtConsole.debug(`SynchedPropertyObjectTwoWayPU[${this.id__()}IP, '${this.info() || 'unknown'}']: set with unchanged value - ignoring.`); 74 return; 75 } 76 77 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value has changed.`); 78 79 if (this.checkIsSupportedValue(newValue)) { 80 // the source_ ObservedProperty will call: this.syncPeerHasChanged(newValue); 81 this.source_.set(newValue); 82 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(newValue); 83 } 84 } 85 86 87 /** 88 * Called when sync peer ObservedPropertyObject or SynchedPropertyObjectTwoWay has changed value 89 * that peer can be in either parent or child component if 'this' is used for a @Link 90 * that peer can be in either ancestor or descendant component if 'this' is used for a @Consume 91 * @param eventSource 92 */ 93 public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU<C>, isSync: boolean = false): void { 94 stateMgmtProfiler.begin('SynchedPropertyTwoWayPU.syncPeerHasChanged'); 95 if (!this.changeNotificationIsOngoing_) { 96 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}' .`) 97 this.notifyPropertyHasChangedPU(isSync); 98 } 99 stateMgmtProfiler.end(); 100 } 101 102 public syncPeerTrackedPropertyHasChanged(eventSource: ObservedPropertyAbstractPU<C>, changedTrackedObjectPropertyName: string, isSync: boolean = false): void { 103 stateMgmtProfiler.begin('SynchedPropertyTwoWayPU.syncPeerTrackedPropertyHasChanged'); 104 if (!this.changeNotificationIsOngoing_) { 105 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerTrackedPropertyHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', changed property '${changedTrackedObjectPropertyName}'.`); 106 this.notifyTrackedObjectPropertyHasChanged(changedTrackedObjectPropertyName, isSync); 107 } 108 stateMgmtProfiler.end(); 109 } 110 111 public getUnmonitored(): C { 112 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: getUnmonitored.`); 113 return (this.source_ ? this.source_.getUnmonitored() : undefined); 114 } 115 116 // get 'read through` from the ObservedProperty 117 public get(): C { 118 stateMgmtProfiler.begin('SynchedPropertyTwoWayPU.get'); 119 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get`) 120 this.recordPropertyDependentUpdate(); 121 const result = this.getUnmonitored(); 122 if (this.shouldInstallTrackedObjectReadCb) { 123 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: @Track optimised mode. Will install read cb func if value is an object`); 124 ObservedObject.registerPropertyReadCb(result, this.onOptimisedObjectPropertyRead, this); 125 } else { 126 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: compatibility mode. `); 127 } 128 129 stateMgmtProfiler.end(); 130 return result; 131 } 132 133 // set 'writes through` to the ObservedProperty 134 public set(newValue: C): void { 135 stateMgmtProfiler.begin('SynchedPropertyTwoWayPU.set'); 136 if (this.getUnmonitored() === newValue) { 137 stateMgmtConsole.debug(`SynchedPropertyObjectTwoWayPU[${this.id__()}IP, '${this.info() || 'unknown'}']: set with unchanged value - nothing to do.`); 138 stateMgmtProfiler.end(); 139 return; 140 } 141 142 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to change.`); 143 144 // avoid circular notifications @Link -> source @State -> other but also back to same @Link 145 this.changeNotificationIsOngoing_ = true; 146 let oldValue = this.getUnmonitored(); 147 this.setObject(newValue); 148 TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ newValue, 149 this.notifyPropertyHasChangedPU, 150 this.notifyTrackedObjectPropertyHasChanged, this); 151 this.changeNotificationIsOngoing_ = false; 152 stateMgmtProfiler.end(); 153 } 154 155 156 protected onOptimisedObjectPropertyRead(readObservedObject: C, readPropertyName: string, isTracked: boolean): void { 157 stateMgmtProfiler.begin('SynchedPropertyTwoWayPU.onOptimisedObjectPropertyRead'); 158 const renderingElmtId = this.getRenderingElmtId(); 159 if (renderingElmtId >= 0) { 160 if (!isTracked) { 161 stateMgmtConsole.applicationError(`${this.debugInfo()}: onOptimisedObjectPropertyRead read NOT TRACKED property '${readPropertyName}' during rendering!`); 162 throw new Error(`Illegal usage of not @Track'ed property '${readPropertyName}' on UI!`); 163 } else { 164 stateMgmtConsole.debug(`${this.debugInfo()}: onOptimisedObjectPropertyRead: ObservedObject property '@Track ${readPropertyName}' read.`); 165 if (this.getUnmonitored() === readObservedObject) { 166 this.recordTrackObjectPropertyDependencyForElmtId(renderingElmtId, readPropertyName) 167 } 168 } 169 } 170 stateMgmtProfiler.end(); 171 } 172 173 private isSameType(a: unknown, b: unknown): [boolean, string, string] { 174 if (a === null && b === null) { 175 return [true, 'null', 'null']; 176 } 177 if (a === null) { 178 return [false, 'null', typeof b]; 179 } 180 if (b === null) { 181 return [false, typeof a, 'null']; 182 } 183 184 // check SimpleType 185 const typeA = typeof a; 186 const typeB = typeof b; 187 if (typeA !== 'object' && typeB !== 'object') { 188 return [typeA === typeB, typeA, typeB]; 189 } 190 // check built-in type 191 const objectTypeA = Object.prototype.toString.call(a); 192 const objectTypeB = Object.prototype.toString.call(b); 193 if (objectTypeA !== objectTypeB) { 194 return [false, objectTypeA, objectTypeB]; 195 } 196 // check class instance 197 const classConstructorA = (a as object).constructor; 198 const classConstructorB = (b as object).constructor; 199 return [classConstructorA === classConstructorB, classConstructorA.name, classConstructorB.name]; 200 } 201 202 /** 203 * Reset the source for the SynchedPropertyTwoWayPU. Only used when build node attached to the main tree 204 * the consume used the default value need find its provide. 205 * step1: save the fake source which created when initializeConsume used default value 206 * step2: add new source, which is provide as the new source and add subscribe for new source 207 * step3: SynchedPropertyTwoWayPU which change source, needs to sync all peers. also for the track property. 208 * step4: need to update the dependent elements synchronously. 209 * @param newSource new source need to reset. For consume, it is provide. 210 */ 211 public resetSource(newSource: ObservedPropertyObjectAbstract<C>): void { 212 let newRaw = ObservedObject.GetRawObject(newSource.getUnmonitored()); 213 let fakeRaw = ObservedObject.GetRawObject(this.source_.getUnmonitored()); 214 // if the new source value type is not same with the old one, cannot connect 215 const [isSame, typeNew, typeFake] = this.isSameType(newRaw, fakeRaw); 216 if (!isSame) { 217 const error = `cannot connect ${this.debugInfo()} (type ${typeFake}) 218 to ${(newSource as unknown as ObservedPropertyObjectPU<any>).debugInfo()} (type ${typeNew}). Their types are not same.`; 219 stateMgmtConsole.applicationError(error); 220 throw new TypeError(error); 221 } 222 this.fakeSourceBackup_ = this.source_; 223 this.source_ = newSource; 224 // register two-way sync to the new source 225 this.source_.addSubscriber(this); 226 if (newRaw === fakeRaw) { 227 stateMgmtConsole.debug(`the new value ${(newSource as unknown as ObservedPropertyObjectPU<any>).debugInfo()} value 228 same with the default value ${this.debugInfo()}. ignore it.`); 229 return; 230 } 231 this.syncFromSource(); 232 } 233 234 public resetFakeSource(): void { 235 if (!this.fakeSourceBackup_) { 236 stateMgmtConsole.warn(`${this.debugInfo()} does not have the fake source backup, need to check the build node does not amount to parent ever`) 237 return; 238 } 239 240 this.source_.removeSubscriber(this); 241 this.source_ = this.fakeSourceBackup_; 242 this.syncFromSource(); 243 } 244 245 private syncFromSource(): void { 246 const isTrack = this.shouldInstallTrackedObjectReadCb; 247 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.source_.getUnmonitored()); 248 this.syncPeerHasChanged(this.source_ as ObservedPropertyAbstractPU<any>, true); 249 let newValue = ObservedObject.GetRawObject(this.source_.getUnmonitored()); 250 let oldValue = ObservedObject.GetRawObject(this.fakeSourceBackup_.getUnmonitored()); 251 if (isTrack && this.shouldInstallTrackedObjectReadCb) { 252 Object.keys(newValue) 253 .forEach(propName => { 254 // Collect only @Track'ed changed properties 255 if (typeof propName === 'string' && Reflect.has(newValue as unknown as object, `${TrackedObject.___TRACKED_PREFIX}${propName}`)) { 256 // if the source is track property, need to notify the property update 257 if (oldValue[propName] !== newValue[propName]) { 258 this.syncPeerTrackedPropertyHasChanged(this.source_ as ObservedPropertyAbstractPU<any>, propName, true); 259 } 260 } 261 }); 262 } 263 264 // sort the view according to the view id 265 const dirtyView = Array.from(SyncedViewRegistry.dirtyNodesList) 266 .map((weak) => weak?.deref()) 267 .filter((view): view is ViewPU => view instanceof ViewPU) 268 .sort((view1, view2) => view1.id__() - view2.id__()); 269 270 dirtyView.forEach((view: ViewPU) => { 271 view.dirtyElementIdsNeedsUpdateSynchronously_.forEach((elementId: number) => { 272 view.UpdateElement(elementId); 273 }) 274 }) 275 SyncedViewRegistry.dirtyNodesList.clear(); 276 } 277} 278 279// class definitions for backward compatibility 280class SynchedPropertyObjectTwoWayPU<C> extends SynchedPropertyTwoWayPU<C> { 281 282} 283 284class SynchedPropertySimpleTwoWayPU<T> extends SynchedPropertyTwoWayPU<T> { 285 286} 287 288