Artifact [54d9aa5b1c]

Artifact 54d9aa5b1c42448c03a07fc25c504d8456b86a5a:


# Copyright (c) 2015, Ashok P. Nadkarni
# All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:

# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.

# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.

# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package require Tcl 8.6

namespace eval promise {
    proc version {} { return 1.1.0 }
}

proc promise::lambda {params body args} {
    # Creates an anonymous procedure and returns a command prefix for it.
    #   params - parameter definitions for the procedure
    #   body - body of the procedures
    #   args - additional arguments to be passed to the procedure when it
    #     is invoked
    #
    # This is just a convenience command since anonymous procedures are
    # commonly useful with promises. The lambda package from tcllib
    # is identical in function.

    return [list ::apply [list $params $body] {*}$args]
}

catch {promise::Promise destroy}
oo::class create promise::Promise {

    # The promise state can be one of
    #  PENDING - Initial state where it has not yet been assigned a
    #            value or error
    #  FULFILLED - The promise has been assigned a value
    #  REJECTED  - The promise has been assigned an error
    #  CHAINED  - The promise is attached to another promise
    variable _state

    # Stores data that is accessed through the setdata/getdata methods.
    # The Promise class itself does not use this.
    variable _clientdata

    # The promise value once it is fulfilled or rejected. In the latter
    # case, it should be an the error message
    variable _value

    # The error dictionary in case promise is rejected
    variable _edict
    
    # Reactions to be notified when the promise is rejected. Each element
    # in this list is a pair consisting of the fulfilment reaction
    # and the rejection reaction. Either element of the pair could be
    # empty signifying no reaction for that case. The list is populated
    # via the then method.
    variable _reactions

    # Reference counting to free up promises since Tcl does not have
    # garbage collection for objects. Garbage collection via reference
    # counting only takes place after at least one done/then reaction
    # is placed on the event queue, not before. Else promises that
    # are immediately resolved on construction would be freed right
    # away before the application even gets a chance to call done/then.
    variable _do_gc
    variable _nrefs

    # If no reject reactions are registered, then the Tcl bgerror
    # handler is invoked. But don't want to do this more than once
    # so track it
    variable _bgerror_done
    
    constructor {cmd} {
        # Create a promise for the asynchronous operation to be initiated
        # by $cmd.
        # cmd - a command prefix that should initiate an asynchronous
        #  operation.
        # The command prefix $cmd is passed an additional argument - the
        # name of this Promise object. It should arrange for one of the
        # object's settle methods [fulfill], [chain] or
        # [reject] to be called when the operation completes.
        
        set _state PENDING
        set _reactions [list ]
        set _do_gc 0
        set _bgerror_done 0
        set _nrefs 0
        array set _clientdata {}
        
        # Errors in the construction command are returned via
        # the standard mechanism of reject.
        #
        if {[catch {
            # For some special cases, $cmd may be "" if the async operation
            # is initiated outside the constructor. This is not a good
            # thing because the error in the initiator will not be
            # trapped via the standard promise error catching mechanism
            # but that's the application's problem (actually pgeturl also
            # uses this).
            if {[llength $cmd]} {
                uplevel #0 [linsert $cmd end [self]]
            }
        } msg edict]} {
            my reject $msg $edict
        }
    }

    destructor {
        # Destroys the object.
        #
        # This method should not be generally called directly as [Promise]
        # objects are garbage collected either automatically or via the [ref]
        # and [unref] methods.
    }
    
    method state {} {
        # Returns the current state of the promise.
        #
        # The promise state may be one of the values 'PENDING',
        # 'FULFILLED', 'REJECTED' or 'CHAINED'
        return $_state
    }

    method getdata {key} {
        # Returns data previously stored through the setdata method.
        #  key - key whose associated values is to be returned.
        # An error will be raised if no value is associated with the key.
        return $_clientdata($key)
    }

    method setdata {key value} {
        # Sets a value to be associated with a key.
        #  key - the lookup key
        #  value - the value to be associated with the key
        # A promise internally maintains a dictionary whose values can
        # be accessed with the [getdata] and [setdata] methods. This
        # dictionary is not used by the Promise class itself but is meant
        # to be used by promise library specializations or applications.
        # Callers need to take care that keys used for a particular
        # promise are sufficiently distinguishable so as to not clash.
        #
        # Returns the value stored with the key.
        set _clientdata($key) $value
    }
    
    method value {} {
        # Returns the settled value for the promise.
        #
        # The returned value may be the fulfilled value or the rejected
        # value depending on whether the associated operation was successfully
        # completed or failed.
        #
        # An error is raised if the promise is not settled yet.
        if {$_state ni {FULFILLED REJECTED}} {
            error "Value is not set."
        }
        return $_value
    }

    method ref {} {
        # Increments the reference count for the object.
        incr _nrefs
    }

    method unref {} {
        # Decrements the reference count for the object.
        #
        # The object may have been destroyed when the call returns.
        incr _nrefs -1
        my GC
    }

    method nrefs {} {
        # Returns the current reference count.
        #
        # Use for debugging only! Note, internal references are not included.
        return $_nrefs
    }
    
    method GC {} {
        if {$_nrefs <= 0 && $_do_gc && [llength $_reactions] == 0} {
            my destroy
        }
    }
    
    method FulfillAttached {value} {
        if {$_state ne "CHAINED"} {
            return
        }
        set _value $value
        set _state FULFILLED
        my ScheduleReactions
        return
    }
    
    method RejectAttached {reason edict} {
        if {$_state ne "CHAINED"} {
            return
        }
        set _value $reason
        set _edict $edict
        set _state REJECTED
        my ScheduleReactions
        return
    }
    
    # Method to invoke to fulfil a promise with a value or another promise.
    method fulfill {value} {
        # Fulfills the promise.
        #   value - the value with which the promise is fulfilled
        #
        # Returns '0' if promise had already been settled and '1' if
        # it was fulfilled by the current call.

        #ruff
        # If the promise has already been settled, the method has no effect.
        if {$_state ne "PENDING"} {
            return 0;             # Already settled
        }
        
        #ruff
        # Otherwise, it is transitioned to the 'FULFILLED' state with
        # the value specified by $value. If there are any fulfillment
        # reactions registered by the [done] or [then] methods, they
        # are scheduled to be run.
        set _value $value
        set _state FULFILLED
        my ScheduleReactions
        return 1
    }

    # Method to invoke to fulfil a promise with a value or another promise.
    method chain {promise} {
        # Chains the promise to another promise.
        #   promise - the [Promise] object to which this promise is to
        #     be chained
        #
        # Returns '0' if promise had already been settled and '1' otherwise.

        #ruff
        # If the promise on which this method is called
        # has already been settled, the method has no effect.
        if {$_state ne "PENDING"} {
            return 0;
        }

        #ruff
        # Otherwise, it is chained to $promise so that it reflects that
        # other promise's state.
        if {[catch {
            $promise done [namespace code {my FulfillAttached}] [namespace code {my RejectAttached}]
        } msg edict]} {
            my reject $msg $edict
        } else {
            set _state CHAINED
        }
        
        return 1
    }

    method reject {reason {edict {}}} {
        # Rejects the promise.
        #   reason - a message string describing the reason for the rejection.
        #   edict - a Tcl error dictionary
        #
        # The $reason and $edict values are passed on to the rejection
        # reactions. By convention, these should be of the form returned
        # by the `catch` or `try` commands in case of errors.
        #
        # Returns '0' if promise had already been settled and '1' if
        # it was rejected by the current call.

        #ruff
        # If the promise has already been settled, the method has no effect.
        if {$_state ne "PENDING"} {
            return 0;             # Already settled
        }

        #ruff
        # Otherwise, it is transitioned to the 'REJECTED' state.  If
        # there are any reject reactions registered by the [done] or
        # [then] methods, they are scheduled to be run.
        
        set _value $reason
        #ruff
        # If $edict is not specified, or specified as an empty string,
        # a suitable error dictionary is constructed in its place
        # to be passed to the reaction.
        if {$edict eq ""} {
            catch {throw {PROMISE REJECTED} $reason} - edict
        }
        set _edict $edict
        set _state REJECTED
        my ScheduleReactions
        return 1
    }

    # Internal method to queue all registered reactions based on
    # whether the promise is succesfully fulfilled or not
    method ScheduleReactions {} {
        if {$_state ni {FULFILLED REJECTED} || [llength $_reactions] == 0 } {
            # Promise is not settled or no reactions registered
            return
        }

        # Note on garbage collection: garbage collection is to be enabled if
        # at least one FULFILLED or REJECTED reaction is registered.
        # Also if the promise is REJECTED but no rejection handlers are run
        # we also schedule a background error.
        # In all cases, CLEANUP reactions do not count.
        foreach reaction $_reactions {
            foreach type {FULFILLED REJECTED} {
                if {[dict exists $reaction $type]} {
                    set _do_gc 1
                    if {$type eq $_state} {
                        set cmd [dict get $reaction $type]
                        if {[llength $cmd]} {
                            if {$type eq "FULFILLED"} {
                                lappend cmd $_value
                            } else {
                                lappend cmd $_value $_edict
                            }
                            set ran_reaction($type) 1
                            # Enqueue the reaction via the event loop
                            after 0 [list after idle $cmd]
                        }
                    }
                }
            }
            if {[dict exists $reaction CLEANUP]} {
                set cmd [dict get $reaction CLEANUP]
                if {[llength $cmd]} {
                    # Enqueue the cleaner via the event loop passing the
                    # *state* as well as the value
                    if {$_state eq "REJECTED"} {
                        lappend cmd $_state $_value $_edict
                    } else {
                        lappend cmd $_state $_value
                    }
                    after 0 [list after idle $cmd]
                    # Note we do not set _do_gc if we only run cleaners
                }
            }
        }
        set _reactions [list ]

        # Check for need to background error (see comments above)
        if {$_state eq "REJECTED" && $_do_gc && ! [info exists ran_reaction(REJECTED)] && ! $_bgerror_done} {
            # TBD - should we also check _nrefs before backgrounding error?

            # Wrap in catch in case $_edict does not follow error conventions
            # or is not even a dictionary
            if {[catch {
                dict get $_edict -level
                dict get $_edict -code
            }]} {
                catch {throw {PROMISE REJECT} $_value} - edict
            } else {
                set edict $_edict
            }
            # TBD - how exactly is level to be handled?
            # If -level is not 0, bgerror barfs because it treates
            # it as TCL_RETURN no matter was -code is
            dict set edict -level 0
            after idle [interp bgerror {}] [list $_value $edict]
            set _bgerror_done 1
        }
        
        my GC
        return 
    } 

    method RegisterReactions {args} {
        # Registers the specified reactions.
        #  args - dictionary keyed by 'CLEANUP', 'FULFILLED', 'REJECTED'
        #     with values being the corresponding reaction callback

        lappend _reactions $args
        my ScheduleReactions
        return
    }
        
    method done {{on_fulfill {}} {on_reject {}}} {
        # Registers reactions to be run when the promise is settled.
        #  on_fulfill - command prefix for the reaction to run
        #    if the promise is fulfilled.
        #    reaction is registered.
        #  on_reject - command prefix for the reaction to run
        #    if the promise is rejected.
        # Reactions are called with an additional argument which is
        # the value with which the promise was settled.
        # 
        # The command may be called multiple times to register multiple
        # reactions to be run at promise settlement. If the promise was
        # already settled at the time the call was made, the reactions
        # are invoked immediately. In all cases, reactions are not called
        # directly, but are invoked by scheduling through the event loop.
        #
        # The method triggers garbage collection of the object if the
        # promise has been settled and any registered reactions have been
        # scheduled. Applications can hold on to the object through
        # appropriate use of the [ref] and [unref] methods.
        #
        # Note that both $on_fulfill and $on_reject may be specified
        # as empty strings if no further action needs to be taken on
        # settlement of the promise. If the promise is rejected, and
        # no rejection reactions are registered, the error is reported
        # via the Tcl 'interp bgerror' facility.

        # TBD - as per the Promise/A+ spec, errors in done should generate
        # a background error (unlike then).

        my RegisterReactions FULFILLED $on_fulfill REJECTED $on_reject

        #ruff
        # The method does not return a value.
        return
    }
    
    method then {on_fulfill {on_reject {}}} {
        # Registers reactions to be run when the promise is settled
        # and returns a new [Promise] object that will be settled by the
        # reactions.
        #  on_fulfill - command prefix for the reaction to run
        #    if the promise is fulfilled. If an empty string, no fulfill
        #    reaction is registered.
        #  on_reject - command prefix for the reaction to run
        #    if the promise is rejected. If unspecified or an empty string,
        #    no reject reaction is registered.
        # Both reactions are called with an additional argument which is
        # the value with which the promise was settled.
        # 
        # The command may be called multiple times to register multiple
        # reactions to be run at promise settlement. If the promise was
        # already settled at the time the call was made, the reactions
        # are invoked immediately. In all cases, reactions are not called
        # directly, but are invoked by scheduling through the event loop.
        #
        # If the reaction that is invoked runs without error, its return
        # value fulfills the new promise returned by the 'then' method.
        # If it raises an exception, the new promise will be rejected
        # with the error message and dictionary from the exception.
        #
        # Alternatively, the reactions can explicitly invoke commands
        # [then_fulfill], [then_reject] or [then_chain] to
        # resolve the returned promise. In this case, the return value
        # (including exceptions) from the reactions are ignored.
        #
        # If 'on_fulfill' (or 'on_reject') is an empty string (or unspecified),
        # the new promise is created and fulfilled (or rejected) with
        # the same value that would have been passed in to the reactions.
        #
        # The method triggers garbage collection of the object if the
        # promise has been settled and registered reactions have been
        # scheduled. Applications can hold on to the object through
        # appropriate use of the [ref] and [unref] methods.
        #
        # Returns a new promise that is settled by the registered reactions.
        
        set then_promise [[self class] new ""]
        my RegisterReactions \
            FULFILLED [list ::promise::_then_reaction $then_promise FULFILLED $on_fulfill] \
            REJECTED [list ::promise::_then_reaction $then_promise REJECTED $on_reject]
        return $then_promise
    }

    # This could be a forward, but then we cannot document it via ruff!
    method catch {on_reject} {
        # Registers reactions to be run when the promise is rejected.
        #   on_reject - command prefix for the reaction
        #     reaction to run if the promise is rejected. If unspecified
        #     or an empty string, no reject reaction is registered. The
        #     reaction is called with an additional argument which is the
        #     value with which the promise was settled.
        # This method is just a wrapper around [then] with the
        # 'on_fulfill' parameter defaulting to an empty string. See
        # the description of that method for details.
        return [my then "" $on_reject]
    }
    
    method cleanup {cleaner} {
        # Registers a reaction to be executed for running cleanup
        # code when the promise is settled.
        #   cleaner - command prefix to run on settlement
        # This method is intended to run a clean up script 
        # when a promise is settled. Its primary use is to avoid duplication
        # of code in the `then` and `catch` handlers for a promise.
        # It may also be called multiple times
        # to clean up intermediate steps when promises are chained.
        # 
        # The method returns a new promise that will be settled
        # as per the following rules.
        # - if the cleaner runs without errors, the returned promise
        #   will reflect the settlement of the promise on which this
        #   method is called.
        # - if the cleaner raises an exception, the returned promise
        #   is rejected with a value consisting of the error message
        #   and dictionary pair.
        #
        # Returns a new promise that is settled based on the cleaner
        set cleaner_promise [[self class] new ""]
        my RegisterReactions CLEANUP [list ::promise::_cleanup_reaction $cleaner_promise $cleaner]
        return $cleaner_promise
    }
}

