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}