• 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.source_ = new ObservedPropertyObjectPU<C>(sourceValue, this, this.getPropSourceObservedPropertyFakeName());
101        this.sourceIsOwnObject = true;
102      }
103    }
104
105    if (this.source_ != undefined) {
106      this.resetLocalValue(this.source_.get(), /* needCopyObject */ true);
107    }
108    stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done!`);
109  }
110
111
112  /*
113  like a destructor, need to call this before deleting
114  the property.
115  */
116  aboutToBeDeleted() {
117    if (this.source_) {
118      this.source_.removeSubscriber(this);
119      if (this.sourceIsOwnObject == true && this.source_.numberOfSubscrbers()==0){
120        stateMgmtConsole.debug(`${this.debugInfo()}: aboutToBeDeleted. owning source_ ObservedPropertySimplePU, calling its aboutToBeDeleted`);
121        this.source_.aboutToBeDeleted();
122     }
123
124      this.source_ = undefined;
125    }
126    super.aboutToBeDeleted();
127  }
128
129  public debugInfoDecorator() : string {
130    return `@Prop (class SynchedPropertyOneWayPU)`;
131  }
132
133  public syncPeerHasChanged(eventSource: ObservedPropertyAbstractPU<C>) {
134
135    if (this.source_ == undefined) {
136      stateMgmtConsole.error(`${this.debugInfo()}: syncPeerHasChanged from peer ${eventSource && eventSource.debugInfo && eventSource.debugInfo()}. source_ undefined. Internal error.`);
137      return;
138    }
139
140    if (eventSource && this.source_ == eventSource) {
141      // defensive programming: should always be the case!
142      const newValue = this.source_.getUnmonitored();
143      if (this.checkIsSupportedValue(newValue)) {
144        stateMgmtConsole.debug(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource && eventSource.debugInfo && eventSource.debugInfo()}', local value about to change.`);
145        if (this.resetLocalValue(newValue, /* needCopyObject */ true)) {
146          this.notifyPropertyHasChangedPU();
147        }
148      }
149    } else {
150      stateMgmtConsole.warn(`${this.debugInfo()}: syncPeerHasChanged: from peer '${eventSource?.debugInfo()}', Unexpected situation. syncPeerHasChanged from different sender than source_. Ignoring event.`)
151    }
152  }
153
154  /**
155   * event emited by wrapped ObservedObject, when one of its property values changes
156   * @param souceObject
157   * @param changedPropertyName
158   */
159  public objectPropertyHasChangedPU(sourceObject: ObservedObject<C>, changedPropertyName: string) {
160    stateMgmtConsole.debug(`${this.debugInfo()}: objectPropertyHasChangedPU: property '${changedPropertyName}' of object value has changed.`);
161    this.notifyPropertyHasChangedPU();
162  }
163
164  public objectPropertyHasBeenReadPU(sourceObject: ObservedObject<C>, changedPropertyName : string) {
165    stateMgmtConsole.debug(`${this.debugInfo()}: objectPropertyHasBeenReadPU: property '${changedPropertyName}' of object value has been read.`);
166    this.notifyPropertyHasBeenReadPU();
167  }
168
169  public getUnmonitored(): C {
170    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: getUnmonitored.`);
171    // unmonitored get access , no call to notifyPropertyRead !
172    return this.localCopyObservedObject_;
173  }
174
175  public get(): C {
176    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: get.`)
177    this.notifyPropertyHasBeenReadPU()
178    return this.localCopyObservedObject_;
179  }
180
181  // assignment to local variable in the form of this.aProp = <object value>
182  // set 'writes through` to the ObservedObject
183  public set(newValue: C): void {
184    if (this.localCopyObservedObject_ === newValue) {
185      stateMgmtConsole.debug(`SynchedPropertyObjectOneWayPU[${this.id__()}IP, '${this.info() || "unknown"}']: set with unchanged value  - nothing to do.`);
186      return;
187    }
188
189    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: set: value about to change.`);
190    if (this.resetLocalValue(newValue, /* needCopyObject */ false)) {
191      this.notifyPropertyHasChangedPU();
192    }
193  }
194
195  // called when updated from parent
196  public reset(sourceChangedValue: C): void {
197    stateMgmtConsole.propertyAccess(`${this.debugInfo()}: reset (update from parent @Component).`);
198    if (this.source_ !== undefined && this.checkIsSupportedValue(sourceChangedValue)) {
199      // if this.source_.set causes an actual change, then, ObservedPropertyObject source_ will call syncPeerHasChanged method
200      this.source_.set(sourceChangedValue);
201    }
202  }
203
204  /*
205    unsubscribe from previous wrapped ObjectObject
206    take a shallow or (TODO) deep copy
207    copied Object might already be an ObservedObject (e.g. becurse of @Observed decorator) or might be raw
208    Therefore, conditionally wrap the object, then subscribe
209    return value true only if localCopyObservedObject_ has been changed
210  */
211    private resetLocalValue(newObservedObjectValue: C, needCopyObject : boolean): boolean {
212      // note: We can not test for newObservedObjectValue == this.localCopyObservedObject_
213      // here because the object might still be the same, but some property of it has changed
214
215      if(!this.checkIsSupportedValue(newObservedObjectValue)) {
216        return;
217      }
218      // unsubscribe from old local copy
219      if (this.localCopyObservedObject_ instanceof SubscribableAbstract) {
220        (this.localCopyObservedObject_ as SubscribableAbstract).removeOwningProperty(this);
221      } else {
222        ObservedObject.removeOwningProperty(this.localCopyObservedObject_, this);
223      }
224
225      // shallow/deep copy value
226      // needed whenever newObservedObjectValue comes from source
227      // not needed on a local set (aka when called from set() method)
228      this.localCopyObservedObject_ = needCopyObject ? this.copyObject(newObservedObjectValue, this.info_) : newObservedObjectValue;
229
230      if (typeof this.localCopyObservedObject_ == "object") {
231        if (this.localCopyObservedObject_ instanceof SubscribableAbstract) {
232          // deep copy will copy Set of subscribers as well. But local copy only has its own subscribers
233          // not those of its parent value.
234          (this.localCopyObservedObject_ as unknown as SubscribableAbstract).clearOwningProperties();
235          (this.localCopyObservedObject_ as unknown as SubscribableAbstract).addOwningProperty(this);
236        } else if (ObservedObject.IsObservedObject(this.localCopyObservedObject_)) {
237          // case: new ObservedObject
238          ObservedObject.addOwningProperty(this.localCopyObservedObject_, this);
239        } else {
240          // wrap newObservedObjectValue raw object as ObservedObject and subscribe to it
241          stateMgmtConsole.propertyAccess(`${this.debugInfo()}: Provided source object's is not proxied (is not a ObservedObject). Wrapping it inside ObservedObject.`);
242          this.localCopyObservedObject_ = ObservedObject.createNew(this.localCopyObservedObject_, this);
243        }
244      }
245      return true;
246  }
247
248  private copyObject(value: C, propName: string): C {
249    // ViewStackProcessor.getApiVersion function is not present in API9
250    // therefore shallowCopyObject will always be used in API version 9 and before
251    // but the code in this file is the same regardless of API version
252    stateMgmtConsole.debug(`${this.debugInfo()}: copyObject: Version: \
253    ${(typeof ViewStackProcessor["getApiVersion"] == "function") ? ViewStackProcessor["getApiVersion"]() : 'unknown'}, \
254    will use ${((typeof ViewStackProcessor["getApiVersion"] == "function") && (ViewStackProcessor["getApiVersion"]() >= 10)) ? 'deep copy' : 'shallow copy'} .`);
255
256    return ((typeof ViewStackProcessor["getApiVersion"] == "function") &&
257      (ViewStackProcessor["getApiVersion"]() >= 10))
258      ? this.deepCopyObject(value, propName)
259      : this.shallowCopyObject(value, propName);
260  }
261
262  // API 9 code path
263  private shallowCopyObject(value: C, propName: string): C {
264    let rawValue = ObservedObject.GetRawObject(value);
265    let copy: C;
266
267    if (!rawValue || typeof rawValue !== 'object') {
268      copy = rawValue;
269    } else if (typeof rawValue != "object") {
270      // FIXME would it be better to throw Exception here?
271      stateMgmtConsole.error(`${this.debugInfo()}: shallowCopyObject: request to copy non-object value, actual type is '${typeof rawValue}'. Internal error! Setting copy:=original value.`);
272      copy = rawValue;
273    } else if (rawValue instanceof Array) {
274      // case Array inside ObservedObject
275      copy = ObservedObject.createNew([...rawValue] as unknown as C, this);
276      Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue));
277    } else if (rawValue instanceof Date) {
278      // case Date inside ObservedObject
279      let d = new Date();
280      d.setTime((rawValue as Date).getTime());
281      // subscribe, also Date gets wrapped / proxied by ObservedObject
282      copy = ObservedObject.createNew(d as unknown as C, this);
283    } else if (rawValue instanceof SubscribableAbstract) {
284      // case SubscribableAbstract, no wrapping inside ObservedObject
285      copy = { ...rawValue };
286      Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue));
287      if (copy instanceof SubscribableAbstract) {
288        // subscribe
289        (copy as unknown as SubscribableAbstract).addOwningProperty(this);
290      }
291    } else if (typeof rawValue == "object") {
292      // case Object that is not Array, not Date, not SubscribableAbstract
293      copy = ObservedObject.createNew({ ...rawValue }, this);
294      Object.setPrototypeOf(copy, Object.getPrototypeOf(rawValue));
295    } else {
296      // TODO in PR "F": change to exception throwing:
297      stateMgmtConsole.error(`${this.debugInfo()}: shallow failed. Attempt to copy unsupported value of type '${typeof rawValue}' .`);
298      copy = rawValue;
299    }
300
301    return copy;
302  }
303
304  // API 10 code path
305  private deepCopyObject(obj: C, variable?: string): C {
306    let copy = SynchedPropertyObjectOneWayPU.deepCopyObjectInternal(obj, variable);
307
308    // this subscribe to the top level object/array of the copy
309    // same as shallowCopy does
310    if ((obj instanceof SubscribableAbstract) &&
311      (copy instanceof SubscribableAbstract)) {
312      (copy as unknown as SubscribableAbstract).addOwningProperty(this);
313    } else if (ObservedObject.IsObservedObject(obj) && ObservedObject.IsObservedObject(copy)) {
314      ObservedObject.addOwningProperty(copy, this);
315    }
316
317    return copy;;
318  }
319
320
321  // do not use this function from outside unless it is for testing purposes.
322  public static deepCopyObjectInternal<C>(obj: C, variable?: string): C {
323    if (!obj || typeof obj !== 'object') {
324      return obj;
325    }
326
327    let stack = new Array<{ name: string }>();
328    let copiedObjects = new Map<Object, Object>();
329
330    return getDeepCopyOfObjectRecursive(obj);
331
332    function getDeepCopyOfObjectRecursive(obj: any): any {
333      if (!obj || typeof obj !== 'object') {
334        return obj;
335      }
336
337      const alreadyCopiedObject = copiedObjects.get(obj);
338      if (alreadyCopiedObject) {
339        let msg = `@Prop deepCopyObject: Found reference to already copied object: Path ${variable ? variable : 'unknown variable'}`;
340        stack.forEach(stackItem => msg += ` - ${stackItem.name}`)
341        stateMgmtConsole.debug(msg);
342        return alreadyCopiedObject;
343      }
344
345      let copy;
346      if (obj instanceof Set) {
347        copy = new Set<any>();
348        for (const setKey of obj.keys()) {
349          stack.push({ name: setKey });
350          copiedObjects.set(obj, copy);
351          copy.add(getDeepCopyOfObjectRecursive(setKey));
352          stack.pop();
353        }
354      } else if (obj instanceof Map) {
355        copy = new Map<any, any>();
356        for (const mapKey of obj.keys()) {
357          stack.push({ name: mapKey });
358          copiedObjects.set(obj, copy);
359          copy.set(mapKey, getDeepCopyOfObjectRecursive(obj.get(mapKey)));
360          stack.pop();
361        }
362      } else if (obj instanceof Date) {
363        copy = new Date()
364        copy.setTime(obj.getTime());
365      } else if (obj instanceof Object) {
366        copy = Array.isArray(obj) ? [] : {};
367        Object.setPrototypeOf(copy, Object.getPrototypeOf(obj));
368        for (const objKey of Object.keys(obj)) {
369          stack.push({ name: objKey });
370          copiedObjects.set(obj, copy);
371          Reflect.set(copy, objKey, getDeepCopyOfObjectRecursive(obj[objKey]));
372          stack.pop();
373        }
374      }
375      return ObservedObject.IsObservedObject(obj) ? ObservedObject.createNew(copy, null) : copy;
376    }
377  }
378}
379
380// class definitions for backward compatibility
381class SynchedPropertySimpleOneWayPU<T> extends SynchedPropertyOneWayPU<T> {
382
383}
384
385class SynchedPropertyObjectOneWayPU<T> extends SynchedPropertyOneWayPU<T> {
386
387}
388