proc promise::_then_reaction {target_promise status cmd value {edict {}}} {
    # Run the specified command and fulfill/reject the target promise
    # accordingly. If the command is empty, the passed-in value is passed
    # on to the target promise.

    # IMPORTANT!!!!
    # MUST BE CALLED FROM EVENT LOOP AT so info level must be 1. Else
    # promise::then_fulfill/then_reject/then_chain will not work
    # Also, Do NOT change the param name target_promise without changing
    # those procs.
    # Oh what a hack to get around lack of closures. Alternative would have
    # been to pass an additional parameter (target_promise)
    # to the application code but then that script would have had to
    # carry that around.

    if {[info level] != 1} {
        error "Internal error: _then_reaction not at level 1"
    }
    
    if {[llength $cmd] == 0} {
        switch -exact -- $status {
            FULFILLED { $target_promise fulfill $value }
            REJECTED  { $target_promise reject $value $edict}
            CHAINED -
            PENDING  -
            default {
                $target_promise reject "Internal error: invalid status $state"
            }
        }
    } else {
        # Invoke the real reaction code and fulfill/reject the target promise.
        # Note the reaction code may have called one of the promise::then_*
        # commands itself and reactions run resulting in the object being
        # freed. Hence resolve using the safe* variants
        # TBD - ideally we would like to execute at global level. However
        # the then_* commands retrieve target_promise from level 1 (here)
        # which they cannot if uplevel #0 is done. So directly invoke.
        if {$status eq "REJECTED"} {
            lappend cmd $value $edict
        } else {
            lappend cmd $value
        }
        if {[catch $cmd reaction_value reaction_edict]} {
            safe_reject $target_promise $reaction_value $reaction_edict
        } else {
            safe_fulfill $target_promise $reaction_value
        }
    }
    return
}

