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