• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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