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