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 157 protected notifyObjectPropertyHasBeenRead(propName: string) { 158 stateMgmtConsole.debug(`SubscribableHandler: notifyObjectPropertyHasBeenRead '${propName}'.`) 159 this.owningProperties_.forEach((subscribedId) => { 160 var owningProperty: IPropertySubscriber = SubscriberManager.Find(subscribedId) 161 if (owningProperty) { 162 // PU code path 163 if ('objectPropertyHasBeenReadPU' in owningProperty) { 164 (owningProperty as unknown as ObservedObjectEventsPUReceiver<any>).objectPropertyHasBeenReadPU(this, propName); 165 } 166 } 167 }); 168 } 169 170 public has(target: Object, property: PropertyKey) : boolean { 171 stateMgmtConsole.debug(`SubscribableHandler: has '${property.toString()}'.`); 172 return (property === ObservedObject.__IS_OBSERVED_OBJECT) ? true : Reflect.has(target, property); 173 } 174 175 public get(target: Object, property: PropertyKey, receiver?: any): any { 176 stateMgmtConsole.debug(`SubscribableHandler: get '${property.toString()}'.`); 177 return (property === ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT) ? target : Reflect.get(target, property, receiver); 178 } 179 180 public set(target: Object, property: PropertyKey, newValue: any): boolean { 181 switch (property) { 182 case SubscribableHandler.SUBSCRIBE: 183 // assignment obsObj[SubscribableHandler.SUBSCRCRIBE] = subscriber 184 this.addOwningProperty(newValue as IPropertySubscriber); 185 return true; 186 break; 187 case SubscribableHandler.UNSUBSCRIBE: 188 // assignment obsObj[SubscribableHandler.UNSUBSCRCRIBE] = subscriber 189 this.removeOwningProperty(newValue as IPropertySubscriber); 190 return true; 191 break; 192 default: 193 if (Reflect.get(target, property) == newValue) { 194 return true; 195 } 196 stateMgmtConsole.debug(`SubscribableHandler: set '${property.toString()}'.`); 197 Reflect.set(target, property, newValue); 198 this.notifyObjectPropertyHasChanged(property.toString(), newValue); 199 return true; 200 break; 201 } 202 203 // unreachable 204 return false; 205 } 206} 207 208class SubscribableDateHandler extends SubscribableHandler { 209 210 constructor(owningProperty: IPropertySubscriber) { 211 super(owningProperty); 212 } 213 214 /** 215 * Get trap for Date type proxy 216 * Functions that modify Date in-place are intercepted and replaced with a function 217 * that executes the original function and notifies the handler of a change. 218 * @param target Original Date object 219 * @param property 220 * @returns 221 */ 222 public get(target: Object, property: PropertyKey): any { 223 224 const dateSetFunctions = new Set(["setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds", 225 "setMilliseconds", "setTime", "setUTCFullYear", "setUTCMonth", "setUTCDate", "setUTCHours", "setUTCMinutes", 226 "setUTCSeconds", "setUTCMilliseconds"]); 227 228 let ret = super.get(target, property); 229 230 if (typeof ret === "function" && property.toString() && dateSetFunctions.has(property.toString())) { 231 const self = this; 232 return function () { 233 // execute original function with given arguments 234 let result = ret.apply(this, arguments); 235 self.notifyObjectPropertyHasChanged(property.toString(), this); 236 return result; 237 }.bind(target) // bind "this" to target inside the function 238 } else if (typeof ret === "function") { 239 ret = ret.bind(target); 240 } 241 return ret; 242 } 243} 244 245 246class ExtendableProxy { 247 constructor(obj: Object, handler: SubscribableHandler) { 248 return new Proxy(obj, handler); 249 } 250} 251 252class ObservedObject<T extends Object> extends ExtendableProxy { 253 254 /** 255 * Factory function for ObservedObjects / 256 * wrapping of objects for proxying 257 * 258 * @param rawObject unproxied Object or ObservedObject 259 * @param objOwner owner of this Object to sign uop for propertyChange 260 * notifications 261 * @returns the rawObject if object is already an ObservedObject, 262 * otherwise the newly created ObservedObject 263 */ 264 public static createNew<T extends Object>(rawObject: T, 265 owningProperty: IPropertySubscriber): T { 266 267 if (rawObject === null || rawObject === undefined) { 268 stateMgmtConsole.error(`ObservedObject.CreateNew, input object must not be null or undefined.`); 269 return rawObject; 270 } 271 272 if (ObservedObject.IsObservedObject(rawObject)) { 273 ObservedObject.addOwningProperty(rawObject, owningProperty); 274 return rawObject; 275 } 276 277 return ObservedObject.createNewInternal<T>(rawObject, owningProperty); 278 } 279 280 public static createNewInternal<T extends Object>(rawObject: T, 281 owningProperty: IPropertySubscriber): T { 282 283 let proxiedObject = new ObservedObject<T>(rawObject, 284 Array.isArray(rawObject) ? new class extends SubscribableHandler { 285 // In-place array modification functions 286 // splice is also in-place modifying function, but we need to handle separately 287 private readonly inPlaceModifications: Set<string> = new Set(["copyWithin", "fill", "reverse", "sort"]); 288 289 constructor(owningProperty: IPropertySubscriber) { 290 super(owningProperty); 291 } 292 293 public get(target: Object, property: PropertyKey, receiver: any): any { 294 let ret = super.get(target, property, receiver); 295 if (ret && typeof ret === "function") { 296 const self = this; 297 const prop = property.toString(); 298 // prop is the function name here 299 if (prop == "splice") { 300 // 'splice' self modifies the array, returns deleted array items 301 // means, alike other self-modifying functions, splice does not return the array itself. 302 return function () { 303 const result = ret.apply(target, arguments); 304 // prop is the function name here 305 // and result is the function return value 306 // functinon modifies none or more properties 307 self.notifyObjectPropertyHasChanged(prop, target); 308 return result; 309 }.bind(proxiedObject); 310 } 311 312 if (self.inPlaceModifications.has(prop)) { 313 // in place modfication function result == target, the raw array modified 314 stateMgmtConsole.debug("return self mod function"); 315 return function () { 316 const result = ret.apply(target, arguments); 317 318 // 'result' is the unproxied object 319 // functinon modifies none or more properties 320 self.notifyObjectPropertyHasChanged(prop, result); 321 322 // returning the 'proxiedObject' ensures that when chain calls also 2nd function call 323 // operates on the proxied object. 324 return proxiedObject; 325 }.bind(proxiedObject); 326 } 327 328 // binding the proxiedObject ensures that modifying functions like push() operate on the 329 // proxied array and each array change is notified. 330 return ret.bind(proxiedObject); 331 } 332 333 return ret; 334 } 335 }(owningProperty) // SubscribableArrayHandlerAnonymous 336 : (rawObject instanceof Date) 337 ? new SubscribableDateHandler(owningProperty) 338 : new SubscribableHandler(owningProperty), 339 owningProperty); 340 341 return proxiedObject as T; 342 } 343 344 /* 345 Return the unproxied object 'inside' the ObservedObject / the ES6 Proxy 346 no set observation, no notification of changes! 347 Use with caution, do not store any references 348 */ 349 static GetRawObject<T extends Object>(obj: T): T { 350 return !ObservedObject.IsObservedObject(obj) ? obj : obj[ObservedObject.__OBSERVED_OBJECT_RAW_OBJECT]; 351 } 352 353 /** 354 * 355 * @param obj anything 356 * @returns true if the parameter is an Object wrpped with a ObservedObject 357 * Note: Since ES6 Proying is transparent, 'instance of' will not work. Use 358 * this static function instead. 359 */ 360 static IsObservedObject(obj: any): boolean { 361 return (obj && (typeof obj === "object") && Reflect.has(obj, ObservedObject.__IS_OBSERVED_OBJECT)); 362 } 363 364 /** 365 * add a subscriber to given ObservedObject 366 * due to the proxy nature this static method approach needs to be used instead of a member 367 * function 368 * @param obj 369 * @param subscriber 370 * @returns false if given object is not an ObservedObject 371 */ 372 public static addOwningProperty(obj: Object, subscriber: IPropertySubscriber): boolean { 373 if (!ObservedObject.IsObservedObject(obj) || subscriber==undefined) { 374 return false; 375 } 376 377 obj[SubscribableHandler.SUBSCRIBE] = subscriber; 378 return true; 379 } 380 381 /** 382 * remove a subscriber to given ObservedObject 383 * due to the proxy nature this static method approach needs to be used instead of a member 384 * function 385 * @param obj 386 * @param subscriber 387 * @returns false if given object is not an ObservedObject 388 */ 389 public static removeOwningProperty(obj: Object, 390 subscriber: IPropertySubscriber): boolean { 391 if (!ObservedObject.IsObservedObject(obj)) { 392 return false; 393 } 394 395 obj[SubscribableHandler.UNSUBSCRIBE] = subscriber; 396 return true; 397 } 398 399 /** 400 * Utility function for debugging the prototype chain of given Object 401 * The given object can be any Object, it is not required to be an ObservedObject 402 * @param object 403 * @returns multi-line string containing info about the prototype chain 404 * on class in class hiararchy per line 405 */ 406 public static tracePrototypeChainOfObject(object: Object | undefined): string { 407 let proto = Object.getPrototypeOf(object); 408 let result = ""; 409 let sepa = ""; 410 while (proto) { 411 result += `${sepa}${ObservedObject.tracePrototype(proto)}`; 412 proto = Object.getPrototypeOf(proto); 413 sepa = ",\n"; 414 } 415 416 return result; 417 } 418 419 /** 420 * Utility function for debugging all functions of given Prototype. 421 * @returns string containing containing names of all functions and members of given Prototype 422 */ 423 public static tracePrototype(proto: any) { 424 if (!proto) { 425 return ""; 426 } 427 428 let result = `${proto.constructor && proto.constructor.name ? proto.constructor.name : '<no class>'}: `; 429 let sepa = ""; 430 for (let name of Object.getOwnPropertyNames(proto)) { 431 result += `${sepa}${name}`; 432 sepa = ", "; 433 }; 434 return result; 435 } 436 437 438 /** 439 * @Observed decorator extends the decorated class. This function returns the prototype of the decorated class 440 * @param proto 441 * @returns prototype of the @Observed decorated class or 'proto' parameter if not @Observed decorated 442 */ 443 public static getPrototypeOfObservedClass(proto: Object): Object { 444 return (proto.constructor && proto.constructor.name == "ObservedClass") 445 ? Object.getPrototypeOf(proto.constructor.prototype) 446 : proto; 447 } 448 449 450 /** 451 * To create a new ObservableObject use CreateNew function 452 * 453 * constructor create a new ObservableObject and subscribe its owner to propertyHasChanged 454 * notifications 455 * @param obj raw Object, if obj is a ObservableOject throws an error 456 * @param objectOwner 457 */ 458 private constructor(obj: T, handler: SubscribableHandler, objectOwningProperty: IPropertySubscriber) { 459 super(obj, handler); 460 461 if (ObservedObject.IsObservedObject(obj)) { 462 stateMgmtConsole.error("ObservableOject constructor: INTERNAL ERROR: after jsObj is observedObject already"); 463 } 464 if (objectOwningProperty != undefined) { 465 this[SubscribableHandler.SUBSCRIBE] = objectOwningProperty; 466 } 467 } // end of constructor 468 469 public static readonly __IS_OBSERVED_OBJECT = Symbol("_____is_observed_object__"); 470 public static readonly __OBSERVED_OBJECT_RAW_OBJECT = Symbol("_____raw_object__"); 471} 472