• 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
55
56// define just once to get just one Symbol
57const __IS_OBSERVED_PROXIED = Symbol("_____is_observed_proxied__");
58
59function Observed(constructor_: any, _?: any): any {
60  stateMgmtConsole.debug(`@Observed class decorator: Overwriting constructor for '${constructor_.name}', gets wrapped inside ObservableObject proxy.`);
61  let ObservedClass = class extends constructor_ {
62    constructor(...args: any) {
63      super(...args);
64      stateMgmtConsole.debug(`@Observed '${constructor_.name}' modified constructor.`);
65      let isProxied = Reflect.has(this, __IS_OBSERVED_PROXIED);
66      Object.defineProperty(this, __IS_OBSERVED_PROXIED, {
67        value: true,
68        enumerable: false,
69        configurable: false,
70        writable: false
71      });
72      if (isProxied) {
73        stateMgmtConsole.debug(`   ... new '${constructor_.name}', is proxied already`);
74        return this;
75      } else {
76        stateMgmtConsole.debug(`   ... new '${constructor_.name}', wrapping inside ObservedObject proxy`);
77        return ObservedObject.createNewInternal(this, undefined);
78      }
79    }
80  };
81  return ObservedClass;
82}
83
84// force tsc to generate the __decorate data structure needed for @Observed
85// tsc will not generate unless the @Observed class decorator is used at least once
86@Observed class __IGNORE_FORCE_decode_GENERATION__ { }
87
88
89/**
90 * class ObservedObject and supporting Handler classes,
91 * Extends from ES6 Proxy. In adding to 'get' and 'set'
92 * the clasess manage subscribers that receive notification
93 * about proxies object being 'read' or 'changed'.
94 *
95 * These classes are framework internal / non-SDK
96 *
97 */
98
99type PropertyReadCbFunc = (readObject: Object, readPropName: string, isTracked: boolean) => void;
100
101class SubscribableHandler {
102  static readonly SUBSCRIBE = Symbol("_____subscribe__");
103  static readonly UNSUBSCRIBE = Symbol("_____unsubscribe__")
104  static readonly COUNT_SUBSCRIBERS = Symbol("____count_subscribers__")
105  static readonly SET_ONREAD_CB = Symbol("_____set_onread_cb__");
106
107  private owningProperties_: Set<number>;
108  private readCbFunc_?: PropertyReadCbFunc;
109
110  constructor(owningProperty: IPropertySubscriber) {
111    this.owningProperties_ = new Set<number>();
112
113    if (owningProperty) {
114      this.addOwningProperty(owningProperty);
115    }
116    stateMgmtConsole.debug(`SubscribableHandler: constructor done`);
117  }
118
119  private isPropertyTracked(obj: Object, property: string): boolean {
120    return Reflect.has(obj, `___TRACKED_${property}`) ||
121      property === TrackedObject.___TRACKED_OPTI_ASSIGNMENT_FAKE_PROP_PROPERTY ||
122      property === TrackedObject.___TRACKED_OPTI_ASSIGNMENT_FAKE_OBJLINK_PROPERTY;
123  }
124
125  addOwningProperty(subscriber: IPropertySubscriber): void {
126    if (subscriber) {
127      stateMgmtConsole.debug(`SubscribableHandler: addOwningProperty: subscriber '${subscriber.id__()}'.`)
128      this.owningProperties_.add(subscriber.id__());
129    } else {
130      stateMgmtConsole.warn(`SubscribableHandler: addOwningProperty: undefined subscriber. - Internal error?`);
131    }
132  }
133
134  /*
135    the inverse function of createOneWaySync or createTwoWaySync
136   */
137  public removeOwningProperty(property: IPropertySubscriber): void {
138    return this.removeOwningPropertyById(property.id__());
139  }
140
141  public removeOwningPropertyById(subscriberId: number): void {
142    stateMgmtConsole.debug(`SubscribableHandler: removeOwningProperty '${subscriberId}'.`)
143    this.owningProperties_.delete(subscriberId);
144  }
145
146  protected notifyObjectPropertyHasChanged(propName: string, newValue: any) {
147    stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasChanged '${propName}'.`)
148    this.owningProperties_.forEach((subscribedId) => {
149      var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId)
150      if (!owningProperty) {
151        stateMgmtConsole.warn(`SubscribableHandler: notifyObjectPropertyHasChanged: unknown subscriber.'${subscribedId}' error!.`);
152        return;
153      }
154
155      // PU code path
156      if ('onTrackedObjectPropertyCompatModeHasChangedPU' in owningProperty) {
157        (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).onTrackedObjectPropertyCompatModeHasChangedPU(this, propName);
158      }
159
160      // FU code path
161      if ('hasChanged' in owningProperty) {
162        (owningProperty as ISinglePropertyChangeSubscriber<any>).hasChanged(newValue);
163      }
164      if ('propertyHasChanged' in owningProperty) {
165        (owningProperty as IMultiPropertiesChangeSubscriber).propertyHasChanged(propName);
166      }
167    });
168  }
169
170  protected notifyTrackedObjectPropertyHasChanged(propName: string): void {
171    stateMgmtConsole.debug(`SubscribableHandler: notifyTrackedObjectPropertyHasChanged '@Track ${propName}'.`)
172    this.owningProperties_.forEach((subscribedId) => {
173      var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId)
174      if (owningProperty && 'onTrackedObjectPropertyHasChangedPU' in owningProperty) {
175        // PU code path with observed object property change tracking optimization
176        (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).onTrackedObjectPropertyHasChangedPU(this, propName);
177      } else {
178        stateMgmtConsole.warn(`SubscribableHandler: notifyTrackedObjectPropertyHasChanged: subscriber.'${subscribedId}' lacks method 'trackedObjectPropertyHasChangedPU' internal error!.`);
179      }
180    });
181    // no need to support FU code path when app uses @Track
182  }
183
184  public has(target: Object, property: PropertyKey): boolean {
185    stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`);
186    return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property);
187  }
188
189  public get(target: Object, property: PropertyKey, receiver?: any): any {
190    switch (property) {
191      case ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT:
192        return target;
193        break;
194      case SubscribableHandler.COUNT_SUBSCRIBERS:
195        return this.owningProperties_.size
196        break;
197      default:
198        const result = Reflect.get(target, property, receiver);
199        let propertyStr : string = String(property);
200        if (this.readCbFunc_ && typeof result !== 'function') {
201          let isTracked = this.isPropertyTracked(target, propertyStr);
202          stateMgmtConsole.debug(`SubscribableHandler: get ObservedObject property '${isTracked ? "@Track " : ""}${propertyStr}' notifying read.`);
203          this.readCbFunc_(receiver, propertyStr, isTracked);
204        } else {
205          // result is function or in compatibility mode (in compat mode cbFunc will never be set)
206          stateMgmtConsole.debug(`SubscribableHandler: get ObservedObject property '${propertyStr}' not notifying read.`);
207        }
208        return result;
209        break;
210    }
211  }
212
213  public set(target: Object, property: PropertyKey, newValue: any): boolean {
214    switch (property) {
215      case SubscribableHandler.SUBSCRIBE:
216        // assignment obsObj[SubscribableHandler.SUBSCRIBE] = subscriber
217        this.addOwningProperty(newValue as IPropertySubscriber);
218        return true;
219        break;
220      case SubscribableHandler.UNSUBSCRIBE:
221        // assignment obsObj[SubscribableHandler.UNSUBSCRIBE] = subscriber
222        this.removeOwningProperty(newValue as IPropertySubscriber);
223        return true;
224        break;
225      case SubscribableHandler.SET_ONREAD_CB:
226        // assignment obsObj[SubscribableHandler.SET_ONREAD_CB] = readCallbackFunc
227        stateMgmtConsole.debug(`SubscribableHandler: setReadingProperty: ${TrackedObject.isCompatibilityMode(target) ? 'not used in compatibility mode' : newValue ? 'set new cb function' : 'unset cb function'}.`);
228        this.readCbFunc_ = TrackedObject.isCompatibilityMode(target) ? undefined : (newValue as (PropertyReadCbFunc | undefined));
229        return true;
230        break;
231      default:
232        // this is added for stability test: Reflect.get target is not object
233        try {
234          if (Reflect.get(target, property) == newValue) {
235            return true;
236          }
237        } catch (error) {
238          ArkTools.print("SubscribableHandler: set", target);
239          stateMgmtConsole.error(`An error occurred in SubscribableHandler set, target type is: ${typeof target}, ${error.message}`);
240          throw error;
241        }
242        Reflect.set(target, property, newValue);
243        const propString = String(property);
244        if (TrackedObject.isCompatibilityMode(target)) {
245          stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '${propString}' (object property tracking compatibility mode).`);
246          this.notifyObjectPropertyHasChanged(propString, newValue);
247        } else {
248          if (this.isPropertyTracked(target, propString)) {
249            stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '@Track ${propString}'.`);
250            this.notifyTrackedObjectPropertyHasChanged(propString);
251          } else {
252            stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '${propString}' (object property tracking mode) is NOT @Tracked!`);
253          }
254        }
255        return true;
256        break;
257    }
258
259    // unreachable
260    return false;
261  }
262}
263
264class SubscribableMapSetHandler extends SubscribableHandler {
265  constructor(owningProperty: IPropertySubscriber) {
266    super(owningProperty);
267  }
268
269  // In-place Map/Set modification functions
270  mutatingFunctions = new Set([
271    /*Map functions*/
272    "set", "clear", "delete",
273    /*Set functions*/
274    "add", "clear", "delete",
275  ]);
276  proxiedFunctions = new Set([
277    /*Map functions*/
278    "set",
279    /*Set functions*/
280    "add"
281  ]);
282
283  /**
284   * Get trap for Map/Set type proxy
285   * Functions that modify Map/Set in-place are intercepted and replaced with a function
286   * that executes the original function and notifies the handler of a change.
287   * @param target Original Map/Set object
288   * @param property
289   * @param receiver Proxied Map/Set object
290   * @returns
291   */
292  get(target, property, receiver) {
293    if (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) {
294      return target;
295    }
296
297    //receiver will fail for internal slot methods of Set and Map
298    //So assign the target as receiver in this case.
299    if (property === Symbol.iterator || property === 'size') {
300      receiver = target;
301    }
302
303    let ret = super.get(target, property, receiver);
304    if (ret && typeof ret === 'function') {
305      const self = this;
306      return function () {
307        // execute original function with given arguments
308        const result = ret.apply(target, arguments);
309        if (self.mutatingFunctions.has(property)) {
310          self.notifyObjectPropertyHasChanged(property, target);
311        }
312        // Only calls to inserting items can be chained, so returning the 'proxiedObject'
313        // ensures that when chain calls also 2nd function call operates on the proxied object.
314        // Otherwise return the original result of the function.
315        return self.proxiedFunctions.has(property) ? receiver : result;
316      }.bind(receiver);
317    }
318
319    return ret;
320  }
321}
322
323class SubscribableDateHandler extends SubscribableHandler {
324
325  constructor(owningProperty: IPropertySubscriber) {
326    super(owningProperty);
327  }
328
329  dateSetFunctions = new Set(["setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds",
330    "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", "setUTCHours", "setUTCMinutes",
331    "setUTCSeconds", "setUTCMilliseconds"]);
332
333  /**
334   * Get trap for Date type proxy
335   * Functions that modify Date in-place are intercepted and replaced with a function
336   * that executes the original function and notifies the handler of a change.
337   * @param target Original Date object
338   * @param property
339   * @returns
340   */
341  public get(target, property): any {
342    let ret = super.get(target, property);
343
344    if (typeof ret === "function") {
345      if (this.dateSetFunctions.has(property)) {
346        const self = this;
347        return function () {
348          // execute original function with given arguments
349          let result = ret.apply(this, arguments);
350          self.notifyObjectPropertyHasChanged(property.toString(), this);
351          return result;
352          // bind "this" to target inside the function
353        }.bind(target)
354      }
355      return ret.bind(target);
356    }
357    return ret;
358  }
359}
360
361class SubscribableArrayHandler extends SubscribableHandler {
362  constructor(owningProperty: IPropertySubscriber) {
363    super(owningProperty);
364  }
365
366  // In-place array modification functions
367  mutatingFunctions = new Set(["splice", "copyWithin", "fill", "reverse", "sort"]);
368  // 'splice' and 'pop' self modifies the array, returns deleted array items
369  // means, alike other self-modifying functions, splice does not return the array itself.
370  specialFunctions = new Set(["splice", "pop"]);
371
372  /**
373   * Get trap for Array type proxy
374   * Functions that modify Array in-place are intercepted and replaced with a function
375   * that executes the original function and notifies the handler of a change.
376   * @param target Original Array object
377   * @param property
378   * @param receiver Proxied Array object
379   * @returns
380   */
381  get(target, property, receiver) {
382    if (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) {
383      return target;
384    }
385
386    let ret = super.get(target, property, receiver);
387    if (ret && typeof ret === "function") {
388      const self = this;
389      const prop = property.toString();
390      if (self.mutatingFunctions.has(prop)) {
391        return function () {
392          const result = ret.apply(target, arguments);
393          // prop is the function name here
394          // and result is the function return value
395          // function modifies none or more properties
396          self.notifyObjectPropertyHasChanged(prop, self.specialFunctions.has(prop) ? target : result);
397          // returning the 'receiver(proxied object)' ensures that when chain calls also 2nd function call
398          // operates on the proxied object.
399          return self.specialFunctions.has(prop) ? result : receiver;
400        }.bind(receiver);
401      }
402      // binding the proxiedObject ensures that modifying functions like push() operate on the
403      // proxied array and each array change is notified.
404      return ret.bind(receiver);
405    }
406    return ret;
407  }
408}
409
410
411class ExtendableProxy {
412  constructor(obj: Object, handler: SubscribableHandler) {
413    return new Proxy(obj, handler);
414  }
415}
416
417class ObservedObject<T extends Object> extends ExtendableProxy {
418
419  /**
420   * Factory function for ObservedObjects /
421   *  wrapping of objects for proxying
422   *
423   * @param rawObject unproxied Object or ObservedObject
424   * @param objOwner owner of this Object to sign uop for propertyChange
425   *          notifications
426   * @returns the rawObject if object is already an ObservedObject,
427   *          otherwise the newly created ObservedObject
428   */
429  public static createNew<T extends Object>(rawObject: T,
430    owningProperty: IPropertySubscriber): T {
431
432    if (rawObject === null || rawObject === undefined) {
433      stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`);
434      return rawObject;
435    }
436
437    if (ObservedObject.IsObservedObject(rawObject)) {
438      ObservedObject.addOwningProperty(rawObject, owningProperty);
439      return rawObject;
440    }
441
442    return ObservedObject.createNewInternal<T>(rawObject, owningProperty);
443  }
444
445  public static createNewInternal<T extends Object>(rawObject: T,
446    owningProperty: IPropertySubscriber): T {
447    let proxiedObject;
448    if (rawObject instanceof Map || rawObject instanceof Set) {
449      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableMapSetHandler(owningProperty), owningProperty);
450    }
451    else if (rawObject instanceof Date) {
452      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableDateHandler(owningProperty), owningProperty);
453    }
454    else if (Array.isArray(rawObject)) {
455      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableArrayHandler(owningProperty), owningProperty);
456    }
457    else {
458      proxiedObject = new ObservedObject(rawObject, new SubscribableHandler(owningProperty), owningProperty);
459    }
460    return proxiedObject as T;
461  }
462
463  /*
464    Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy
465    no set observation, no notification of changes!
466    Use with caution, do not store any references
467  */
468  static GetRawObject<T extends Object>(obj: T): T {
469    return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT];
470  }
471
472  /**
473   *
474   * @param obj anything
475   * @returns true if the parameter is an Object wrpped with a ObservedObject
476   * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use
477   * this static function instead.
478   */
479  static IsObservedObject(obj: any): boolean {
480    return (obj && (typeof obj === "object") && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT));
481  }
482
483  /**
484   * add a subscriber to given ObservedObject
485   * due to the proxy nature this static method approach needs to be used instead of a member
486   * function
487   * @param obj
488   * @param subscriber
489   * @returns false if given object is not an ObservedObject
490   */
491  public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean {
492    if (!ObservedObject.IsObservedObject(obj) || subscriber == undefined) {
493      return false;
494    }
495
496    obj[SubscribableHandler.SUBSCRIBE] = subscriber;
497    return true;
498  }
499
500  /**
501   * remove a subscriber to given ObservedObject
502   * due to the proxy nature this static method approach needs to be used instead of a member
503   * function
504   * @param obj
505   * @param subscriber
506   * @returns false if given object is not an ObservedObject
507   */
508  public static removeOwningProperty(obj: Object,
509    subscriber: IPropertySubscriber): boolean {
510    if (!ObservedObject.IsObservedObject(obj)) {
511      return false;
512    }
513
514    obj[SubscribableHandler.UNSUBSCRIBE] = subscriber;
515    return true;
516  }
517
518  /**
519   *
520   * @param obj any Object
521   * @returns return number of subscribers to the given ObservedObject
522   * or false if given object is not an ObservedObject
523   */
524  public static countSubscribers(obj: Object): number | false {
525    return ObservedObject.IsObservedObject(obj) ? obj[SubscribableHandler.COUNT_SUBSCRIBERS] : false;
526  }
527
528  /*
529    set or unset callback function to be called when a property has been called
530  */
531  public static registerPropertyReadCb(obj: Object, readPropCb: PropertyReadCbFunc): boolean {
532    if (!ObservedObject.IsObservedObject(obj)) {
533      return false;
534    }
535    obj[SubscribableHandler.SET_ONREAD_CB] = readPropCb;
536    return true;
537  }
538
539  public static unregisterPropertyReadCb(obj: Object): boolean {
540    if (!ObservedObject.IsObservedObject(obj)) {
541      return false;
542    }
543    obj[SubscribableHandler.SET_ONREAD_CB] = undefined;
544    return true;
545  }
546
547
548  /**
549   * Utility function for debugging the prototype chain of given Object
550   * The given object can be any Object, it is not required to be an ObservedObject
551   * @param object
552   * @returns multi-line string containing info about the prototype chain
553   * on class in class hiararchy per line
554   */
555  public static tracePrototypeChainOfObject(object: Object | undefined): string {
556    let proto = Object.getPrototypeOf(object);
557    let result = "";
558    let sepa = "";
559    while (proto) {
560      result += `${sepa}${ObservedObject.tracePrototype(proto)}`;
561      proto = Object.getPrototypeOf(proto);
562      sepa = ",\n";
563    }
564
565    return result;
566  }
567
568  /**
569   * Utility function for debugging all functions of given Prototype.
570   * @returns string containing containing names of all functions and members of given Prototype
571   */
572  public static tracePrototype(proto: any) {
573    if (!proto) {
574      return "";
575    }
576
577    let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `;
578    let sepa = "";
579    for (let name of Object.getOwnPropertyNames(proto)) {
580      result += `${sepa}${name}`;
581      sepa = ", ";
582    };
583    return result;
584  }
585
586
587  /**
588   * @Observed  decorator extends the decorated class. This function returns the prototype of the decorated class
589   * @param proto
590   * @returns prototype of the @Observed decorated class or 'proto' parameter if not  @Observed decorated
591   */
592  public static getPrototypeOfObservedClass(proto: Object): Object {
593    return (proto.constructor && proto.constructor.name == "ObservedClass")
594      ? Object.getPrototypeOf(proto.constructor.prototype)
595      : proto;
596  }
597
598
599  /**
600   * To create a new ObservableObject use CreateNew function
601   *
602   * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged
603   * notifications
604   * @param obj  raw Object, if obj is a ObservableOject throws an error
605   * @param objectOwner
606   */
607  private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) {
608    super(obj, handler);
609
610    if (ObservedObject.IsObservedObject(obj)) {
611      stateMgmtConsole.error("ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already");
612    }
613    if (objectOwningProperty != undefined) {
614      this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty;
615    }
616  } // end of constructor
617
618  public static readonly __IS_OBSERVED_OBJECT = Symbol("_____is_observed_object__");
619  public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol("_____raw_object__");
620}
621