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