1/* 2 * Copyright (c) 2021-2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 17 18/** 19* @Observed class decorator 20* 21* usage: 22* @Observed class ClassA { ... } 23* 24* Causes every instance of decorated clss to be automatically wrapped inside an ObservedObject. 25* 26* Implemented by extending the decroaetd class by class named 'ObservableObjectClass'. 27* 28* It is permisstable to decorate the base and the extended class like thisNote: I 29* @Observed class ClassA { ...} 30* @Observed class ClassB extends ClassA { ... } 31* and use 32* a = new ClassA(); 33* b = new ClassB(); 34* Only one ES6 Proxy is added. 35* 36* 37* Take note the decorator implementation extends the prototype chain. 38* 39* The prototype chain of a in above example is 40* - ObservableObjectClass prototype 41* - ClassA prototype 42* - Object prototype 43* 44* Snd the prototype chain of b is 45* - ObservableObjectClass prototype 46* - ClassB prototype 47* - ObservableObjectClass prototype 48* - ClassA prototype 49* - Object prototype 50* 51* The @Observed decorator is public, part of the SDK, starting from API 9. 52* 53*/ 54// define just once to get just one Symbol 55const __IS_OBSERVED_PROXIED = Symbol("_____is_observed_proxied__"); 56 57function Observed(constructor_: any, _?: any): any { 58 stateMgmtConsole.debug(`@Observed class decorator: Overwriting constructor for '${constructor_.name}', gets wrapped inside ObservableObject proxy.`); 59 let ObservedClass = class extends constructor_ { 60 constructor(...args: any) { 61 super(...args); 62 stateMgmtConsole.debug(`@Observed '${constructor_.name}' modified constructor.`); 63 let isProxied = Reflect.has(this, __IS_OBSERVED_PROXIED); 64 Object.defineProperty(this, __IS_OBSERVED_PROXIED, { 65 value: true, 66 enumerable: false, 67 configurable: false, 68 writable: false 69 }); 70 if (isProxied) { 71 stateMgmtConsole.debug(` ... new '${constructor_.name}', is proxied already`); 72 return this; 73 } else { 74 stateMgmtConsole.debug(` ... new '${constructor_.name}', wrapping inside ObservedObject proxy`); 75 return ObservedObject.createNewInternal(this, undefined); 76 } 77 } 78 }; 79 return ObservedClass; 80} 81 82// force tsc to generate the __decorate data structure needed for @Observed 83// tsc will not generate unless the @Observed class decorator is used at least once 84@Observed class __IGNORE_FORCE_decode_GENERATION__ { } 85 86 87/** 88 * class ObservedObject and supporting Handler classes, 89 * Extends from ES6 Proxy. In adding to 'get' and 'set' 90 * the clasess manage subscribers that receive notification 91 * about proxies object being 'read' or 'changed'. 92 * 93 * These classes are framework internal / non-SDK 94 * 95 */ 96 97class SubscribableHandler { 98 static readonly SUBSCRIBE = Symbol("_____subscribe__"); 99 static readonly UNSUBSCRIBE = Symbol("_____unsubscribe__") 100 101 private owningProperties_: Set<number>; 102 103 constructor(owningProperty: IPropertySubscriber) { 104 this.owningProperties_ = new Set<number>(); 105 if (owningProperty) { 106 this.addOwningProperty(owningProperty); 107 } 108 stateMgmtConsole.debug(`SubscribableHandler: constructor done`); 109 } 110 111 addOwningProperty(subscriber: IPropertySubscriber): void { 112 if (subscriber) { 113 stateMgmtConsole.debug(`SubscribableHandler: addOwningProperty: subscriber '${subscriber.id__()}'.`) 114 this.owningProperties_.add(subscriber.id__()); 115 } else { 116 stateMgmtConsole.warn(`SubscribableHandler: addOwningProperty: undefined subscriber. - Internal error?`); 117 } 118 } 119 120 /* 121 the inverse function of createOneWaySync or createTwoWaySync 122 */ 123 public removeOwningProperty(property: IPropertySubscriber): void { 124 return this.removeOwningPropertyById(property.id__()); 125 } 126 127 public removeOwningPropertyById(subscriberId: number): void { 128 stateMgmtConsole.debug(`SubscribableHandler: removeOwningProperty '${subscriberId}'.`) 129 this.owningProperties_.delete(subscriberId); 130 } 131 132 133 protected notifyObjectPropertyHasChanged(propName: string, newValue: any) { 134 stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasChanged '${propName}'.`) 135 this.owningProperties_.forEach((subscribedId) => { 136 var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId) 137 if (owningProperty) { 138 if ('objectPropertyHasChangedPU' in owningProperty) { 139 // PU code path 140 (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasChangedPU(this, propName); 141 } 142 143 // FU code path 144 if ('hasChanged' in owningProperty) { 145 (owningProperty as ISinglePropertyChangeSubscriber<any>).hasChanged(newValue); 146 } 147 if ('propertyHasChanged' in owningProperty) { 148 (owningProperty as IMultiPropertiesChangeSubscriber).propertyHasChanged(propName); 149 } 150 } else { 151 stateMgmtConsole.warn(`SubscribableHandler: notifyObjectPropertyHasChanged: unknown subscriber.'${subscribedId}' error!.`); 152 } 153 }); 154 } 155 156 // notify a property has been 'read' 157 // this functionality is in preparation for observed computed variables 158 // enable calling from 'get' trap handler functions to this function once 159 // adding support for observed computed variables 160 protected notifyObjectPropertyHasBeenRead(propName: string) { 161 stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasBeenRead '${propName}'.`) 162 this.owningProperties_.forEach((subscribedId) => { 163 var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId) 164 if (owningProperty) { 165 // PU code path 166 if ('objectPropertyHasBeenReadPU' in owningProperty) { 167 (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasBeenReadPU(this, propName); 168 } 169 } 170 }); 171 } 172 173 public has(target: Object, property: PropertyKey) : boolean { 174 stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`); 175 return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property); 176 } 177 178 public get(target: Object, property: PropertyKey, receiver?: any): any { 179 stateMgmtConsole.debug(`SubscribableHandler: get '${property.toString()}'.`); 180 return (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) ? target : Reflect.get(target, property, receiver); 181 } 182 183 public set(target: Object, property: PropertyKey, newValue: any): boolean { 184 switch (property) { 185 case SubscribableHandler.SUBSCRIBE: 186 // assignment obsObj[SubscribableHandler.SUBSCRCRIBE] = subscriber 187 this.addOwningProperty(newValue as IPropertySubscriber); 188 return true; 189 break; 190 case SubscribableHandler.UNSUBSCRIBE: 191 // assignment obsObj[SubscribableHandler.UNSUBSCRCRIBE] = subscriber 192 this.removeOwningProperty(newValue as IPropertySubscriber); 193 return true; 194 break; 195 default: 196 if (Reflect.get(target, property) == newValue) { 197 return true; 198 } 199 stateMgmtConsole.debug(`SubscribableHandler: set '${property.toString()}'.`); 200 Reflect.set(target, property, newValue); 201 this.notifyObjectPropertyHasChanged(property.toString(), newValue); 202 return true; 203 break; 204 } 205 206 // unreachable 207 return false; 208 } 209} 210 211class SubscribableDateHandler extends SubscribableHandler { 212 213 constructor(owningProperty: IPropertySubscriber) { 214 super(owningProperty); 215 } 216 217 /** 218 * Get trap for Date type proxy 219 * Functions that modify Date in-place are intercepted and replaced with a function 220 * that executes the original function and notifies the handler of a change. 221 * @param target Original Date object 222 * @param property 223 * @returns 224 */ 225 public get(target: Object, property: PropertyKey): any { 226 227 const dateSetFunctions = new Set(["setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds", 228 "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", "setUTCHours", "setUTCMinutes", 229 "setUTCSeconds", "setUTCMilliseconds"]); 230 231 let ret = super.get(target, property); 232 233 if (typeof ret === "function" && property.toString() && dateSetFunctions.has(property.toString())) { 234 const self = this; 235 return function () { 236 // execute original function with given arguments 237 let result = ret.apply(this, arguments); 238 self.notifyObjectPropertyHasChanged(property.toString(), this); 239 return result; 240 }.bind(target) // bind "this" to target inside the function 241 } else if (typeof ret === "function") { 242 ret = ret.bind(target); 243 } 244 return ret; 245 } 246} 247 248 249class ExtendableProxy { 250 constructor(obj: Object, handler: SubscribableHandler) { 251 return new Proxy(obj, handler); 252 } 253} 254 255class ObservedObject<T extends Object> extends ExtendableProxy { 256 257 /** 258 * Factory function for ObservedObjects / 259 * wrapping of objects for proxying 260 * 261 * @param rawObject unproxied Object or ObservedObject 262 * @param objOwner owner of this Object to sign uop for propertyChange 263 * notifications 264 * @returns the rawObject if object is already an ObservedObject, 265 * otherwise the newly created ObservedObject 266 */ 267 public static createNew<T extends Object>(rawObject: T, 268 owningProperty: IPropertySubscriber): T { 269 270 if (rawObject === null || rawObject === undefined) { 271 stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`); 272 return rawObject; 273 } 274 275 if (ObservedObject.IsObservedObject(rawObject)) { 276 ObservedObject.addOwningProperty(rawObject, owningProperty); 277 return rawObject; 278 } 279 280 return ObservedObject.createNewInternal<T>(rawObject, owningProperty); 281 } 282 283 public static createNewInternal<T extends Object>(rawObject: T, 284 owningProperty: IPropertySubscriber): T { 285 286 let proxiedObject = new ObservedObject<T>(rawObject, 287 Array.isArray(rawObject) ? new class extends SubscribableHandler { 288 // In-place array modification functions 289 // splice is also in-place modifying function, but we need to handle separately 290 private readonly inPlaceModifications: Set<string> = new Set(["copyWithin", "fill", "reverse", "sort"]); 291 292 constructor(owningProperty: IPropertySubscriber) { 293 super(owningProperty); 294 } 295 296 public get(target: Object, property: PropertyKey, receiver: any): any { 297 let ret = super.get(target, property, receiver); 298 if (ret && typeof ret === "function") { 299 const self = this; 300 const prop = property.toString(); 301 // prop is the function name here 302 if (prop == "splice") { 303 // 'splice' self modifies the array, returns deleted array items 304 // means, alike other self-modifying functions, splice does not return the array itself. 305 return function () { 306 const result = ret.apply(target, arguments); 307 // prop is the function name here 308 // and result is the function return value 309 // functinon modifies none or more properties 310 self.notifyObjectPropertyHasChanged(prop, target); 311 return result; 312 }.bind(proxiedObject); 313 } 314 315 if (self.inPlaceModifications.has(prop)) { 316 // in place modfication function result == target, the raw array modified 317 stateMgmtConsole.debug("return self mod function"); 318 return function () { 319 const result = ret.apply(target, arguments); 320 321 // 'result' is the unproxied object 322 // functinon modifies none or more properties 323 self.notifyObjectPropertyHasChanged(prop, result); 324 325 // returning the 'proxiedObject' ensures that when chain calls also 2nd function call 326 // operates on the proxied object. 327 return proxiedObject; 328 }.bind(proxiedObject); 329 } 330 331 // binding the proxiedObject ensures that modifying functions like push() operate on the 332 // proxied array and each array change is notified. 333 return ret.bind(proxiedObject); 334 } 335 336 return ret; 337 } 338 }(owningProperty) // SubscribableArrayHandlerAnonymous 339 : (rawObject instanceof Date) 340 ? new SubscribableDateHandler(owningProperty) 341 : new SubscribableHandler(owningProperty), 342 owningProperty); 343 344 return proxiedObject as T; 345 } 346 347 /* 348 Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy 349 no set observation, no notification of changes! 350 Use with caution, do not store any references 351 */ 352 static GetRawObject<T extends Object>(obj: T): T { 353 return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT]; 354 } 355 356 /** 357 * 358 * @param obj anything 359 * @returns true if the parameter is an Object wrpped with a ObservedObject 360 * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use 361 * this static function instead. 362 */ 363 static IsObservedObject(obj: any): boolean { 364 return (obj && (typeof obj === "object") && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT)); 365 } 366 367 /** 368 * add a subscriber to given ObservedObject 369 * due to the proxy nature this static method approach needs to be used instead of a member 370 * function 371 * @param obj 372 * @param subscriber 373 * @returns false if given object is not an ObservedObject 374 */ 375 public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean { 376 if (!ObservedObject.IsObservedObject(obj) || subscriber==undefined) { 377 return false; 378 } 379 380 obj[SubscribableHandler.SUBSCRIBE] = subscriber; 381 return true; 382 } 383 384 /** 385 * remove a subscriber to given ObservedObject 386 * due to the proxy nature this static method approach needs to be used instead of a member 387 * function 388 * @param obj 389 * @param subscriber 390 * @returns false if given object is not an ObservedObject 391 */ 392 public static removeOwningProperty(obj: Object, 393 subscriber: IPropertySubscriber): boolean { 394 if (!ObservedObject.IsObservedObject(obj)) { 395 return false; 396 } 397 398 obj[SubscribableHandler.UNSUBSCRIBE] = subscriber; 399 return true; 400 } 401 402 /** 403 * Utility function for debugging the prototype chain of given Object 404 * The given object can be any Object, it is not required to be an ObservedObject 405 * @param object 406 * @returns multi-line string containing info about the prototype chain 407 * on class in class hiararchy per line 408 */ 409 public static tracePrototypeChainOfObject(object: Object | undefined): string { 410 let proto = Object.getPrototypeOf(object); 411 let result = ""; 412 let sepa = ""; 413 while (proto) { 414 result += `${sepa}${ObservedObject.tracePrototype(proto)}`; 415 proto = Object.getPrototypeOf(proto); 416 sepa = ",\n"; 417 } 418 419 return result; 420 } 421 422 /** 423 * Utility function for debugging all functions of given Prototype. 424 * @returns string containing containing names of all functions and members of given Prototype 425 */ 426 public static tracePrototype(proto: any) { 427 if (!proto) { 428 return ""; 429 } 430 431 let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `; 432 let sepa = ""; 433 for (let name of Object.getOwnPropertyNames(proto)) { 434 result += `${sepa}${name}`; 435 sepa = ", "; 436 }; 437 return result; 438 } 439 440 441 /** 442 * @Observed decorator extends the decorated class. This function returns the prototype of the decorated class 443 * @param proto 444 * @returns prototype of the @Observed decorated class or 'proto' parameter if not @Observed decorated 445 */ 446 public static getPrototypeOfObservedClass(proto: Object): Object { 447 return (proto.constructor && proto.constructor.name == "ObservedClass") 448 ? Object.getPrototypeOf(proto.constructor.prototype) 449 : proto; 450 } 451 452 453 /** 454 * To create a new ObservableObject use CreateNew function 455 * 456 * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged 457 * notifications 458 * @param obj raw Object, if obj is a ObservableOject throws an error 459 * @param objectOwner 460 */ 461 private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) { 462 super(obj, handler); 463 464 if (ObservedObject.IsObservedObject(obj)) { 465 stateMgmtConsole.error("ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already"); 466 } 467 if (objectOwningProperty != undefined) { 468 this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty; 469 } 470 } // end of constructor 471 472 public static readonly __IS_OBSERVED_OBJECT = Symbol("_____is_observed_object__"); 473 public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol("_____raw_object__"); 474} 475