EsAsyncZone
Description
A zone represents an environment that remains stable across asynchronous calls.
Code is always executed in the context of a zone, available as EsAsyncZone>>current. The initial VAST environment runs in the context of the default zone (EsAsyncZone>>root). Code can be run in a different zone using either Block>>asyncZoned, to create a new zone, or EsAsyncZone>>run to run code in the context of an existing zone which was created earlier using EsAsyncZone>>fork.
Developers can create a new zone that overrides some functionality of an existing zone. For example, custom zones can replace of modify the behavior of scheduling tasks or how uncaught errors are handled.
The <EsAsyncZone> class should not be subclassed, but instead users can provide custom zones by forking an existing zone (usually EsAsyncZone>>current) with a <EsAsyncZoneSpec>. This is similar to creating a new class that extends the base EsAsyncZone class and that overrides some methods, except without actually creating a new class. Instead, the overriding methods are provided as functions that explicitly take the equivalent of their own class, the super class and the self object as parameters.
Asynchronous callbacks always run in the context of the zone where they were scheduled. This is implemented using two steps:
1. the callback is first registered using one of #registerCallback:, #registerUnaryCallback:, or #registerBinaryCallback:. This allows the zone to record that a callback exists and potentially modify it (by
returning a different callback). The code doing the registration (e.g., EsFuture>>then:) also remembers the current zone so that it can later run the callback in that zone.
2. At a later point the registered callback is run in the remembered zone, using one of #run:, #runUnary: or #runBinary:.
This is all handled internally by the platform code and most users don't need to worry about it. However, developers of new asynchronous operations, provided by the underlying system, must follow the protocol to be zone compatible.
For convenience, zones provide #bindCallback: (and the corresponding #bindUnaryCallback: and #bindBinaryCallback:) to make it easier to respect the zone contract: these methods first invoke the corresponding register methods and then wrap the returned function so that it runs in the current zone when it is later asynchronously invoked.
Similarly, zones provide #bindCallbackGuarded: (and the corresponding #bindUnaryCallbackGuarded: and
#bindBinaryCallbackGuarded:), when the callback should be invoked through EsAsyncZone>>runGuarded:.
Instance State
zoneHandlers: <KeyedCollection> of <EsAtom> -. <EsAsyncZoneHandler>
valueMap: <KeyedCollection> of <Object> -> <Object>
delegate: <EsAsyncZoneDelegate>
parentDelegate: <EsAsyncZoneDelegate>
Class Methods
current
  Answers the zone that is currently active.
     (see class comments for description)

     Answers:
        <EsAsyncZone>
root
  Answer the root zone.
     (see class comments for description)

     Answers:
        <EsAsyncZone>
Instance Methods
at:
  Retrieves the zone-value associated with @aKey.

     If this zone does not contain the value looks up the same key in the
     parent zone. If the @aKey is not found returns `nil`.

     By controlling access to the key, a zone can grant or deny access to the
     zone value.

     Argument:
        aKey - <Object>
     Answers:
        <Object> could be nil for not found
bindCallback:
  Registers the provided @callback and returns a function that will
     execute in this zone.

     Equivalent to:
     ```smalltalk
      registered := self registerCallback: callback.
     ^[self run: registered]
     ```

     Arguments:
        callback - <Block | DirectedMessage>
     Answers:
        <Block> same arg count as @callback expects
bindCallbackGuarded:
  Registers the provided @callback and returns a function that will
     execute in this zone.

     When the function executes, errors are caught and treated as uncaught
     errors.

     Equivalent to:
     ```smalltalk
     registered := self registerCallback: callback.
     ^[self runGuarded: registered]
     ```

     Arguments:
        callback - <Block | DirectedMessage>
     Answers:
        <Block> same arg count as @callback expects
errorCallback:stackTrace:
  Intercepts errors when added programmatically to an <EsFuture> or <EsStream>.

     When calling EsPromise>>completeError:, EsStreamController>>addError:stackTrace:],
     or some <EsFuture> creational apis, the current zone is allowed to intercept
     and replace the error.

     Future constructors invoke this function when the error is received
     directly, for example with [Future.error], or when the error is caught
     synchronously, for example with [Future.sync].

     There is no guarantee that an error is only sent through [errorCallback]
     once. Libraries that use intermediate controllers or completers might
     end up invoking [errorCallback] multiple times.

     Returns `null` if no replacement is desired. Otherwise returns an instance
     of [AsyncError] holding the new pair of error and stack trace.

     Custom zones may intercept this operation.

     Implementations of a new asynchronous primitive that converts synchronous
     errors to asynchronous errors rarely need to invoke [errorCallback], since
     errors are usually reported through future completers or stream
     controllers.

     Arguments:
        error - <Object>
        stackTrace - <EsAsyncStackTrace>
     Answers:
        <EsAsyncError>