proc promise::_cleanup_reaction {target_promise cleaner state value {edict {}}} {
    # Run the specified cleaner and fulfill/reject the target promise
    # accordingly. If the cleaner executes without error, the original
    # value and state is passed on. If the cleaner executes with error
    # the promise is rejected.

    if {[llength $cleaner] == 0} {
        switch -exact -- $state {
            FULFILLED { $target_promise fulfill $value }
            REJECTED  { $target_promise reject $value $edict }
            CHAINED -
            PENDING  -
            default {
                $target_promise reject "Internal error: invalid state $state"
            }
        }
    } else {
        if {[catch {uplevel #0 $cleaner} err edict]} {
            # Cleaner failed. Reject the target promise
            $target_promise reject $err $edict
        } else {
            # Cleaner completed without errors, pass on the original value
            if {$state eq "FULFILLED"} {
                $target_promise fulfill $value
            } else {
                $target_promise reject $value $edict
            }
        }
    }
    return
}

proc promise::then_fulfill {value} {
    # Fulfills the promise returned by a [then] method call from
    # within its reaction.
    #  value - the value with which to fulfill the promise
    #
    # The [Promise.then] method is a mechanism to chain asynchronous
    # reactions by registering them on a promise. It returns a new
    # promise which is settled by the return value from the reaction,
    # or by the reaction calling one of three commands - 'then_fulfill',
    # [then_reject] or [then_chain]. Calling 'then_fulfill' fulfills
    # the promise returned by the 'then' method that queued the currently
    # running reaction.
    #
    # It is an error to call this command from outside a reaction
    # that was queued via the [then] method on a promise.
    
    # TBD - what if someone calls this from within a uplevel #0 ? The
    # upvar will be all wrong
    upvar #1 target_promise target_promise
    if {![info exists target_promise]} {
        set msg "promise::then_fulfill called in invalid context."
        throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
    }
    $target_promise fulfill $value
}

proc promise::then_chain {promise} {
    # Chains the promise returned by a [then] method call to
    # another promise.
    #  promise - the promise to which the promise returned by [then] is
    #     to be chained
    #
    # The [Promise.then] method is a mechanism to chain asynchronous
    # reactions by registering them on a promise. It returns a new
    # promise which is settled by the return value from the reaction,
    # or by the reaction calling one of three commands - [then_fulfill],
    # 'then_reject' or [then_chain]. Calling 'then_chain' chains
    # the promise returned by the 'then' method that queued the currently
    # running reaction to $promise so that the former will be settled
    # based on the latter.
    #
    # It is an error to call this command from outside a reaction
    # that was queued via the [then] method on a promise.
    upvar #1 target_promise target_promise
    if {![info exists target_promise]} {
        set msg "promise::then_chain called in invalid context."
        throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
    }
    $target_promise chain $promise
}

proc promise::then_reject {reason edict} {
    # Rejects the promise returned by a [then] method call from
    # within its reaction.
    #   reason - a message string describing the reason for the rejection.
    #   edict - a Tcl error dictionary
    # The [Promise.then] method is a mechanism to chain asynchronous
    # reactions by registering them on a promise. It returns a new
    # promise which is settled by the return value from the reaction,
    # or by the reaction calling one of three commands - [then_fulfill],
    # 'then_reject' or [then_chain]. Calling 'then_reject' rejects
    # the promise returned by the 'then' method that queued the currently
    # running reaction.
    #
    # It is an error to call this command from outside a reaction
    # that was queued via the [then] method on a promise.
    upvar #1 target_promise target_promise
    if {![info exists target_promise]} {
        set msg "promise::then_reject called in invalid context."
        throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
    }
    $target_promise reject $reason $edict
}

proc promise::all {promises} {
    # Returns a promise that fulfills or rejects when all promises
    # in the $promises argument have fulfilled or any one has rejected.
    #   promises - a list of Promise objects
    # If any of $promises rejects, then the promise returned by the
    # command will reject with the same value. Otherwise, the promise
    # will fulfill when all promises have fulfilled.
    # The resolved value will be a list of the resolved
    # values of the contained promises.
    
    set all_promise [Promise new [lambda {promises prom} {
        set npromises [llength $promises]
        if {$npromises == 0} {
            $prom fulfill {}
            return
        }

        # Ask each promise to update us when resolved.
        foreach promise $promises {
            $promise done \
                [list ::promise::_all_helper $prom $promise FULFILLED] \
                [list ::promise::_all_helper $prom $promise REJECTED]
        }

        # We keep track of state with a dictionary that will be
        # stored in $prom with the following keys:
        #  PROMISES - the list of promises in the order passed
        #  PENDING_COUNT - count of unresolved promises
        #  RESULTS - dictionary keyed by promise and containing resolved value
        set all_state [list PROMISES $promises PENDING_COUNT $npromises RESULTS {}]
        
        $prom setdata ALLPROMISES $all_state
    } $promises]]
                 
    return $all_promise
}

proc promise::all* args {
    # Returns a promise that fulfills or rejects when all promises
    # in the $args argument have fulfilled or any one has rejected.
    # args - list of Promise objects
    # This command is identical to the all command except that it takes
    # multiple arguments, each of which is a Promise object. See [all]
    # for a description.
    return [all $args]
}

# Callback for promise::all.
#  all_promise - the "master" promise returned by the all call.
#  done_promise - the promise whose callback is being serviced.
#  resolution - whether the current promise was resolved with "FULFILLED"
#   or "REJECTED"
#  value - the value of the currently fulfilled promise or error description
#   in case rejected
#  edict - error dictionary (if promise was rejected)
proc promise::_all_helper {all_promise done_promise resolution value {edict {}}} {
    if {![info object isa object $all_promise]} {
        # The object has been deleted. Naught to do
        return
    }
    if {[$all_promise state] ne "PENDING"} {
        # Already settled. This can happen when a tracked promise is
        # rejected and another tracked promise gets settled afterwards.
        return
    }
    if {$resolution eq "REJECTED"} {
        # This promise failed. Immediately reject the master promise
        # TBD - can we somehow indicate which promise failed ?
        $all_promise reject $value $edict
        return
    }

    # Update the state of the resolved tracked promise
    set all_state [$all_promise getdata ALLPROMISES]
    dict set all_state RESULTS $done_promise $value
    dict incr all_state PENDING_COUNT -1
    $all_promise setdata ALLPROMISES $all_state

    # If all promises resolved, resolve the all promise
    if {[dict get $all_state PENDING_COUNT] == 0} {
        set values {}
        foreach prom [dict get $all_state PROMISES] {
            lappend values [dict get $all_state RESULTS $prom]
        }
        $all_promise fulfill $values
    }
    return
}

proc promise::race {promises} {
    # Returns a promise that fulfills or rejects when any promise
    # in the $promises argument is fulfilled or rejected.
    #   promises - a list of Promise objects
    # The returned promise will fulfill and reject with the same value
    # as the first promise in $promises that fulfills or rejects.
    set race_promise [Promise new [lambda {promises prom} {
        if {[llength $promises] == 0} {
            catch {throw {PROMISE RACE EMPTYSET} "No promises specified."} reason edict
            $prom reject $reason $edict
            return
        }
        # Use safe_*, do not directly call methods since $prom may be
        # gc'ed once settled
        foreach promise $promises {
            $promise done [list ::promise::safe_fulfill $prom ] [list ::promise::safe_reject $prom]
        }
    } $promises]]

    return $race_promise
}

proc promise::race* {args} {
    # Returns a promise that fulfills or rejects when any promise
    # in the passed arguments is fulfilled or rejected.
    #   args - list of Promise objects
    # This command is identical to the 'race' command except that it takes
    # multiple arguments, each of which is a Promise object. See [race]
    # for a description.
    return [race $args]
}

proc promise::await {prom} {
    # Waits for a promise to be settled and returns its resolved value.
    #   prom - the promise that is to be waited on
    # This command may only be used from within a procedure constructed
    # with the [async] command or any code invoked from it.
    #
    # Returns the resolved value of $prom if it is fulfilled or raises an error
    # if it is rejected.
    set coro [info coroutine]
    if {$coro eq ""} {
        throw {PROMISE AWAIT NOTCORO} "await called from outside a coroutine"
    }
    $prom done [list $coro success] [list $coro fail]
    lassign [yieldto return -level 0] status val ropts
    if {$status eq "success"} {
        return $val
    } else {
        return -options $ropts $val
    }
}

proc promise::async {name paramdefs body} {
    # Defines an procedure that will run a script asynchronously as a coroutine.
    # name - name of the procedure
    # paramdefs - the parameter definitions to the procedure in the same
    #   form as passed to the standard 'proc' command
    # body - the script to be executed
    #
    # When the defined procedure $name is called, it runs the supplied $body 
    # within a new coroutine. The return value from the $name procedure call
    # will be a promise that will be fulfilled when the coroutine completes
    # normally or rejected if it completes with an error.
    #
    # Note that the passed $body argument is not the body of the
    # the procedure $name. Rather it is run as an anonymous procedure in 
    # the coroutine but in the same namespace context as $name. Thus the
    # caller or the $body script must not make any assumptions about
    # relative stack levels, use of 'uplevel' etc.
    #
    # The primary purpose of this command is to make it easy, in
    # conjunction with the [await] command, to wrap a sequence of asynchronous
    # operations as a single computational unit.
    #
    # Returns a promise that will be settled with the result of the script.
    if {![string equal -length 2 "$name" "::"]} {
        set ns [uplevel 1 namespace current]
        set name ${ns}::$name
    } else {
        set ns ::
    }
    set tmpl {
        proc %NAME% {%PARAMDEFS%} {
            set p [promise::Promise new [promise::lambda {real_args prom} {
                coroutine ::promise::async#[info cmdcount] {*}[promise::lambda {p args} {
                    upvar #1 _current_async_promise current_p
                    set current_p $p
                    set status [catch [list apply [list {%PARAMDEFS%} {%BODY%} %NS%] {*}$args] res ropts]
                    if {$status == 0} {
                        $p fulfill $res
                    } else {
                        $p reject $res $ropts
                    }
                } $prom {*}$real_args]
            } [lrange [info level 0] 1 end]]]
            return $p
        }
    }
    eval [string map [list %NAME% $name \
                          %PARAMDEFS% $paramdefs \
                          %BODY% $body \
                          %NS% $ns] $tmpl]
}

