1/* 2 * Copyright (c) 2022-2023 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 * ObservedPropertyObjectPU 18 * implementation of @State and @Provide decorated variables of type class object 19 * 20 * all definitions in this file are framework internal 21 * 22 * class that holds an actual property value of type T 23 * uses its base class to manage subscribers to this 24 * property. 25*/ 26 27class ObservedPropertyPU<T> extends ObservedPropertyAbstractPU<T> 28 implements PeerChangeEventReceiverPU<T>, ObservedObjectEventsPUReceiver<T> { 29 30 private wrappedValue_: T; 31 32 public staticWatchId?: number; 33 34 public _setInteropValueForStaticState?: setValue<T>; 35 36 constructor(localInitValue: T, owningView: IPropertySubscriber, propertyName: PropertyInfo) { 37 super(owningView, propertyName); 38 39 this.setValueInternal(localInitValue); 40 this.setDecoratorInfo('@State'); 41 } 42 43 aboutToBeDeleted(unsubscribeMe?: IPropertySubscriber) { 44 this.unsubscribeWrappedObject(); 45 this.removeSubscriber(unsubscribeMe); 46 super.aboutToBeDeleted(); 47 } 48 49 /** 50 * Called by a SynchedPropertyObjectTwoWayPU (@Link, @Consume) that uses this as sync peer when it has changed 51 * @param eventSource 52 */ 53 public syncPeerHasChanged(eventSource : ObservedPropertyAbstractPU<T>, isSync: boolean = false) : void { 54 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerHasChanged: from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}'.`); 55 this.notifyPropertyHasChangedPU(isSync); 56 } 57 58 public syncPeerTrackedPropertyHasChanged(eventSource: ObservedPropertyAbstractPU<T>, 59 changedTrackedObjectPropertyName: string, isSync: boolean = false): void { 60 stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerTrackedPropertyHasChanged: from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', changed property '${changedTrackedObjectPropertyName}'.`); 61 this.notifyTrackedObjectPropertyHasChanged(changedTrackedObjectPropertyName, isSync); 62 } 63 64 /** 65 * Wrapped ObservedObjectPU has changed 66 * @param souceObject 67 * @param changedPropertyName 68 */ 69 public objectPropertyHasChangedPU(souceObject: ObservedObject<T>, changedPropertyName : string) { 70 stateMgmtConsole.debug(`${this.debugInfo()}: objectPropertyHasChangedPU: contained ObservedObject property \ 71 '${changedPropertyName}' has changed.`); 72 this.notifyPropertyHasChangedPU(); 73 } 74 75 private unsubscribeWrappedObject() { 76 if (this.wrappedValue_) { 77 if (this.wrappedValue_ instanceof SubscribableAbstract) { 78 (this.wrappedValue_ as SubscribableAbstract).removeOwningProperty(this); 79 // for interop 80 } else if (InteropConfigureStateMgmt.instance.needsInterop() && this.staticWatchId && typeof this.wrappedValue_ === 'object' && 81 'removeWatchSubscriber' in this.wrappedValue_ && typeof this.wrappedValue_.removeWatchSubscriber === 'function') { 82 this.wrappedValue_.removeWatchSubscriber(this.staticWatchId); 83 } else { 84 ObservedObject.removeOwningProperty(this.wrappedValue_, this); 85 86 // make sure the ObservedObject no longer has a read callback function 87 // assigned to it 88 ObservedObject.unregisterPropertyReadCb(this.wrappedValue_); 89 } 90 } 91 } 92 93 /* 94 actually update this.wrappedValue_ 95 called needs to do value change check 96 and also notify with this.aboutToChange(); 97 */ 98 private setValueInternal(newValue: T): boolean { 99 stateMgmtProfiler.begin('ObservedPropertyPU.setValueInternal'); 100 if (newValue === this.wrappedValue_) { 101 stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || 'unknown'}'] newValue unchanged`); 102 stateMgmtProfiler.end(); 103 return false; 104 } 105 106 if (!this.checkIsSupportedValue(newValue)) { 107 stateMgmtProfiler.end(); 108 return false; 109 } 110 111 this.unsubscribeWrappedObject(); 112 113 // for interop 114 if (InteropConfigureStateMgmt.instance.needsInterop() && newValue && typeof newValue === 'object' && isStaticProxy(newValue)) { 115 this.wrappedValue_ = InteropExtractorModule.getInteropObservedObject(newValue, this); 116 stateMgmtProfiler.end(); 117 return true; 118 } 119 120 if (!newValue || typeof newValue !== 'object') { 121 // undefined, null, simple type: 122 // nothing to subscribe to in case of new value undefined || null || simple type 123 this.wrappedValue_ = newValue; 124 } else if (newValue instanceof SubscribableAbstract) { 125 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an SubscribableAbstract, subscribing to it.`); 126 this.wrappedValue_ = newValue; 127 (this.wrappedValue_ as unknown as SubscribableAbstract).addOwningProperty(this); 128 } else if (ObservedObject.IsObservedObject(newValue)) { 129 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an ObservedObject already`); 130 ObservedObject.addOwningProperty(newValue, this); 131 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(newValue); 132 this.wrappedValue_ = newValue; 133 } else { 134 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: setValueInternal: new value is an Object, needs to be wrapped in an ObservedObject.`); 135 this.wrappedValue_ = ObservedObject.createNew(newValue, this); 136 this.shouldInstallTrackedObjectReadCb = TrackedObject.needsPropertyReadCb(this.wrappedValue_); 137 } 138 stateMgmtProfiler.end(); 139 return true; 140 } 141 142 public get(): T { 143 stateMgmtProfiler.begin('ObservedPropertyPU.get'); 144 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get`); 145 this.recordPropertyDependentUpdate(); 146 if (this.shouldInstallTrackedObjectReadCb) { 147 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: @Track optimised mode. Will install read cb func if value is an object`); 148 ObservedObject.registerPropertyReadCb(this.wrappedValue_, this.onOptimisedObjectPropertyRead, this); 149 } else { 150 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get: compatibility mode. `); 151 } 152 stateMgmtProfiler.end(); 153 return this.wrappedValue_; 154 } 155 156 public getUnmonitored(): T { 157 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: getUnmonitored.`); 158 // unmonitored get access , no call to notifyPropertyRead ! 159 return this.wrappedValue_; 160 } 161 162 public set(newValue: T): void { 163 if (this.wrappedValue_ === newValue) { 164 stateMgmtConsole.debug(`ObservedPropertyObjectPU[${this.id__()}, '${this.info() || 'unknown'}']: set with unchanged value - ignoring.`); 165 return; 166 } 167 stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to changed.`); 168 const oldValue = this.wrappedValue_; 169 if (this.setValueInternal(newValue)) { 170 TrackedObject.notifyObjectValueAssignment(/* old value */ oldValue, /* new value */ this.wrappedValue_, 171 this.notifyPropertyHasChangedPU, 172 this.notifyTrackedObjectPropertyHasChanged, this); 173 // for interop 174 if (InteropConfigureStateMgmt.instance.needsInterop()) { 175 InteropExtractorModule.setStaticValueForInterop(this, newValue); 176 } 177 } 178 } 179 180 protected onOptimisedObjectPropertyRead(readObservedObject: T, readPropertyName: string, isTracked: boolean) : void { 181 stateMgmtProfiler.begin('ObservedProperty.onOptimisedObjectPropertyRead'); 182 const renderingElmtId = this.getRenderingElmtId(); 183 if (renderingElmtId >= 0) { 184 if (!isTracked) { 185 stateMgmtConsole.applicationError(`${this.debugInfo()}: onOptimisedObjectPropertyRead read NOT TRACKED property '${readPropertyName}' during rendering!`); 186 throw new Error(`Illegal usage of not @Track'ed property '${readPropertyName}' on UI!`); 187 } else { 188 stateMgmtConsole.debug(`${this.debugInfo()}: onOptimisedObjectPropertyRead: ObservedObject property '@Track ${readPropertyName}' read.`); 189 // only record dependency when 190 // 1 - currently rendering or re-rendering 191 // TODO room for further optimization: if not an expression in updateFunc, only first time render needs to record 192 // because there can be change to depended variables unless one of the bindings is a JS expression 193 // 2 - the changed ObservedObject is the wrapped object. The situation where it can be different is after a value assignment. 194 if (this.getUnmonitored() === readObservedObject) { 195 this.recordTrackObjectPropertyDependencyForElmtId(renderingElmtId, readPropertyName); 196 } 197 } 198 } 199 stateMgmtProfiler.end(); 200 } 201} 202 203// class definitions for backward compatibility 204class ObservedPropertyObjectPU<T> extends ObservedPropertyPU<T> { 205 206} 207 208class ObservedPropertySimplePU<T> extends ObservedPropertyPU<T> { 209 210} 211