001package arez.component.internal; 002 003import arez.ActionFlags; 004import arez.Arez; 005import arez.ArezContext; 006import arez.Component; 007import arez.ComputableValue; 008import arez.Disposable; 009import arez.ObservableValue; 010import arez.Procedure; 011import arez.SafeProcedure; 012import arez.Task; 013import arez.annotations.ArezComponent; 014import arez.annotations.CascadeDispose; 015import arez.annotations.Observe; 016import arez.component.ComponentObservable; 017import grim.annotations.OmitClinit; 018import grim.annotations.OmitSymbol; 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Objects; 023import javax.annotation.Nonnull; 024import javax.annotation.Nullable; 025import static org.realityforge.braincheck.Guards.*; 026 027/** 028 * The "kernel" of the components generated by the annotation processor. 029 * This class exists so that code common across multiple components is not present in every 030 * generated class but is instead in a single location. This results in smaller, faster code. 031 */ 032@OmitClinit 033public final class ComponentKernel 034 implements Disposable, ComponentObservable 035{ 036 /** 037 * The component has been created, but not yet initialized. 038 */ 039 private final static byte COMPONENT_CREATED = 0; 040 /** 041 * The components constructor has been called, the {@link ArezContext} field initialized (if necessary), 042 * and the synthetic id has been generated (if required). 043 */ 044 private final static byte COMPONENT_INITIALIZED = 1; 045 /** 046 * The reactive elements have been created (i.e. the {@link ObservableValue}, {@link arez.Observer}, 047 * {@link ComputableValue} etc.). The {@link arez.annotations.PostConstruct} has NOT been invoked nor 048 * has the {@link Component} been instantiated. This means the component is ready to be interacted with 049 * in a {@link arez.annotations.PostConstruct} method but has not been fully constructed. 050 */ 051 private final static byte COMPONENT_CONSTRUCTED = 2; 052 /** 053 * The {@link arez.annotations.PostConstruct} method has been invoked and 054 * the {@link Component} has been instantiated. Observers have been scheduled but the scheduler 055 * has not been triggered. 056 */ 057 private final static byte COMPONENT_COMPLETE = 3; 058 /** 059 * The scheduler has been triggered and any {@link Observe} methods have been invoked if runtime managed. 060 */ 061 private final static byte COMPONENT_READY = 4; 062 /** 063 * The component is disposing. 064 */ 065 private final static byte COMPONENT_DISPOSING = -2; 066 /** 067 * The component has been disposed. 068 */ 069 private final static byte COMPONENT_DISPOSED = -1; 070 /** 071 * Reference to the context to which this component belongs. 072 */ 073 @OmitSymbol( unless = "arez.enable_zones" ) 074 @Nullable 075 private final ArezContext _context; 076 /** 077 * A human consumable name for component. It should be non-null if {@link Arez#areNamesEnabled()} returns 078 * <code>true</code> and <code>null</code> otherwise. 079 */ 080 @Nullable 081 @OmitSymbol( unless = "arez.enable_names" ) 082 private final String _name; 083 /** 084 * The runtime managed synthetic id for component. This will be 0 if the component has supplied a custom 085 * id via a method annotated with {@link arez.annotations.ComponentId} or the annotation processor has 086 * determined that no id is required. The id must be supplied with a non-zero value if: 087 * 088 * <ul> 089 * <li>the component declared it requires an id (i.e. {@link ArezComponent#requireId()} is <code>true</code>) but 090 * no method annotated with {@link arez.annotations.ComponentId} is present on the components type.</li> 091 * <li>The runtime requires an id as part of debugging infrastructure. (i.e. @link Arez#areNamesEnabled(), {@link Arez#areRegistriesEnabled()} 092 * or {@link Arez#areNativeComponentsEnabled()} returns <code>true</code>.</li> 093 * </ul> 094 */ 095 private final int _id; 096 /** 097 * The initialization state of the component. Possible values are defined by the constants in the 098 * this class however this field is only used for determining whether a component 099 * is disposed when invariant checking is disabled so states other than {@link #COMPONENT_DISPOSING} are not set 100 * when invariant checking is disabled. 101 */ 102 private byte _state; 103 /** 104 * The native component associated with the component. This should be non-null if {@link Arez#areNativeComponentsEnabled()} 105 * returns <code>true</code> and <code>null</code> otherwise. 106 */ 107 @OmitSymbol( unless = "arez.enable_native_components" ) 108 @Nullable 109 private final Component _component; 110 /** 111 * This callback is invoked before the component is disposed. 112 */ 113 @Nullable 114 private final SafeProcedure _preDisposeCallback; 115 /** 116 * This callback is invoked to dispose the reactive elements of the component. 117 */ 118 @Nullable 119 private final SafeProcedure _disposeCallback; 120 /** 121 * This callback is invoked after the component is disposed. 122 */ 123 @Nullable 124 private final SafeProcedure _postDisposeCallback; 125 /** 126 * The mechanisms to notify downstream elements that the component has been disposed. This should be non-null 127 * if the {@link ArezComponent#disposeNotifier()} is enabled, and <code>null</code> otherwise. 128 */ 129 @Nullable 130 private final Map<Object, SafeProcedure> _onDisposeListeners; 131 /** 132 * Mechanism for implementing {@link ComponentObservable} on the component. 133 */ 134 @Nullable 135 private final ObservableValue<Boolean> _componentObservable; 136 /** 137 * Mechanism for implementing {@link ArezComponent#disposeOnDeactivate()} on the component. 138 */ 139 @Nullable 140 private final ComputableValue<Boolean> _disposeOnDeactivate; 141 /** 142 * Guard to ensure we never try to schedule a dispose multiple times, otherwise the underlying task 143 * system will detect multiple tasks with the same name and object. 144 */ 145 private boolean _disposeScheduled; 146 147 public ComponentKernel( @Nullable final ArezContext context, 148 @Nullable final String name, 149 final int id, 150 @Nullable final Component component, 151 @Nullable final SafeProcedure preDisposeCallback, 152 @Nullable final SafeProcedure disposeCallback, 153 @Nullable final SafeProcedure postDisposeCallback, 154 final boolean notifyOnDispose, 155 final boolean isComponentObservable, 156 final boolean disposeOnDeactivate ) 157 { 158 if ( Arez.shouldCheckApiInvariants() ) 159 { 160 apiInvariant( () -> Arez.areZonesEnabled() || null == context, 161 () -> "Arez-0100: ComponentKernel passed a context but Arez.areZonesEnabled() is false" ); 162 apiInvariant( () -> Arez.areNamesEnabled() || null == name, 163 () -> "Arez-0156: ComponentKernel passed a name '" + name + 164 "' but Arez.areNamesEnabled() returns false." ); 165 apiInvariant( () -> !Arez.areNativeComponentsEnabled() || 166 null == component || 167 0 == id || 168 ( (Integer) id ).equals( component.getId() ), 169 () -> "Arez-0222: ComponentKernel named '" + name + 170 "' passed an id " + id + " and a component but the component had a different id (" + 171 Objects.requireNonNull( component ).getId() + ")" ); 172 } 173 174 if ( Arez.shouldCheckApiInvariants() ) 175 { 176 _state = COMPONENT_INITIALIZED; 177 } 178 _name = Arez.areNamesEnabled() ? name : null; 179 _context = Arez.areZonesEnabled() ? context : null; 180 _component = Arez.areNativeComponentsEnabled() ? Objects.requireNonNull( component ) : null; 181 _id = id; 182 _onDisposeListeners = notifyOnDispose ? new HashMap<>() : null; 183 _preDisposeCallback = Arez.areNativeComponentsEnabled() ? null : preDisposeCallback; 184 _disposeCallback = Arez.areNativeComponentsEnabled() ? null : disposeCallback; 185 _postDisposeCallback = Arez.areNativeComponentsEnabled() ? null : postDisposeCallback; 186 _componentObservable = isComponentObservable ? createComponentObservable() : null; 187 _disposeOnDeactivate = disposeOnDeactivate ? createDisposeOnDeactivate() : null; 188 } 189 190 @Nonnull 191 private ComputableValue<Boolean> createDisposeOnDeactivate() 192 { 193 return getContext().computable( Arez.areNativeComponentsEnabled() ? getComponent() : null, 194 Arez.areNamesEnabled() ? getName() + ".disposeOnDeactivate" : null, 195 this::observe0, 196 null, 197 this::scheduleDispose, 198 ComputableValue.Flags.PRIORITY_HIGHEST ); 199 } 200 201 private void scheduleDispose() 202 { 203 /* 204 * Guard against a scenario where due to interleaving of scheduled tasks a component is disposed due, 205 * to deactivation and then is re-observed and deactivated again prior to the dispose task running. 206 * This scenario was thought to be practically impossible but several applications did the impossible. 207 * 208 * There is still a bug or at least an ambiguity where a disposeOnDeactivate component deactivates, schedules 209 * dispose and then activates before the dispose task runs. Should the dispose be aborted or should it go ahead? 210 * Currently the Arez API does not expose a flag indicating whether computableValues are observed and not possible 211 * to implement the first strategy even though it may seem to be the right one. 212 */ 213 if ( !_disposeScheduled ) 214 { 215 _disposeScheduled = true; 216 getContext().task( Arez.areNamesEnabled() ? getName() + ".disposeOnDeactivate.task" : null, 217 this::dispose, 218 Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK ); 219 } 220 } 221 222 @Nonnull 223 private ObservableValue<Boolean> createComponentObservable() 224 { 225 return getContext().observable( Arez.areNativeComponentsEnabled() ? getComponent() : null, 226 Arez.areNamesEnabled() ? getName() + ".isDisposed" : null, 227 Arez.arePropertyIntrospectorsEnabled() ? () -> _state > 0 : null ); 228 } 229 230 @Override 231 public boolean observe() 232 { 233 if ( Arez.shouldCheckApiInvariants() ) 234 { 235 apiInvariant( () -> null != _disposeOnDeactivate || null != _componentObservable, 236 () -> "Arez-0221: ComponentKernel.observe() invoked on component named '" + getName() + 237 "' but observing is not enabled for component." ); 238 } 239 if ( null != _disposeOnDeactivate ) 240 { 241 return isNotDisposed() ? _disposeOnDeactivate.get() : false; 242 } 243 else 244 { 245 return observe0(); 246 } 247 } 248 249 /** 250 * Internal observe method that may be directly used or used from computable if disposeOnDeactivate is true. 251 */ 252 private boolean observe0() 253 { 254 assert null != _componentObservable; 255 final boolean isNotDisposed = isNotDisposed(); 256 if ( isNotDisposed ) 257 { 258 _componentObservable.reportObserved(); 259 } 260 return isNotDisposed; 261 } 262 263 @Override 264 public void dispose() 265 { 266 if ( isNotDisposed() ) 267 { 268 // Note that his state transition occurs outside the guard as it is required to compute isDisposed() state 269 _state = COMPONENT_DISPOSING; 270 if ( Arez.areNativeComponentsEnabled() ) 271 { 272 assert null != _component; 273 _component.dispose(); 274 } 275 else 276 { 277 getContext().safeAction( Arez.areNamesEnabled() ? getName() + ".dispose" : null, 278 this::performDispose, 279 ActionFlags.NO_VERIFY_ACTION_REQUIRED ); 280 } 281 if ( Arez.shouldCheckApiInvariants() ) 282 { 283 _state = COMPONENT_DISPOSED; 284 } 285 } 286 } 287 288 private void performDispose() 289 { 290 invokeCallbackIfNecessary( _preDisposeCallback ); 291 releaseResources(); 292 invokeCallbackIfNecessary( _disposeCallback ); 293 invokeCallbackIfNecessary( _postDisposeCallback ); 294 } 295 296 private void invokeCallbackIfNecessary( @Nullable final SafeProcedure callback ) 297 { 298 if ( null != callback ) 299 { 300 callback.call(); 301 } 302 } 303 304 @Override 305 public boolean isDisposed() 306 { 307 return _state < 0; 308 } 309 310 private void releaseResources() 311 { 312 if ( null != _onDisposeListeners ) 313 { 314 notifyOnDisposeListeners(); 315 } 316 // If native components are enabled, these elements are registered with native component 317 // and will thus be disposed as part 318 if ( !Arez.areNativeComponentsEnabled() ) 319 { 320 Disposable.dispose( _componentObservable ); 321 Disposable.dispose( _disposeOnDeactivate ); 322 } 323 } 324 325 /** 326 * Notify an OnDispose listeners that have been added to the component. 327 * This method MUST only be called if the component has enabled onDisposeNotify feature. 328 */ 329 public void notifyOnDisposeListeners() 330 { 331 assert null != _onDisposeListeners; 332 for ( final Map.Entry<Object, SafeProcedure> entry : new ArrayList<>( _onDisposeListeners.entrySet() ) ) 333 { 334 final Object key = entry.getKey(); 335 /* 336 * There is scenarios where there is multiple elements being simultaneously disposed and 337 * the @CascadeDispose has not triggered so a disposed object is in this list waiting to 338 * be called back. If the callback is triggered and the @CascadeDispose is on an observable 339 * property then the framework will attempt to null field and generate invariant failures 340 * or runtime errors unless we skip the callback and just remove the listener. 341 */ 342 if ( !Disposable.isDisposed( key ) ) 343 { 344 entry.getValue().call(); 345 } 346 } 347 } 348 349 /** 350 * Return true if the component has been initialized. 351 * 352 * @return true if the component has been initialized. 353 */ 354 public boolean hasBeenInitialized() 355 { 356 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 357 return COMPONENT_CREATED != _state; 358 } 359 360 /** 361 * Return true if the component has been constructed. 362 * 363 * @return true if the component has been constructed. 364 */ 365 public boolean hasBeenConstructed() 366 { 367 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 368 return hasBeenInitialized() && COMPONENT_INITIALIZED != _state; 369 } 370 371 /** 372 * Return true if the component has been completed. 373 * 374 * @return true if the component has been completed. 375 */ 376 public boolean hasBeenCompleted() 377 { 378 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 379 return hasBeenConstructed() && COMPONENT_CONSTRUCTED != _state; 380 } 381 382 /** 383 * Return true if the component is in COMPONENT_CONSTRUCTED state. 384 * 385 * @return true if the component is in COMPONENT_CONSTRUCTED state. 386 */ 387 public boolean isConstructed() 388 { 389 return COMPONENT_CONSTRUCTED == _state; 390 } 391 392 /** 393 * Return true if the component is in COMPONENT_COMPLETE state. 394 * 395 * @return true if the component is in COMPONENT_COMPLETE state. 396 */ 397 public boolean isComplete() 398 { 399 return COMPONENT_COMPLETE == _state; 400 } 401 402 /** 403 * Return true if the component is ready. 404 * 405 * @return true if the component is ready. 406 */ 407 public boolean isReady() 408 { 409 return COMPONENT_READY == _state; 410 } 411 412 /** 413 * Return true if the component is NOT ready. 414 * 415 * @return true if the component is NOT ready. 416 */ 417 public boolean isNotReady() 418 { 419 return !isReady(); 420 } 421 422 /** 423 * Return true if the component is disposing. 424 * 425 * @return true if the component is disposing. 426 */ 427 public boolean isDisposing() 428 { 429 return COMPONENT_DISPOSING == _state; 430 } 431 432 /** 433 * Return true if the component is active and can be interacted with. 434 * This means that the component has been constructed and has not started to be disposed. 435 * 436 * @return true if the component is active. 437 */ 438 public boolean isActive() 439 { 440 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 441 return COMPONENT_CONSTRUCTED == _state || COMPONENT_COMPLETE == _state || COMPONENT_READY == _state; 442 } 443 444 /** 445 * Describe component state. This is usually used to provide error messages. 446 * 447 * @return a string description of the state. 448 */ 449 @Nonnull 450 public String describeState() 451 { 452 return describeState( _state ); 453 } 454 455 @Nonnull 456 private String describeState( final int state ) 457 { 458 assert Arez.shouldCheckInvariants() || Arez.shouldCheckApiInvariants(); 459 switch ( state ) 460 { 461 case ComponentKernel.COMPONENT_CREATED: 462 return "created"; 463 case ComponentKernel.COMPONENT_INITIALIZED: 464 return "initialized"; 465 case ComponentKernel.COMPONENT_CONSTRUCTED: 466 return "constructed"; 467 case ComponentKernel.COMPONENT_COMPLETE: 468 return "complete"; 469 case ComponentKernel.COMPONENT_READY: 470 return "ready"; 471 case ComponentKernel.COMPONENT_DISPOSING: 472 return "disposing"; 473 default: 474 assert ComponentKernel.COMPONENT_DISPOSED == state; 475 return "disposed"; 476 } 477 } 478 479 /** 480 * Transition component state from {@link ComponentKernel#COMPONENT_INITIALIZED} to {@link ComponentKernel#COMPONENT_CONSTRUCTED}. 481 */ 482 public void componentConstructed() 483 { 484 if ( Arez.shouldCheckApiInvariants() ) 485 { 486 apiInvariant( () -> COMPONENT_INITIALIZED == _state, 487 () -> "Arez-0219: Bad state transition from " + describeState( _state ) + 488 " to " + describeState( COMPONENT_CONSTRUCTED ) + 489 " on component named '" + getName() + "'." ); 490 _state = COMPONENT_CONSTRUCTED; 491 } 492 } 493 494 /** 495 * Transition component state from {@link ComponentKernel#COMPONENT_INITIALIZED} to 496 * {@link ComponentKernel#COMPONENT_CONSTRUCTED} and then to {@link ComponentKernel#COMPONENT_READY}. 497 * This should only be called if there is active elements that are part of the component that need to be scheduled, 498 * otherwise the component can transition directly to ready. 499 */ 500 public void componentComplete() 501 { 502 completeNativeComponent(); 503 if ( Arez.shouldCheckApiInvariants() ) 504 { 505 apiInvariant( () -> COMPONENT_CONSTRUCTED == _state, 506 () -> "Arez-0220: Bad state transition from " + describeState( _state ) + 507 " to " + describeState( COMPONENT_COMPLETE ) + 508 " on component named '" + getName() + "'." ); 509 _state = COMPONENT_COMPLETE; 510 } 511 // Trigger scheduler so active parts of components can react 512 getContext().triggerScheduler(); 513 makeComponentReady(); 514 } 515 516 /** 517 * Transition component state from {@link ComponentKernel#COMPONENT_CONSTRUCTED} to {@link ComponentKernel#COMPONENT_READY}. 518 * This should be invoked rather than {@link #componentComplete()} if there is no active elements of the component that 519 * need to be scheduled. 520 */ 521 public void componentReady() 522 { 523 completeNativeComponent(); 524 makeComponentReady(); 525 } 526 527 /** 528 * Mark the native component if present as complete. 529 */ 530 private void completeNativeComponent() 531 { 532 if ( Arez.areNativeComponentsEnabled() ) 533 { 534 assert null != _component; 535 _component.complete(); 536 } 537 } 538 539 private void makeComponentReady() 540 { 541 if ( Arez.shouldCheckApiInvariants() ) 542 { 543 apiInvariant( () -> COMPONENT_CONSTRUCTED == _state || COMPONENT_COMPLETE == _state, 544 () -> "Arez-0218: Bad state transition from " + describeState( _state ) + 545 " to " + describeState( COMPONENT_READY ) + 546 " on component named '" + getName() + "'." ); 547 _state = COMPONENT_READY; 548 } 549 } 550 551 /** 552 * Return the context in which this component was created. 553 * 554 * @return the associated context. 555 */ 556 @Nonnull 557 public ArezContext getContext() 558 { 559 return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context(); 560 } 561 562 /** 563 * Invoke the setter in a transaction. 564 * If a transaction is active then invoke the setter directly, otherwise wrap the setter in an action. 565 * 566 * @param name the name of the action if it is needed. 567 * @param setter the setter action to invoke. 568 */ 569 public void safeSetObservable( @Nullable final String name, @Nonnull final SafeProcedure setter ) 570 { 571 if ( getContext().isTransactionActive() ) 572 { 573 setter.call(); 574 } 575 else 576 { 577 getContext().safeAction( Arez.areNamesEnabled() ? name : null, setter ); 578 } 579 } 580 581 /** 582 * Invoke the setter in a transaction. 583 * If a transaction is active then invoke the setter directly, otherwise wrap the setter in an action. 584 * 585 * @param name the name of the action if it is needed. 586 * @param setter the setter action to invoke. 587 * @throws Throwable if setter throws an exception. 588 */ 589 public void setObservable( @Nullable final String name, @Nonnull final Procedure setter ) 590 throws Throwable 591 { 592 if ( getContext().isTransactionActive() ) 593 { 594 setter.call(); 595 } 596 else 597 { 598 getContext().action( Arez.areNamesEnabled() ? name : null, setter ); 599 } 600 } 601 602 /** 603 * Return the name of the component. 604 * This method should NOT be invoked unless {@link Arez#areNamesEnabled()} returns true and will throw an 605 * exception if invariant checking is enabled. 606 * 607 * @return the name of the component. 608 */ 609 @Nonnull 610 public String getName() 611 { 612 if ( Arez.shouldCheckApiInvariants() ) 613 { 614 apiInvariant( Arez::areNamesEnabled, 615 () -> "Arez-0164: ComponentKernel.getName() invoked when Arez.areNamesEnabled() returns false." ); 616 } 617 assert null != _name; 618 return _name; 619 } 620 621 /** 622 * Return the synthetic id associated with the component. 623 * This method MUST NOT be invoked if a synthetic id is not present and will generate an invariant failure 624 * when invariants are enabled. 625 * 626 * @return the synthetic id associated with the component. 627 */ 628 public int getId() 629 { 630 if ( Arez.shouldCheckApiInvariants() ) 631 { 632 apiInvariant( () -> 0 != _id, 633 () -> "Arez-0213: Attempted to unexpectedly invoke ComponentKernel.getId() method to access " + 634 "synthetic id on component named '" + getName() + "'." ); 635 } 636 return _id; 637 } 638 639 /** 640 * Return the native component associated with the component. 641 * This method MUST NOT be invoked if native components are disabled. 642 * 643 * @return the native component associated with the component. 644 */ 645 @OmitSymbol( unless = "arez.enable_native_components" ) 646 @Nonnull 647 public Component getComponent() 648 { 649 if ( Arez.shouldCheckApiInvariants() ) 650 { 651 apiInvariant( () -> null != _component, 652 () -> "Arez-0216: ComponentKernel.getComponent() invoked when Arez.areNativeComponentsEnabled() " + 653 "returns false on component named '" + getName() + "'." ); 654 } 655 assert null != _component; 656 return _component; 657 } 658 659 /** 660 * Add the listener to notify list under key. 661 * This method MUST NOT be invoked after {@link #dispose()} has been invoked. 662 * This method should not be invoked if another listener has been added with the same key without 663 * being removed. 664 * 665 * <p>If the key implements {@link Disposable} and {@link Disposable#isDisposed()} returns <code>true</code> 666 * when invoking the calback then the callback will be skipped. This rare situation only occurs when there is 667 * circular dependency in the object model usually involving {@link CascadeDispose}.</p> 668 * 669 * @param key the key to uniquely identify listener. 670 * @param action the listener callback. 671 */ 672 public void addOnDisposeListener( @Nonnull final Object key, @Nonnull final SafeProcedure action ) 673 { 674 assert null != _onDisposeListeners; 675 if ( Arez.shouldCheckApiInvariants() ) 676 { 677 invariant( this::isNotDisposed, 678 () -> "Arez-0170: Attempting to add OnDispose listener but ComponentKernel has been disposed." ); 679 invariant( () -> !_onDisposeListeners.containsKey( key ), 680 () -> "Arez-0166: Attempting to add OnDispose listener with key '" + key + 681 "' but a listener with that key already exists." ); 682 } 683 _onDisposeListeners.put( key, action ); 684 } 685 686 /** 687 * Remove the listener with specified key from the notify list. 688 * This method should only be invoked when a listener has been added for specific key using 689 * {@link #addOnDisposeListener(Object, SafeProcedure)} and has not been removed by another 690 * call to this method. 691 * 692 * @param key the key under which the listener was previously added. 693 */ 694 public void removeOnDisposeListener( @Nonnull final Object key ) 695 { 696 assert null != _onDisposeListeners; 697 // This method can be called when the notifier is disposed to avoid the caller (i.e. per-component 698 // generated code) from checking the disposed state of the notifier before invoking this method. 699 // This is necessary in a few rare circumstances but requiring the caller to check before invocation 700 // increases the generated code size. 701 final SafeProcedure removed = _onDisposeListeners.remove( key ); 702 if ( Arez.shouldCheckApiInvariants() ) 703 { 704 invariant( () -> null != removed, 705 () -> "Arez-0167: Attempting to remove OnDispose listener with key '" + key + 706 "' but no such listener exists." ); 707 } 708 } 709 710 boolean hasOnDisposeListeners() 711 { 712 return null != _onDisposeListeners; 713 } 714 715 @Nonnull 716 Map<Object, SafeProcedure> getOnDisposeListeners() 717 { 718 assert null != _onDisposeListeners; 719 return _onDisposeListeners; 720 } 721 722 @Nonnull 723 @Override 724 public String toString() 725 { 726 if ( Arez.areNamesEnabled() ) 727 { 728 return getName(); 729 } 730 else 731 { 732 return super.toString(); 733 } 734 } 735}