proc promise::async_fulfill {val} {
    # Fulfills a promise for an async procedure with the specified value.
    #  val - the value with which to fulfill the promise
    # This command must only be called with the context of an [async]
    # procedure.
    #
    # Returns an empty string.
    upvar #1 _current_async_promise current_p
    if {![info exists current_p]} {
        error "async_fulfill called from outside an async context."
    }
    $current_p fulfill $val
    return
}

proc promise::async_reject {val {edict {}}} {
    # Rejects a promise for an async procedure with the specified value.
    #  val - the value with which to reject the promise
    #  edict - error dictionary for rejection
    # This command must only be called with the context of an [async]
    # procedure.
    #
    # Returns an empty string.
    upvar #1 _current_async_promise current_p
    if {![info exists current_p]} {
        error "async_reject called from outside an async context."
    }
    $current_p reject $val $edict
    return
}

proc promise::async_chain {prom} {
    # Chains a promise for an async procedure to the specified promise.
    #  prom - the promise to which the async promise is to be linked.
    # This command must only be called with the context of an [async]
    # procedure.
    #
    # Returns an empty string.
    upvar #1 _current_async_promise current_p
    if {![info exists current_p]} {
        error "async_chain called from outside an async context."
    }
    $current_p chain $prom
    return
}

proc promise::pfulfilled {value} {
    # Returns a new promise that is already fulfilled with the specified value.
    #  value - the value with which to fulfill the created promise
    return [Promise new [lambda {value prom} {
        $prom fulfill $value
    } $value]]
}

