1/* 2 * Copyright (c) 2022 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 * ObservedPropertyAbstractPU aka ObservedPropertyAbstract for partial update 18 * 19 * all definitions in this file are framework internal 20 */ 21 22abstract class ObservedPropertyAbstractPU<T> extends ObservedPropertyAbstract<T> 23implements ISinglePropertyChangeSubscriber<T>, IMultiPropertiesChangeSubscriber, IMultiPropertiesReadSubscriber 24// these interfaces implementations are all empty functions, overwrite FU base class implementations. 25{ 26 static readonly DelayedNotifyChangesEnum=class { 27 static readonly do_not_delay = 0; 28 static readonly delay_none_pending = 1; 29 static readonly delay_notification_pending = 2; 30 }; 31 32 private owningView_ : ViewPU = undefined; 33 34 // PU code stores object references to dependencies directly as class variable 35 // SubscriberManager is not used for lookup in PU code path to speedup updates 36 protected subscriberRefs_: Set<IPropertySubscriber>; 37 38 // when owning ViewPU is inActive, delay notifying changes 39 private delayedNotification_: number = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay; 40 41 // install when current value is ObservedObject and the value type is not using compatibility mode 42 // note value may change for union type variables when switching an object from one class to another. 43 protected shouldInstallTrackedObjectReadCb : boolean = false; 44 private dependentElmtIdsByProperty_ = new PropertyDependencies(); 45 46 constructor(subscriber: IPropertySubscriber, viewName: PropertyInfo) { 47 super(subscriber, viewName); 48 Object.defineProperty(this, 'owningView_', {writable: true, enumerable: false}); 49 Object.defineProperty(this, 'subscriberRefs_', 50 {writable: true, enumerable: false, value: new Set<IPropertySubscriber>()}); 51 if(subscriber) { 52 if (subscriber instanceof ViewPU) { 53 this.owningView_ = subscriber; 54 } else { 55 this.subscriberRefs_.add(subscriber); 56 } 57 } 58 } 59 60 aboutToBeDeleted() { 61 super.aboutToBeDeleted(); 62 this.subscriberRefs_.clear(); 63 this.owningView_ = undefined; 64 } 65 66 // dump info about variable decorator to string 67 // e.g. @State/Provide, @Link/Consume, etc. 68 public abstract debugInfoDecorator() : string; 69 70 // dump basic info about this variable to a string, non-recursive, no subscriber info 71 public debugInfo() : string { 72 const propSource : string | false = this.isPropSourceObservedPropertyFakeName(); 73 return (propSource) 74 ? `internal source (ObservedPropertyPU) of @Prop ${propSource} [${this.id__()}]` 75 : `${this.debugInfoDecorator()} '${this.info()}'[${this.id__()}] <${this.debugInfoOwningView()}>`; 76 } 77 78 public debugInfoOwningView() : string { 79 return `${this.owningView_ ? this.owningView_.debugInfo__() : "owning @Component UNKNOWN"}`; 80 } 81 82 // dump info about owning view and subscribers (PU ones only) 83 // use function only for debug output and DFX. 84 public debugInfoSubscribers(): string { 85 return (this.owningView_) 86 ? `|--Owned by ${this.debugInfoOwningView()} ` 87 : `|--Owned by: owning view not known`; 88 } 89 90 public debugInfoSyncPeers(): string { 91 if (!this.subscriberRefs_.size) { 92 return "|--Sync peers: none"; 93 } 94 let result: string = `|--Sync peers: {`; 95 let sepa: string = ""; 96 this.subscriberRefs_.forEach((subscriber: IPropertySubscriber) => { 97 if ("debugInfo" in subscriber) { 98 result += `\n ${sepa}${(subscriber as ObservedPropertyAbstractPU<any>).debugInfo()}`; 99 sepa = ", "; 100 } 101 }); 102 result += "\n }" 103 return result; 104 } 105 106 public debugInfoDependentElmtIds(): string { 107 return this.dependentElmtIdsByProperty_.dumpInfoDependencies(); 108 } 109 110 /* for @Prop value from source we need to generate a @State 111 that observes when this value changes. This ObservedPropertyPU 112 sits inside SynchedPropertyOneWayPU. 113 below methods invent a fake variable name for it 114 */ 115 protected getPropSourceObservedPropertyFakeName(): string { 116 return `${this.info()}_prop_fake_state_source___`; 117 } 118 119 protected isPropSourceObservedPropertyFakeName(): string | false { 120 return this.info().endsWith("_prop_fake_state_source___") 121 ? this.info().substring(0, this.info().length - "_prop_fake_state_source___".length) 122 : false; 123 } 124 125 /* 126 Virtualized version of the subscription mechanism - add subscriber 127 Overrides implementation in ObservedPropertyAbstract<T> 128 */ 129 public addSubscriber(subscriber: ISinglePropertyChangeSubscriber<T>):void { 130 if (subscriber) { 131 // ObservedPropertyAbstract will also add subscriber to 132 // SubscriberManager map and to its own Set of subscribers as well 133 // Something to improve in the future for PU path. 134 // subscribeMe should accept IPropertySubscriber interface 135 super.subscribeMe(subscriber as ISinglePropertyChangeSubscriber<T>); 136 this.subscriberRefs_.add(subscriber); 137 } 138 } 139 140 /* 141 Virtualized version of the subscription mechanism - remove subscriber 142 Overrides implementation in ObservedPropertyAbstract<T> 143 */ 144 public removeSubscriber(subscriber: IPropertySubscriber, id?: number):void { 145 if (subscriber) { 146 this.subscriberRefs_.delete(subscriber); 147 if (!id) { 148 id = subscriber.id__(); 149 } 150 } 151 super.unlinkSuscriber(id); 152 } 153 154 /** 155 * put the property to delayed notification mode 156 * feature is only used for @StorageLink/Prop, @LocalStorageLink/Prop 157 */ 158 public enableDelayedNotification() : void { 159 if (this.delayedNotification_ != ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending) { 160 stateMgmtConsole.debug(`${this.constructor.name}: enableDelayedNotification.`); 161 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_none_pending; 162 } 163 } 164 165 /* 166 when moving from inActive to active state the owning ViewPU calls this function 167 This solution is faster than ViewPU polling each variable to send back a viewPropertyHasChanged event 168 with the elmtIds 169 170 returns undefined if variable has _not_ changed 171 returns dependentElementIds_ Set if changed. This Set is empty if variable is not used to construct the UI 172 */ 173 public moveElmtIdsForDelayedUpdate(): Set<number> | undefined { 174 const result = (this.delayedNotification_ === ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending) ? 175 this.dependentElmtIdsByProperty_.getAllPropertyDependencies() : 176 undefined; 177 stateMgmtConsole.debug(`${this.debugInfo()}: moveElmtIdsForDelayedUpdate: elmtIds that need delayed update \ 178 ${result ? Array.from(result).toString() : 'no delayed notifications'} .`); 179 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay; 180 return result; 181 } 182 183 protected notifyPropertyRead() { 184 stateMgmtConsole.error(`${this.debugInfo()}: notifyPropertyRead, DO NOT USE with PU. Use notifyReadCb mechanism.`); 185 186 } 187 188 // notify owning ViewPU and peers of a variable assignment 189 // also property/item changes to ObservedObjects of class object type, which use compat mode 190 // Date and Array are notified as if there had been an assignment. 191 protected notifyPropertyHasChangedPU() { 192 stateMgmtProfiler.begin("ObservedPropertyAbstractPU.notifyPropertyHasChangedPU"); 193 stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`) 194 if (this.owningView_) { 195 if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { 196 // send viewPropertyHasChanged right away 197 this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies()); 198 } else { 199 // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending 200 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; 201 } 202 } 203 this.subscriberRefs_.forEach((subscriber) => { 204 if (subscriber) { 205 if ('syncPeerHasChanged' in subscriber) { 206 (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerHasChanged(this); 207 } else { 208 stateMgmtConsole.warn(`${this.debugInfo()}: notifyPropertyHasChangedPU: unknown subscriber ID 'subscribedId' error!`); 209 } 210 } 211 }); 212 stateMgmtProfiler.end(); 213 } 214 215 216 // notify owning ViewPU and peers of a ObservedObject @Track property's assignment 217 protected notifyTrackedObjectPropertyHasChanged(changedPropertyName : string) : void { 218 stateMgmtProfiler.begin("ObservedPropertyAbstract.notifyTrackedObjectPropertyHasChanged"); 219 stateMgmtConsole.debug(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged.`) 220 if (this.owningView_) { 221 if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { 222 // send viewPropertyHasChanged right away 223 this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getTrackedObjectPropertyDependencies(changedPropertyName, "notifyTrackedObjectPropertyHasChanged")); 224 } else { 225 // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending 226 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; 227 } 228 } 229 this.subscriberRefs_.forEach((subscriber) => { 230 if (subscriber) { 231 if ('syncPeerTrackedPropertyHasChanged' in subscriber) { 232 (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerTrackedPropertyHasChanged(this, changedPropertyName); 233 } else { 234 stateMgmtConsole.warn(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged: unknown subscriber ID 'subscribedId' error!`); 235 } 236 } 237 }); 238 stateMgmtProfiler.end(); 239 } 240 241 protected abstract onOptimisedObjectPropertyRead(readObservedObject : T, readPropertyName : string, isTracked : boolean) : void; 242 243 public markDependentElementsDirty(view: ViewPU) { 244 // TODO ace-ets2bundle, framework, complicated apps need to update together 245 // this function will be removed after a short transition period. 246 stateMgmtConsole.warn(`${this.debugInfo()}: markDependentElementsDirty no longer supported. App will work ok, but 247 please update your ace-ets2bundle and recompile your application!`); 248 } 249 250 public numberOfSubscrbers(): number { 251 return this.subscriberRefs_.size + (this.owningView_ ? 1 : 0); 252 } 253 254 /* 255 type checking for any supported type, as required for union type support 256 see 1st parameter for explanation what is allowed 257 258 FIXME this expects the Map, Set patch to go in 259 */ 260 261 protected checkIsSupportedValue(value: T): boolean { 262 return this.checkNewValue( 263 `undefined, null, number, boolean, string, or Object but not function`, 264 value, 265 () => ((typeof value == "object" && typeof value != "function") 266 || typeof value == "number" || typeof value == "string" || typeof value == "boolean") 267 || (value == undefined || value == null) 268 ); 269 } 270 271 /* 272 type checking for allowed Object type value 273 see 1st parameter for explanation what is allowed 274 275 FIXME this expects the Map, Set patch to go in 276 */ 277 protected checkIsObject(value: T): boolean { 278 return this.checkNewValue( 279 `undefined, null, Object including Array and instance of SubscribableAbstract and excluding function, Set, and Map`, 280 value, 281 () => (value == undefined || value == null || (typeof value == "object")) 282 ); 283 } 284 285 /* 286 type checking for allowed simple types value 287 see 1st parameter for explanation what is allowed 288 */ 289 protected checkIsSimple(value: T): boolean { 290 return this.checkNewValue( 291 `undefined, number, boolean, string`, 292 value, 293 () => (value == undefined || typeof value == "number" || typeof value == "string" || typeof value == "boolean") 294 ); 295 } 296 297 protected checkNewValue(isAllowedComment : string, newValue: T, validator: (value: T) => boolean) : boolean { 298 if (validator(newValue)) { 299 return true; 300 } 301 302 // report error 303 // current implementation throws an Exception 304 errorReport.varValueCheckFailed({ 305 customComponent: this.debugInfoOwningView(), 306 variableDeco: this.debugInfoDecorator(), 307 variableName: this.info(), 308 expectedType: isAllowedComment, 309 value: newValue 310 }); 311 312 // never gets here if errorReport.varValueCheckFailed throws an exception 313 // but should not depend on its implementation 314 return false; 315 } 316 317 318 /** 319 * factory function for concrete 'object' or 'simple' ObservedProperty object 320 * depending if value is Class object 321 * or simple type (boolean | number | string) 322 * @param value 323 * @param owningView 324 * @param thisPropertyName 325 * @returns either 326 */ 327 static CreateObservedObject<C>(value: C, owningView: IPropertySubscriber, thisPropertyName: PropertyInfo) 328 : ObservedPropertyAbstract<C> { 329 return (typeof value === "object") ? 330 new ObservedPropertyObject(value, owningView, thisPropertyName) 331 : new ObservedPropertySimple(value, owningView, thisPropertyName); 332 } 333 334 335 /** 336 * If owning viewPU is currently rendering or re-rendering a UINode, return its elmtId 337 * return -1 otherwise 338 * ViewPU caches the info, it does not request the info from C++ side (by calling 339 * ViewStackProcessor.GetElmtIdToAccountFor(); as done in earlier implementation 340 */ 341 protected getRenderingElmtId() : number { 342 return (this.owningView_) ? this.owningView_.getCurrentlyRenderedElmtId() : -1; 343 } 344 345 346 /** 347 * during 'get' access recording take note of the created component and its elmtId 348 * and add this component to the list of components who are dependent on this property 349 */ 350 protected recordPropertyDependentUpdate() : void { 351 const elmtId = this.getRenderingElmtId(); 352 if (elmtId < 0) { 353 // not access recording 354 return; 355 } 356 stateMgmtConsole.debug(`${this.debugInfo()}: recordPropertyDependentUpdate: add (state) variable dependency for elmtId ${elmtId}.`) 357 this.dependentElmtIdsByProperty_.addPropertyDependency(elmtId); 358 } 359 360 /** record dependency ObservedObject + propertyName -> elmtId 361 * caller ensures renderingElmtId >= 0 362 */ 363 protected recordTrackObjectPropertyDependencyForElmtId(renderingElmtId : number, readTrackedPropertyName : string) : void { 364 stateMgmtConsole.debug(`${this.debugInfo()}: recordTrackObjectPropertyDependency on elmtId ${renderingElmtId}.`) 365 this.dependentElmtIdsByProperty_.addTrackedObjectPropertyDependency(readTrackedPropertyName, renderingElmtId); 366 } 367 368 public purgeDependencyOnElmtId(rmElmtId: number): void { 369 this.dependentElmtIdsByProperty_?.purgeDependenciesForElmtId(rmElmtId); 370 } 371 372 public SetPropertyUnchanged(): void { 373 // function to be removed 374 // keep it here until transpiler is updated. 375 } 376 377 // unified Appstorage, what classes to use, and the API 378 public createLink(subscribeOwner?: IPropertySubscriber, 379 linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> { 380 throw new Error(`${this.debugInfo()}: createLink: Can not create a AppStorage 'Link' from this property.`); 381 } 382 383 public createProp(subscribeOwner?: IPropertySubscriber, 384 linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> { 385 throw new Error(`${this.debugInfo()}: createProp: Can not create a AppStorage 'Prop' from a @State property. `); 386 } 387 388 /* 389 Below empty functions required to keep as long as this class derives from FU version 390 ObservedPropertyAbstract. Need to overwrite these functions to do nothing for PU 391 */ 392 protected notifyHasChanged(_: T) { 393 stateMgmtConsole.error(`${this.debugInfo()}: notifyHasChanged, DO NOT USE with PU. Use syncPeerHasChanged() \ 394 or onTrackedObjectProperty(CompatMode)HasChangedPU()`); 395 } 396 397 398 /** 399 * event emitted by wrapped ObservedObject, when one of its property values changes 400 * for class objects when in compatibility mode 401 * for Array, Date instances always 402 * @param souceObject 403 * @param changedPropertyName 404 */ 405 public onTrackedObjectPropertyHasChangedPU(sourceObject: ObservedObject<T>, changedPropertyName: string) { 406 stateMgmtConsole.debug(`${this.debugInfo()}: onTrackedObjectPropertyHasChangedPU: property '${changedPropertyName}' of \ 407 object value has changed.`) 408 409 this.notifyTrackedObjectPropertyHasChanged(changedPropertyName); 410 } 411 412 /** 413 * event emitted by wrapped ObservedObject, when one of its property values changes 414 * for class objects when in compatibility mode 415 * for Array, Date instances always 416 * @param souceObject 417 * @param changedPropertyName 418 */ 419 public onTrackedObjectPropertyCompatModeHasChangedPU(sourceObject: ObservedObject<T>, changedPropertyName: string) { 420 stateMgmtConsole.debug(`${this.debugInfo()}: onTrackedObjectPropertyCompatModeHasChangedPU: property '${changedPropertyName}' of \ 421 object value has changed.`) 422 423 this.notifyPropertyHasChangedPU(); 424 } 425 426 427 hasChanged(_: T): void { 428 // unused for PU 429 // need to overwrite impl of base class with empty function. 430 } 431 432 propertyHasChanged(_?: PropertyInfo): void { 433 // unused for PU 434 // need to overwrite impl of base class with empty function. 435 } 436 437 propertyRead(_?: PropertyInfo): void { 438 // unused for PU 439 // need to overwrite impl of base class with empty function. 440 } 441} 442 443class PropertyDependencies { 444 445 // dependencies for property -> elmtId 446 // variable read during render adds elmtId 447 // variable assignment causes elmtId to need re-render. 448 // UINode with elmtId deletion needs elmtId to be removed from all records, see purgeDependenciesForElmtId 449 private propertyDependencies_: Set<number> = new Set<number>(); 450 451 public getAllPropertyDependencies(): Set<number> { 452 stateMgmtConsole.debug(` ... variable value assignment: returning affected elmtIds ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 453 return this.propertyDependencies_; 454 } 455 456 public addPropertyDependency(elmtId: number): void { 457 this.propertyDependencies_.add(elmtId); 458 stateMgmtConsole.debug(` ... variable value read: add dependent elmtId ${elmtId} - updated list of dependent elmtIds: ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 459 } 460 461 public purgeDependenciesForElmtId(rmElmtId: number): void { 462 stateMgmtConsole.debug(` ...purge all dependencies for elmtId ${rmElmtId} `); 463 this.propertyDependencies_.delete(rmElmtId); 464 stateMgmtConsole.debug(` ... updated list of elmtIds dependent on variable assignment: ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 465 this.trackedObjectPropertyDependencies_.forEach((propertyElmtId, propertyName) => { 466 propertyElmtId.delete(rmElmtId); 467 stateMgmtConsole.debug(` ... updated dependencies on objectProperty '${propertyName}' changes: ${JSON.stringify(Array.from(propertyElmtId))}`); 468 }); 469 } 470 471 // dependencies on individual object properties 472 private trackedObjectPropertyDependencies_: Map<string, Set<number>> = new Map<string, Set<number>>(); 473 474 public addTrackedObjectPropertyDependency(readProperty: string, elmtId: number): void { 475 let dependentElmtIds = this.trackedObjectPropertyDependencies_.get(readProperty); 476 if (!dependentElmtIds) { 477 dependentElmtIds = new Set<number>(); 478 this.trackedObjectPropertyDependencies_.set(readProperty, dependentElmtIds); 479 } 480 dependentElmtIds.add(elmtId); 481 stateMgmtConsole.debug(` ... object property '${readProperty}' read: add dependent elmtId ${elmtId} - updated list of dependent elmtIds: ${JSON.stringify(Array.from(dependentElmtIds))}`); 482 } 483 484 public getTrackedObjectPropertyDependencies(changedObjectProperty: string, debugInfo: string): Set<number> { 485 const dependentElmtIds = this.trackedObjectPropertyDependencies_.get(changedObjectProperty) || new Set<number>(); 486 stateMgmtConsole.debug(` ... property '@Track ${changedObjectProperty}': returning affected elmtIds ${JSON.stringify(Array.from(dependentElmtIds))}`); 487 return dependentElmtIds; 488 } 489 490 public dumpInfoDependencies(): string { 491 let result = `dependencies: variable assignment (or object prop change in compat mode) affects elmtIds: ${JSON.stringify(Array.from(this.propertyDependencies_))} \n`; 492 this.trackedObjectPropertyDependencies_.forEach((propertyElmtId, propertyName) => { 493 result += ` property '@Track ${propertyName}' change affects elmtIds: ${JSON.stringify(Array.from(propertyElmtId))} \n`; 494 }); 495 return result; 496 } 497 498} 499