• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2021-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
18/**
19* @Observed class decorator
20*
21* usage:
22*    @Observed class ClassA { ... }
23*
24* Causes every instance of decorated clss to be automatically wrapped inside an ObservedObject.
25*
26* Implemented by extending the decroaetd class by class named 'ObservableObjectClass'.
27*
28* It is permisstable to decorate the base and the extended class like thisNote: I
29*   @Observed class ClassA { ...}
30*   @Observed class ClassB extends ClassA { ... }
31* and use
32*   a = new ClassA();
33*   b = new ClassB();
34* Only one ES6 Proxy is added.
35*
36*
37* Take note the decorator implementation extends the prototype chain.
38*
39* The prototype chain of a in above example is
40*  - ObservableObjectClass prototype
41*  - ClassA prototype
42*  - Object prototype
43*
44* Snd the prototype chain of b is
45*  - ObservableObjectClass prototype
46*  - ClassB prototype
47*  - ObservableObjectClass prototype
48*  - ClassA prototype
49*  - Object prototype
50*
51* The @Observed decorator is public, part of the SDK, starting from API 9.
52*
53*/
54// define just once to get just one Symbol
55const __IS_OBSERVED_PROXIED = Symbol("_____is_observed_proxied__");
56
57function Observed(constructor_: any, _?: any): any {
58  stateMgmtConsole.debug(`@Observed class decorator: Overwriting constructor for '${constructor_.name}', gets wrapped inside ObservableObject proxy.`);
59  let ObservedClass = class extends constructor_ {
60    constructor(...args: any) {
61      super(...args);
62      stateMgmtConsole.debug(`@Observed '${constructor_.name}' modified constructor.`);
63      let isProxied = Reflect.has(this, __IS_OBSERVED_PROXIED);
64      Object.defineProperty(this, __IS_OBSERVED_PROXIED, {
65        value: true,
66        enumerable: false,
67        configurable: false,
68        writable: false
69      });
70      if (isProxied) {
71        stateMgmtConsole.debug(`   ... new '${constructor_.name}', is proxied already`);
72        return this;
73      } else {
74        stateMgmtConsole.debug(`   ... new '${constructor_.name}', wrapping inside ObservedObject proxy`);
75        return ObservedObject.createNewInternal(this, undefined);
76      }
77    }
78  };
79  return ObservedClass;
80}
81
82// force tsc to generate the __decorate data structure needed for @Observed
83// tsc will not generate unless the @Observed class decorator is used at least once
84@Observed class __IGNORE_FORCE_decode_GENERATION__ { }
85
86
87/**
88 * class ObservedObject and supporting Handler classes,
89 * Extends from ES6 Proxy. In adding to 'get' and 'set'
90 * the clasess manage subscribers that receive notification
91 * about proxies object being 'read' or 'changed'.
92 *
93 * These classes are framework internal / non-SDK
94 *
95 */
96
97class SubscribableHandler {
98  static readonly SUBSCRIBE = Symbol("_____subscribe__");
99  static readonly UNSUBSCRIBE = Symbol("_____unsubscribe__")
100
101  private owningProperties_: Set<number>;
102
103  constructor(owningProperty: IPropertySubscriber) {
104    this.owningProperties_ = new Set<number>();
105    if (owningProperty) {
106      this.addOwningProperty(owningProperty);
107    }
108    stateMgmtConsole.debug(`SubscribableHandler: constructor done`);
109  }
110
111  addOwningProperty(subscriber: IPropertySubscriber): void {
112    if (subscriber) {
113      stateMgmtConsole.debug(`SubscribableHandler: addOwningProperty: subscriber '${subscriber.id__()}'.`)
114      this.owningProperties_.add(subscriber.id__());
115    } else {
116      stateMgmtConsole.warn(`SubscribableHandler: addOwningProperty: undefined subscriber. - Internal error?`);
117    }
118  }
119
120  /*
121      the inverse function of createOneWaySync or createTwoWaySync
122    */
123  public removeOwningProperty(property: IPropertySubscriber): void {
124    return this.removeOwningPropertyById(property.id__());
125  }
126
127  public removeOwningPropertyById(subscriberId: number): void {
128    stateMgmtConsole.debug(`SubscribableHandler: removeOwningProperty '${subscriberId}'.`)
129    this.owningProperties_.delete(subscriberId);
130  }
131
132
133  protected notifyObjectPropertyHasChanged(propName: string, newValue: any) {
134    stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasChanged '${propName}'.`)
135    this.owningProperties_.forEach((subscribedId) => {
136      var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId)
137      if (owningProperty) {
138        if ('objectPropertyHasChangedPU' in owningProperty) {
139          // PU code path
140          (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasChangedPU(this, propName);
141        }
142
143        // FU code path
144        if ('hasChanged' in owningProperty) {
145          (owningProperty as ISinglePropertyChangeSubscriber<any>).hasChanged(newValue);
146        }
147        if ('propertyHasChanged' in owningProperty) {
148          (owningProperty as IMultiPropertiesChangeSubscriber).propertyHasChanged(propName);
149        }
150      } else {
151        stateMgmtConsole.warn(`SubscribableHandler: notifyObjectPropertyHasChanged: unknown subscriber.'${subscribedId}' error!.`);
152      }
153    });
154  }
155
156
157  protected notifyObjectPropertyHasBeenRead(propName: string) {
158    stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasBeenRead '${propName}'.`)
159    this.owningProperties_.forEach((subscribedId) => {
160      var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId)
161      if (owningProperty) {
162        // PU code path
163        if ('objectPropertyHasBeenReadPU' in owningProperty) {
164          (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasBeenReadPU(this, propName);
165        }
166      }
167    });
168  }
169
170  public has(target: Object, property: PropertyKey) : boolean {
171    stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`);
172    return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property);
173  }
174
175  public get(target: Object, property: PropertyKey, receiver?: any): any {
176    stateMgmtConsole.debug(`SubscribableHandler: get '${property.toString()}'.`);
177    return (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) ? target : Reflect.get(target, property, receiver);
178  }
179
180  public set(target: Object, property: PropertyKey, newValue: any): boolean {
181    switch (property) {
182      case SubscribableHandler.SUBSCRIBE:
183        // assignment obsObj[SubscribableHandler.SUBSCRCRIBE] = subscriber
184        this.addOwningProperty(newValue as IPropertySubscriber);
185        return true;
186        break;
187      case SubscribableHandler.UNSUBSCRIBE:
188        // assignment obsObj[SubscribableHandler.UNSUBSCRCRIBE] = subscriber
189        this.removeOwningProperty(newValue as IPropertySubscriber);
190        return true;
191        break;
192      default:
193        if (Reflect.get(target, property) == newValue) {
194          return true;
195        }
196        stateMgmtConsole.debug(`SubscribableHandler: set '${property.toString()}'.`);
197        Reflect.set(target, property, newValue);
198        this.notifyObjectPropertyHasChanged(property.toString(), newValue);
199        return true;
200        break;
201    }
202
203    // unreachable
204    return false;
205  }
206}
207
208class SubscribableDateHandler extends SubscribableHandler {
209
210  constructor(owningProperty: IPropertySubscriber) {
211    super(owningProperty);
212  }
213
214  /**
215   * Get trap for Date type proxy
216   * Functions that modify Date in-place are intercepted and replaced with a function
217   * that executes the original function and notifies the handler of a change.
218   * @param target Original Date object
219   * @param property
220   * @returns
221   */
222  public get(target: Object, property: PropertyKey): any {
223
224    const dateSetFunctions = new Set(["setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds",
225      "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", "setUTCHours", "setUTCMinutes",
226      "setUTCSeconds", "setUTCMilliseconds"]);
227
228    let ret = super.get(target, property);
229
230    if (typeof ret === "function" && property.toString() && dateSetFunctions.has(property.toString())) {
231      const self = this;
232      return function () {
233        // execute original function with given arguments
234        let result = ret.apply(this, arguments);
235        self.notifyObjectPropertyHasChanged(property.toString(), this);
236        return result;
237      }.bind(target) // bind "this" to target inside the function
238    } else if (typeof ret === "function") {
239      ret = ret.bind(target);
240    }
241    return ret;
242  }
243}
244
245
246class ExtendableProxy {
247  constructor(obj: Object, handler: SubscribableHandler) {
248    return new Proxy(obj, handler);
249  }
250}
251
252class ObservedObject<T extends Object> extends ExtendableProxy {
253
254  /**
255   * Factory function for ObservedObjects /
256   *  wrapping of objects for proxying
257   *
258   * @param rawObject unproxied Object or ObservedObject
259   * @param objOwner owner of this Object to sign uop for propertyChange
260   *          notifications
261   * @returns the rawObject if object is already an ObservedObject,
262   *          otherwise the newly created ObservedObject
263   */
264  public static createNew<T extends Object>(rawObject: T,
265    owningProperty: IPropertySubscriber): T {
266
267    if (rawObject === null || rawObject === undefined) {
268      stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`);
269      return rawObject;
270    }
271
272    if (ObservedObject.IsObservedObject(rawObject)) {
273      ObservedObject.addOwningProperty(rawObject, owningProperty);
274      return rawObject;
275    }
276
277    return ObservedObject.createNewInternal<T>(rawObject, owningProperty);
278  }
279
280  public static createNewInternal<T extends Object>(rawObject: T,
281    owningProperty: IPropertySubscriber): T {
282
283    let proxiedObject = new ObservedObject<T>(rawObject,
284      Array.isArray(rawObject) ? new class extends SubscribableHandler {
285        // In-place array modification functions
286        // splice is also in-place modifying function, but we need to handle separately
287        private readonly inPlaceModifications: Set<string> = new Set(["copyWithin", "fill", "reverse", "sort"]);
288
289        constructor(owningProperty: IPropertySubscriber) {
290          super(owningProperty);
291        }
292
293        public get(target: Object, property: PropertyKey, receiver: any): any {
294          let ret = super.get(target, property, receiver);
295          if (ret && typeof ret === "function") {
296            const self = this;
297            const prop = property.toString();
298            // prop is the function name here
299            if (prop == "splice") {
300              // 'splice' self modifies the array, returns deleted array items
301              // means, alike other self-modifying functions, splice does not return the array itself.
302              return function () {
303                const result = ret.apply(target, arguments);
304                // prop is the function name here
305                // and result is the function return value
306                // functinon modifies none or more properties
307                self.notifyObjectPropertyHasChanged(prop, target);
308                return result;
309              }.bind(proxiedObject);
310            }
311
312            if (self.inPlaceModifications.has(prop)) {
313              // in place modfication function result == target, the raw array modified
314              stateMgmtConsole.debug("return self mod function");
315              return function () {
316                const result = ret.apply(target, arguments);
317
318                // 'result' is the unproxied object
319                // functinon modifies none or more properties
320                self.notifyObjectPropertyHasChanged(prop, result);
321
322                // returning the 'proxiedObject' ensures that when chain calls also 2nd function call
323                // operates on the proxied object.
324                return proxiedObject;
325              }.bind(proxiedObject);
326            }
327
328            // binding the proxiedObject ensures that modifying functions like push() operate on the
329            // proxied array and each array change is notified.
330            return ret.bind(proxiedObject);
331          }
332
333          return ret;
334        }
335      }(owningProperty) // SubscribableArrayHandlerAnonymous
336        : (rawObject instanceof Date)
337          ? new SubscribableDateHandler(owningProperty)
338          : new SubscribableHandler(owningProperty),
339      owningProperty);
340
341    return proxiedObject as T;
342  }
343
344  /*
345    Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy
346    no set observation, no notification of changes!
347    Use with caution, do not store any references
348  */
349  static GetRawObject<T extends Object>(obj: T): T {
350    return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT];
351  }
352
353  /**
354   *
355   * @param obj anything
356   * @returns true if the parameter is an Object wrpped with a ObservedObject
357   * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use
358   * this static function instead.
359   */
360  static IsObservedObject(obj: any): boolean {
361    return (obj && (typeof obj === "object") && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT));
362  }
363
364  /**
365   * add a subscriber to given ObservedObject
366   * due to the proxy nature this static method approach needs to be used instead of a member
367   * function
368   * @param obj
369   * @param subscriber
370   * @returns false if given object is not an ObservedObject
371   */
372  public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean {
373    if (!ObservedObject.IsObservedObject(obj) || subscriber==undefined) {
374      return false;
375    }
376
377    obj[SubscribableHandler.SUBSCRIBE] = subscriber;
378    return true;
379  }
380
381  /**
382   * remove a subscriber to given ObservedObject
383   * due to the proxy nature this static method approach needs to be used instead of a member
384   * function
385   * @param obj
386   * @param subscriber
387   * @returns false if given object is not an ObservedObject
388   */
389  public static removeOwningProperty(obj: Object,
390    subscriber: IPropertySubscriber): boolean {
391    if (!ObservedObject.IsObservedObject(obj)) {
392      return false;
393    }
394
395    obj[SubscribableHandler.UNSUBSCRIBE] = subscriber;
396    return true;
397  }
398
399  /**
400   * Utility function for debugging the prototype chain of given Object
401   * The given object can be any Object, it is not required to be an ObservedObject
402   * @param object
403   * @returns multi-line string containing info about the prototype chain
404   * on class in class hiararchy per line
405   */
406  public static tracePrototypeChainOfObject(object: Object | undefined): string {
407    let proto = Object.getPrototypeOf(object);
408    let result = "";
409    let sepa = "";
410    while (proto) {
411      result += `${sepa}${ObservedObject.tracePrototype(proto)}`;
412      proto = Object.getPrototypeOf(proto);
413      sepa = ",\n";
414    }
415
416    return result;
417  }
418
419  /**
420   * Utility function for debugging all functions of given Prototype.
421   * @returns string containing containing names of all functions and members of given Prototype
422   */
423  public static tracePrototype(proto: any) {
424    if (!proto) {
425      return "";
426    }
427
428    let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `;
429    let sepa = "";
430    for (let name of Object.getOwnPropertyNames(proto)) {
431      result += `${sepa}${name}`;
432      sepa = ", ";
433    };
434    return result;
435  }
436
437
438  /**
439   * @Observed  decorator extends the decorated class. This function returns the prototype of the decorated class
440   * @param proto
441   * @returns prototype of the @Observed decorated class or 'proto' parameter if not  @Observed decorated
442   */
443  public static getPrototypeOfObservedClass(proto: Object): Object {
444    return (proto.constructor && proto.constructor.name == "ObservedClass")
445      ? Object.getPrototypeOf(proto.constructor.prototype)
446      : proto;
447  }
448
449
450  /**
451   * To create a new ObservableObject use CreateNew function
452   *
453   * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged
454   * notifications
455   * @param obj  raw Object, if obj is a ObservableOject throws an error
456   * @param objectOwner
457   */
458  private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) {
459    super(obj, handler);
460
461    if (ObservedObject.IsObservedObject(obj)) {
462      stateMgmtConsole.error("ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already");
463    }
464    if (objectOwningProperty != undefined) {
465      this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty;
466    }
467  } // end of constructor
468
469  public static readonly __IS_OBSERVED_OBJECT = Symbol("_____is_observed_object__");
470  public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol("_____raw_object__");
471}
472