• 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  // notify a property has been 'read'
157  // this functionality is in preparation for observed computed variables
158  // enable calling from 'get' trap handler functions to this function once
159  // adding support for observed computed variables
160  protected notifyObjectPropertyHasBeenRead(propName: string) {
161    stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasBeenRead '${propName}'.`)
162    this.owningProperties_.forEach((subscribedId) => {
163      var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId)
164      if (owningProperty) {
165        // PU code path
166        if ('objectPropertyHasBeenReadPU' in owningProperty) {
167          (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasBeenReadPU(this, propName);
168        }
169      }
170    });
171  }
172
173  public has(target: Object, property: PropertyKey) : boolean {
174    stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`);
175    return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property);
176  }
177
178  public get(target: Object, property: PropertyKey, receiver?: any): any {
179    stateMgmtConsole.debug(`SubscribableHandler: get '${property.toString()}'.`);
180    return (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) ? target : Reflect.get(target, property, receiver);
181  }
182
183  public set(target: Object, property: PropertyKey, newValue: any): boolean {
184    switch (property) {
185      case SubscribableHandler.SUBSCRIBE:
186        // assignment obsObj[SubscribableHandler.SUBSCRCRIBE] = subscriber
187        this.addOwningProperty(newValue as IPropertySubscriber);
188        return true;
189        break;
190      case SubscribableHandler.UNSUBSCRIBE:
191        // assignment obsObj[SubscribableHandler.UNSUBSCRCRIBE] = subscriber
192        this.removeOwningProperty(newValue as IPropertySubscriber);
193        return true;
194        break;
195      default:
196        if (Reflect.get(target, property) == newValue) {
197          return true;
198        }
199        stateMgmtConsole.debug(`SubscribableHandler: set '${property.toString()}'.`);
200        Reflect.set(target, property, newValue);
201        this.notifyObjectPropertyHasChanged(property.toString(), newValue);
202        return true;
203        break;
204    }
205
206    // unreachable
207    return false;
208  }
209}
210
211class SubscribableDateHandler extends SubscribableHandler {
212
213  constructor(owningProperty: IPropertySubscriber) {
214    super(owningProperty);
215  }
216
217  /**
218   * Get trap for Date type proxy
219   * Functions that modify Date in-place are intercepted and replaced with a function
220   * that executes the original function and notifies the handler of a change.
221   * @param target Original Date object
222   * @param property
223   * @returns
224   */
225  public get(target: Object, property: PropertyKey): any {
226
227    const dateSetFunctions = new Set(["setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds",
228      "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", "setUTCHours", "setUTCMinutes",
229      "setUTCSeconds", "setUTCMilliseconds"]);
230
231    let ret = super.get(target, property);
232
233    if (typeof ret === "function" && property.toString() && dateSetFunctions.has(property.toString())) {
234      const self = this;
235      return function () {
236        // execute original function with given arguments
237        let result = ret.apply(this, arguments);
238        self.notifyObjectPropertyHasChanged(property.toString(), this);
239        return result;
240      }.bind(target) // bind "this" to target inside the function
241    } else if (typeof ret === "function") {
242      ret = ret.bind(target);
243    }
244    return ret;
245  }
246}
247
248
249class ExtendableProxy {
250  constructor(obj: Object, handler: SubscribableHandler) {
251    return new Proxy(obj, handler);
252  }
253}
254
255class ObservedObject<T extends Object> extends ExtendableProxy {
256
257  /**
258   * Factory function for ObservedObjects /
259   *  wrapping of objects for proxying
260   *
261   * @param rawObject unproxied Object or ObservedObject
262   * @param objOwner owner of this Object to sign uop for propertyChange
263   *          notifications
264   * @returns the rawObject if object is already an ObservedObject,
265   *          otherwise the newly created ObservedObject
266   */
267  public static createNew<T extends Object>(rawObject: T,
268    owningProperty: IPropertySubscriber): T {
269
270    if (rawObject === null || rawObject === undefined) {
271      stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`);
272      return rawObject;
273    }
274
275    if (ObservedObject.IsObservedObject(rawObject)) {
276      ObservedObject.addOwningProperty(rawObject, owningProperty);
277      return rawObject;
278    }
279
280    return ObservedObject.createNewInternal<T>(rawObject, owningProperty);
281  }
282
283  public static createNewInternal<T extends Object>(rawObject: T,
284    owningProperty: IPropertySubscriber): T {
285
286    let proxiedObject = new ObservedObject<T>(rawObject,
287      Array.isArray(rawObject) ? new class extends SubscribableHandler {
288        // In-place array modification functions
289        // splice is also in-place modifying function, but we need to handle separately
290        private readonly inPlaceModifications: Set<string> = new Set(["copyWithin", "fill", "reverse", "sort"]);
291
292        constructor(owningProperty: IPropertySubscriber) {
293          super(owningProperty);
294        }
295
296        public get(target: Object, property: PropertyKey, receiver: any): any {
297          let ret = super.get(target, property, receiver);
298          if (ret && typeof ret === "function") {
299            const self = this;
300            const prop = property.toString();
301            // prop is the function name here
302            if (prop == "splice") {
303              // 'splice' self modifies the array, returns deleted array items
304              // means, alike other self-modifying functions, splice does not return the array itself.
305              return function () {
306                const result = ret.apply(target, arguments);
307                // prop is the function name here
308                // and result is the function return value
309                // functinon modifies none or more properties
310                self.notifyObjectPropertyHasChanged(prop, target);
311                return result;
312              }.bind(proxiedObject);
313            }
314
315            if (self.inPlaceModifications.has(prop)) {
316              // in place modfication function result == target, the raw array modified
317              stateMgmtConsole.debug("return self mod function");
318              return function () {
319                const result = ret.apply(target, arguments);
320
321                // 'result' is the unproxied object
322                // functinon modifies none or more properties
323                self.notifyObjectPropertyHasChanged(prop, result);
324
325                // returning the 'proxiedObject' ensures that when chain calls also 2nd function call
326                // operates on the proxied object.
327                return proxiedObject;
328              }.bind(proxiedObject);
329            }
330
331            // binding the proxiedObject ensures that modifying functions like push() operate on the
332            // proxied array and each array change is notified.
333            return ret.bind(proxiedObject);
334          }
335
336          return ret;
337        }
338      }(owningProperty) // SubscribableArrayHandlerAnonymous
339        : (rawObject instanceof Date)
340          ? new SubscribableDateHandler(owningProperty)
341          : new SubscribableHandler(owningProperty),
342      owningProperty);
343
344    return proxiedObject as T;
345  }
346
347  /*
348    Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy
349    no set observation, no notification of changes!
350    Use with caution, do not store any references
351  */
352  static GetRawObject<T extends Object>(obj: T): T {
353    return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT];
354  }
355
356  /**
357   *
358   * @param obj anything
359   * @returns true if the parameter is an Object wrpped with a ObservedObject
360   * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use
361   * this static function instead.
362   */
363  static IsObservedObject(obj: any): boolean {
364    return (obj && (typeof obj === "object") && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT));
365  }
366
367  /**
368   * add a subscriber to given ObservedObject
369   * due to the proxy nature this static method approach needs to be used instead of a member
370   * function
371   * @param obj
372   * @param subscriber
373   * @returns false if given object is not an ObservedObject
374   */
375  public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean {
376    if (!ObservedObject.IsObservedObject(obj) || subscriber==undefined) {
377      return false;
378    }
379
380    obj[SubscribableHandler.SUBSCRIBE] = subscriber;
381    return true;
382  }
383
384  /**
385   * remove a subscriber to given ObservedObject
386   * due to the proxy nature this static method approach needs to be used instead of a member
387   * function
388   * @param obj
389   * @param subscriber
390   * @returns false if given object is not an ObservedObject
391   */
392  public static removeOwningProperty(obj: Object,
393    subscriber: IPropertySubscriber): boolean {
394    if (!ObservedObject.IsObservedObject(obj)) {
395      return false;
396    }
397
398    obj[SubscribableHandler.UNSUBSCRIBE] = subscriber;
399    return true;
400  }
401
402  /**
403   * Utility function for debugging the prototype chain of given Object
404   * The given object can be any Object, it is not required to be an ObservedObject
405   * @param object
406   * @returns multi-line string containing info about the prototype chain
407   * on class in class hiararchy per line
408   */
409  public static tracePrototypeChainOfObject(object: Object | undefined): string {
410    let proto = Object.getPrototypeOf(object);
411    let result = "";
412    let sepa = "";
413    while (proto) {
414      result += `${sepa}${ObservedObject.tracePrototype(proto)}`;
415      proto = Object.getPrototypeOf(proto);
416      sepa = ",\n";
417    }
418
419    return result;
420  }
421
422  /**
423   * Utility function for debugging all functions of given Prototype.
424   * @returns string containing containing names of all functions and members of given Prototype
425   */
426  public static tracePrototype(proto: any) {
427    if (!proto) {
428      return "";
429    }
430
431    let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `;
432    let sepa = "";
433    for (let name of Object.getOwnPropertyNames(proto)) {
434      result += `${sepa}${name}`;
435      sepa = ", ";
436    };
437    return result;
438  }
439
440
441  /**
442   * @Observed  decorator extends the decorated class. This function returns the prototype of the decorated class
443   * @param proto
444   * @returns prototype of the @Observed decorated class or 'proto' parameter if not  @Observed decorated
445   */
446  public static getPrototypeOfObservedClass(proto: Object): Object {
447    return (proto.constructor && proto.constructor.name == "ObservedClass")
448      ? Object.getPrototypeOf(proto.constructor.prototype)
449      : proto;
450  }
451
452
453  /**
454   * To create a new ObservableObject use CreateNew function
455   *
456   * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged
457   * notifications
458   * @param obj  raw Object, if obj is a ObservableOject throws an error
459   * @param objectOwner
460   */
461  private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) {
462    super(obj, handler);
463
464    if (ObservedObject.IsObservedObject(obj)) {
465      stateMgmtConsole.error("ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already");
466    }
467    if (objectOwningProperty != undefined) {
468      this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty;
469    }
470  } // end of constructor
471
472  public static readonly __IS_OBSERVED_OBJECT = Symbol("_____is_observed_object__");
473  public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol("_____raw_object__");
474}
475