All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arez.ObservableValue Maven / Gradle / Ivy

There is a newer version: 0.213
Show newest version
package arez;

import arez.spy.ComputableValueActivateEvent;
import arez.spy.ComputableValueDeactivateEvent;
import arez.spy.ObservableValueChangeEvent;
import arez.spy.ObservableValueDisposeEvent;
import arez.spy.ObservableValueInfo;
import arez.spy.PropertyAccessor;
import arez.spy.PropertyMutator;
import grim.annotations.OmitSymbol;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.realityforge.braincheck.Guards.*;

/**
 * The observable represents state that can be observed within the system.
 */
public final class ObservableValue
  extends Node
{
  /**
   * The value of _workState when the ObservableValue is should longer be used.
   */
  static final int DISPOSED = -2;
  /**
   * The value that _workState is set to to optimize the detection of duplicate,
   * existing and new dependencies during tracking completion.
   */
  static final int IN_CURRENT_TRACKING = -1;
  /**
   * The value that _workState is when the observer has been added as new dependency
   * to derivation.
   */
  static final int NOT_IN_CURRENT_TRACKING = 0;
  private final List _observers = new ArrayList<>();
  /**
   * True if deactivation has been requested.
   * Used to avoid adding duplicates to pending deactivation list.
   */
  private boolean _pendingDeactivation;
  /**
   * The workState variable contains some data used during processing of observable
   * at various stages.
   *
   * Within the scope of a tracking transaction, it is set to the id of the tracking
   * observer if the observable was observed. This enables an optimization that skips
   * adding this observer to the same observer multiple times. This optimization sometimes
   * ignored as nested transactions that observe the same observer will reset this value.
   *
   * When completing a tracking transaction the value may be set to {@link #IN_CURRENT_TRACKING}
   * or {@link #NOT_IN_CURRENT_TRACKING} but should be set to {@link #NOT_IN_CURRENT_TRACKING} after
   * {@link Transaction#completeTracking()} method is completed..
   */
  private int _workState;
  /**
   * The state of the observer that is least stale.
   * This cached value is used to avoid redundant propagations.
   */
  private int _leastStaleObserverState = Observer.Flags.STATE_UP_TO_DATE;
  /**
   * The observer that created this observable if any.
   */
  @Nullable
  private final Observer _observer;
  /**
   * The component that this observable is contained within.
   * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but may also be null if
   * the observable is a "top-level" observable.
   */
  @OmitSymbol( unless = "arez.enable_native_components" )
  @Nullable
  private final Component _component;
  /**
   * The accessor method to retrieve the value.
   * This should only be set if {@link Arez#arePropertyIntrospectorsEnabled()} is true but may also be elided if the
   * value should not be accessed even by DevTools.
   */
  @OmitSymbol( unless = "arez.enable_property_introspection" )
  @Nullable
  private final PropertyAccessor _accessor;
  /**
   * The mutator method to change the value.
   * This should only be set if {@link Arez#arePropertyIntrospectorsEnabled()} is true but may also be elided if the
   * value should not be mutated even by DevTools.
   */
  @OmitSymbol( unless = "arez.enable_property_introspection" )
  @Nullable
  private final PropertyMutator _mutator;
  /**
   * Cached info object associated with element.
   * This should be null if {@link Arez#areSpiesEnabled()} is false;
   */
  @OmitSymbol( unless = "arez.enable_spies" )
  @Nullable
  private ObservableValueInfo _info;

  ObservableValue( @Nullable final ArezContext context,
                   @Nullable final Component component,
                   @Nullable final String name,
                   @Nullable final Observer observer,
                   @Nullable final PropertyAccessor accessor,
                   @Nullable final PropertyMutator mutator )
  {
    super( context, name );
    _component = Arez.areNativeComponentsEnabled() ? component : null;
    _observer = observer;
    _accessor = accessor;
    _mutator = mutator;
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> Arez.areNativeComponentsEnabled() || null == component,
                 () -> "Arez-0054: ObservableValue named '" + getName() + "' has component specified but " +
                       "Arez.areNativeComponentsEnabled() is false." );
    }
    if ( Arez.shouldCheckApiInvariants() )
    {
      apiInvariant( () -> Arez.arePropertyIntrospectorsEnabled() || null == accessor,
                    () -> "Arez-0055: ObservableValue named '" + getName() + "' has accessor specified but " +
                          "Arez.arePropertyIntrospectorsEnabled() is false." );
      apiInvariant( () -> Arez.arePropertyIntrospectorsEnabled() || null == mutator,
                    () -> "Arez-0056: ObservableValue named '" + getName() + "' has mutator specified but " +
                          "Arez.arePropertyIntrospectorsEnabled() is false." );
    }
    if ( null != _observer )
    {
      // This invariant can not be checked if Arez.shouldEnforceTransactionType() is false as
      // the variable has yet to be assigned and no transaction mode set. Thus just skip the
      // check in this scenario.
      if ( Arez.shouldCheckInvariants() )
      {
        invariant( () -> !Arez.shouldEnforceTransactionType() || _observer.isComputableValue(),
                   () -> "Arez-0057: ObservableValue named '" + getName() + "' has observer specified but " +
                         "observer is not part of a ComputableValue." );
      }
      assert !Arez.areNamesEnabled() || _observer.getName().equals( name );
    }
    if ( !isComputableValue() )
    {
      if ( null != _component )
      {
        _component.addObservableValue( this );
      }
      else if ( Arez.areRegistriesEnabled() )
      {
        getContext().registerObservableValue( this );
      }
    }
  }

  @Override
  public void dispose()
  {
    if ( isNotDisposed() )
    {
      getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, this::performDispose );
      // All dependencies should have been released by the time it comes to deactivate phase.
      // The ObservableValue has been marked as changed, forcing all observers to re-evaluate and
      // ultimately this will result in their removal of this ObservableValue as a dependency as
      // it is an error to invoke reportObserved(). Once all dependencies are removed then
      // this ObservableValue will be deactivated if it is a ComputableValue. Thus no need to call
      // queueForDeactivation() here.
      if ( isComputableValue() )
      {
        /*
         * Dispose the owner first so that it is removed as a dependency and thus will not have a reaction
         * scheduled.
         */
        getObserver().dispose();
      }
      else
      {
        if ( willPropagateSpyEvents() )
        {
          reportSpyEvent( new ObservableValueDisposeEvent( asInfo() ) );
        }
        if ( null != _component )
        {
          _component.removeObservableValue( this );
        }
        else if ( Arez.areRegistriesEnabled() )
        {
          getContext().deregisterObservableValue( this );
        }
      }
    }
  }

  private void performDispose()
  {
    reportChanged();
    getContext().getTransaction().reportDispose( this );
    _workState = DISPOSED;
  }

  @Override
  public boolean isDisposed()
  {
    return DISPOSED == _workState;
  }

  @OmitSymbol( unless = "arez.enable_property_introspection" )
  @Nullable
  PropertyAccessor getAccessor()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( Arez::arePropertyIntrospectorsEnabled,
                 () -> "Arez-0058: Attempt to invoke getAccessor() on ObservableValue named '" + getName() +
                       "' when Arez.arePropertyIntrospectorsEnabled() returns false." );
    }
    return _accessor;
  }

  @OmitSymbol( unless = "arez.enable_property_introspection" )
  @Nullable
  PropertyMutator getMutator()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( Arez::arePropertyIntrospectorsEnabled,
                 () -> "Arez-0059: Attempt to invoke getMutator() on ObservableValue named '" + getName() +
                       "' when Arez.arePropertyIntrospectorsEnabled() returns false." );
    }
    return _mutator;
  }

  void markAsPendingDeactivation()
  {
    _pendingDeactivation = true;
  }

  boolean isPendingDeactivation()
  {
    return _pendingDeactivation;
  }

  void resetPendingDeactivation()
  {
    _pendingDeactivation = false;
  }

  int getLastTrackerTransactionId()
  {
    return _workState;
  }

  void setLastTrackerTransactionId( final int lastTrackerTransactionId )
  {
    setWorkState( lastTrackerTransactionId );
  }

  void setWorkState( final int workState )
  {
    _workState = workState;
  }

  boolean isInCurrentTracking()
  {
    return IN_CURRENT_TRACKING == _workState;
  }

  void putInCurrentTracking()
  {
    _workState = IN_CURRENT_TRACKING;
  }

  void removeFromCurrentTracking()
  {
    _workState = NOT_IN_CURRENT_TRACKING;
  }

  @Nonnull
  Observer getObserver()
  {
    assert null != _observer;
    return _observer;
  }

  /**
   * 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.
   */
  boolean canDeactivate()
  {
    return isComputableValue() && !getObserver().isKeepAlive();
  }

  boolean canDeactivateNow()
  {
    return canDeactivate() && !hasObservers() && 0 == getObserver().getComputableValue().getKeepAliveRefCount();
  }

  /**
   * Return true if this observable is derived from an observer.
   */
  boolean isComputableValue()
  {
    return null != _observer;
  }

  /**
   * Return true if observable is notifying observers.
   */
  boolean isActive()
  {
    return null == _observer || _observer.isActive();
  }

  /**
   * Deactivate the observable.
   * This means that the observable no longer has any listeners and can release resources associated
   * with generating values. (i.e. remove observers on any observables that are used to compute the
   * value of this observable).
   */
  void deactivate()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0060: Attempt to invoke deactivate on ObservableValue named '" + getName() +
                       "' when there is no active transaction." );
      invariant( this::canDeactivate,
                 () -> "Arez-0061: Invoked deactivate on ObservableValue named '" + getName() + "' but " +
                       "ObservableValue can not be deactivated. Either owner is null or the associated " +
                       "ComputableValue has keepAlive enabled." );
    }
    assert null != _observer;
    if ( _observer.isActive() )
    {
      // We do not need to send deactivate even if the computable value was accessed from within an action
      // and has no associated observers. There has been no associated "Activate" event so there need not
      // be a deactivate event.
      final boolean shouldPropagateDeactivateEvent = willPropagateSpyEvents() && !getObservers().isEmpty();

      /*
       * It is possible for the owner to already be deactivated if dispose() is explicitly
       * called within the transaction.
       */
      _observer.setState( Observer.Flags.STATE_INACTIVE );
      if ( willPropagateSpyEvents() && shouldPropagateDeactivateEvent )
      {
        reportSpyEvent( new ComputableValueDeactivateEvent( _observer.getComputableValue().asInfo() ) );
      }
    }
  }

  /**
   * Activate the observable.
   * The reverse of {@link #deactivate()}.
   */
  void activate()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0062: Attempt to invoke activate on ObservableValue named '" + getName() +
                       "' when there is no active transaction." );
      invariant( () -> null != _observer,
                 () -> "Arez-0063: Invoked activate on ObservableValue named '" + getName() + "' when owner is null." );
      assert null != _observer;
      invariant( _observer::isInactive,
                 () -> "Arez-0064: Invoked activate on ObservableValue named '" + getName() + "' when " +
                       "ObservableValue is already active." );
    }
    assert null != _observer;
    _observer.setState( Observer.Flags.STATE_UP_TO_DATE );
    if ( willPropagateSpyEvents() )
    {
      reportSpyEvent( new ComputableValueActivateEvent( _observer.getComputableValue().asInfo() ) );
    }
  }

  @Nonnull
  List getObservers()
  {
    return _observers;
  }

  boolean hasObservers()
  {
    return getObservers().size() > 0;
  }

  boolean hasObserver( @Nonnull final Observer observer )
  {
    return getObservers().contains( observer );
  }

  void addObserver( @Nonnull final Observer observer )
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0065: Attempt to invoke addObserver on ObservableValue named '" + getName() +
                       "' when there is no active transaction." );
      invariantObserversLinked();
      invariant( () -> !hasObserver( observer ),
                 () -> "Arez-0066: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
                       "named '" + getName() + "' when observer is already observing ObservableValue." );
      invariant( this::isNotDisposed,
                 () -> "Arez-0067: Attempting to add observer named '" + observer.getName() + "' to " +
                       "ObservableValue named '" + getName() + "' when ObservableValue is disposed." );
      invariant( observer::isNotDisposed,
                 () -> "Arez-0068: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
                       "named '" + getName() + "' when observer is disposed." );
      invariant( () -> !isComputableValue() ||
                       observer.canObserveLowerPriorityDependencies() ||
                       observer.getTask().getPriority().ordinal() >= getObserver().getTask().getPriority().ordinal(),
                 () -> "Arez-0183: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
                       "named '" + getName() + "' where the observer is scheduled at a " +
                       observer.getTask().getPriority() + " priority but the ObservableValue's owner is scheduled " +
                       "at a " + getObserver().getTask().getPriority() + " priority." );
      invariant( () -> getContext().getTransaction().getTracker() == observer,
                 () -> "Arez-0203: Attempting to add observer named '" + observer.getName() + "' to ObservableValue " +
                       "named '" + getName() + "' but the observer is not the tracker in transaction named '" +
                       getContext().getTransaction().getName() + "'." );
    }
    rawAddObserver( observer );
  }

  void rawAddObserver( @Nonnull final Observer observer )
  {
    getObservers().add( observer );

    final int state = observer.getLeastStaleObserverState();
    if ( _leastStaleObserverState > state )
    {
      _leastStaleObserverState = state;
    }
  }

  void removeObserver( @Nonnull final Observer observer )
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0069: Attempt to invoke removeObserver on ObservableValue named '" + getName() + "' " +
                       "when there is no active transaction." );
      invariantObserversLinked();
      invariant( () -> hasObserver( observer ),
                 () -> "Arez-0070: Attempting to remove observer named '" + observer.getName() + "' from " +
                       "ObservableValue named '" + getName() + "' when observer is not observing ObservableValue." );
    }
    final List observers = getObservers();
    observers.remove( observer );
    if ( canDeactivateNow() )
    {
      queueForDeactivation();
    }
    if ( Arez.shouldCheckInvariants() )
    {
      invariantObserversLinked();
    }
  }

  void queueForDeactivation()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0071: Attempt to invoke queueForDeactivation on ObservableValue named '" + getName() +
                       "' when there is no active transaction." );
      invariant( this::canDeactivateNow,
                 () -> "Arez-0072: Attempted to invoke queueForDeactivation() on ObservableValue named '" + getName() +
                       "' but ObservableValue is not able to be deactivated." );
      invariant( () -> !hasObservers(),
                 () -> "Arez-0073: Attempted to invoke queueForDeactivation() on ObservableValue named '" + getName() +
                       "' but ObservableValue has observers." );
    }
    if ( !isPendingDeactivation() )
    {
      getContext().getTransaction().queueForDeactivation( this );
    }
  }

  void setLeastStaleObserverState( final int leastStaleObserverState )
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( () -> getContext().isTransactionActive(),
                 () -> "Arez-0074: Attempt to invoke setLeastStaleObserverState on ObservableValue named '" +
                       getName() + "' when there is no active transaction." );
      invariant( () -> Observer.Flags.isActive( leastStaleObserverState ),
                 () -> "Arez-0075: Attempt to invoke setLeastStaleObserverState on ObservableValue named '" +
                       getName() + "' with invalid value " + Observer.Flags.getStateName( leastStaleObserverState ) +
                       "." );
    }
    _leastStaleObserverState = leastStaleObserverState;
  }

  int getLeastStaleObserverState()
  {
    return _leastStaleObserverState;
  }

  /**
   * Notify Arez that this observable has been "observed" in the current transaction.
   * Before invoking this method, a transaction MUST be active but it may be read-only or read-write.
   */
  public void reportObserved()
  {
    getContext().getTransaction().observe( this );
  }

  /**
   * Notify Arez that this observable has been "observed" if a tracking transaction is active.
   */
  public void reportObservedIfTrackingTransactionActive()
  {
    if ( getContext().isTrackingTransactionActive() )
    {
      reportObserved();
    }
  }

  /**
   * Check that pre-conditions are satisfied before changing observable value.
   * In production mode this will typically be a no-op. This method should be invoked
   * before state is modified. Before invoking this method, a read-write transaction MUST be active.
   */
  @OmitSymbol( unless = "arez.check_invariants" )
  public void preReportChanged()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      getContext().getTransaction().preReportChanged( this );
    }
  }

  /**
   * Notify Arez that this observable has changed.
   * This is called when the observable has definitely changed.
   * Before invoking this method, a read-write transaction MUST be active.
   */
  public void reportChanged()
  {
    if ( willPropagateSpyEvents() )
    {
      // isDisposed is checked as we call reportChanged() from performDispose() after dispose has started
      // and thus it is no longer valid to call getObservableValue()
      reportSpyEvent( new ObservableValueChangeEvent( asInfo(), isDisposed() ? null : getObservableValue() ) );
    }
    getContext().getTransaction().reportChanged( this );
  }

  void reportChangeConfirmed()
  {
    if ( willPropagateSpyEvents() )
    {
      reportSpyEvent( new ObservableValueChangeEvent( asInfo(), getObservableValue() ) );
    }
    getContext().getTransaction().reportChangeConfirmed( this );
  }

  void reportPossiblyChanged()
  {
    getContext().getTransaction().reportPossiblyChanged( this );
  }

  /**
   * Return the value from observable if introspectors are enabled and an accessor has been supplied.
   */
  @Nullable
  private Object getObservableValue()
  {
    if ( Arez.arePropertyIntrospectorsEnabled() && null != getAccessor() )
    {
      try
      {
        return getAccessor().get();
      }
      catch ( final Throwable ignored )
      {
      }
    }
    return null;
  }

  /**
   * Return the info associated with this class.
   *
   * @return the info associated with this class.
   */
  @SuppressWarnings( "ConstantConditions" )
  @OmitSymbol( unless = "arez.enable_spies" )
  @Nonnull
  ObservableValueInfo asInfo()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      invariant( Arez::areSpiesEnabled,
                 () -> "Arez-0196: ObservableValue.asInfo() invoked but Arez.areSpiesEnabled() returned false." );
    }
    if ( Arez.areSpiesEnabled() && null == _info )
    {
      _info = new ObservableValueInfoImpl( this );
    }
    return Arez.areSpiesEnabled() ? _info : null;
  }

  void invariantOwner()
  {
    if ( Arez.shouldCheckInvariants() && null != _observer )
    {
      invariant( () -> Objects.equals( _observer.getComputableValue().getObservableValue(), this ),
                 () -> "Arez-0076: ObservableValue named '" + getName() + "' has owner specified but owner does " +
                       "not link to ObservableValue as derived value." );
    }
  }

  void invariantObserversLinked()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      getObservers().forEach( observer ->
                                invariant( () -> observer.getDependencies().contains( this ),
                                           () -> "Arez-0077: ObservableValue named '" + getName() + "' has observer " +
                                                 "named '" + observer.getName() + "' which does not contain " +
                                                 "ObservableValue as dependency." ) );
    }
  }

  void invariantLeastStaleObserverState()
  {
    if ( Arez.shouldCheckInvariants() )
    {
      final int leastStaleObserverState =
        getObservers().stream().
          map( Observer::getLeastStaleObserverState ).
          min( Comparator.naturalOrder() ).orElse( Observer.Flags.STATE_UP_TO_DATE );
      invariant( () -> leastStaleObserverState >= _leastStaleObserverState,
                 () -> "Arez-0078: Calculated leastStaleObserverState on ObservableValue named '" +
                       getName() + "' is '" + Observer.Flags.getStateName( leastStaleObserverState ) +
                       "' which is unexpectedly less than cached value '" +
                       Observer.Flags.getStateName( _leastStaleObserverState ) + "'." );
    }
  }

  @Nullable
  Component getComponent()
  {
    return _component;
  }

  @OmitSymbol
  int getWorkState()
  {
    return _workState;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy