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