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