proc promise::prejected {value {edict {}}} {
    # Returns a new promise that is already rejected.
    #  value - the value with which to reject the promise
    #  edict - error dictionary for rejection
    # By convention, $value should be of the format returned by
    # [rejection].
    return [Promise new [lambda {value edict prom} {
        $prom reject $value $edict
    } $value $edict]]
}

proc promise::eventloop {prom} {
    # Waits in the eventloop until the specified promise is settled.
    #  prom - the promise to be waited on
    # The command enters the event loop in similar fashion to the
    # Tcl [vwait] command except that instead of waiting on a variable
    # the command waits for the specified promise to be settled. As such
    # it has the same caveats as the vwait command in terms of care
    # being taken in nested calls etc.
    #
    # The primary use of the command is at the top level of a script
    # to wait for one or more promise based tasks to be completed. Again,
    # similar to the vwait forever idiom.
    # 
    #
    # Returns the resolved value of $prom if it is fulfilled or raises an error
    # if it is rejected.

    set varname [namespace current]::_pwait_[info cmdcount]
    $prom done \
        [lambda {varname result} {
            set $varname [list success $result]
        } $varname] \
        [lambda {varname error ropts} {
            set $varname [list fail $error $ropts]
        } $varname]
    vwait $varname
    lassign [set $varname] status result ropts
    if {$status eq "success"} {
        return $result
    } else {
        return -options $ropts $result
    }
}

