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