errorZone
  The error zone is responsible for dealing with uncaught errors.

     This is the closest parent zone of this zone that provides a
     #handleUncaughtError:stackTrace: method.

     Asynchronous errors never cross zone boundaries between zones with
     different error handlers.

     Example:
     ```smalltalk

     | future |

     'The asynchronous error is caught by the custom zone which prints asynchronous error'.
     [future := EsFuture error: 'asynchronous error'] asyncZonedCatch: [:e | Transcript show: e asString].

     'The following catch: handler is never evaluated because the custom zone created by #asyncZoneCatch:
      provided the error handler'
     future catch: [:e | EsAsyncError signal: 'never reached']
     ```

     Note that errors cannot enter a child zone with a different error handler
     either:
     ```smalltalk

     | future |

     [
        'The following asynchronous error is *not* caught by the `catch:`  in the nested zone,
        since errors are not to cross zone boundaries with different error handlers.
        Instead the error is handled by the current error handler, printing 'Caught by outer zone: asynchronous error''.
        future := EsFuture error: 'asynchronous error'
        [future catch: [:e | EsAsyncError signal: 'never reached']] asyncZonedCatch: [:e | EsAsyncError signal: 'never reached'].
     ] asyncZonedCatch: [:e | Transcript show: ('Caught by outer zone %1' bindWith: e asString)]
     ```

     Answers:
        <EsAsyncZone>
fork
  Creates a new zone as a child zone of this zone.

     The specification entries are inherited from the parent zone (`self`).

     Answers:
        <EsAsyncZone> forked zone
