rubymotion/BubbleWrap

View on GitHub
motion/reactor/deferrable.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module BubbleWrap
  module Reactor
    # Provides a mixin for deferrable jobs.
    module Deferrable

      # def self.included(base)
      #   base.extend ::BubbleWrap::Reactor::Future
      # end

      # 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.
      def callback(&blk)
        return unless blk
        @deferred_status ||= :unknown
        if @deferred_status == :succeeded
          execute_block(&blk)
        elsif @deferred_status != :failed
          @callbacks ||= []
          @callbacks.unshift blk
        end
      end

      # Cancels an outstanding timeout if any. Undoes the action of timeout.
      def cancel_timeout
        @deferred_timeout ||= nil
        if @deferred_timeout
          @deferred_timeout.cancel
          @deferred_timeout = nil
        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.
      def errback(&blk)
        return unless blk
        @deferred_status ||= :unknown
        if @deferred_status == :failed
          execute_block(&blk)
          blk.call(*@deferred_args)
        elsif @deferred_status != :succeeded
          @errbacks ||= []
          @errbacks.unshift blk
        end
      end

      def execute_block(&blk)
        return unless blk
        blk.call(*@deferred_args)
      end

      def delegate(delegate)
        callback_delegate(delegate)
        errback_delegate(delegate)
        self
      end

      def errback_delegate(delegate)
        errback do |*args|
          delegate.fail *args
        end
        self
      end

      def callback_delegate(delegate)
        callback do |*args|
          delegate.succeed *args
        end
        self
      end

      # Sugar for set_deferred_status(:failed, …)
      def fail(*args)
        set_deferred_status :failed, *args
      end
      alias set_deferred_failure fail

      # 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.
      def set_deferred_status(status, *args)
        cancel_timeout
        @errbacks ||= nil
        @callbacks ||= nil
        @deferred_status = status
        @deferred_args = args
        case @deferred_status
        when :succeeded
          if @callbacks
            while cb = @callbacks.pop
              execute_block(&cb)
            end
          end
          @errbacks.clear if @errbacks
        when :failed
          if @errbacks
            while eb = @errbacks.pop
              execute_block(&eb)
            end
          end
          @callbacks.clear if @callbacks
        end
      end

      # Sugar for set_deferred_status(:succeeded, …)
      def succeed(*args)
        set_deferred_status :succeeded, *args
      end
      alias set_deferred_success succeed

      # 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 = Timer.new(seconds) {me.fail}
      end

      def deferred_status
        @deferred_status ||= :unknown
      end

      def deferred_args
        @deferred_args
      end

    end
  end
end