• 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
59type Constructor = { new(...args: any[]): any };
60
61function Observed<T extends Constructor>(BaseClass: T): T {
62  stateMgmtConsole.debug(`@Observed class decorator: Overwriting constructor for '${BaseClass.name}', gets wrapped inside ObservableObject proxy.`);
63
64  // prevent use of V1 @Track inside V2 @ObservedV2 class
65  if (BaseClass.prototype && Reflect.has(BaseClass.prototype, ObserveV2.SYMBOL_REFS)) {
66    const error = `'@Observed class ${BaseClass?.name}': invalid use of V1 @Track decorator inside V2 @ObservedV2 class. Need to fix class definition to use @Track.`;
67    stateMgmtConsole.error(error);
68    throw new Error(error);
69  }
70
71  return class extends BaseClass {
72    constructor(...args: any) {
73      super(...args);
74      stateMgmtConsole.debug(`@Observed '${BaseClass.name}' modified constructor.`);
75      ConfigureStateMgmt.instance.usingPUObservedTrack(`@Observed`, BaseClass.name);
76      let isProxied = Reflect.has(this, __IS_OBSERVED_PROXIED);
77      Object.defineProperty(this, __IS_OBSERVED_PROXIED, {
78        value: true,
79        enumerable: false,
80        configurable: false,
81        writable: false
82      });
83      if (isProxied) {
84        stateMgmtConsole.debug(`   ... new '${BaseClass.name}', is proxied already`);
85        return this;
86      } else {
87        stateMgmtConsole.debug(`   ... new '${BaseClass.name}', wrapping inside ObservedObject proxy`);
88        return ObservedObject.createNewInternal(this, undefined);
89      }
90    }
91  };
92}
93
94/**
95 * class ObservedObject and supporting Handler classes,
96 * Extends from ES6 Proxy. In adding to 'get' and 'set'
97 * the clasess manage subscribers that receive notification
98 * about proxies object being 'read' or 'changed'.
99 *
100 * These classes are framework internal / non-SDK
101 *
102 */
103
104type PropertyReadCbFunc = (readObject: Object, readPropName: string, isTracked: boolean) => void;
105
106class SubscribableHandler {
107  static readonly SUBSCRIBE = Symbol('_____subscribe__');
108  static readonly UNSUBSCRIBE = Symbol('_____unsubscribe__');
109  static readonly COUNT_SUBSCRIBERS = Symbol('____count_subscribers__');
110  static readonly SET_ONREAD_CB = Symbol('_____set_onread_cb__');
111  static readonly RAW_THIS = Symbol('_____raw_this');
112  static readonly ENABLE_V2_COMPATIBLE = Symbol('_____enablev2_compatible');
113  static readonly MAKE_V1_OBSERVED = Symbol('___makev1_observed__');
114
115  private owningProperties_: Set<number>;
116  private readCbFunc_?: PropertyReadCbFunc;
117  private obSelf_?: ObservedPropertyAbstractPU<any>;
118  protected enableV2Compatible_ : boolean;
119
120  constructor(owningProperty: IPropertySubscriber) {
121    this.owningProperties_ = new Set<number>();
122
123    if (owningProperty) {
124      this.addOwningProperty(owningProperty);
125    }
126    this.enableV2Compatible_ = false;
127    stateMgmtConsole.debug(`SubscribableHandler: constructor done`);
128  }
129
130  protected isPropertyTracked(obj: Object, property: string): boolean {
131    return Reflect.has(obj, `___TRACKED_${property}`) ||
132      property === TrackedObject.___TRACKED_OPTI_ASSIGNMENT_FAKE_PROP_PROPERTY ||
133      property === TrackedObject.___TRACKED_OPTI_ASSIGNMENT_FAKE_OBJLINK_PROPERTY;
134  }
135
136  addOwningProperty(subscriber: IPropertySubscriber): void {
137    if (subscriber) {
138      stateMgmtConsole.debug(`SubscribableHandler: addOwningProperty: subscriber '${subscriber.id__()}'.`);
139      this.owningProperties_.add(subscriber.id__());
140    } else {
141      stateMgmtConsole.warn(`SubscribableHandler: addOwningProperty: undefined subscriber.`);
142    }
143  }
144
145  /*
146    the inverse function of createOneWaySync or createTwoWaySync
147   */
148  public removeOwningProperty(property: IPropertySubscriber): void {
149    return this.removeOwningPropertyById(property.id__());
150  }
151
152  public removeOwningPropertyById(subscriberId: number): void {
153    stateMgmtConsole.debug(`SubscribableHandler: removeOwningProperty '${subscriberId}'.`);
154    this.owningProperties_.delete(subscriberId);
155  }
156
157  protected notifyObjectPropertyHasChanged(propName: string, newValue: any) {
158    stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasChanged '${propName}'.`);
159    this.owningProperties_.forEach((subscribedId) => {
160      const owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId);
161      if (!owningProperty) {
162        stateMgmtConsole.warn(`SubscribableHandler: notifyObjectPropertyHasChanged: unknown subscriber.'${subscribedId}' error!.`);
163        return;
164      }
165
166      // PU code path
167      if ('onTrackedObjectPropertyCompatModeHasChangedPU' in owningProperty) {
168        (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).onTrackedObjectPropertyCompatModeHasChangedPU(this, propName);
169        return;
170      }
171
172      // FU code path
173      if ('hasChanged' in owningProperty) {
174        (owningProperty as ISinglePropertyChangeSubscriber<any>).hasChanged(newValue);
175      }
176      if ('propertyHasChanged' in owningProperty) {
177        (owningProperty as IMultiPropertiesChangeSubscriber).propertyHasChanged(propName);
178      }
179    });
180  }
181
182  protected notifyTrackedObjectPropertyHasChanged(propName: string): void {
183    stateMgmtConsole.debug(`SubscribableHandler: notifyTrackedObjectPropertyHasChanged '@Track ${propName}'.`);
184    this.owningProperties_.forEach((subscribedId) => {
185      const owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId);
186      if (owningProperty && 'onTrackedObjectPropertyHasChangedPU' in owningProperty) {
187        // PU code path with observed object property change tracking optimization
188        (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).onTrackedObjectPropertyHasChangedPU(this, propName);
189      } else {
190        stateMgmtConsole.warn(`SubscribableHandler: notifyTrackedObjectPropertyHasChanged: subscriber.'${subscribedId}' lacks method 'trackedObjectPropertyHasChangedPU' internal error!.`);
191      }
192    });
193    // no need to support FU code path when app uses @Track
194  }
195
196  public has(target: Object, property: PropertyKey): boolean {
197    stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`);
198    return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property);
199  }
200
201  public get(target: Object, property: PropertyKey, receiver?: any): any {
202    // Optimizes get operations by handling symbol properties separately
203    // This allows non-symbol properties to bypass the switch block, improving performance
204      if (typeof property === 'symbol') {
205        switch (property) {
206          case ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT:
207            return target;
208          case SubscribableHandler.COUNT_SUBSCRIBERS:
209            return this.owningProperties_.size;
210          case ObserveV2.SYMBOL_REFS:
211          case ObserveV2.V2_DECO_META:
212          case ObserveV2.SYMBOL_MAKE_OBSERVED:
213            // return result unmonitored
214            return Reflect.get(target, property, receiver);
215          case ObserveV2.SYMBOL_PROXY_GET_TARGET:
216            return undefined;
217          case SubscribableHandler.ENABLE_V2_COMPATIBLE:
218            return this.enableV2Compatible_;
219          default:
220            break;
221        }
222      }
223      const result = Reflect.get(target, property, receiver);
224      let propertyStr: string = String(property);
225      if (this.readCbFunc_ && typeof result !== 'function' && this.obSelf_ !== undefined) {
226        let isTracked = this.isPropertyTracked(target, propertyStr);
227        stateMgmtConsole.propertyAccess(`SubscribableHandler: get ObservedObject property '${isTracked ? '@Track ' : ''}${propertyStr}' notifying read.`);
228        this.readCbFunc_.call(this.obSelf_, receiver, propertyStr, isTracked);
229
230        // If the property is tracked and V2 compatibility is enabled,
231        // add dependency view model object for V1V2 compatibility
232        if (isTracked && this.enableV2Compatible_) {
233          ObserveV2.getObserve().addRefV2Compatibility(target, propertyStr);
234
235          // do same as V2 proxy, call to autoProxyObject:
236          // Array, Set, Map length functions fireChange(object, OB_LENGTH)
237          if (typeof result === 'object' && (Array.isArray(result) || result instanceof Set || result instanceof Map)) {
238            ObserveV2.getObserve().addRefV2Compatibility(result, ObserveV2.OB_LENGTH);
239          }
240        }
241      } else {
242        // result is function or in compatibility mode (in compat mode cbFunc will never be set)
243        stateMgmtConsole.propertyAccess(`SubscribableHandler: get ObservedObject property '${propertyStr}' not notifying read.`);
244
245        // add dependency view model object for V1V2 compatibility
246        if (this.enableV2Compatible_ && typeof result !== 'function') {
247          ObserveV2.getObserve().addRefV2Compatibility(target, propertyStr);
248
249          // do same as V2 proxy, call to autoProxyObject:
250          // Array, Set, Map length functions fireChange(object, OB_LENGTH)
251          if (typeof result === 'object' && (Array.isArray(result) || result instanceof Set || result instanceof Map)) {
252            ObserveV2.getObserve().addRefV2Compatibility(result, ObserveV2.OB_LENGTH);
253          }
254        }
255      }
256      return result;
257  }
258
259  public set(target: Object, property: PropertyKey, newValue: any): boolean {
260    // Optimizes set operations by handling symbol properties separately
261    // This allows non-symbol properties to bypass the switch block, improving performance
262    if (typeof property === 'symbol') {
263      switch (property) {
264        case SubscribableHandler.SUBSCRIBE:
265          // assignment obsObj[SubscribableHandler.SUBSCRIBE] = subscriber
266          this.addOwningProperty(newValue as IPropertySubscriber);
267          return true;
268        case SubscribableHandler.UNSUBSCRIBE:
269          // assignment obsObj[SubscribableHandler.UNSUBSCRIBE] = subscriber
270          this.removeOwningProperty(newValue as IPropertySubscriber);
271          return true;
272        case SubscribableHandler.SET_ONREAD_CB:
273          // assignment obsObj[SubscribableHandler.SET_ONREAD_CB] = readCallbackFunc
274          stateMgmtConsole.debug(`SubscribableHandler: setReadingProperty: ${TrackedObject.isCompatibilityMode(target) ? 'not used in compatibility mode' : newValue ? 'set new cb function' : 'unset cb function'}.`);
275          this.readCbFunc_ = TrackedObject.isCompatibilityMode(target) ? undefined : (newValue as (PropertyReadCbFunc | undefined));
276          return true;
277        case SubscribableHandler.RAW_THIS:
278          this.obSelf_ = TrackedObject.isCompatibilityMode(target) ? undefined : newValue;
279          return true;
280        case SubscribableHandler.ENABLE_V2_COMPATIBLE:
281          this.enableV2Compatible_ = true;
282          return true;
283        case ObserveV2.SYMBOL_PROXY_GET_TARGET:
284          // Do nothing, just return
285          return true;
286        default:
287          break;
288      }
289    }
290
291    if (Reflect.get(target, property) === newValue) {
292      return true;
293    }
294
295    Reflect.set(target, property, newValue);
296    const propString = String(property);
297    if (TrackedObject.isCompatibilityMode(target)) {
298      stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '${propString}' (object property tracking compatibility mode).`);
299      this.notifyObjectPropertyHasChanged(propString, newValue);
300    } else {
301      if (this.isPropertyTracked(target, propString)) {
302        stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '@Track ${propString}'.`);
303        this.notifyTrackedObjectPropertyHasChanged(propString);
304
305      } else {
306        stateMgmtConsole.debug(`SubscribableHandler: set ObservedObject property '${propString}' (object property tracking mode) is NOT @Tracked!`);
307        return true;
308      }
309    }
310
311    // mark view model object 'target' property 'propString' as changed
312    // Notify affected elements and ensure its nested objects are V2-compatible
313    if (this.enableV2Compatible_) {
314      ObserveV2.getObserve().fireChange(target, propString);
315      ObservedObject.enableV2CompatibleNoWarn(newValue);
316    }
317    return true;
318  }
319}
320
321
322class SubscribableMapSetHandler extends SubscribableHandler {
323  constructor(owningProperty: IPropertySubscriber) {
324    super(owningProperty);
325  }
326
327  // In-place Map/Set modification functions
328  mutatingFunctions = new Set([
329    /*Map functions*/
330    'set', 'clear', 'delete',
331    /*Set functions*/
332    'add', 'clear', 'delete',
333  ]);
334  proxiedFunctions = new Set([
335    /*Map functions*/
336    'set',
337    /*Set functions*/
338    'add'
339  ]);
340
341  /**
342   * Get trap for Map/Set type proxy
343   * Functions that modify Map/Set in-place are intercepted and replaced with a function
344   * that executes the original function and notifies the handler of a change.
345   * @param target Original Map/Set object
346   * @param property
347   * @param receiver Proxied Map/Set object
348   * @returns
349   */
350  get(target, property, receiver) {
351    if (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) {
352      return target;
353    }
354
355    if (this.enableV2Compatible_) {
356      return this.getV2Compatible(target, property, receiver);
357    }
358
359    //receiver will fail for internal slot methods of Set and Map
360    //So assign the target as receiver in this case.
361    if (property === Symbol.iterator || property === 'size') {
362      receiver = target;
363    }
364
365    let ret = super.get(target, property, receiver);
366    if (ret && typeof ret === 'function') {
367      const self = this;
368      return function () {
369        // execute original function with given arguments
370        const result = ret.apply(target, arguments);
371        if (self.mutatingFunctions.has(property)) {
372          self.notifyObjectPropertyHasChanged(property, target);
373        }
374        // Only calls to inserting items can be chained, so returning the 'proxiedObject'
375        // ensures that when chain calls also 2nd function call operates on the proxied object.
376        // Otherwise return the original result of the function.
377        return self.proxiedFunctions.has(property) ? receiver : result;
378      }.bind(receiver);
379    }
380
381    return ret;
382  }
383
384  // Note: The code of this function is duplicated with an adaptation for
385  // enableV2Compatibility from SetMapProxyHandler.get function
386  private getV2Compatible(target: any, key: string | symbol, receiver: any): any {
387    if (typeof key === 'symbol') {
388      if (key === Symbol.iterator) {
389        // this.getTarget not needed in V2 compat, always is target
390        const conditionalTarget = target;
391        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, SetMapProxyHandler.OB_MAP_SET_ANY_PROPERTY);
392        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
393        return (...args): any => target[key](...args);
394      }
395      if (key === ObserveV2.SYMBOL_PROXY_GET_TARGET) {
396        return undefined;
397      }
398      if (key === SubscribableHandler.ENABLE_V2_COMPATIBLE) {
399        return this.enableV2Compatible_;
400      }
401      return target[key];
402    }
403
404    stateMgmtConsole.debug(`SetMapProxyHandler get key '${key}'`);
405    // this.getTarget not needed in V2 compat, always is target
406    const conditionalTarget = target;
407
408    if (key === 'size') {
409      ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
410      return target[key];
411    }
412
413    // same as in V1, do not like in V2, no V1 autoProxy with V2Compatibility
414    let ret = super.get(target, key, receiver);
415
416    if (typeof (ret) !== 'function') {
417      ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, key);
418      // change from V2 proxy: condition is never true in V2Compat:
419      return ret;
420    }
421
422    if (key === 'has') {
423      return (prop): boolean => {
424        const ret = target.has(prop);
425        if (ret) {
426          ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, prop);
427        } else {
428          ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
429        }
430        return ret;
431      };
432    }
433    if (key === 'delete') {
434      const self = this;
435      return (prop): boolean => {
436        if (target.has(prop)) {
437          const res: boolean = target.delete(prop);
438          ObserveV2.getObserve().fireChange(conditionalTarget, prop);
439          ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH);
440
441          // mutatingFunctions has 'delete'
442          // added for V1 notification
443          self.notifyObjectPropertyHasChanged(key, target);
444          return res;
445        } else {
446          return false;
447        }
448      };
449    }
450    if (key === 'clear') {
451      const self = this;
452      return (): void => {
453        if (target.size > 0) {
454          target.forEach((_, prop) => {
455             ObserveV2.getObserve().fireChange(conditionalTarget, prop.toString(), undefined, true);
456          });
457          target.clear();
458          ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH);
459          ObserveV2.getObserve().fireChange(conditionalTarget, SetMapProxyHandler.OB_MAP_SET_ANY_PROPERTY);
460          // mutatingFunctions has 'clear'
461          // added for V1 notification
462          self.notifyObjectPropertyHasChanged(key, target);
463        }
464      };
465    }
466    if (key === 'keys' || key === 'values' || key === 'entries') {
467      return (): any => {
468        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, SetMapProxyHandler.OB_MAP_SET_ANY_PROPERTY);
469        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
470        return target[key]();
471      };
472    }
473
474    // change from V2 proxy: Sendable types unsupported in V1
475    if (target instanceof Set) {
476      if (key === 'add') {
477        const self = this;
478        return (val): any => {
479          if (target.has(val)) {
480            return receiver;
481          }
482          target.add(val);
483          ObserveV2.getObserve().fireChange(conditionalTarget, val);
484          ObserveV2.getObserve().fireChange(conditionalTarget, SetMapProxyHandler.OB_MAP_SET_ANY_PROPERTY);
485          ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH);
486
487          // mutatingFunctions has 'add'
488          // yes V1 notifies the function name!
489          self.notifyObjectPropertyHasChanged(key, target);
490          ObservedObject.enableV2CompatibleNoWarn(val);
491          return receiver;
492        };
493      }
494
495      if (key === 'forEach') {
496        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
497        return function (callbackFn: (value: any, value2: any, set: Set<any>) => void): any {
498          // need to execute it target because it is the internal function for build-in type, and proxy does not have such slot.
499          // if necessary, addref for each item in Set and also wrap proxy for makeObserved if it is Object.
500          // currently, just execute it in target because there is no Component need to iterate Set, only Array
501          const result = ret.call(target, callbackFn);
502          return result;
503        };
504      }
505      // Bind to receiver ==> functions are observed
506      return (typeof ret === 'function') ? ret.bind(receiver) : ret;
507    }
508
509    // change from V2 proxy: Sendable types unsupported in V1
510    if (target instanceof Map) {
511      if (key === 'get') {
512        return (prop): any => {
513          if (target.has(prop)) {
514            ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, prop);
515          } else {
516            ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
517          }
518          let item = target.get(prop);
519          // change from V2 proxy, this condition is never true in V2Compat
520          // (typeof item === 'object' && this.isMakeObserved_) ? RefInfo.get(item)[RefInfo.MAKE_OBSERVED_PROXY] :
521
522
523          // do same as V2 proxy, call to autoProxyObject:
524          // Array, Set, Map length functions fireChange(object, OB_LENGTH)
525          if (typeof item === 'object' && (Array.isArray(item) || item instanceof Set || item instanceof Map)) {
526            ObserveV2.getObserve().addRefV2Compatibility(item, ObserveV2.OB_LENGTH);
527          }
528          return item;
529        };
530      }
531      if (key === 'set') {
532        const self = this;
533        return (prop, val): any => {
534          if (!target.has(prop)) {
535            target.set(prop, val);
536            ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH);
537          } else if (target.get(prop) !== val) {
538            target.set(prop, val);
539            ObserveV2.getObserve().fireChange(conditionalTarget, prop);
540          }
541          ObserveV2.getObserve().fireChange(conditionalTarget, SetMapProxyHandler.OB_MAP_SET_ANY_PROPERTY);
542
543          // mutatingFunctions has 'set'
544          // added for V1 notification
545          self.notifyObjectPropertyHasChanged(key, target);
546          ObservedObject.enableV2CompatibleNoWarn(val);
547          return receiver;
548        };
549      }
550      if (key === 'forEach') {
551        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
552        return function (callbackFn: (value: any, key: any, map: Map<any, any>) => void): any {
553          // need to execute it target because it is the internal function for build-in type, and proxy does not have such slot.
554          // if necessary, addref for each item in Map and also wrap proxy for makeObserved if it is Object.
555          // currently, just execute it in target because there is no Component need to iterate Map, only Array
556          const result = ret.call(target, callbackFn);
557          return result;
558        };
559      }
560    }
561    // Bind to receiver ==> functions are observed
562    return (typeof ret === 'function') ? ret.bind(receiver) : ret;
563  }
564
565}
566
567class SubscribableDateHandler extends SubscribableHandler {
568
569  constructor(owningProperty: IPropertySubscriber) {
570    super(owningProperty);
571  }
572
573  dateSetFunctions = new Set(['setFullYear', 'setMonth', 'setDate', 'setHours', 'setMinutes', 'setSeconds',
574    'setMilliseconds', 'setTime', 'setUTCFullYear', 'setUTCMonth', 'setUTCDate', 'setUTCHours', 'setUTCMinutes',
575    'setUTCSeconds', 'setUTCMilliseconds']);
576
577  /**
578   * Get trap for Date type proxy
579   * Functions that modify Date in-place are intercepted and replaced with a function
580   * that executes the original function and notifies the handler of a change.
581   * @param target Original Date object
582   * @param property
583   * @returns
584   */
585  public get(target, property): any {
586    let ret = super.get(target, property);
587
588    if (typeof ret === 'function') {
589      const self = this;
590      if (this.dateSetFunctions.has(property)) {
591        return function () {
592          // execute original function with given arguments
593          let result = ret.apply(this, arguments);
594          self.notifyObjectPropertyHasChanged(property.toString(), this);
595          // enableV2Compatibility handling to fire Date change
596          if (self.enableV2Compatible_) {
597            ObserveV2.getObserve().fireChange(target, ObjectProxyHandler.OB_DATE);
598          }
599
600          return result;
601          // bind 'this' to target inside the function
602        }.bind(target)
603      } else if (self.enableV2Compatible_) {
604        ObserveV2.getObserve().addRefV2Compatibility(target, ObjectProxyHandler.OB_DATE);
605      }
606      return ret.bind(target);
607    }
608    return ret;
609  }
610}
611
612class SubscribableArrayHandler extends SubscribableHandler {
613  constructor(owningProperty: IPropertySubscriber) {
614    super(owningProperty);
615  }
616
617  // In-place array modification functions
618  mutatingFunctions = new Set(['splice', 'copyWithin', 'fill', 'reverse', 'sort']);
619  // 'splice' and 'pop' self modifies the array, returns deleted array items
620  // means, alike other self-modifying functions, splice does not return the array itself.
621  specialFunctions = new Set(['splice', 'pop']);
622
623  /**
624   * Get trap for Array type proxy
625   * Functions that modify Array in-place are intercepted and replaced with a function
626   * that executes the original function and notifies the handler of a change.
627   * @param target Original Array object
628   * @param property
629   * @param receiver Proxied Array object
630   * @returns
631   */
632  get(target, property, receiver) {
633    if (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) {
634      return target;
635    }
636
637    if (this.enableV2Compatible_) {
638      return this.getV2Compatible(target, property, receiver);
639    }
640
641    let ret = super.get(target, property, receiver);
642    if (ret && typeof ret === 'function') {
643      const self = this;
644      const prop = property.toString();
645      if (self.mutatingFunctions.has(prop)) {
646        return function () {
647          const result = ret.apply(target, arguments);
648          // prop is the function name here
649          // and result is the function return value
650          // function modifies none or more properties
651          self.notifyObjectPropertyHasChanged(prop, self.specialFunctions.has(prop) ? target : result);
652          // returning the 'receiver(proxied object)' ensures that when chain calls also 2nd function call
653          // operates on the proxied object.
654          return self.specialFunctions.has(prop) ? result : receiver;
655        }.bind(receiver);
656      }
657      // binding the proxiedObject ensures that modifying functions like push() operate on the
658      // proxied array and each array change is notified.
659      return ret.bind(receiver);
660    }
661    return ret;
662  }
663
664  // Note: This function's implementation is similar to ArrayProxyHandler.get method
665  // to support the enableV2Compatibility for arrays of observed objects.
666  private getV2Compatible(target: any, key: string | symbol, receiver: any): any {
667    if (typeof key === 'symbol') {
668      if (key === Symbol.iterator) {
669        const conditionalTarget = target;
670        ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
671        return (...args): any => target[key](...args);
672      }
673      if (key === ObserveV2.SYMBOL_PROXY_GET_TARGET) {
674        return undefined;
675      }
676      if (key === SubscribableHandler.ENABLE_V2_COMPATIBLE) {
677        return this.enableV2Compatible_;
678      }
679      return target[key];
680    }
681
682    const conditionalTarget = target;
683
684    let ret = super.get(target, key, receiver);
685
686    if (key === 'length') {
687      ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
688      return ret;
689    }
690
691    if (typeof ret !== 'function') {
692      ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, key);
693
694      // do same as V2 proxy, call to autoProxyObject:
695      // Array, Set, Map length functions fireChange(object, OB_LENGTH)
696      if (typeof ret === 'object' && (Array.isArray(ret) || ret instanceof Set || ret instanceof Map)) {
697        ObserveV2.getObserve().addRefV2Compatibility(ret, ObserveV2.OB_LENGTH);
698      }
699
700      return ret;
701    }
702
703    if (ArrayProxyHandler.arrayMutatingFunctions.has(key)) {
704      const self = this;
705      return function (...args): any {
706        // potential compat issue with pure V1
707        // get above uses bind(receiver) for specific functions
708        // causes array changes made by the function are noticed by the proxy
709        const result = ret.call(target, ...args);
710        ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH);
711
712        // addRefV2Compatibility on newly added V1 observed objects
713        args.forEach(arg => {
714          ObservedObject.enableV2CompatibleNoWarn(arg);
715        });
716
717        // v1 handling to notify property change in pure V1 case
718        self.notifyObjectPropertyHasChanged(key, self.specialFunctions.has(key) ? target : result);
719
720        // returning the 'receiver(proxied object)' ensures that when chain calls also 2nd function call
721        // operates on the proxied object.
722        return receiver;
723      };
724    } else if (ArrayProxyHandler.arrayLengthChangingFunctions.has(key)) {
725      const self = this;
726      return function (...args): any {
727        // get above 'get' uses bind(receiver) which causes array changes made by the
728        // function to be noticed by the proxy. Is this causing compat issue with
729        // pure V1?
730
731        // To detect actual changed range, Repeat needs original length before changes
732        // Also copy the args in case they are changed in 'ret' execution
733        const repeatArgs = (key === 'splice') ? [target.length, ...args] : [...args];
734
735        const result = ret.call(target, ...args);
736
737        const excludeSet: Set<number> | undefined = ArrayProxyHandler.tryFastRelayout(conditionalTarget, key,
738          repeatArgs);
739        ObserveV2.getObserve().fireChange(conditionalTarget, ObserveV2.OB_LENGTH, excludeSet);
740
741        // apply enableV2CompatibleNoWarn on newly added V1 observed objects.
742        args.forEach(arg => {
743          ObservedObject.enableV2CompatibleNoWarn(arg);
744        });
745
746        // v1 handling to notify property change in pure V1 case
747        self.notifyObjectPropertyHasChanged(key, self.specialFunctions.has(key) ? target : result);
748        return result;
749      };
750    } else if (!SendableType.isArray(target)) {
751      return ret.bind(receiver);
752    } else if (key === 'forEach') {
753      // V1 does not support Sendable
754      // the following seems dead code
755      ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, ObserveV2.OB_LENGTH);
756      return function (callbackFn: (value: any, index: number, array: Array<any>) => void): any {
757        const result = ret.call(target, (value: any, index: number, array: Array<any>) => {
758          // Collections.Array will report BusinessError: The foreach cannot be bound if call "receiver".
759          // because the passed parameter is not the instance of the container class.
760          // so we must call "target" here to deal with the collections situations.
761          // But we also need to addref for each index.
762          ObserveV2.getObserve().addRefV2Compatibility(conditionalTarget, index.toString());
763          callbackFn(typeof value === 'object' ? RefInfo.get(value).proxy : value, index, receiver);
764        });
765        return result;
766      };
767    } else {
768      return ret.bind(target);
769    }
770  }
771
772  // Introduced a separate set function in ArrayHandlers to reduce multiple condition checks, specifically
773  // for handling fireChange on array length changes.
774  // If property is a symbol, super.set gets called used to manage all symbol-related cases.
775  public set(target: Array<any>, property: PropertyKey, newValue: any): boolean {
776
777    if (typeof property === 'symbol') {
778      // Handle symbols using the parent class
779      return super.set(target, property, newValue);
780    }
781
782    let oldArrayLength: number | undefined;
783
784    if (Reflect.get(target, property) === newValue) {
785      return true;
786    }
787    if (this.enableV2Compatible_) {
788      oldArrayLength = target.length;
789    }
790
791    Reflect.set(target, property, newValue);
792    const propString = String(property);
793
794    if (TrackedObject.isCompatibilityMode(target)) {
795      stateMgmtConsole.debug(`SubscribableArrayHandler: set ObservedObject property '${propString}' (object property tracking compatibility mode).`);
796      this.notifyObjectPropertyHasChanged(propString, newValue);
797    } else {
798      if (this.isPropertyTracked(target, propString)) {
799        stateMgmtConsole.debug(`SubscribableArrayHandler: set ObservedObject property '@Track ${propString}'.`);
800        this.notifyTrackedObjectPropertyHasChanged(propString);
801      } else {
802        stateMgmtConsole.debug(`SubscribableArrayHandler: set ObservedObject property '${propString}' (object property tracking mode) is NOT @Tracked!`);
803        return true;
804      }
805    }
806
807    if (this.enableV2Compatible_) {
808      const arrayLenChanged = target.length !== oldArrayLength;
809      let excludeSet: Set<number> | undefined = ArrayProxyHandler.tryFastRelayout(target, 'set', [property]);
810      ObserveV2.getObserve().fireChange(target, arrayLenChanged ? ObserveV2.OB_LENGTH : propString, excludeSet);
811      ObservedObject.enableV2CompatibleNoWarn(newValue);
812    }
813    //
814    return true;
815  }
816}
817
818
819class ExtendableProxy {
820  constructor(obj: Object, handler: SubscribableHandler) {
821    return new Proxy(obj, handler);
822  }
823}
824
825class ObservedObject<T extends Object> extends ExtendableProxy {
826
827  /**
828   * Factory function for ObservedObjects /
829   *  wrapping of objects for proxying
830   *
831   * @param rawObject unproxied Object or ObservedObject
832   * @param objOwner owner of this Object to sign uop for propertyChange
833   *          notifications
834   * @returns the rawObject if object is already an ObservedObject,
835   *          otherwise the newly created ObservedObject
836   */
837  public static createNew<T extends Object>(rawObject: T,
838    owningProperty: IPropertySubscriber): T {
839
840    if (rawObject === null || rawObject === undefined) {
841      stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`);
842      return rawObject;
843    }
844
845    if (ObservedObject.IsObservedObject(rawObject)) {
846      ObservedObject.addOwningProperty(rawObject, owningProperty);
847      return rawObject;
848    }
849
850    return ObservedObject.createNewInternal<T>(rawObject, owningProperty);
851  }
852
853  public static createNewInternal<T extends Object>(rawObject: T,
854    owningProperty: IPropertySubscriber): T {
855    let proxiedObject;
856    if (rawObject instanceof Map || rawObject instanceof Set) {
857      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableMapSetHandler(owningProperty), owningProperty);
858    }
859    else if (rawObject instanceof Date) {
860      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableDateHandler(owningProperty), owningProperty);
861    }
862    else if (Array.isArray(rawObject)) {
863      proxiedObject = new ObservedObject<T>(rawObject, new SubscribableArrayHandler(owningProperty), owningProperty);
864    }
865    else {
866      proxiedObject = new ObservedObject(rawObject, new SubscribableHandler(owningProperty), owningProperty);
867    }
868    return proxiedObject as T;
869  }
870
871  /*
872    Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy
873    no set observation, no notification of changes!
874    Use with caution, do not store any references
875  */
876  static GetRawObject<T extends Object>(obj: T): T {
877    return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT];
878  }
879
880  /**
881   *
882   * @param obj anything
883   * @returns true if the parameter is an Object wrpped with a ObservedObject
884   * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use
885   * this static function instead.
886   */
887  static IsObservedObject(obj: any): boolean {
888    return (obj && (typeof obj === 'object') && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT));
889  }
890
891  /**
892   * add a subscriber to given ObservedObject
893   * due to the proxy nature this static method approach needs to be used instead of a member
894   * function
895   * @param obj
896   * @param subscriber
897   * @returns false if given object is not an ObservedObject
898   */
899  public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean {
900    if (!ObservedObject.IsObservedObject(obj) || !subscriber) {
901      return false;
902    }
903
904    obj[SubscribableHandler.SUBSCRIBE] = subscriber;
905    return true;
906  }
907
908  /**
909   * remove a subscriber to given ObservedObject
910   * due to the proxy nature this static method approach needs to be used instead of a member
911   * function
912   * @param obj
913   * @param subscriber
914   * @returns false if given object is not an ObservedObject
915   */
916  public static removeOwningProperty(obj: Object,
917    subscriber: IPropertySubscriber): boolean {
918    if (!ObservedObject.IsObservedObject(obj)) {
919      return false;
920    }
921
922    obj[SubscribableHandler.UNSUBSCRIBE] = subscriber;
923    return true;
924  }
925
926
927  /**
928   * Function called from sdk UIUtilsImpl to enable V2 compatibility with V1 component
929   * Marks an observed object as V2-compatible and recursively processes its nested properties
930   *
931   * @param obj - The observed object to be made V2-compatible.
932  */
933  public static enableV2Compatible(obj: Object) : void {
934    // Return if the object is a simple type
935    if (obj === null || obj === undefined || typeof obj !== 'object') {
936      stateMgmtConsole.warn(`enableV2Compatibility: input object must not be null or undefined.`);
937      return;
938    }
939
940    if (!ObservedObject.IsObservedObject(obj)) {
941      stateMgmtConsole.warn(`enableV2Compatibility cannot be applied for an object without V1 observation.`);
942      return;
943    }
944    if (ObserveV2.IsObservedObjectV2(obj) || ObserveV2.IsMakeObserved(obj) || ObserveV2.IsProxiedObservedV2(obj)) {
945      stateMgmtConsole.warn(`enableV2Compatibility cannot be applied for an object with V2 observation already enabled.`);
946      return;
947    }
948
949    this.enableV2CompatibleInternal(obj);
950  }
951
952  // This function behaves like `enableV2Compatible`, but suppresses warnings for non-observed objects,
953  // allowing nested non-observed objects to be processed without triggering logs.
954  public static enableV2CompatibleNoWarn(obj: Object, visitedObjects: Set<Object> = new Set()): void {
955    if (!ObservedObject.IsObservedObject(obj)) {
956      return;
957    }
958
959    if (ObserveV2.IsObservedObjectV2(obj) || ObserveV2.IsMakeObserved(obj) || ObserveV2.IsProxiedObservedV2(obj)) {
960      return;
961    }
962
963    this.enableV2CompatibleInternal(obj, visitedObjects);
964  }
965
966
967 /**
968  * Recursively enables V2 compatibility on the given object and its nested properties.
969  *  If compatibility mode is enabled, it recursively processes the nested object
970  * else, it checks if the object's properties are tracked and recursively processes only those.
971  *
972  * @param obj - The object to be observed for V1 changes.
973  * optional @param visitedObjects: Set object to record if the object is already processed or not
974  *
975  * @returns void
976  *
977  */
978  public static enableV2CompatibleInternal(obj: Object, visitedObjects: Set<Object> = new Set()): void {
979    // If the object has already been visited, return to avoid circular reference issues
980    if (visitedObjects.has(obj)) {
981      return;
982    }
983
984    // Get the unproxied/raw object
985    const rawObj = ObservedObject.GetRawObject(obj);
986
987    // Early return if rawObj is null or not an object
988    if (!rawObj || typeof rawObj !== 'object') {
989      return;
990    }
991    stateMgmtConsole.debug(`enableV2CompatibleInternal object of class '${obj?.constructor?.name}'`);
992    // Mark the object as visited to prevent circular references in future calls
993    visitedObjects.add(obj);
994
995    obj[SubscribableHandler.ENABLE_V2_COMPATIBLE] = true;
996
997    // Recursively process Array elements
998    if (Array.isArray(rawObj)) {
999      rawObj.forEach(item => this.enableV2CompatibleNoWarn(item));
1000    } else if ((rawObj instanceof Map) || (rawObj instanceof Set)) { // Recursively process nested Map values
1001      for (const item of rawObj.values()) {
1002        this.enableV2CompatibleNoWarn(item);
1003      }
1004    } else { // If the object is a plain object, process its values recursively
1005      Object.values(rawObj).forEach(value => this.enableV2CompatibleNoWarn(value));
1006    }
1007  }
1008
1009  // return is given object V1 proxies and V2 compatibility has been enabled on it
1010  public static isEnableV2CompatibleInternal(obj: Object): boolean {
1011    return ObservedObject.IsObservedObject(obj) && (obj[SubscribableHandler.ENABLE_V2_COMPATIBLE] === true);
1012  }
1013
1014
1015  /**
1016   * Enables V1 change observation on the given object, unless it already has V1 or V2 observation enabled.
1017   *
1018   * This function is intended for use inside a @ComponentV2 or plain ArkTS to prepare a viewmodel object
1019   * before passing it to a @Component (V1). If the object is already observed (either via the @Observed decorator,
1020   * V1 observation, or V2 observation), no further observation is applied.
1021   * If the object is an instance of collection set of Array/Map/Set, no further observation is applied.
1022   *
1023   * @param obj - The object to be observed for V1 changes.
1024   * @returns The observed object, or the original object if it is already observed.
1025   *
1026   * report an application warning Throws an error if the object is incompatible with V1 change observation.
1027   */
1028  public static makeV1Observed<T extends Object>(obj: T) : T {
1029    if (obj === null || typeof obj !== 'object') {
1030      stateMgmtConsole.error(`makeV1Observed: input object must not be null or undefined.`);
1031      return obj;
1032    }
1033
1034    if (ObservedObject.IsObservedObject(obj)) {
1035      stateMgmtConsole.warn('makeV1Observed: object is already V1 observed. Nothing to do.');
1036      return obj;
1037    }
1038    if (ObserveV2.IsObservedObjectV2(obj) || ObserveV2.IsMakeObserved(obj) || ObserveV2.IsProxiedObservedV2(obj)) {
1039      stateMgmtConsole.applicationWarn('makeV1Observed: object is V2 observed. makeV1Observed cannot be applied.');
1040      return obj;
1041    }
1042
1043    if (SendableType.isContainer(obj)) {
1044      stateMgmtConsole.applicationWarn('makeV1Observed: Cannot be applied to Map, Set or Array collections.');
1045      return obj;
1046    }
1047
1048    obj[SubscribableHandler.MAKE_V1_OBSERVED] = true;
1049
1050    return ObservedObject.createNew(obj, undefined);
1051  }
1052
1053
1054  // return is given object V1 proxies
1055  public static isMakeV1Observed(obj: Object): boolean {
1056    return (obj[SubscribableHandler.MAKE_V1_OBSERVED] === true);
1057  }
1058
1059  /**
1060   *
1061   * @param obj any Object
1062   * @returns return number of subscribers to the given ObservedObject
1063   * or false if given object is not an ObservedObject
1064   */
1065  public static countSubscribers(obj: Object): number | false {
1066    return ObservedObject.IsObservedObject(obj) ? obj[SubscribableHandler.COUNT_SUBSCRIBERS] : false;
1067  }
1068
1069  /*
1070    set or unset callback function to be called when a property has been called
1071  */
1072  public static registerPropertyReadCb(obj: Object, readPropCb: PropertyReadCbFunc, obSelf: ObservedPropertyAbstractPU<any>): boolean {
1073    if (!ObservedObject.IsObservedObject(obj)) {
1074      return false;
1075    }
1076    obj[SubscribableHandler.SET_ONREAD_CB] = readPropCb;
1077    obj[SubscribableHandler.RAW_THIS] = obSelf;
1078    return true;
1079  }
1080
1081  public static unregisterPropertyReadCb(obj: Object): boolean {
1082    if (!ObservedObject.IsObservedObject(obj)) {
1083      return false;
1084    }
1085    obj[SubscribableHandler.SET_ONREAD_CB] = undefined;
1086    obj[SubscribableHandler.RAW_THIS] = undefined;
1087    return true;
1088  }
1089
1090
1091  /**
1092   * Utility function for debugging the prototype chain of given Object
1093   * The given object can be any Object, it is not required to be an ObservedObject
1094   * @param object
1095   * @returns multi-line string containing info about the prototype chain
1096   * on class in class hiararchy per line
1097   */
1098  public static tracePrototypeChainOfObject(object: Object | undefined): string {
1099    let proto = Object.getPrototypeOf(object);
1100    let result = '';
1101    let sepa = '';
1102    while (proto) {
1103      result += `${sepa}${ObservedObject.tracePrototype(proto)}`;
1104      proto = Object.getPrototypeOf(proto);
1105      sepa = ',\n';
1106    }
1107
1108    return result;
1109  }
1110
1111  /**
1112   * Utility function for debugging all functions of given Prototype.
1113   * @returns string containing containing names of all functions and members of given Prototype
1114   */
1115  public static tracePrototype(proto: any) {
1116    if (!proto) {
1117      return '';
1118    }
1119
1120    let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `;
1121    let sepa = '';
1122    for (let name of Object.getOwnPropertyNames(proto)) {
1123      result += `${sepa}${name}`;
1124      sepa = ', ';
1125    };
1126    return result;
1127  }
1128
1129
1130  /**
1131   * @Observed  decorator extends the decorated class. This function returns the prototype of the decorated class
1132   * @param proto
1133   * @returns prototype of the @Observed decorated class or 'proto' parameter if not  @Observed decorated
1134   */
1135  public static getPrototypeOfObservedClass(proto: Object): Object {
1136    return (proto.constructor && proto.constructor.name === 'ObservedClass')
1137      ? Object.getPrototypeOf(proto.constructor.prototype)
1138      : proto;
1139  }
1140
1141
1142  /**
1143   * To create a new ObservableObject use CreateNew function
1144   *
1145   * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged
1146   * notifications
1147   * @param obj  raw Object, if obj is a ObservableOject throws an error
1148   * @param objectOwner
1149   */
1150  private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) {
1151    super(obj, handler);
1152
1153    if (ObservedObject.IsObservedObject(obj)) {
1154      stateMgmtConsole.error('ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already');
1155    }
1156    if (objectOwningProperty) {
1157      this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty;
1158    }
1159  } // end of constructor
1160
1161  public static readonly __IS_OBSERVED_OBJECT = Symbol('_____is_observed_object__');
1162  public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol('_____raw_object__');
1163}
1164