proc promise::pgeturl {url args} {
    # Returns a promise that will be fulfilled when the a URL is fetched.
    #   url - the URL to fetch
    #   args - arguments to pass to the [http::geturl] command
    # This command invokes the asynchronous form of the [http::geturl] command
    # of the 'http' package. If the operation completes with a status of
    # 'ok', the returned promise is fulfilled with the contents of the
    # http state array (see the documentation of [http::geturl]). If the
    # the status is anything else, the promise is rejected with
    # the 'reason' parameter to the reaction containing the error message
    # and the 'edict' parameter containing the Tcl error dictionary
    # with an additional key 'http_state', containing the
    # contents of the http state array.
    
    uplevel #0 {package require http}
    proc pgeturl {url args} {
        set prom [Promise new [lambda {http_args prom} {
            http::geturl {*}$http_args -command [promise::lambda {prom tok} {
                upvar #0 $tok http_state
                if {$http_state(status) eq "ok"} {
                    $prom fulfill [array get http_state]
                } else {
                    if {[info exists http_state(error)]} {
                        set msg [lindex $http_state(error) 0]
                    }
                    if {![info exists msg] || $msg eq ""} {
                        set msg "Error retrieving URL."
                    }
                    catch {throw {PROMISE PGETURL} $msg} msg edict
                    dict set edict http_state [array get http_state]
                    $prom reject $msg $edict
                }
                http::cleanup $tok
            } $prom]
        } [linsert $args 0 $url]]]
        return $prom
    }
    tailcall pgeturl $url {*}$args
}

