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