EsFuture
An object representing a delayed computation.
An EsFuture is used to represent a potential value, or error, that will be available at some time in the future. Receivers of an EsFuture can register handler blocks that handle the value or error once it is available. For example:
future := self getFuture.
future
then: [:value | self handleValue: value]
catch: [:error | self handleError: error]
An EsFuture can be completed in two ways: with a value ("the future succeeds") or with an error ("the future fails"). Users can install handler blocks for each case.
In some cases we say that a future is completed with another future. This is a short way of stating that the future is completed in the same way, with the same value or error, as the other future once that completes.
The result of registering a pair of handler blocks is a new EsFuture (the "successor") which in turn is completed with the result of invoking the corresponding handler block. The successor is completed with an error if the invoked handler raises an exception. For example:
promise := EsPromise new.
future := promise future.
successor := future
then: [:value |
"Evaluated when the future is completed with a value.
The successor is completed with the value 42"
42]
catch: [:error |
"The successor is completed with 499 or an Exception"
self canHandle ifTrue: [499] ifFalse: [Exception signal: e asString]].
If a future does not have a successor when it completes with an error, it forwards the error message to the error-handler for uncaught errors. (@See EsAsyncUncaughtErrorHandler>>handleError:) This behavior makes sure that no error is silently dropped. However, it also means that error handlers should be installed early, so that they are present as soon as a future is completed with an error. The following example demonstrates this potential bug:
promise := EsPromise new.
[| future |
future := promise future.
(Delay forMilliseconds: 100) wait.
"The error-handler is not attached until 100 ms after the future has
been received. If the future fails before that, the error is
forwarded to the global error-handler, even though there is code
(just below) to eventually handle the error."
future
then: [:value | self useValue: value]
catch: [:error | self handleError: error]] fork.
promise completeError: 'error'
Synchronous vs Asynchronous Example:
"Synchronous code."
[ | value |
value := self value.
self assert: [ value isNil]
] on: Exception do: [:ex |
ex exitWith: 499
]
Equivalent asynchronous code, based on futures:
future := EsFuture on: [self foo].
(future then: [:value | self bar: value]) catch: [:e | 499]
Similar to the synchronous code, the error handler (registered with #catch: is handling any errors thrown by either #foo or #bar. If the error-handler had been registered as the onError parameter of a #then:catch: call, it would not catch errors from the #bar call.
Futures can have more than one callback-pair registered. Each successor is treated independently and is handled as if it was the only successor.
A future may also fail to ever complete. In that case, no handler blocks are called.
Class Methods
all:
  Waits for multiple futures to complete and collects their results.

     Answers a future which will complete once all the provided futures
     have completed, either with their results, or with an error if any
     of the provided futures fail.

     The value of the answered future will be a list of all the values that
     were produced in the order that the futures are provided by iterating
     over @futures

     If any future completes with an error,
     then the answered future completes with that error.
     If further futures also complete with errors, those errors are discarded.

     All futures must complete before the answered future is completed (still with
     the first error; the remaining errors are silently dropped

     Arguments:
        futures - <SequenceableCollection>
     Answers:
        <EsFuture>
all:eagerError:onCleanUp:
  Waits for multiple futures to complete and collects their results.

     Answers a future which will complete once all the provided futures
     have completed, either with their results, or with an error if any
     of the provided futures fail.

     The value of the answered future will be a list of all the values that
     were produced in the order that the futures are provided by iterating
     over @futures

     If any future completes with an error,
     then the answered future completes with that error.
     If further futures also complete with errors, those errors are discarded.

     If @eagerError is true, the answered future completes with an error
     immediately on the first error from one of the futures. Otherwise all
     futures must complete before the answered future is completed (still with
     the first error; the remaining errors are silently dropped).

     In the case of an error, @onCleanUp (if provided), is invoked on any
     non-null result of successful futures.
     This makes it possible to cleanUp resources that would otherwise be
     lost (since the answered future does not provide access to these values).
     @onCleanUp is not evaluated if there is no error.

     Arguments:
        futures - <SequenceableCollection>
        eagerError - <Boolean>
        onCleanUp - <Block>
     Answers:
        <EsFuture>
all:onCleanUp:
  Waits for multiple futures to complete and collects their results.

     Answers a future which will complete once all the provided futures
     have completed, either with their results, or with an error if any
     of the provided futures fail.

     The value of the answered future will be a list of all the values that
     were produced in the order that the futures are provided by iterating
     over @futures

     If any future completes with an error,
     then the answered future completes with that error.
     If further futures also complete with errors, those errors are discarded.

     In the case of an error, @onCleanUp (if provided), is invoked on any
     non-null result of successful futures.
     This makes it possible to cleanUp resources that would otherwise be
     lost (since the answered future does not provide access to these values).
     @onCleanUp is not evaluated if there is no error.

     Arguments:
        futures - <SequenceableCollection>
        onCleanUp - <Block> 0-arg
     Answers:
        <EsFuture>
any:
  Answers the result of the first future in @futures to complete with
     a value or an error.
     If @futures is empty, or none of the futures complete, then the answered
     future never completes

     Arguments:
        futures - <SequenceableCollection>
     Answers:
        <EsFuture>
error:
  Answers a future that completes with an error @anError
     and default stack trace

     The created future will be completed with an error in a future
     smalltalk process leaving enough time for error handlers to be
     registered.
     If an error handler isn't registered before the future completes, the
     error will be considered unhandled

     @Note: Use EsPromise to create a future and complete it later

     Arguments:
        anError - <Object>
     Answers:
        <EsFuture>
error:stackTrace:
  Answers a future that completes with an error @anError
     and optional stack trace.

     The created future will be completed with an error in a future
     smalltalk process leaving enough time for error handlers to be
     registered.
     If an error handler isn't registered before the future completes, the
     error will be considered unhandled

     @Note: Use EsPromise to create a future and complete it later

     Arguments:
        anError - <Object>
        aStackTrace - <EsAsyncStackTrace>
     Answers:
        <EsFuture>
forEach:do:
  Evaluates @aBlock for each element in @elements.

     Evaluates @aBlock with each element in @elements in order.
     If the evaluation of @aBlock answers an EsFuture, the iteration waits
     until the future is completed before continuing with the next element.

     Answers an EsFuture that completes with nil when all elements have been
     processed.

     Non-EsFuture answered values, and completion-values of answered EsFutures,
     are discarded.

     Any error from @aBlock will stop the iteration and be reported in the answered
     EsFuture

     Arguments:
        elements - <SequenceableCollection>
        aBlock - <Block> 1-arg
     Answers:
        <EsFuture>
new
  Answer a new instance of this future

     Answers:
        <EsFuture>
on:
  Creates a future containing the result of calling @aComputation
     asynchronously with EsAsyncTaskScheduler>>scheduleTask:.

     If the result of executing @aComputation raises an exception,
     the answered future is completed with the error.

     If the answered value is itself an EsFuture, completion of
     the created future will wait until the answered future completes,
     and will then complete with the same result.

     If a non-future value is answered, the answered future is completed
     with that value.

     If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
     then its #value is taken, which in most cases is just the object itself.
     The future will complete with the object>>value

     Arguments:
        aComputation - <Block> - The value of the Block is evaluated
                                  <DirectedMessage> The receiver>>selector is sent
                                  <Object> Object>>value is sent
     Answers:
        <EsFuture>
on:delayed:
  Creates a future that runs its computation after a delay.

     @aComputation will be executed after the given @aDurationOrMs has passed,
     and the future is completed with the result of the computation.

     If @aComputation answers a future,
     the future answered by this #on:delayed: method will complete with the value or
     error of that future.

     If the duration is 0 or less, it completes no sooner than when a scheduled
     task submitted at the lowest priority is able to exectute a separate timer
     process at the highest prioirty.  This is to allow for all scheduled tasks to
     run before the high-priority timer comples the task.

     If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
     then its #value is taken, which in most cases is just the object itself.
     The future will eventually complete with the object>>value

     If evaluating @aComputation raises an error, the created future will complete
     with that error.

     @see also EsPromise for a way to create and complete a future at a later time
     that isn't necessarily after a known fixed duration

     Arguments:
        aComputation - <Block> - The value of the Block is evaluated
                                  <DirectedMessage> The receiver>>selector is sent
                                  <Object> Object>>value is sent
        aDurationOrMs - <Duration>duration object
                                    <Integer> duration in milliseconds
     Answers:
        <EsFuture>
sync:
  Answer a future containing the result of immediately calling
     @aComputation

     If calling @aComputation raises an error, the answered future is completed with the
     error.

     If calling @aComputation answers a future, that future is answered.

     If calling @aComputation answers a non-future value, a future is answered
     which has beem completed with that value.

     If @aComputation is a non-evaluatable object (i.e. not a Block or Message),
     then its #value is taken, which in most cases is just the object itself.
     The future will complete with the object>>value

     Arguments:
        aComputation - <Block> - The value of the Block is evaluated
                                  <DirectedMessage> The receiver>>selector is sent
                                  <Object> Object>>value is sent
     Answers:
        <EsFuture>
value:
  Answers a future completed with @aValue.

     If @aValue is a future, the created future waits for @aValue
     future to complete, then completes with the same result.

     If @aValue is not a future, the created future is completed
     with @aValue value.

     Use <EsPromise> to create a future and complete it later

     Arguments:
        aValue - <Object> value object or future
     Answers:
        <EsFuture>
Instance Methods
catch:
  Handles errors emitted by this future unconditionally.

     This is the asynchronous equivalent of an 'on:do:' expression.

     @see EsFuture>>catch:if: for more details.

     Arguments:
        onError - <Block> 0, 1 to 2-arg with error and stackTrace
     Answers:
        <EsFuture>
catch:if:
  Handles errors emitted by this future.

     This is the asynchronous equivalent of an 'on:do:' expression.

     Answers a new future that will be completed with either the result of
     this future or the result of evaluating @onError action.

     If this future completes with a value,
     the answered future completes with the same value.

     If this future completes with an error,
     then @shouldCatchError is first evaluated with the error value.

     If @shouldCatchError answers false, the exception is not handled by this '#catch:if'
     and the answered future completes with the same error and stack trace as this future.

     If @shouldCatchError answers true, @onError is evaluated with the error and possibly
     stack trace, and the answered future is completed with the result of this call in exactly
     the same way as for '#then:catch:` onError evaluation.

     If @shouldCatchError is nil, it defaults to a block that always answers true.
     The evaluation of @shouldCatchError should not throw, but if it does, it is handled as
     if the evaluation of @onError had thrown.

     Note that futures don't delay reporting of errors until listeners are
     added. If the first `#catch:` (or `#then:`) call happens after this future
     has completed with an error then the error is reported as unhandled error.

     Arguments:
        onError - <Block> 0, 1 or 2-arg with error and stackTrace
        shouldCatchError - <Block> test block to determine if catch is applicable
                                       <UndefinedObject> implicit test block [true]
     Answers:
        <EsFuture>
ensure:
  Registers an @onCompletion action to be called when this future completes.

     The @onCompletion action is evaluated when this future completes, whether it
     does so with a value or an error.

     This is the asynchronous equivalent of an 'ensure' block.

     The future answered by this call, 'f', will complete the same way as this future
     unless an error occurs in the @onCompletion action, or in a future answered by
     @onCompletion.  If the evaluation of @onCompletion does not answer a future,
     its answered value is ignored

     If the evaluation of @onCompletion raises an error, the 'f' is completed with the
     error.

     If the evaluation of @onCompletion answers a future, 'f2', then completion of 'f'
     is delayed until 'f2' completes.  If 'f2' completes with an error, that will be the result
     of 'f' too.  The value of 'f2' is always ignored.

     This method is equivalent to:

     EsFuture>>ensure: onCompletion
        ^self 
            then: [:v | | f2 |  
                f2 := onCompletion value.
                f2 isEsFutureCompatible ifTrue: [f2 then: [v] ifFalse: [v]]
            catch: [:e | | f2 |
                f2 := onCompletion value.
                f2 isEsFutureCompatible ifTrue: [f2 then: [Exception signal: e]] ifFalse: [Exception signal: e]].

    Arguments:
        onCompletion - <Block> 0-arg
    Answers:
        <EsFuture>
hasEsFutureInterface
  Answer true if this object has an EsFuture interface, false otherwise.

     Answers:
        <Boolean>
then:
  Register a handler to be evaluated when this future completes with a value.

     When this future completes with a value,
     the @onValue handler will be evaluated with that value.
     If this future is already completed, the handler will not be evaluated
     immediately, but will be scheduled to run in a later task.
     The @onValue handler can accept either zero or one argument
     where the argument is the value object.

     Answers a new future which is completed with the result of the evaluation of @onValue
     (if this future completes with a value)

     If this future completes with an error,
     the error is forwarded directly to the answered future.

     @see EsFuture>>then:catch: for more complete details

     Arguments:
        onValue - <Block> 0 or 1-arg injected with completed value (if any)
     Answers:
        <EsFuture>
then:catch:
  Register handlers to be evaluated when this future completes.

     When this future completes with a value,
     the @onValue handler will be evaluated with that value.
     If this future is already completed, the handler will not be evaluated
     immediately, but will be scheduled to run in a later task.
     The @onValue handler can accept either zero or one argument
     where the argument is the value object.

     If @onError is provided, and this future completes with an error,
     the @onError handler is evaluated with that error and its stack trace.
     The @onError handler can accept either zero, one or two arguments
     where the arguments are the error object and stack trace.
     The @onError handler must answer a value or future that can be used
     to complete the answered future.

     Answers a new future which is completed with the result of the evaluation of @onValue
     (if this future completes with a value) or to @onError (if this future completes with an error).

     If the invoked handler raises an error,
     the answered future is completed with the raised error and stack trace for the error.
     In the case of @onError,
     if the exception raised is identical to the error argument to @onError,
     the raised error is considered a rethrow and the original stack trace is used
     instead.

     If the handler answers a future
     the future answered by #then: will be completed with
     the same result as the future answered by the handler.

     If @onError is not given, and this future completes with an error,
     the error is forwarded directly to the answered future.

     In most cases, it is more readable to use #catch: separately, possibly
     with an #if: parameter (i.e. catch:if:), instead of handling both value
     and error in a single #then: call.

     Note that futures don't delay reporting of errors until listeners are
     added. If the first #then: or #catch: call happens after this future
     has completed with an error then the error is reported as unhandled error.
     See the description in the class comments.

     Arguments:
        onValue - <Block> 0 or 1-arg injected with completed value (if any)
        onError - <Block> 0, 1 or 2-arg injected with completed error and stack trace (if any)
     Answers:
        <EsFuture>
timeout:then:
  Timeout the future computation after @aDurationOrMs has passed.

     Answers a new future that completes with the same value as this future,
     if this future completes in time.

     If this future does not complete before @aDurationOrMs has passed,
     the @onTimeout block is evaluated instead, and its result (whether it
     returns or raises) is used as the result of the answered future.
     The @onTimeout block can answer a value or a future.

     If @onTimeout is nil, a timeout will cause the answered future to complete
     with a timeout exception

     Arguments:
        aDurationOrMs - <Duration> duration object
                                    <Integer> duration in milliseconds
        onTimeout - <Block> 0-arg block
     Answers:
        <EsFuture>
waitFor
  BLOCKING: Wait for the future computation to complete.
     Answers the result of the future.

     @Note: Usage of Blocking APIs are typically discouraged in
     asynchronous programming.  However, in certain situations
     it can be necessary to block, so it is provided.

    Answers:
        <Object>
waitFor:
  BLOCKING: Wait for at most @aDurationOrMs for the future computation
     to complete.  Answers true if the future completes in time, 
     otherwise answers false.

     @Note: Usage of Blocking APIs are typically discouraged in
     asynchronous programming.  However, in certain situations
     it can be necessary to block, so it is provided.

     Arguments:
        aDurationOrMs - <Duration> duration object
                                    <Integer> duration in milliseconds
    Answers:
        <Boolean>
Last modified date: 02/23/2021