lib/volt/reactive/computation.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'set'

module Volt
  class Computation
    @@current     = nil
    @@flush_queue = Set.new

    def self.current=(val)
      @@current = val
    end

    def self.current
      @@current
    end

    # @param [Proc] the code to run when the computation needs to compute
    def initialize(computation)
      @computation   = computation
      @invalidations = []
    end

    # Runs the computation, called on initial run and
    # when changed!
    def compute!(initial_run=false)
      @invalidated = false

      unless @stopped

        @computing = true
        begin
          run_in do
            if @computation.arity > 0
              # Pass in the Computation so it can be canceled from within
              @computation.call(self)
            else
              @computation.call
            end
          end
        rescue => e
          if initial_run
            # Re-raise if we are in the initial run
            raise
          else
            # Sometimes we get nil as the exception, not sure if thats an opal
            # issue or what.
            if e
              msg = "Exception During Compute: " + e.inspect
              msg += "\n" + e.backtrace.join("\n") if e.respond_to?(:backtrace)
              Volt.logger.error(msg)

              if RUBY_PLATFORM == 'opal'
                `console.log(e);`
              end
            end
          end
        ensure
          @computing = false
        end
      end
    end

    def on_invalidate(&callback)
      if @invalidated
        # Call invalidate now, since its already invalidated
        # Computation.run_without_tracking do
        queue_flush!
        callback.call
        # end
      else
        # Store the invalidation
        @invalidations << callback
      end
    end

    # Calling invalidate removes the computation from all of
    # its dependencies.  This keeps its dependencies from
    # invalidating it again.
    def invalidate!
      unless @invalidated
        @invalidated = true

        queue_flush! unless @stopped

        invalidations  = @invalidations
        @invalidations = []

        invalidations.each(&:call)
      end
    end

    # Stop re-run of the computations
    def stop
      unless @stopped
        @stopped = true
        invalidate!
      end
    end

    def stopped?
      @stopped
    end

    # Runs in this computation as the current computation, returns the computation
    def run_in
      previous            = Computation.current
      Computation.current = self
      begin
        yield
      ensure
        Computation.current = previous
      end

      self
    end

    # Run a block without tracking any dependencies
    def self.run_without_tracking
      previous            = Computation.current
      Computation.current = nil
      begin
        return_value        = yield
      ensure
        Computation.current = previous
      end
      return_value
    end

    def self.flush!
      fail "Can't flush while in a flush" if @flushing

      @flushing = true
      # clear any timers
      @@timer    = nil

      computations  = @@flush_queue
      @@flush_queue = Set.new

      computations.each(&:compute!)

      @flushing = false
    end

    def queue_flush!
      @@flush_queue << self

      # If we are in the browser, we queue a flush for the next tick
      # If we are not in the browser, the user must manually flush
      if Volt.in_browser?
        unless @@timer
          # Flush once everything else has finished running
          @@timer = `setImmediate(function() { self.$class()['$flush!'](); })`
        end
      end
    end
  end
end

class Proc
  def watch!
    computation = Volt::Computation.new(self)

    # Initial run
    computation.compute!(true)

    # return the computation
    computation
  end

  # Watches a proc until the value returned equals the passed
  # in value.  When the value matches, the block is called.
  #
  # @param the value to match
  # @return [Volt::Computation] the initial computation is returned.
  def watch_until!(value, &block)
    computation = proc do |comp|
      # First fetch the value
      result = call

      if result == value
        # Values match

        # call the block
        Volt::Computation.run_without_tracking do
          block.call
        end

        # stop the computation
        comp.stop
      end
    end.watch!

    computation
  end

  # Does an watch and if the result is a promise, resolves the promise.
  # #watch_and_resolve! takes two procs, one for the promise resolution (then), and
  # one for promise rejection (fail).
  #
  # Example:
  #   -> { }
  def watch_and_resolve!(success, failure=nil, yield_nil_for_unresolved_promise=false)
    # Keep results between runs
    result = nil

    computation = proc do |comp|
      result = call
      last_promise = nil

      if result.is_a?(Promise)
        last_promise = result

        # Often you want a to be alerted that an unresolved promise is waiting
        # to be resolved.
        if yield_nil_for_unresolved_promise && !result.resolved?
          success.call(nil)
        end

        # The handler gets called once the promise resolves or is rejected.
        handler = lambda do |&after_handle|
          # Check to make sure that a new value didn't get reactively pushed
          # before the promise resolved.
          if last_promise.is_a?(Promise) && last_promise == result
            # Don't resolve if the computation was stopped
            unless comp.stopped?
              # Call the passed in proc
              after_handle.call
            end

            # Clear result for GC
            result = nil
          end

        end

        result.then do |final|
          # Call the success proc passing in the resolved value
          handler.call { success.call(final) }
        end.fail do |err|
          # call the fail callback, passing in the error
          handler.call { failure.call(err) if failure }
        end
      else
        success.call(result)

        # Clear result for GC
        result = nil
      end
    end.watch!

    # Return the computation
    computation
  end
end