• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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