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; 33 public changeNotificationIsOngoing_: boolean = false; 34 35 // PU code stores object references to dependencies directly as class variable 36 // SubscriberManager is not used for lookup in PU code path to speedup updates 37 protected subscriberRefs_: Set<IPropertySubscriber>; 38 39 // when owning ViewPU is inActive, delay notifying changes 40 private delayedNotification_: number = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay; 41 42 // install when current value is ObservedObject and the value type is not using compatibility mode 43 // note value may change for union type variables when switching an object from one class to another. 44 protected shouldInstallTrackedObjectReadCb : boolean = false; 45 private dependentElmtIdsByProperty_ = new PropertyDependencies(); 46 47 constructor(subscriber: IPropertySubscriber, viewName: PropertyInfo) { 48 super(subscriber, viewName); 49 Object.defineProperty(this, 'owningView_', {writable: true, enumerable: false, value: undefined}); 50 Object.defineProperty(this, 'subscriberRefs_', 51 {writable: true, enumerable: false, value: new Set<IPropertySubscriber>()}); 52 if (subscriber) { 53 if (subscriber instanceof ViewPU) { 54 this.owningView_ = subscriber; 55 } else { 56 this.subscriberRefs_.add(subscriber); 57 } 58 } 59 } 60 61 aboutToBeDeleted() { 62 super.aboutToBeDeleted(); 63 this.subscriberRefs_.clear(); 64 this.owningView_ = undefined; 65 } 66 67 private decoratorInfo_?: string; 68 69 public setDecoratorInfo(decorate: string) { 70 this.decoratorInfo_ = decorate; 71 } 72 73 // dump info about variable decorator to string 74 // e.g. @State, @Link, etc. 75 public debugInfoDecorator() : string { 76 return this.decoratorInfo_; 77 } 78 79 // dump basic info about this variable to a string, non-recursive, no subscriber info 80 public debugInfo() : string { 81 const propSource : string | false = this.isPropSourceObservedPropertyFakeName(); 82 return (propSource) 83 ? `internal source (ObservedPropertyPU) of @Prop ${propSource} [${this.id__()}]` 84 : `${this.debugInfoDecorator()} '${this.info()}'[${this.id__()}] <${this.debugInfoOwningView()}>`; 85 } 86 87 public debugInfoOwningView() : string { 88 return `${this.owningView_ ? this.owningView_.debugInfo__() : 'owning @Component UNKNOWN'}`; 89 } 90 91 // dump info about owning view and subscribers (PU ones only) 92 // use function only for debug output and DFX. 93 public debugInfoSubscribers(): string { 94 return (this.owningView_) 95 ? `|--Owned by ${this.debugInfoOwningView()} ` 96 : `|--Owned by: owning view not known`; 97 } 98 99 public debugInfoSyncPeers(): string { 100 if (!this.subscriberRefs_.size) { 101 return '|--Sync peers: none'; 102 } 103 let result: string = `|--Sync peers: {`; 104 let sepa: string = ''; 105 this.subscriberRefs_.forEach((subscriber: IPropertySubscriber) => { 106 if ('debugInfo' in subscriber) { 107 result += `\n ${sepa}${(subscriber as ObservedPropertyAbstractPU<any>).debugInfo()}`; 108 sepa = ', '; 109 } 110 }); 111 result += '\n }'; 112 return result; 113 } 114 115 public debugInfoDependentElmtIds(dumpDependantElements: boolean = false): string { 116 return this.dependentElmtIdsByProperty_.dumpInfoDependencies(this.owningView_, dumpDependantElements); 117 } 118 119 public dumpDependentElmtIdsObj(isTrackedMode: boolean, isProfiler: boolean): PropertyDependenciesInfo { 120 return this.dependentElmtIdsByProperty_.dumpInfoDependenciesObj(this.owningView_, isTrackedMode, isProfiler); 121 } 122 123 public debugInfoElmtId(elmtId: number): string { 124 if (this.owningView_) { 125 return this.owningView_.debugInfoElmtId(elmtId) as string; 126 } 127 return '<unknown element id ' + elmtId + ', missing owning view>'; 128 } 129 130 public debugInfoDependentComponents(): string | Object { 131 let result: string = `|--Dependent elements: `; 132 let sepa: string = '; '; 133 let sepaDiff: string = ''; 134 const dumpDependantElements = true; 135 136 let queue: Array<ObservedPropertyAbstractPU<any>> = [this]; 137 let seen = new Set<ObservedPropertyAbstractPU<any>>(); 138 139 while (queue.length) { 140 let item = queue.shift(); 141 seen.add(item); 142 143 if (item !== this) { 144 result += `${sepa}${item.debugInfoOwningView()}`; 145 } 146 result += `${sepaDiff}${item.debugInfoDependentElmtIds(dumpDependantElements)}`; // new dependent elements 147 sepaDiff = ', '; 148 149 item.subscriberRefs_.forEach((subscriber: IPropertySubscriber) => { 150 if ((subscriber instanceof ObservedPropertyAbstractPU)) { 151 if (!seen.has(subscriber)) { 152 queue.push(subscriber); 153 } 154 } 155 }); 156 } 157 return result; 158 } 159 160 /**/ 161 public hasDependencies(): boolean { 162 return this.dependentElmtIdsByProperty_.hasDependencies(); 163 } 164 165 public getDependencies(): Set<number> { 166 return this.dependentElmtIdsByProperty_.getAllPropertyDependencies(); 167 } 168 169 /* for @Prop value from source we need to generate a @State 170 that observes when this value changes. This ObservedPropertyPU 171 sits inside SynchedPropertyOneWayPU. 172 below methods invent a fake variable name for it 173 */ 174 protected getPropSourceObservedPropertyFakeName(): string { 175 return `${this.info()}_prop_fake_state_source___`; 176 } 177 178 protected isPropSourceObservedPropertyFakeName(): string | false { 179 return this.info() && this.info().endsWith('_prop_fake_state_source___') 180 ? this.info().substring(0, this.info().length - '_prop_fake_state_source___'.length) 181 : false; 182 } 183 184 public getOwningView(): TargetInfo { 185 return { componentName: this.owningView_?.constructor.name, id: this.owningView_?.id__() }; 186 } 187 188 public dumpSyncPeers(isProfiler: boolean, changedTrackPropertyName?: string): ObservedPropertyInfo<T>[] { 189 let res: ObservedPropertyInfo<T>[] = []; 190 this.subscriberRefs_.forEach((subscriber: IPropertySubscriber) => { 191 if ('debugInfo' in subscriber) { 192 const observedProp = subscriber as ObservedPropertyAbstractPU<any>; 193 res.push(stateMgmtDFX.getObservedPropertyInfo(observedProp, isProfiler, changedTrackPropertyName)); 194 } 195 }); 196 return res; 197 } 198 199 protected onDumpProfiler(changedTrackPropertyName?: string): void { 200 let res: DumpInfo = new DumpInfo(); 201 res.viewInfo = { componentName: this.owningView_?.constructor.name, id: this.owningView_?.id__() }; 202 res.observedPropertiesInfo.push(stateMgmtDFX.getObservedPropertyInfo(this, true, changedTrackPropertyName)); 203 if (this.owningView_) { 204 try { 205 this.owningView_.sendStateInfo(JSON.stringify(res)); 206 } catch (error) { 207 stateMgmtConsole.applicationError(`${this.debugInfo()} has error in sendStateInfo: ${(error as Error).message}`); 208 } 209 } 210 } 211 212 /* 213 Virtualized version of the subscription mechanism - add subscriber 214 Overrides implementation in ObservedPropertyAbstract<T> 215 */ 216 public addSubscriber(subscriber: ISinglePropertyChangeSubscriber<T>):void { 217 if (subscriber) { 218 // ObservedPropertyAbstract will also add subscriber to 219 // SubscriberManager map and to its own Set of subscribers as well 220 // Something to improve in the future for PU path. 221 // subscribeMe should accept IPropertySubscriber interface 222 super.subscribeMe(subscriber as ISinglePropertyChangeSubscriber<T>); 223 this.subscriberRefs_.add(subscriber); 224 } 225 } 226 227 /* 228 Virtualized version of the subscription mechanism - remove subscriber 229 Overrides implementation in ObservedPropertyAbstract<T> 230 */ 231 public removeSubscriber(subscriber: IPropertySubscriber, id?: number):void { 232 if (subscriber) { 233 this.subscriberRefs_.delete(subscriber); 234 if (!id) { 235 id = subscriber.id__(); 236 } 237 } 238 super.unlinkSuscriber(id); 239 } 240 241 /** 242 * put the property to delayed notification mode 243 * feature is only used for @StorageLink/Prop, @LocalStorageLink/Prop 244 */ 245 public enableDelayedNotification() : void { 246 if (this.delayedNotification_ !== ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending) { 247 stateMgmtConsole.debug(`${this.constructor.name}: enableDelayedNotification.`); 248 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_none_pending; 249 } 250 } 251 252 /* 253 when moving from inActive to active state the owning ViewPU calls this function 254 This solution is faster than ViewPU polling each variable to send back a viewPropertyHasChanged event 255 with the elmtIds 256 257 returns undefined if variable has _not_ changed 258 returns dependentElementIds_ Set if changed. This Set is empty if variable is not used to construct the UI 259 */ 260 public moveElmtIdsForDelayedUpdate(isReused: boolean = false): Set<number> | undefined { 261 const result = (this.delayedNotification_ === ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending) ? 262 this.dependentElmtIdsByProperty_.getAllPropertyDependencies() : 263 undefined; 264 stateMgmtConsole.debug(`${this.debugInfo()}: moveElmtIdsForDelayedUpdate: elmtIds that need delayed update \ 265 ${result ? Array.from(result).toString() : 'no delayed notifications'} .`); 266 if (isReused && !this.owningView_.isViewActive()) { 267 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_none_pending; 268 } else { 269 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay; 270 } 271 return result; 272 } 273 274 protected notifyPropertyRead() { 275 stateMgmtConsole.error(`${this.debugInfo()}: notifyPropertyRead, DO NOT USE with PU. Use notifyReadCb mechanism.`); 276 277 } 278 279 // notify owning ViewPU and peers of a variable assignment 280 // also property/item changes to ObservedObjects of class object type, which use compat mode 281 // Date and Array are notified as if there had been an assignment. 282 protected notifyPropertyHasChangedPU(isSync: boolean = false) : void { 283 stateMgmtProfiler.begin('ObservedPropertyAbstractPU.notifyPropertyHasChangedPU'); 284 stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`); 285 if (this.owningView_) { 286 if (this.delayedNotification_ === ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { 287 if (!isSync) { 288 // send viewPropertyHasChanged right away 289 this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies()); 290 } else { 291 this.owningView_.collectElementsNeedToUpdateSynchronously(this.info_, this.dependentElmtIdsByProperty_.getAllPropertyDependencies(), true); 292 } 293 // send changed observed property to profiler 294 // only will be true when enable profiler 295 if (stateMgmtDFX.enableProfiler) { 296 stateMgmtConsole.debug(`notifyPropertyHasChangedPU in profiler mode`); 297 this.onDumpProfiler(); 298 } 299 } else { 300 // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending 301 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; 302 } 303 } 304 this.subscriberRefs_.forEach((subscriber) => { 305 if (subscriber && typeof subscriber === 'object' && 'syncPeerHasChanged' in subscriber) { 306 (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerHasChanged(this, isSync); 307 } else { 308 stateMgmtConsole.warn(`${this.debugInfo()}: notifyPropertyHasChangedPU: unknown subscriber ID 'subscribedId' error!`); 309 } 310 }); 311 stateMgmtProfiler.end(); 312 } 313 314 315 // notify owning ViewPU and peers of a ObservedObject @Track property's assignment 316 protected notifyTrackedObjectPropertyHasChanged(changedPropertyName : string, isSync: boolean = false) : void { 317 stateMgmtProfiler.begin('ObservedPropertyAbstract.notifyTrackedObjectPropertyHasChanged'); 318 stateMgmtConsole.debug(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged.`); 319 if (this.owningView_) { 320 if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) { 321 // send viewPropertyHasChanged right away 322 if (!isSync) { 323 this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElmtIdsByProperty_.getTrackedObjectPropertyDependencies(changedPropertyName, 'notifyTrackedObjectPropertyHasChanged')); 324 } else { 325 this.owningView_.collectElementsNeedToUpdateSynchronously(this.info_, 326 this.dependentElmtIdsByProperty_.getTrackedObjectPropertyDependencies(changedPropertyName, 'notifyTrackedObjectPropertyHasChanged'), false); 327 } 328 329 // send changed observed property to profiler 330 // only will be true when enable profiler 331 if (stateMgmtDFX.enableProfiler) { 332 stateMgmtConsole.debug(`notifyPropertyHasChangedPU in profiler mode`); 333 this.onDumpProfiler(changedPropertyName); 334 } 335 } else { 336 // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending 337 this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending; 338 } 339 } 340 this.subscriberRefs_.forEach((subscriber) => { 341 if (subscriber) { 342 if ('syncPeerTrackedPropertyHasChanged' in subscriber) { 343 (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerTrackedPropertyHasChanged(this, changedPropertyName, isSync); 344 } else { 345 stateMgmtConsole.warn(`${this.debugInfo()}: notifyTrackedObjectPropertyHasChanged: unknown subscriber ID 'subscribedId' error!`); 346 } 347 } 348 }); 349 stateMgmtProfiler.end(); 350 } 351 352 protected abstract onOptimisedObjectPropertyRead(readObservedObject : T, readPropertyName : string, isTracked : boolean) : void; 353 354 public markDependentElementsDirty(view: ViewPU) { 355 // TODO ace-ets2bundle, framework, complicated apps need to update together 356 // this function will be removed after a short transition period. 357 stateMgmtConsole.warn(`${this.debugInfo()}: markDependentElementsDirty no longer supported. App will work ok, but 358 please update your ace-ets2bundle and recompile your application!`); 359 } 360 361 public numberOfSubscrbers(): number { 362 return this.subscriberRefs_.size + (this.owningView_ ? 1 : 0); 363 } 364 365 /* 366 type checking for any supported type, as required for union type support 367 see 1st parameter for explanation what is allowed 368 369 FIXME this expects the Map, Set patch to go in 370 */ 371 372 protected checkIsSupportedValue(value: T): boolean { 373 // FIXME enable the check when V1-V2 interoperability is forbidden 374 // && !ObserveV2.IsProxiedObservedV2(value) 375 let res = ((typeof value === 'object' && typeof value !== 'function' && 376 !ObserveV2.IsObservedObjectV2(value) && 377 !ObserveV2.IsMakeObserved(value)) || 378 typeof value === 'number' || 379 typeof value === 'string' || 380 typeof value === 'boolean' || 381 value === undefined || 382 value === null); 383 384 if (!res) { 385 errorReport.varValueCheckFailed({ 386 customComponent: this.debugInfoOwningView(), 387 variableDeco: this.debugInfoDecorator(), 388 variableName: this.info(), 389 expectedType: `undefined, null, number, boolean, string, or Object but not function, not V2 @ObservedV2 / @Trace class, and makeObserved return value either`, 390 value: value 391 }); 392 } 393 return res; 394 } 395 396 /* 397 type checking for allowed Object type value 398 see 1st parameter for explanation what is allowed 399 400 FIXME this expects the Map, Set patch to go in 401 */ 402 protected checkIsObject(value: T): boolean { 403 let res = ((typeof value === 'object' && typeof value !== 'function' && !ObserveV2.IsObservedObjectV2(value)) || 404 value === undefined || value === null); 405 if (!res) { 406 errorReport.varValueCheckFailed({ 407 customComponent: this.debugInfoOwningView(), 408 variableDeco: this.debugInfoDecorator(), 409 variableName: this.info(), 410 expectedType: `undefined, null, Object including Array and instance of SubscribableAbstract, excluding function and V2 @Observed/@Trace object`, 411 value: value 412 }); 413 } 414 return res; 415 } 416 417 /* 418 type checking for allowed simple types value 419 see 1st parameter for explanation what is allowed 420 */ 421 protected checkIsSimple(value: T): boolean { 422 let res = (value === undefined || typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean'); 423 if (!res) { 424 errorReport.varValueCheckFailed({ 425 customComponent: this.debugInfoOwningView(), 426 variableDeco: this.debugInfoDecorator(), 427 variableName: this.info(), 428 expectedType: `undefined, number, boolean, string`, 429 value: value 430 }); 431 } 432 return res; 433 } 434 435 protected checkNewValue(isAllowedComment : string, newValue: T, validator: (value: T) => boolean) : boolean { 436 if (validator(newValue)) { 437 return true; 438 } 439 440 // report error 441 // current implementation throws an Exception 442 errorReport.varValueCheckFailed({ 443 customComponent: this.debugInfoOwningView(), 444 variableDeco: this.debugInfoDecorator(), 445 variableName: this.info(), 446 expectedType: isAllowedComment, 447 value: newValue 448 }); 449 450 // never gets here if errorReport.varValueCheckFailed throws an exception 451 // but should not depend on its implementation 452 return false; 453 } 454 455 456 /** 457 * factory function for concrete 'object' or 'simple' ObservedProperty object 458 * depending if value is Class object 459 * or simple type (boolean | number | string) 460 * @param value 461 * @param owningView 462 * @param thisPropertyName 463 * @returns either 464 */ 465 static CreateObservedObject<C>(value: C, owningView: IPropertySubscriber, thisPropertyName: PropertyInfo) 466 : ObservedPropertyAbstract<C> { 467 return (typeof value === 'object') ? 468 new ObservedPropertyObject(value, owningView, thisPropertyName) 469 : new ObservedPropertySimple(value, owningView, thisPropertyName); 470 } 471 472 473 /** 474 * If owning viewPU is currently rendering or re-rendering a UINode, return its elmtId 475 * return notRecordingDependencies (-1) otherwise 476 * ViewPU caches the info, it does not request the info from C++ side (by calling 477 * ViewStackProcessor.GetElmtIdToAccountFor(); as done in earlier implementation 478 */ 479 protected getRenderingElmtId() : number { 480 return (this.owningView_) ? this.owningView_.getCurrentlyRenderedElmtId() : UINodeRegisterProxy.notRecordingDependencies; 481 } 482 483 484 /** 485 * during 'get' access recording take note of the created component and its elmtId 486 * and add this component to the list of components who are dependent on this property 487 */ 488 protected recordPropertyDependentUpdate() : void { 489 const elmtId = this.getRenderingElmtId(); 490 if (elmtId === UINodeRegisterProxy.notRecordingDependencies) { 491 // not access recording 492 return; 493 } 494 if (elmtId === UINodeRegisterProxy.monitorIllegalV1V2StateAccess) { 495 const error = `${this.debugInfo()}: recordPropertyDependentUpdate trying to use V1 state to init/update child V2 @Component. Application error`; 496 stateMgmtConsole.applicationError(error); 497 throw new TypeError(error); 498 } 499 500 stateMgmtConsole.debug(`${this.debugInfo()}: recordPropertyDependentUpdate: add (state) variable dependency for elmtId ${elmtId}.`); 501 this.dependentElmtIdsByProperty_.addPropertyDependency(elmtId); 502 } 503 504 /** record dependency ObservedObject + propertyName -> elmtId 505 * caller ensures renderingElmtId >= 0 506 */ 507 protected recordTrackObjectPropertyDependencyForElmtId(renderingElmtId : number, readTrackedPropertyName : string) : void { 508 stateMgmtConsole.debug(`${this.debugInfo()}: recordTrackObjectPropertyDependency on elmtId ${renderingElmtId}.`); 509 this.dependentElmtIdsByProperty_.addTrackedObjectPropertyDependency(readTrackedPropertyName, renderingElmtId); 510 } 511 512 public purgeDependencyOnElmtId(rmElmtId: number): void { 513 this.dependentElmtIdsByProperty_?.purgeDependenciesForElmtId(rmElmtId); 514 } 515 516 public SetPropertyUnchanged(): void { 517 // function to be removed 518 // keep it here until transpiler is updated. 519 } 520 521 // unified Appstorage, what classes to use, and the API 522 public createLink(subscribeOwner?: IPropertySubscriber, 523 linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> { 524 throw new Error(`${this.debugInfo()}: createLink: Can not create a AppStorage 'Link' from this property.`); 525 } 526 527 public createProp(subscribeOwner?: IPropertySubscriber, 528 linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> { 529 throw new Error(`${this.debugInfo()}: createProp: Can not create a AppStorage 'Prop' from a @State property. `); 530 } 531 532 /* 533 Below empty functions required to keep as long as this class derives from FU version 534 ObservedPropertyAbstract. Need to overwrite these functions to do nothing for PU 535 */ 536 protected notifyHasChanged(_: T) { 537 stateMgmtConsole.error(`${this.debugInfo()}: notifyHasChanged, DO NOT USE with PU. Use syncPeerHasChanged() \ 538 or onTrackedObjectProperty(CompatMode)HasChangedPU()`); 539 } 540 541 542 /** 543 * event emitted by wrapped ObservedObject, when one of its property values changes 544 * for class objects when in compatibility mode 545 * for Array, Date instances always 546 * @param souceObject 547 * @param changedPropertyName 548 */ 549 public onTrackedObjectPropertyHasChangedPU(sourceObject: ObservedObject<T>, changedPropertyName: string) { 550 stateMgmtConsole.debug(`${this.debugInfo()}: onTrackedObjectPropertyHasChangedPU: property '${changedPropertyName}' of \ 551 object value has changed.`); 552 553 this.notifyTrackedObjectPropertyHasChanged(changedPropertyName); 554 } 555 556 /** 557 * event emitted by wrapped ObservedObject, when one of its property values changes 558 * for class objects when in compatibility mode 559 * for Array, Date instances always 560 * @param souceObject 561 * @param changedPropertyName 562 */ 563 public onTrackedObjectPropertyCompatModeHasChangedPU(sourceObject: ObservedObject<T>, changedPropertyName: string) { 564 stateMgmtConsole.debug(`${this.debugInfo()}: onTrackedObjectPropertyCompatModeHasChangedPU: property '${changedPropertyName}' of \ 565 object value has changed.`); 566 567 this.notifyPropertyHasChangedPU(); 568 } 569 570 571 hasChanged(_: T): void { 572 // unused for PU 573 // need to overwrite impl of base class with empty function. 574 } 575 576 propertyHasChanged(_?: PropertyInfo): void { 577 // unused for PU 578 // need to overwrite impl of base class with empty function. 579 } 580 581 propertyRead(_?: PropertyInfo): void { 582 // unused for PU 583 // need to overwrite impl of base class with empty function. 584 } 585} 586 587class PropertyDependencies { 588 589 // dependencies for property -> elmtId 590 // variable read during render adds elmtId 591 // variable assignment causes elmtId to need re-render. 592 // UINode with elmtId deletion needs elmtId to be removed from all records, see purgeDependenciesForElmtId 593 private propertyDependencies_: Set<number> = new Set<number>(); 594 595 public getAllPropertyDependencies(): Set<number> { 596 stateMgmtConsole.debug(` ... variable value assignment: returning affected elmtIds ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 597 return this.propertyDependencies_; 598 } 599 600 public addPropertyDependency(elmtId: number): void { 601 this.propertyDependencies_.add(elmtId); 602 stateMgmtConsole.debug(` ... variable value read: add dependent elmtId ${elmtId} - updated list of dependent elmtIds: ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 603 } 604 605 public purgeDependenciesForElmtId(rmElmtId: number): void { 606 stateMgmtConsole.debug(` ...purge all dependencies for elmtId ${rmElmtId} `); 607 this.propertyDependencies_.delete(rmElmtId); 608 stateMgmtConsole.debug(` ... updated list of elmtIds dependent on variable assignment: ${JSON.stringify(Array.from(this.propertyDependencies_))}`); 609 this.trackedObjectPropertyDependencies_.forEach((propertyElmtId, propertyName) => { 610 propertyElmtId.delete(rmElmtId); 611 stateMgmtConsole.debug(` ... updated dependencies on objectProperty '${propertyName}' changes: ${JSON.stringify(Array.from(propertyElmtId))}`); 612 }); 613 } 614 615 // dependencies on individual object properties 616 private trackedObjectPropertyDependencies_: Map<string, Set<number>> = new Map<string, Set<number>>(); 617 618 public addTrackedObjectPropertyDependency(readProperty: string, elmtId: number): void { 619 let dependentElmtIds = this.trackedObjectPropertyDependencies_.get(readProperty); 620 if (!dependentElmtIds) { 621 dependentElmtIds = new Set<number>(); 622 this.trackedObjectPropertyDependencies_.set(readProperty, dependentElmtIds); 623 } 624 dependentElmtIds.add(elmtId); 625 stateMgmtConsole.debug(` ... object property '${readProperty}' read: add dependent elmtId ${elmtId} - updated list of dependent elmtIds: ${JSON.stringify(Array.from(dependentElmtIds))}`); 626 } 627 628 public getTrackedObjectPropertyDependencies(changedObjectProperty: string, debugInfo: string): Set<number> { 629 const dependentElmtIds = this.trackedObjectPropertyDependencies_.get(changedObjectProperty) || new Set<number>(); 630 stateMgmtConsole.debug(` ... property '@Track ${changedObjectProperty}': returning affected elmtIds ${JSON.stringify(Array.from(dependentElmtIds))}`); 631 return dependentElmtIds; 632 } 633 634 public dumpInfoDependencies(owningView: ViewPU | undefined = undefined, dumpDependantElements): string { 635 const formatElmtId = owningView ? (elmtId => owningView.debugInfoElmtId(elmtId)) : (elmtId => elmtId); 636 let result: string = ''; 637 const arr = Array.from(this.propertyDependencies_).map(formatElmtId); 638 if (dumpDependantElements) { 639 return (arr.length > 1 ? arr.join(', ') : arr[0]); 640 } 641 if (!this.trackedObjectPropertyDependencies_.size) { 642 result += `dependencies: variable assignment affects elmtIds: ${Array.from(this.propertyDependencies_).map(formatElmtId).join(', ')}`; 643 return result; 644 } 645 this.trackedObjectPropertyDependencies_.forEach((propertyElmtId, propertyName) => { 646 result += `dependencies: property '@Track ${propertyName}' change affects elmtIds: ${Array.from(propertyElmtId).map(formatElmtId).join(', ')}`; 647 }); 648 return result; 649 } 650 651 652 public dumpInfoDependenciesObj(owningView: ViewPU | undefined = undefined, isTrackedMode: boolean, isProfiler: boolean): PropertyDependenciesInfo { 653 654 const formatElmtId = owningView ? (elmtId => owningView.debugInfoElmtId(elmtId, isProfiler)) : (elmtId => elmtId); 655 656 let trackedObjectPropertyDependenciesDumpInfo: Map<string, Array<ElementType | number | string>> = new Map<string, Array<ElementType | number | string>>(); 657 658 this.trackedObjectPropertyDependencies_.forEach((propertyElmtId, propertyName) => { 659 trackedObjectPropertyDependenciesDumpInfo.set(propertyName, Array.from(propertyElmtId).map(formatElmtId)); 660 }); 661 662 let PropertyDependenciesInfo: PropertyDependenciesInfo = { 663 mode: isTrackedMode ? 'Track Mode' : 'Compatible Mode', 664 trackPropertiesDependencies: MapInfo.toObject(trackedObjectPropertyDependenciesDumpInfo).keyToValue, 665 propertyDependencies: Array.from(this.propertyDependencies_).map(formatElmtId), 666 } 667 return PropertyDependenciesInfo; 668 } 669 670 public hasDependencies() : boolean { 671 return this.propertyDependencies_.size > 0 || this.trackedObjectPropertyDependencies_.size > 0; 672 } 673} 674