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}