proc promise::ptimer {millisecs {value "Timer expired."}} {
    # Returns a promise that will be fulfilled when the specified time has
    # elapsed.
    #  millisecs - time interval in milliseconds
    #  value - the value with which the promise is to be fulfilled
    # In case of errors (e.g. if $milliseconds is not an integer), the
    # promise is rejected with the 'reason' parameter set to an error
    # message and the 'edict' parameter set to a Tcl error dictionary.
    #
    # Also see [ptimeout] which is similar but rejects the promise instead
    # of fulfilling it.
    
    return [Promise new [lambda {millisecs value prom} {
        if {![string is integer -strict $millisecs]} {
            # We don't allow "idle", "cancel" etc. as an argument to after
            throw {PROMISE TIMER INVALID} "Invalid timeout value \"$millisecs\"."
        }
        after $millisecs [list promise::safe_fulfill $prom $value]
    } $millisecs $value]]
}

proc promise::ptimeout {millisecs {value "Operation timed out."}} {
    # Returns a promise that will be rejected when the specified time has
    # elapsed.
    #  millisecs - time interval in milliseconds
    #  value - the value with which the promise is to be rejected
    # In case of errors (e.g. if $milliseconds is not an integer), the
    # promise is rejected with the 'reason' parameter set to $value
    # and the 'edict' parameter set to a Tcl error dictionary.
    #
    # Also see [ptimer] which is similar but fulfills the promise instead
    # of rejecting it.

    return [Promise new [lambda {millisecs value prom} {
        if {![string is integer -strict $millisecs]} {
            # We don't want to accept "idle", "cancel" etc. for after
            throw {PROMISE TIMER INVALID} "Invalid timeout value \"$millisecs\"."
        }
        after $millisecs [::promise::lambda {prom msg} {
            catch {throw {PROMISE TIMER EXPIRED} $msg} msg edict
            ::promise::safe_reject $prom $msg $edict
        } $prom $value]
    } $millisecs $value]]
}

proc promise::pconnect {args} {
    # Returns a promise that will be fulfilled when the a socket connection
    # is completed.
    #  args - arguments to be passed to the Tcl 'socket' command
    # This is a wrapper for the async version of the Tcl 'socket' command.
    # If the connection completes, the promise is fulfilled with the
    # socket handle.
    # In case of errors (e.g. if the address cannot be fulfilled), the
    # promise is rejected with the 'reason' parameter containing the
    # error message and the 'edict' parameter containing the Tcl error
    # dictionary.
    # 
    return [Promise new [lambda {so_args prom} {
        set so [socket -async {*}$so_args]
        fileevent $so writable [promise::lambda {prom so} {
            fileevent $so writable {}
            set err [chan configure $so -error]
            if {$err eq ""} {
                $prom fulfill $so
            } else {
                catch {throw {PROMISE PCONNECT FAIL} $err} err edict
                $prom reject $err $edict
            }
        } $prom $so]
    } $args]]
}

proc promise::_read_channel {prom chan data} {
    set newdata [read $chan]
    if {[string length $newdata] || ![eof $chan]} {
        append data $newdata
        fileevent $chan readable [list [namespace current]::_read_channel $prom $chan $data]
        return
    }

    # EOF
    set code [catch {
        # Need to make the channel blocking else no error is returned
        # on the close
        fileevent $chan readable {}
        fconfigure $chan -blocking 1
        close $chan
    } result edict]
    if {$code} {
        safe_reject $prom $result $edict
    } else {
        safe_fulfill $prom $data
    }
}

proc promise::pexec {args} {
    # Runs an external program and returns a promise for its output.
    #  args - program and its arguments as passed to the Tcl 'open' call
    #    for creating pipes
    # If the program runs without errors, the promise is fulfilled by its
    # standard output content. Otherwise
    # promise is rejected.
    #
    # Returns a promise that will be settled by the result of the program
    return [Promise new [lambda {open_args prom} {
        set chan [open |$open_args r]
        fconfigure $chan -blocking 0
        fileevent $chan readable [list promise::_read_channel $prom $chan ""]
    } $args]]
}        

proc promise::safe_fulfill {prom value} {
    # Fulfills the specified promise.
    #  prom - the [Promise] object to be fulfilled
    #  value - the fulfillment value
    # This is a convenience command that checks if $prom still exists
    # and if so fulfills it with $value.
    #
    # Returns 0 if the promise does not exist any more, else the return
    # value from its [fulfill] method.
    if {![info object isa object $prom]} {
        # The object has been deleted. Naught to do
        return 0
    }
    return [$prom fulfill $value]
}

