001package arez;
002
003import arez.spy.ComputableValueActivateEvent;
004import arez.spy.ComputableValueDeactivateEvent;
005import arez.spy.ObservableValueChangeEvent;
006import arez.spy.ObservableValueDisposeEvent;
007import arez.spy.ObservableValueInfo;
008import arez.spy.PropertyAccessor;
009import arez.spy.PropertyMutator;
010import grim.annotations.OmitSymbol;
011import java.util.ArrayList;
012import java.util.Comparator;
013import java.util.List;
014import java.util.Objects;
015import javax.annotation.Nonnull;
016import javax.annotation.Nullable;
017import static org.realityforge.braincheck.Guards.*;
018
019/**
020 * The observable represents state that can be observed within the system.
021 */
022public final class ObservableValue<T>
023  extends Node
024{
025  /**
026   * The value of _workState when the ObservableValue is should longer be used.
027   */
028  static final int DISPOSED = -2;
029  /**
030   * The value that _workState is set to to optimize the detection of duplicate,
031   * existing and new dependencies during tracking completion.
032   */
033  static final int IN_CURRENT_TRACKING = -1;
034  /**
035   * The value that _workState is when the observer has been added as new dependency
036   * to derivation.
037   */
038  static final int NOT_IN_CURRENT_TRACKING = 0;
039  private final List<Observer> _observers = new ArrayList<>();
040  /**
041   * True if deactivation has been requested.
042   * Used to avoid adding duplicates to pending deactivation list.
043   */
044  private boolean _pendingDeactivation;
045  /**
046   * The workState variable contains some data used during processing of observable
047   * at various stages.
048   *
049   * Within the scope of a tracking transaction, it is set to the id of the tracking
050   * observer if the observable was observed. This enables an optimization that skips
051   * adding this observer to the same observer multiple times. This optimization sometimes
052   * ignored as nested transactions that observe the same observer will reset this value.
053   *
054   * When completing a tracking transaction the value may be set to {@link #IN_CURRENT_TRACKING}
055   * or {@link #NOT_IN_CURRENT_TRACKING} but should be set to {@link #NOT_IN_CURRENT_TRACKING} after
056   * {@link Transaction#completeTracking()} method is completed..
057   */
058  private int _workState;
059  /**
060   * The state of the observer that is least stale.
061   * This cached value is used to avoid redundant propagations.
062   */
063  private int _leastStaleObserverState = Observer.Flags.STATE_UP_TO_DATE;
064  /**
065   * The observer that created this observable if any.
066   */
067  @Nullable
068  private final Observer _observer;
069  /**
070   * The component that this observable is contained within.
071   * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but may also be null if
072   * the observable is a "top-level" observable.
073   */
074  @OmitSymbol( unless = "arez.enable_native_components" )
075  @Nullable
076  private final Component _component;
077  /**
078   * The accessor method to retrieve the value.
079   * This should only be set if {@link Arez#arePropertyIntrospectorsEnabled()} is true but may also be elided if the
080   * value should not be accessed even by DevTools.
081   */
082  @OmitSymbol( unless = "arez.enable_property_introspection" )
083  @Nullable
084  private final PropertyAccessor<T> _accessor;
085  /**
086   * The mutator method to change the value.
087   * This should only be set if {@link Arez#arePropertyIntrospectorsEnabled()} is true but may also be elided if the
088   * value should not be mutated even by DevTools.
089   */
090  @OmitSymbol( unless = "arez.enable_property_introspection" )
091  @Nullable
092  private final PropertyMutator<T> _mutator;
093  /**
094   * Cached info object associated with element.
095   * This should be null if {@link Arez#areSpiesEnabled()} is false;
096   */
097  @OmitSymbol( unless = "arez.enable_spies" )
098  @Nullable
099  private ObservableValueInfo _info;
100
101  ObservableValue( @Nullable final ArezContext context,
102                   @Nullable final Component component,
103                   @Nullable final String name,
104                   @Nullable final Observer observer,
105                   @Nullable final PropertyAccessor<T> accessor,
106                   @Nullable final PropertyMutator<T> mutator )
107  {
108    super( context, name );
109    _component = Arez.areNativeComponentsEnabled() ? component : null;
110    _observer = observer;
111    _accessor = accessor;
112    _mutator = mutator;
113    if ( Arez.shouldCheckInvariants() )
114    {
115      invariant( () -> Arez.areNativeComponentsEnabled() || null == component,
116                 () -> "Arez-0054: ObservableValue named '" + getName() + "' has component specified but " +
117                       "Arez.areNativeComponentsEnabled() is false." );
118    }
119    if ( Arez.shouldCheckApiInvariants() )
120    {
121      apiInvariant( () -> Arez.arePropertyIntrospectorsEnabled() || null == accessor,
122                    () -> "Arez-0055: ObservableValue named '" + getName() + "' has accessor specified but " +
123                          "Arez.arePropertyIntrospectorsEnabled() is false." );
124      apiInvariant( () -> Arez.arePropertyIntrospectorsEnabled() || null == mutator,
125                    () -> "Arez-0056: ObservableValue named '" + getName() + "' has mutator specified but " +
126                          "Arez.arePropertyIntrospectorsEnabled() is false." );
127    }
128    if ( null != _observer )
129    {
130      // This invariant can not be checked if Arez.shouldEnforceTransactionType() is false as
131      // the variable has yet to be assigned and no transaction mode set. Thus just skip the
132      // check in this scenario.
133      if ( Arez.shouldCheckInvariants() )
134      {
135        invariant( () -> !Arez.shouldEnforceTransactionType() || _observer.isComputableValue(),
136                   () -> "Arez-0057: ObservableValue named '" + getName() + "' has observer specified but " +
137                         "observer is not part of a ComputableValue." );
138      }
139      assert !Arez.areNamesEnabled() || _observer.getName().equals( name );
140    }
141    if ( !isComputableValue() )
142    {
143      if ( null != _component )
144      {
145        _component.addObservableValue( this );
146      }
147      else if ( Arez.areRegistriesEnabled() )
148      {
149        getContext().registerObservableValue( this );
150      }
151    }
152  }
153
154  @Override
155  public void dispose()
156  {
157    if ( isNotDisposed() )
158    {
159      getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, this::performDispose );
160      // All dependencies should have been released by the time it comes to deactivate phase.
161      // The ObservableValue has been marked as changed, forcing all observers to re-evaluate and
162      // ultimately this will result in their removal of this ObservableValue as a dependency as
163      // it is an error to invoke reportObserved(). Once all dependencies are removed then
164      // this ObservableValue will be deactivated if it is a ComputableValue. Thus no need to call
165      // queueForDeactivation() here.
166      if ( isComputableValue() )
167      {
168        /*
169         * Dispose the owner first so that it is removed as a dependency and thus will not have a reaction
170         * scheduled.
171         */
172        getObserver().dispose();
173      }
174      else
175      {
176        if ( willPropagateSpyEvents() )
177        {
178          reportSpyEvent( new ObservableValueDisposeEvent( asInfo() ) );
179        }
180        if ( null != _component )
181        {
182          _component.removeObservableValue( this );
183        }
184        else if ( Arez.areRegistriesEnabled() )
185        {
186          getContext().deregisterObservableValue( this );
187        }
188      }
189    }
190  }
191
192  private void performDispose()
193  {
194    reportChanged();
195    getContext().getTransaction().reportDispose( this );
196    _workState = DISPOSED;
197  }
198
199  @Override
200  public boolean isDisposed()
201  {
202    return DISPOSED == _workState;
203  }
204
205  @OmitSymbol( unless = "arez.enable_property_introspection" )
206  @Nullable
207  PropertyAccessor<T> getAccessor()
208  {
209    if ( Arez.shouldCheckInvariants() )
210    {
211      invariant( Arez::arePropertyIntrospectorsEnabled,
212                 () -> "Arez-0058: Attempt to invoke getAccessor() on ObservableValue named '" + getName() +
213                       "' when Arez.arePropertyIntrospectorsEnabled() returns false." );
214    }
215    return _accessor;
216  }
217
218  @OmitSymbol( unless = "arez.enable_property_introspection" )
219  @Nullable
220  PropertyMutator<T> getMutator()
221  {
222    if ( Arez.shouldCheckInvariants() )
223    {
224      invariant( Arez::arePropertyIntrospectorsEnabled,
225                 () -> "Arez-0059: Attempt to invoke getMutator() on ObservableValue named '" + getName() +
226                       "' when Arez.arePropertyIntrospectorsEnabled() returns false." );
227    }
228    return _mutator;
229  }
230
231  void markAsPendingDeactivation()
232  {
233    _pendingDeactivation = true;
234  }
235
236  boolean isPendingDeactivation()
237  {
238    return _pendingDeactivation;
239  }
240
241  void resetPendingDeactivation()
242  {
243    _pendingDeactivation = false;
244  }
245
246  int getLastTrackerTransactionId()
247  {
248    return _workState;
249  }
250
251  void setLastTrackerTransactionId( final int lastTrackerTransactionId )
252  {
253    setWorkState( lastTrackerTransactionId );
254  }
255
256  void setWorkState( final int workState )
257  {
258    _workState = workState;
259  }
260
261  boolean isInCurrentTracking()
262  {
263    return IN_CURRENT_TRACKING == _workState;
264  }
265
266  void putInCurrentTracking()
267  {
268    _workState = IN_CURRENT_TRACKING;
269  }
270
271  void removeFromCurrentTracking()
272  {
273    _workState = NOT_IN_CURRENT_TRACKING;
274  }
275
276  @Nonnull
277  Observer getObserver()
278  {
279    assert null != _observer;
280    return _observer;
281  }
282
283  /**
284   * Return true if this observable can deactivate when it is no longer observed and has no keepAlive locks and activate when it is observed again.
285   */
286  boolean canDeactivate()
287  {
288    return isComputableValue() && !getObserver().isKeepAlive();
289  }
290
291  boolean canDeactivateNow()
292  {
293    return canDeactivate() && !hasObservers() && 0 == getObserver().getComputableValue().getKeepAliveRefCount();
294  }
295
296  /**
297   * Return true if this observable is derived from an observer.
298   */
299  boolean isComputableValue()
300  {
301    return null != _observer;
302  }
303
304  /**
305   * Return true if observable is notifying observers.
306   */
307  boolean isActive()
308  {
309    return null == _observer || _observer.isActive();
310  }
311
312  /**
313   * Deactivate the observable.
314   * This means that the observable no longer has any listeners and can release resources associated
315   * with generating values. (i.e. remove observers on any observables that are used to compute the
316   * value of this observable).
317   */
318  void deactivate()
319  {
320    if ( Arez.shouldCheckInvariants() )
321    {
322      invariant( () -> getContext().isTransactionActive(),
323                 () -> "Arez-0060: Attempt to invoke deactivate on ObservableValue named '" + getName() +
324                       "' when there is no active transaction." );
325      invariant( this::canDeactivate,
326                 () -> "Arez-0061: Invoked deactivate on ObservableValue named '" + getName() + "' but " +
327                       "ObservableValue can not be deactivated. Either owner is null or the associated " +
328                       "ComputableValue has keepAlive enabled." );
329    }
330    assert null != _observer;
331    if ( _observer.isActive() )
332    {
333      // We do not need to send deactivate even if the computable value was accessed from within an action
334      // and has no associated observers. There has been no associated "Activate" event so there need not
335      // be a deactivate event.
336      final boolean shouldPropagateDeactivateEvent = willPropagateSpyEvents() && !getObservers().isEmpty();
337
338      /*
339       * It is possible for the owner to already be deactivated if dispose() is explicitly
340       * called within the transaction.
341       */
342      _observer.setState( Observer.Flags.STATE_INACTIVE );
343      if ( willPropagateSpyEvents() && shouldPropagateDeactivateEvent )
344      {
345        reportSpyEvent( new ComputableValueDeactivateEvent( _observer.getComputableValue().asInfo() ) );
346      }
347    }
348  }
349
350  /**
351   * Activate the observable.
352   * The reverse of {@link #deactivate()}.
353   */
354  void activate()
355  {
356    if ( Arez.shouldCheckInvariants() )
357    {
358      invariant( () -> getContext().isTransactionActive(),
359                 () -> "Arez-0062: Attempt to invoke activate on ObservableValue named '" + getName() +
360                       "' when there is no active transaction." );
361      invariant( () -> null != _observer,
362                 () -> "Arez-0063: Invoked activate on ObservableValue named '" + getName() + "' when owner is null." );
363      assert null != _observer;
364      invariant( _observer::isInactive,
365                 () -> "Arez-0064: Invoked activate on ObservableValue named '" + getName() + "' when " +
366                       "ObservableValue is already active." );
367    }
368    assert null != _observer;
369    _observer.setState( Observer.Flags.STATE_UP_TO_DATE );
370    if ( willPropagateSpyEvents() )
371    {
372      reportSpyEvent( new ComputableValueActivateEvent( _observer.getComputableValue().asInfo() ) );
373    }
374  }
375
376  @Nonnull
377  List<Observer> getObservers()
378  {
379    return _observers;
380  }
381
382  boolean hasObservers()
383  {
384    return getObservers().size() > 0;
385  }
386
387  boolean hasObserver( @Nonnull final Observer observer )
388  {
389    return getObservers().contains( observer );
390  }
391
392  void addObserver( @Nonnull final Observer observer )
393  {
394    if ( Arez.shouldCheckInvariants() )
395    {
396      invariant( () -> getContext().isTransactionActive(),
397                 () -> "Arez-0065: Attempt to invoke addObserver on ObservableValue named '" + getName() +
398                       "' when there is no active transaction." );
399      invariantObserversLinked();
400      invariant( () -> !hasObserver( observer ),
401                 () -> "Arez-0066: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
402                       "named '" + getName() + "' when observer is already observing ObservableValue." );
403      invariant( this::isNotDisposed,
404                 () -> "Arez-0067: Attempting to add observer named '" + observer.getName() + "' to " +
405                       "ObservableValue named '" + getName() + "' when ObservableValue is disposed." );
406      invariant( observer::isNotDisposed,
407                 () -> "Arez-0068: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
408                       "named '" + getName() + "' when observer is disposed." );
409      invariant( () -> !isComputableValue() ||
410                       observer.canObserveLowerPriorityDependencies() ||
411                       observer.getTask().getPriority().ordinal() >= getObserver().getTask().getPriority().ordinal(),
412                 () -> "Arez-0183: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
413                       "named '" + getName() + "' where the observer is scheduled at a " +
414                       observer.getTask().getPriority() + " priority but the ObservableValue's owner is scheduled " +
415                       "at a " + getObserver().getTask().getPriority() + " priority." );
416      invariant( () -> getContext().getTransaction().getTracker() == observer,
417                 () -> "Arez-0203: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
418                       "named '" + getName() + "' but the observer is not the tracker in transaction named '" +
419                       getContext().getTransaction().getName() + "'." );
420    }
421    rawAddObserver( observer );
422  }
423
424  void rawAddObserver( @Nonnull final Observer observer )
425  {
426    getObservers().add( observer );
427
428    final int state = observer.getLeastStaleObserverState();
429    if ( _leastStaleObserverState > state )
430    {
431      _leastStaleObserverState = state;
432    }
433  }
434
435  void removeObserver( @Nonnull final Observer observer )
436  {
437    if ( Arez.shouldCheckInvariants() )
438    {
439      invariant( () -> getContext().isTransactionActive(),
440                 () -> "Arez-0069: Attempt to invoke removeObserver on ObservableValue named '" + getName() + "' " +
441                       "when there is no active transaction." );
442      invariantObserversLinked();
443      invariant( () -> hasObserver( observer ),
444                 () -> "Arez-0070: Attempting to remove observer named '" + observer.getName() + "' from " +
445                       "ObservableValue named '" + getName() + "' when observer is not observing ObservableValue." );
446    }
447    final List<Observer> observers = getObservers();
448    observers.remove( observer );
449    if ( canDeactivateNow() )
450    {
451      queueForDeactivation();
452    }
453    if ( Arez.shouldCheckInvariants() )
454    {
455      invariantObserversLinked();
456    }
457  }
458
459  void queueForDeactivation()
460  {
461    if ( Arez.shouldCheckInvariants() )
462    {
463      invariant( () -> getContext().isTransactionActive(),
464                 () -> "Arez-0071: Attempt to invoke queueForDeactivation on ObservableValue named '" + getName() +
465                       "' when there is no active transaction." );
466      invariant( this::canDeactivateNow,
467                 () -> "Arez-0072: Attempted to invoke queueForDeactivation() on ObservableValue named '" + getName() +
468                       "' but ObservableValue is not able to be deactivated." );
469      invariant( () -> !hasObservers(),
470                 () -> "Arez-0073: Attempted to invoke queueForDeactivation() on ObservableValue named '" + getName() +
471                       "' but ObservableValue has observers." );
472    }
473    if ( !isPendingDeactivation() )
474    {
475      getContext().getTransaction().queueForDeactivation( this );
476    }
477  }
478
479  void setLeastStaleObserverState( final int leastStaleObserverState )
480  {
481    if ( Arez.shouldCheckInvariants() )
482    {
483      invariant( () -> getContext().isTransactionActive(),
484                 () -> "Arez-0074: Attempt to invoke setLeastStaleObserverState on ObservableValue named '" +
485                       getName() + "' when there is no active transaction." );
486      invariant( () -> Observer.Flags.isActive( leastStaleObserverState ),
487                 () -> "Arez-0075: Attempt to invoke setLeastStaleObserverState on ObservableValue named '" +
488                       getName() + "' with invalid value " + Observer.Flags.getStateName( leastStaleObserverState ) +
489                       "." );
490    }
491    _leastStaleObserverState = leastStaleObserverState;
492  }
493
494  int getLeastStaleObserverState()
495  {
496    return _leastStaleObserverState;
497  }
498
499  /**
500   * Notify Arez that this observable has been "observed" in the current transaction.
501   * Before invoking this method, a transaction <b>MUST</b> be active but it may be read-only or read-write.
502   */
503  public void reportObserved()
504  {
505    getContext().getTransaction().observe( this );
506  }
507
508  /**
509   * Notify Arez that this observable has been "observed" if a tracking transaction is active.
510   */
511  public void reportObservedIfTrackingTransactionActive()
512  {
513    if ( getContext().isTrackingTransactionActive() )
514    {
515      reportObserved();
516    }
517  }
518
519  /**
520   * Check that pre-conditions are satisfied before changing observable value.
521   * In production mode this will typically be a no-op. This method should be invoked
522   * before state is modified. Before invoking this method, a read-write transaction <b>MUST</b> be active.
523   */
524  @OmitSymbol( unless = "arez.check_invariants" )
525  public void preReportChanged()
526  {
527    if ( Arez.shouldCheckInvariants() )
528    {
529      getContext().getTransaction().preReportChanged( this );
530    }
531  }
532
533  /**
534   * Notify Arez that this observable has changed.
535   * This is called when the observable has definitely changed.
536   * Before invoking this method, a read-write transaction <b>MUST</b> be active.
537   */
538  public void reportChanged()
539  {
540    if ( willPropagateSpyEvents() )
541    {
542      // isDisposed is checked as we call reportChanged() from performDispose() after dispose has started
543      // and thus it is no longer valid to call getObservableValue()
544      reportSpyEvent( new ObservableValueChangeEvent( asInfo(), isDisposed() ? null : getObservableValue() ) );
545    }
546    getContext().getTransaction().reportChanged( this );
547  }
548
549  void reportChangeConfirmed()
550  {
551    if ( willPropagateSpyEvents() )
552    {
553      reportSpyEvent( new ObservableValueChangeEvent( asInfo(), getObservableValue() ) );
554    }
555    getContext().getTransaction().reportChangeConfirmed( this );
556  }
557
558  void reportPossiblyChanged()
559  {
560    getContext().getTransaction().reportPossiblyChanged( this );
561  }
562
563  /**
564   * Return the value from observable if introspectors are enabled and an accessor has been supplied.
565   */
566  @Nullable
567  private Object getObservableValue()
568  {
569    if ( Arez.arePropertyIntrospectorsEnabled() && null != getAccessor() )
570    {
571      try
572      {
573        return getAccessor().get();
574      }
575      catch ( final Throwable ignored )
576      {
577      }
578    }
579    return null;
580  }
581
582  /**
583   * Return the info associated with this class.
584   *
585   * @return the info associated with this class.
586   */
587  @SuppressWarnings( "ConstantConditions" )
588  @OmitSymbol( unless = "arez.enable_spies" )
589  @Nonnull
590  ObservableValueInfo asInfo()
591  {
592    if ( Arez.shouldCheckInvariants() )
593    {
594      invariant( Arez::areSpiesEnabled,
595                 () -> "Arez-0196: ObservableValue.asInfo() invoked but Arez.areSpiesEnabled() returned false." );
596    }
597    if ( Arez.areSpiesEnabled() && null == _info )
598    {
599      _info = new ObservableValueInfoImpl( this );
600    }
601    return Arez.areSpiesEnabled() ? _info : null;
602  }
603
604  void invariantOwner()
605  {
606    if ( Arez.shouldCheckInvariants() && null != _observer )
607    {
608      invariant( () -> Objects.equals( _observer.getComputableValue().getObservableValue(), this ),
609                 () -> "Arez-0076: ObservableValue named '" + getName() + "' has owner specified but owner does " +
610                       "not link to ObservableValue as derived value." );
611    }
612  }
613
614  void invariantObserversLinked()
615  {
616    if ( Arez.shouldCheckInvariants() )
617    {
618      getObservers().forEach( observer ->
619                                invariant( () -> observer.getDependencies().contains( this ),
620                                           () -> "Arez-0077: ObservableValue named '" + getName() + "' has observer " +
621                                                 "named '" + observer.getName() + "' which does not contain " +
622                                                 "ObservableValue as dependency." ) );
623    }
624  }
625
626  void invariantLeastStaleObserverState()
627  {
628    if ( Arez.shouldCheckInvariants() )
629    {
630      final int leastStaleObserverState =
631        getObservers().stream().
632          map( Observer::getLeastStaleObserverState ).
633          min( Comparator.naturalOrder() ).orElse( Observer.Flags.STATE_UP_TO_DATE );
634      invariant( () -> leastStaleObserverState >= _leastStaleObserverState,
635                 () -> "Arez-0078: Calculated leastStaleObserverState on ObservableValue named '" +
636                       getName() + "' is '" + Observer.Flags.getStateName( leastStaleObserverState ) +
637                       "' which is unexpectedly less than cached value '" +
638                       Observer.Flags.getStateName( _leastStaleObserverState ) + "'." );
639    }
640  }
641
642  @Nullable
643  Component getComponent()
644  {
645    return _component;
646  }
647
648  @OmitSymbol
649  int getWorkState()
650  {
651    return _workState;
652  }
653}