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