fork:
  Creates a new zone as a child zone of this zone.

     The new zone uses the performable symbols or blocks in the given
     @specification to override the current's zone behavior.
     All specification entries that are `nil` inherit the behavior from the parent zone (`self`).

     The new zone inherits the stored values (accessed through #valueAt:)
     of this zone.

     Note that the fork operation is interceptible. A zone can thus change
     the zone specification (or zone values), giving the forking zone full
     control over the child zone.

     Arguments:
        specification - <EsAsyncSpecification>
     Answers:
        <EsAsyncZone> forked zone
fork:with:
  Creates a new zone as a child zone of this zone.

     The new zone uses the performable symbols or blocks in the given
     @specification to override the current's zone behavior.
     All specification entries that are `nil` inherit the behavior from the parent zone (`self`).

     The new zone inherits the stored values (accessed through #valueAt:)
     of this zone and updates them with values from @zoneValues, which either
     adds new values or overrides existing ones.

     Note that the fork operation is interceptible. A zone can thus change
     the zone specification (or zone values), giving the forking zone full
     control over the child zone.

     Arguments:
        specification - <EsAsyncSpecification>
        zoneValues - <KeyedCollection>
     Answers:
        <EsAsyncZone> forked zone
forkWith:
  Creates a new zone as a child zone of this zone.

     The parent zone (`self`) specification is used.

     The new zone inherits the stored values (accessed through #valueAt:)
     of this zone and updates them with values from @zoneValues, which either
     adds new values or overrides existing ones.

     Note that the fork operation is interceptible. A zone can thus change
     the zone specification (or zone values), giving the forking zone full
     control over the child zone.

     Arguments:
        zoneValues - <KeyedCollection>
     Answers:
        <EsAsyncZone> forked zone
handleUncaughtError:stackTrace:
  Handles uncaught asynchronous errors.

     There are two kind of asynchronous errors that are handled by this
     function:
     1. Uncaught errors that were raised in asynchronous callbacks.
     2. Asynchronous errors that are pushed through <EsFuture> and <EsStream>
        chains, but for which nobody registered an error handler.
        Most asynchronous classes, like <EsFuture> or <EsStream> push errors to their
        listeners. Errors are propagated this way until either a listener handles
        the error (for example with EsFuture>>catch:, or no listener is
        available anymore. In the latter case, futures and streams invoke the
        zone's #handleUncaughtError:stackTrace:.

     By default, when handled by the root zone, uncaught asynchronous errors are
     treated like uncaught synchronous exceptions.

     Arguments:
        error - <Object>
        stackTrace - <EsStackTrace>
     Answers:
        <EsAsyncZone> self
isEsAsyncRootZone
  Polymorphic test

     Answers:
        <Boolean>
isEsAsyncZone
  Polymorphic test

     Answers:
        <Boolean>
parent
  The parent zone of the this zone.

     Is `nil` if `self` is a root zone.

     Zones are created by #fork: on an existing zone, or by Block>>asyncZoned which
     forks the `current` zone. The new zone's parent zone is the zone it was
     forked from.

     Answers:
        <EsAsyncZone>
registerCallback:
  Registers the given callback in this zone.

     When implementing an asynchronous primitive that uses callbacks, the
     callback must be registered using #registerCallback: at the point where the
     user provides the callback. This allows zones to record other information
     that they need at the same time, perhaps even wrapping the callback, so
     that the callback is prepared when it is later run in the same zones
     (using #run:*). For example, a zone may decide
     to store the stack trace (at the time of the registration) with the
     callback.

     Answers the callback that should be used in place of the provided
     @callback. Frequently zones simply return the original callback.

     Custom zones may intercept this operation. The default implementation in
     EsAsyncZone class>>root returns the original callback unchanged.

     Arguments:
        callback - <Block | DirectedMessage> n-arg
     Answers:
        <Block | DirectedMessage> same arg count that @callback expects
run:
  Executes @action in this zone.

     By default (as implemented in the #root zone), runs @action
     with EsAsyncZone class>>current set to this zone.

     If @action] raises an exception, the synchronous exception is not caught by the zone's
     error handler. Use #runGuarded: to achieve that.

     Since the root zone is the only zone that should modify the value of
     EsAsyncZone class>>current, custom zones intercepting run should always delegate to their
     parent zone. They may take actions before and after the call.

     Arguments:
        action - <Block | DirectedMessage> 0-arg
     Answers:
        <Object> return value of block or directed message
run:with:
  Executes the given @action with @argument in this zone

     As #run: except that @action is called with one argument instead of
     none.

     Arguments:
        action - <Block | DirectedMessage> 1-arg culled block
        argument - <Object>
     Answers:
        <Object> return value of block or directed message
run:with:with:
  Executes the given @action with @argument1 and @argument2 in this zone

     As #run: except that @action is called with two arguments instead of
     none.

     Arguments:
        action - <Block | DirectedMessage> 2-arg
        argument1 - <Object>
        argument2 - <Object>
     Answers:
        <Object> return value of block or directed message
run:withArguments:
  Executes the given @action with @arguments in this zone

     As #run: except that @action is called with 1 array arguments instead of
     none.

     Arguments:
        action - <Block | DirectedMessage> 1-arg
        arguments - <Array>
     Answers:
        <Object> return value of block or directed message
runGuarded:
  Executes @action in this zone.

     By default (as implemented in the #root zone), runs @action
     with EsAsyncZone class>>current set to this zone.

     If @action] raises an exception, the synchronous exception is not caught by the zone's
     error handler. Use #runGuarded: to achieve that.

     Since the root zone is the only zone that should modify the value of
     EsAsyncZone class>>current, custom zones intercepting run should always delegate to their
     parent zone. They may take actions before and after the call.

     Arguments:
        action - <Block | DirectedMessage> 0-arg
     Answers:
        <EsAsyncZone> self
runGuarded:with:
  Executes the given @action with @argument in this zone and catches
     synchronous errors

     See #runGuarded:

     Arguments:
        action - <Block | DirectedMessage> 1-arg
        argument - <Object>
     Answers:
        <EsAsyncZone> self
runGuarded:with:with:
  Executes the given @action with @argument1 and @argument2 and catches
     synchronous errors

     See #runGuarded:

     Arguments:
        action - <Block | DirectedMessage> 2-arg
        argument1 - <Object>
        argument2 - <Object>
     Answers:
        <EsAsyncZone> self
runGuarded:withArguments:
  Executes the given @action with @arguments and catches
     synchronous errors

     See #runGuarded:

     Arguments:
        action - <Block | DirectedMessage
        arguments - <Array>
     Answers:
        <EsAsyncZone> self
scheduleTask:
  Runs @callback asynchronously in this zone.

     <EsAsyncTaskScheduler> `scheduleTask:` delegates to the EsAsyncZone>>current zone's
     #scheduleTask:. The root zone's implementation interacts with the
     Smalltalk process scheduler to schedule the given callback as an async task.

     Custom zones may intercept this operation (for example to wrap the given
     @callback), or to implement their own async scheduler.
     In the latter case, they will usually still use the parent zone's
     EsAsyncZoneDelegate>>scheduleTask:in:] to attach themselves to the existing
     process scheduler.

     Arguments:
        callback - <Block | DirectedMessage> 0-arg
Last modified date: 04/21/2022