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}