# $Id: deferrable.rb 668 2008-01-04 23:00:34Z blackhedd $ # # Author:: Francis Cianfrocca (gmail: blackhedd) # Homepage:: http://rubyeventmachine.com # Date:: 16 Jul 2006 # # See EventMachine and EventMachine::Connection for documentation and # usage examples. # #---------------------------------------------------------------------------- # # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved. # Gmail: blackhedd # # This program is free software; you can redistribute it and/or modify # it under the terms of either: 1) the GNU General Public License # as published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version; or 2) Ruby's License. # # See the file COPYING for complete licensing information. # #--------------------------------------------------------------------------- # # require 'forwardable' module EventMachine module Deferrable # Specify a block to be executed if and when the Deferrable object receives # a status of :succeeded. See #set_deferred_status for more information. # # Calling this method on a Deferrable object whose status is not yet known # will cause the callback block to be stored on an internal list. # If you call this method on a Deferrable whose status is :succeeded, the # block will be executed immediately, receiving the parameters given to the # prior #set_deferred_status call. # #-- # If there is no status, add a callback to an internal list. # If status is succeeded, execute the callback immediately. # If status is failed, do nothing. # def callback &block return unless block if @deferred_status == :succeeded block.call(*@deferred_args) elsif @deferred_status != :failed @callbacks ||= [] @callbacks.unshift block # << block end end # Specify a block to be executed if and when the Deferrable object receives # a status of :failed. See #set_deferred_status for more information. #-- # If there is no status, add an errback to an internal list. # If status is failed, execute the errback immediately. # If status is succeeded, do nothing. # def errback &block return unless block if @deferred_status == :failed block.call(*@deferred_args) elsif @deferred_status != :succeeded @errbacks ||= [] @errbacks.unshift block # << block end end # Sets the "disposition" (status) of the Deferrable object. See also the large set of # sugarings for this method. # Note that if you call this method without arguments, # no arguments will be passed to the callback/errback. # If the user has coded these with arguments, then the # user code will throw an argument exception. # Implementors of deferrable classes must # document the arguments they will supply to user callbacks. # # OBSERVE SOMETHING VERY SPECIAL here: you may call this method even # on the INSIDE of a callback. This is very useful when a previously-registered # callback wants to change the parameters that will be passed to subsequently-registered # ones. # # You may give either :succeeded or :failed as the status argument. # # If you pass :succeeded, then all of the blocks passed to the object using the #callback # method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks # passed to the object using #errback will be discarded. # # If you pass :failed, then all of the blocks passed to the object using the #errback # method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks # passed to the object using # callback will be discarded. # # If you pass any arguments to #set_deferred_status in addition to the status argument, # they will be passed as arguments to any callbacks or errbacks that are executed. # It's your responsibility to ensure that the argument lists specified in your callbacks and # errbacks match the arguments given in calls to #set_deferred_status, otherwise Ruby will raise # an ArgumentError. # # -- # We're shifting callbacks off and discarding them as we execute them. # This is valid because by definition callbacks are executed no more than # once. It also has the magic effect of permitting recursive calls, which # means that a callback can call #set_deferred_status and change the parameters # that will be sent to subsequent callbacks down the chain. # # Changed @callbacks and @errbacks from push/shift to unshift/pop, per suggestion # by Kirk Haines, to work around the memory leak bug that still exists in many Ruby # versions. # # Changed 15Sep07: after processing callbacks or errbacks, CLEAR the other set of # handlers. This gets us a little closer to the behavior of Twisted's "deferred," # which only allows status to be set once. Prior to making this change, it was possible # to "succeed" a Deferrable (triggering its callbacks), and then immediately "fail" it, # triggering its errbacks! That is clearly undesirable, but it's just as undesirable # to raise an exception is status is set more than once on a Deferrable. The latter # behavior would invalidate the idiom of resetting arguments by setting status from # within a callback or errback, but more seriously it would cause spurious errors # if a Deferrable was timed out and then an attempt was made to succeed it. See the # comments under the new method #timeout. # def set_deferred_status status, *args cancel_timeout @deferred_status = status @deferred_args = args case @deferred_status when :succeeded if @callbacks while cb = @callbacks.pop cb.call(*@deferred_args) end end @errbacks.clear if @errbacks when :failed if @errbacks while eb = @errbacks.pop eb.call(*@deferred_args) end end @callbacks.clear if @callbacks end end # Setting a timeout on a Deferrable causes it to go into the failed state after # the Timeout expires (passing no arguments to the object's errbacks). # Setting the status at any time prior to a call to the expiration of the timeout # will cause the timer to be cancelled. #-- # # def timeout seconds cancel_timeout me = self @deferred_timeout = EventMachine::Timer.new(seconds) {me.fail} end # Cancels an outstanding timeout if any. Undoes the action of #timeout. # # def cancel_timeout if @deferred_timeout @deferred_timeout.cancel @deferred_timeout = nil end end # Equivalent to set_deferred_status(:succeeded, ...) # def set_deferred_success *args set_deferred_status :succeeded, *args end # Equivalent to set_deferred_status(:failed, ...) # def set_deferred_failure *args set_deferred_status :failed, *args end # And still more sugar # def succeed *args set_deferred_success(*args) end # Can't get enough sugar # def fail *args set_deferred_failure(*args) end end # DefaultDeferrable is an otherwise empty class that includes Deferrable. # This is very useful when you just need to return a Deferrable object # as a way of communicating deferred status to some other part of a program. # class DefaultDeferrable include Deferrable end end