• 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
22
23abstract class ObservedPropertyAbstractPU<T> extends ObservedPropertyAbstract<T>
24implements ISinglePropertyChangeSubscriber<T>, IMultiPropertiesChangeSubscriber, IMultiPropertiesReadSubscriber
25// these interfaces implementations are all empty functions, overwrite FU base class implementations.
26{
27  static readonly DelayedNotifyChangesEnum=class  {
28    static readonly do_not_delay = 0;
29    static readonly delay_none_pending = 1;
30    static readonly delay_notification_pending = 2;
31  };
32
33  private owningView_ : ViewPU = undefined;
34
35  private dependentElementIds_: Set<number> = new Set<number>();
36
37  // PU code stores object references to dependencies directly as class variable
38  // SubscriberManager is not used for lookup in PU code path to speedup updates
39  protected subscriberRefs_: Set<IPropertySubscriber>;
40
41  // when owning ViewPU is inActive, delay notifying changes
42  private delayedNotification_: number = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay;
43
44  constructor(subscriber: IPropertySubscriber, viewName: PropertyInfo) {
45    super(subscriber, viewName);
46    Object.defineProperty(this, 'owningView_', {writable: true, enumerable: false});
47    Object.defineProperty(this, 'subscriberRefs_',
48      {writable: true, enumerable: false, value: new Set<IPropertySubscriber>()});
49    if(subscriber) {
50      if (subscriber instanceof ViewPU) {
51        this.owningView_ = subscriber;
52      } else {
53        this.subscriberRefs_.add(subscriber);
54      }
55    }
56  }
57
58  aboutToBeDeleted() {
59    super.aboutToBeDeleted();
60    this.subscriberRefs_.clear();
61    this.owningView_ = undefined;
62  }
63
64  // dump info about variable decorator to string
65  // e.g. @State/Provide, @Link/Consume, etc.
66  public abstract debugInfoDecorator() : string;
67
68  // dump basic info about this variable to a string, non-recursive, no subscriber info
69  public debugInfo() : string {
70    const propSource : string | false = this.isPropSourceObservedPropertyFakeName();
71    return (propSource)
72    ? `internal source (ObservedPropertyPU) of @Prop ${propSource} [${this.id__()}]`
73    : `${this.debugInfoDecorator()} '${this.info()}'[${this.id__()}] <${this.debugInfoOwningView()}>`;
74  }
75
76  public debugInfoOwningView() : string {
77    return `${this.owningView_ ? this.owningView_.debugInfo() : "owning @Component UNKNOWN"}`;
78  }
79
80  // dump info about owning view and subscribers (PU ones only)
81  // use function only for debug output and DFX.
82  public debugInfoSubscribers(): string {
83    return (this.owningView_)
84      ? `owned by ${this.debugInfoOwningView()} `
85      : `owned by: owning view not known`;
86  }
87
88  public debugInfoSyncPeers(): string {
89    if (!this.subscriberRefs_.size) {
90      return "sync peers: none";
91    }
92    let result: string = `sync peers:\n`;
93    let sepa: string = "";
94    this.subscriberRefs_.forEach((subscriber: IPropertySubscriber) => {
95      if ("debugInfo" in subscriber) {
96        result += `    ${sepa}${(subscriber as ObservedPropertyAbstractPU<any>).debugInfo()}`;
97        sepa = ", ";
98      }
99    });
100    return result;
101  }
102
103  public debugInfoDependentElmtIds(): string {
104    if (!this.dependentElementIds_.size) {
105      return `dependent components: no dependent elmtIds`;
106    }
107    let result: string = this.dependentElementIds_.size < 25
108      ? `dependent components: ${this.dependentElementIds_.size} elmtIds: `
109      : `WARNING: high number of dependent components (consider app redesign): ${this.dependentElementIds_.size} elmtIds: `;
110    let sepa: string = "";
111    this.dependentElementIds_.forEach((elmtId: number) => {
112      result+=`${sepa}${this.owningView_.debugInfoElmtId(elmtId)}`;
113      sepa = ", ";
114    });
115    return result;
116  }
117
118  /* for @Prop value from source we need to generate a @State
119     that observes when this value changes. This ObservedPropertyPU
120     sits inside SynchedPropertyOneWayPU.
121     below methods invent a fake variable name for it
122  */
123  protected getPropSourceObservedPropertyFakeName(): string {
124    return `${this.info()}_prop_fake_state_source___`;
125  }
126
127  protected isPropSourceObservedPropertyFakeName(): string | false {
128    return this.info().endsWith("_prop_fake_state_source___")
129      ? this.info().substring(0, this.info().length - "_prop_fake_state_source___".length)
130      : false;
131  }
132
133  /*
134    Virtualized version of the subscription mechanism - add subscriber
135    Overrides implementation in ObservedPropertyAbstract<T>
136  */
137  public addSubscriber(subscriber: ISinglePropertyChangeSubscriber<T>):void {
138    if (subscriber) {
139      // ObservedPropertyAbstract will also add subscriber to
140      // SubscriberManager map and to its own Set of subscribers as well
141      // Something to improve in the future for PU path.
142      // subscribeMe should accept IPropertySubscriber interface
143      super.subscribeMe(subscriber as ISinglePropertyChangeSubscriber<T>);
144      this.subscriberRefs_.add(subscriber);
145    }
146  }
147
148  /*
149    Virtualized version of the subscription mechanism - remove subscriber
150    Overrides implementation in ObservedPropertyAbstract<T>
151  */
152  public removeSubscriber(subscriber: IPropertySubscriber, id?: number):void {
153    if (subscriber) {
154      this.subscriberRefs_.delete(subscriber);
155      if (!id) {
156        id = subscriber.id__();
157      }
158    }
159    super.unlinkSuscriber(id);
160  }
161
162
163  /**
164   * put the property to delayed notification mode
165   * feature is only used for @StorageLink/Prop, @LocalStorageLink/Prop
166   */
167  public enableDelayedNotification() : void {
168  if (this.delayedNotification_ != ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending) {
169      stateMgmtConsole.debug(`${this.constructor.name}: enableDelayedNotification.`);
170      this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_none_pending;
171    }
172  }
173
174  /*
175     when moving from inActive to active state the owning ViewPU calls this function
176     This solution is faster than ViewPU polling each variable to send back a viewPropertyHasChanged event
177     with the elmtIds
178
179    returns undefined if variable has _not_ changed
180    returns dependentElementIds_ Set if changed. This Set is empty if variable is not used to construct the UI
181  */
182  public moveElmtIdsForDelayedUpdate(): Set<number> | undefined {
183    const result = (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending)
184      ? this.dependentElementIds_
185      : undefined;
186    stateMgmtConsole.debug(`${this.debugInfo()}: moveElmtIdsForDelayedUpdate: elmtIds that need delayed update \
187                      ${result ? Array.from(result).toString() : 'no delayed notifications'} .`);
188    this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay;
189    return result;
190  }
191
192  protected notifyPropertyRead() {
193    stateMgmtConsole.error(`${this.debugInfo()}: notifyPropertyRead, DO NOT USE with PU. Use \
194                      notifyPropertyHasBeenReadPU`);
195  }
196
197  protected notifyPropertyHasBeenReadPU() {
198    stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasBeenReadPU.`)
199    this.subscriberRefs_.forEach((subscriber) => {
200      if (subscriber) {
201        // TODO
202        // propertyHasBeenReadPU is not use in the code
203        // defined by interface that is not used either: PropertyReadEventListener
204        // Maybe compiler generated code has it?
205        if ('propertyHasBeenReadPU' in subscriber) {
206          (subscriber as unknown as PropertyReadEventListener<T>).propertyHasBeenReadPU(this);
207        }
208      }
209    });
210    this.recordDependentUpdate();
211  }
212
213  protected notifyPropertyHasChangedPU() {
214    stateMgmtConsole.debug(`${this.debugInfo()}: notifyPropertyHasChangedPU.`)
215    if (this.owningView_) {
216      if (this.delayedNotification_ == ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.do_not_delay) {
217        // send viewPropertyHasChanged right away
218        this.owningView_.viewPropertyHasChanged(this.info_, this.dependentElementIds_);
219      } else {
220        // mark this @StorageLink/Prop or @LocalStorageLink/Prop variable has having changed and notification of viewPropertyHasChanged delivery pending
221        this.delayedNotification_ = ObservedPropertyAbstractPU.DelayedNotifyChangesEnum.delay_notification_pending;
222      }
223    }
224    this.subscriberRefs_.forEach((subscriber) => {
225      if (subscriber) {
226        if ('syncPeerHasChanged' in subscriber) {
227          (subscriber as unknown as PeerChangeEventReceiverPU<T>).syncPeerHasChanged(this);
228        } else  {
229          stateMgmtConsole.warn(`${this.debugInfo()}: notifyPropertyHasChangedPU: unknown subscriber ID 'subscribedId' error!`);
230        }
231      }
232    });
233  }
234
235
236  public markDependentElementsDirty(view: ViewPU) {
237    // TODO ace-ets2bundle, framework, complicated apps need to update together
238    // this function will be removed after a short transition period.
239    stateMgmtConsole.warn(`${this.debugInfo()}: markDependentElementsDirty no longer supported. App will work ok, but
240        please update your ace-ets2bundle and recompile your application!`);
241  }
242
243  public numberOfSubscrbers(): number {
244    return this.subscriberRefs_.size + (this.owningView_ ? 1 : 0);
245  }
246
247  /*
248   type checking for any supported type, as required for union type support
249    see 1st parameter for explanation what is allowed
250
251    FIXME this expects the Map, Set patch to go in
252  */
253
254  protected checkIsSupportedValue(value: T): boolean {
255    return this.checkNewValue(
256      `undefined, null, number, boolean, string, or Object but not function`,
257      value,
258      () => ((typeof value == "object" && typeof value != "function")
259        || typeof value == "number" || typeof value == "string" || typeof value == "boolean")
260        || (value == undefined || value == null)
261    );
262  }
263
264  /*
265    type checking for allowed Object type value
266    see 1st parameter for explanation what is allowed
267
268      FIXME this expects the Map, Set patch to go in
269    */
270  protected checkIsObject(value: T): boolean {
271    return this.checkNewValue(
272      `undefined, null, Object including Array and instance of SubscribableAbstract and excluding function, Set, and Map`,
273      value,
274      () => (value == undefined || value == null || (typeof value == "object"))
275    );
276  }
277
278  /*
279    type checking for allowed simple types value
280    see 1st parameter for explanation what is allowed
281 */
282  protected checkIsSimple(value: T): boolean {
283    return this.checkNewValue(
284      `undefined, number, boolean, string`,
285      value,
286      () => (value == undefined || typeof value == "number" || typeof value == "string" || typeof value == "boolean")
287    );
288  }
289
290  protected checkNewValue(isAllowedComment : string, newValue: T, validator: (value: T) => boolean) : boolean {
291    if (validator(newValue)) {
292      return true;
293    }
294
295    // report error
296    // current implementation throws an Exception
297    errorReport.varValueCheckFailed({
298      customComponent: this.debugInfoOwningView(),
299      variableDeco: this.debugInfoDecorator(),
300      variableName: this.info(),
301      expectedType: isAllowedComment,
302      value: newValue
303    });
304
305    // never gets here if errorReport.varValueCheckFailed throws an exception
306    // but should nto depend on its implementation
307    return false;
308  }
309
310
311  /**
312   * factory function for concrete 'object' or 'simple' ObservedProperty object
313   * depending if value is Class object
314   * or simple type (boolean | number | string)
315   * @param value
316   * @param owningView
317   * @param thisPropertyName
318   * @returns either
319   */
320  static CreateObservedObject<C>(value: C, owningView: IPropertySubscriber, thisPropertyName: PropertyInfo)
321    : ObservedPropertyAbstract<C> {
322    return (typeof value === "object") ?
323      new ObservedPropertyObject(value, owningView, thisPropertyName)
324      : new ObservedPropertySimple(value, owningView, thisPropertyName);
325  }
326
327
328  /**
329   * during 'get' access recording take note of the created component and its elmtId
330   * and add this component to the list of components who are dependent on this property
331   */
332  protected recordDependentUpdate() {
333    const elmtId = ViewStackProcessor.GetElmtIdToAccountFor();
334    if (elmtId < 0) {
335      // not access recording
336      return;
337    }
338    stateMgmtConsole.debug(`${this.debugInfo()}: recordDependentUpdate on elmtId ${elmtId}.`)
339    this.dependentElementIds_.add(elmtId);
340  }
341
342
343  public purgeDependencyOnElmtId(rmElmtId: number): void {
344    stateMgmtConsole.debug(`${this.debugInfo()}: purgeDependencyOnElmtId ${rmElmtId}`);
345    this.dependentElementIds_.delete(rmElmtId);
346  }
347
348  public SetPropertyUnchanged(): void {
349    // function to be removed
350    // keep it here until transpiler is updated.
351  }
352
353  // FIXME check, is this used from AppStorage.
354  // unified Appstorage, what classes to use, and the API
355  public createLink(subscribeOwner?: IPropertySubscriber,
356    linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> {
357      throw new Error(`${this.debugInfo()}: createLink: Can not create a AppStorage 'Link' from this property.`);
358  }
359
360  public createProp(subscribeOwner?: IPropertySubscriber,
361    linkPropName?: PropertyInfo): ObservedPropertyAbstractPU<T> {
362      throw new Error(`${this.debugInfo()}: createProp: Can not create a AppStorage 'Prop' from a @State property. `);
363  }
364
365  /*
366    Below empty functions required to keep as long as this class derives from FU version
367    ObservedPropertyAbstract. Need to overwrite these functions to do nothing for PU
368    */
369    protected notifyHasChanged(_: T) {
370      stateMgmtConsole.error(`${this.debugInfo()}: notifyHasChanged, DO NOT USE with PU. Use syncPeerHasChanged() \
371                                            or objectPropertyHasChangedPU()`);
372    }
373
374    hasChanged(_: T): void {
375      // unused for PU
376      // need to overwrite impl of base class with empty function.
377    }
378
379    propertyHasChanged(_?: PropertyInfo): void {
380      // unused for PU
381      // need to overwrite impl of base class with empty function.
382    }
383
384    propertyRead(_?: PropertyInfo): void {
385      // unused for PU
386      // need to overwrite impl of base class with empty function.
387    }
388}