proc promise::safe_reject {prom value {edict {}}} {
    # Rejects the specified promise.
    #  prom - the [Promise] object to be fulfilled
    #  value - see [Promise.reject]
    #  edict - see [Promise.reject]
    # This is a convenience command that checks if $prom still exists
    # and if so rejects it with the specified arguments.
    #
    # Returns 0 if the promise does not exist any more, else the return
    # value from its [reject] method.
    if {![info object isa object $prom]} {
        # The object has been deleted. Naught to do
        return
    }
    $prom reject $value $edict
}

proc promise::ptask {script} {
    # Creates a new Tcl thread to run the specified script and returns
    # a promise for the script results.
    #   script - script to run in the thread
    # Returns a promise that will be settled by the result of the script
    #
    # The `ptask` command runs the specified script in a new Tcl
    # thread. The promise returned from this command will be fulfilled
    # with the result of the script if it completes
    # successfully. Otherwise, the promise will be rejected with an
    # with the 'reason' parameter containing the error message
    # and the 'edict' parameter containing the Tcl error dictionary
    # from the script failure.
    #
    # Note that $script is a standalone script in that it is executed
    # in a new thread with a virgin Tcl interpreter. Any packages used
    # by $script have to be explicitly loaded, variables defined in the
    # the current interpreter will not be available in $script and so on.
    #
    # The command requires the Thread package to be loaded.

    uplevel #0 package require Thread
    proc [namespace current]::ptask script { 
        return [Promise new [lambda {script prom} {
            set thread_script [string map [list %PROM% $prom %TID% [thread::id] %SCRIPT% $script] {
                set retcode [catch {%SCRIPT%} result edict]
                if {$retcode == 0 || $retcode == 2} {
                    # ok or return
                    set response [list ::promise::safe_fulfill %PROM% $result]
                } else {
                    set response [list ::promise::safe_reject %PROM% $result $edict]
                }
                thread::send -async %TID% $response
            }]
            thread::create $thread_script
        } $script]]
    }
    tailcall [namespace current]::ptask $script
}

proc promise::pworker {tpool script} {
    # Runs a script in a worker thread from a thread pool and
    # returns a promise for the same.
    #   tpool - thread pool identifier
    #   script - script to run in the worker thread
    # Returns a promise that will be settled by the result of the script
    #
    # The Thread package allows creation of a thread pool with the
    # 'tpool create' command. The `pworker` command runs the specified
    # script in a worker thread from a thread pool. The promise
    # returned from this command will be fulfilled with the result of
    # the script if it completes successfully.
    # Otherwise, the promise will be rejected with an
    # with the 'reason' parameter containing the error message
    # and the 'edict' parameter containing the Tcl error dictionary
    # from the script failure.
    #
    # Note that $script is a standalone script in that it is executed
    # in a new thread with a virgin Tcl interpreter. Any packages used
    # by $script have to be explicitly loaded, variables defined in the
    # the current interpreter will not be available in $script and so on.

    # No need for package require Thread since if tpool is passed to
    # us, Thread must already be loaded
    return [Promise new [lambda {tpool script prom} {
        set thread_script [string map [list %PROM% $prom %TID% [thread::id] %SCRIPT% $script] {
            set retcode [catch {%SCRIPT%} result edict]
            if {$retcode == 0 || $retcode == 2} {
                set response [list ::promise::safe_fulfill %PROM% $result]
            } else {
                set response [list ::promise::safe_reject %PROM% $result $edict]
            }
            thread::send -async %TID% $response
        }]
        tpool::post -detached -nowait $tpool $thread_script
    } $tpool $script]]
}

if {0} {
    package require http
    proc checkurl {url} {
        set prom [promise::Promise new [promise::lambda {url prom} {
            http::geturl $url -method HEAD -command [promise::lambda {prom tok} {
                upvar #0 $tok http_state
                $prom fulfill [list $http_state(url) $http_state(status)]
                ::http::cleanup $tok
            } $prom]
        } $url]]
        return $prom
    }

    proc checkurls {urls} {
        return [promise::all [lmap url $urls {checkurl $url}]]
    }

    [promise::all [
                   list [
                         promise::ptask {expr 1+1}
                        ] [
                           promise::ptask {expr 2+2}
                          ]
                  ]] done [promise::lambda val {puts [tcl::mathop::* {*}$val]}] 
}

package provide promise [promise::version]

if {[info exists ::argv0] &&
    [file tail [info script]] eq [file tail $::argv0]} {
    set filename [file tail [info script]]
    if {[llength $::argv] == 0} {
        puts "Usage: [file tail [info nameofexecutable]] $::argv0 dist|install|tm|version"
        exit 1
    }
    switch -glob -- [lindex $::argv 0] {
        ver* { puts [promise::version] }
        tm -
        dist* {
            if {[file extension $filename] ne ".tm"} {
                set dir [file join [file dirname [info script]] .. build]
                file mkdir $dir
                file copy -force [info script] [file join $dir [file rootname $filename]-[promise::version].tm]
            } else {
                error "Cannot create distribution from a .tm file"
            }
        }
        install {
            set dir [file join [tcl::pkgconfig get libdir,runtime] tcl8 8.6]
            if {[file extension $filename] eq ".tm"} {
                # We already are a .tm with version number
                set target $filename
            } else {
                set target [file rootname $filename]-[promise::version].tm
            }
            file copy -force [info script] [file join $dir $target]
        }
        default {
            puts stderr "Unknown option/command \"[lindex $::argv 0]\""
            exit 1
        }
    }
}