qqshfox/alo7-net

View on GitHub
lib/alo7/net.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'fiber'
require 'eventmachine'
require 'em-synchrony'
require 'alo7/net/version'
require 'alo7/net/connection'
require 'alo7/net/server'
require 'alo7/net/client'
require 'alo7/net/error'
require 'alo7/net/defer'

module Alo7
  module Net
    # Run an event loop in the fiber way.
    #
    # @overload run(block)
    # @overload run(&block)
    #
    # @param block [MethodObject] the block to run
    # @return [void]
    #
    # @example Starting an event loop in the current thread to run the "Hello, world"-like Echo server example
    #
    #     !!!ruby
    #     require 'alo7-net'
    #
    #     class EchoServer < Alo7::Net::Server
    #       def receive_data(data)
    #         send_data data
    #       end
    #     end
    #
    #     Alo7::Net.run do
    #       EchoServer.listen 3000
    #     end
    #
    # @note This method blocks the calling thread.
    # @note This method only returns if the event loop stopped.
    #
    # @see stop
    def self.run(blk = nil, &block)
      EM.synchrony(blk || block)
    end

    # Run an event loop and then terminate the loop as soon as the block completes.
    #
    # @param block [MethodObject] the block to run once
    # @return [void]
    def self.run_once(&block)
      run do
        block.call
        stop
      end
    end

    # Stop the running event loop.
    #
    # @return [void]
    #
    # @example Stopping a running event loop.
    #
    #     !!!ruby
    #     require 'alo7-net'
    #
    #     class OnceClient < Alo7::Net::Client
    #       def post_init
    #         puts 'sending a dump HTTP request'
    #         send_data "GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n"
    #       end
    #
    #       def receive_data(data)
    #         puts "data.length: #{data.length}"
    #         puts 'stopping the event loop now'
    #         Net.stop
    #       end
    #     end
    #
    #     Alo7::Net.run do
    #       OnceClient.connect 'www.google.com', 80
    #     end
    #
    # @see run
    def self.stop
      EM.stop_event_loop
    end

    # Initiate a TCP server on the specified IP address and port.
    #
    # @overload listen(handler, host, port, *args)
    #   @param handler [ClassObject] a module or class that has an innerclass named `Impl`
    #   @param host [String] host to listen on
    #   @param port [Integer] port to listen on
    #   @param *args passed to the initializer of the handler
    # @overload listen(handler, port, *args)
    #   @param handler [ClassObject] a module or class that has an innerclass named `Impl`
    #   @param port [Integer] port to listen on
    #   @param *args passed to the initializer of the handler
    # @yield [handler] initiated when a connection is made
    # @return [Integer] the internal signature
    #
    # @raise (see check_handler!)
    #
    # @see connect
    def self.listen(handler, host_or_port, port = nil, *args)
      check_handler! handler
      if port
        host = host_or_port
      else
        host = 'localhost'
        port = host_or_port
      end
      EM.start_server host, port, handler::Impl, handler, *args do |server|
        yield server.handler if block_given?
      end
    end

    # Initiate a TCP connection to a remote server and set up event handling for
    # the connection.
    #
    # @param handler [ClassObject] a module or class that has an innerclass named `Impl`
    # @param host [String] host to connect to
    # @param port [Integer] port to connect to
    # @return [handler] the initiated handler instance
    #
    # @raise (see check_handler!)
    #
    # @note This requires event loop to be running. (see {run}).
    #
    # @see listen
    # @see reconnect
    def self.connect(handler, host, port, *args)
      check_handler! handler
      connection = EM.connect host, port, handler::Impl, handler, *args
      connection.handler
    end

    # Connect to a given host/port and re-use the provided handler instance.
    #
    # @param handler [#impl] a instance that has a property named `impl`
    # @param host [String] host to reconnect to
    # @param port [Integer] port to reconnect to
    # @return [handler] the previous handler instance
    #
    # @raise [ArgumentError] if the handler doesn't implement a method named impl
    #
    # @see connect
    # @see listen
    def self.reconnect(handler, host, port)
      raise ArgumentError, 'must provide a impl property' unless handler.respond_to? :impl
      connection = EM.reconnect host, port, handler.impl
      connection.handler
    end

    # Wait the defer succeed and then return anything the defer returns. Or
    # raise the exception from the defer.
    #
    # @param defer [Defer] defer to wait
    # @return anything the defer returns
    #
    # @raise [Exception] the exception returns from defer
    def self.await(defer)
      result = EM::Synchrony.sync defer
      raise result if result.is_a? Exception
      result
    end

    # Execute the block in a fiber.
    #
    # @param args passed as arguments to the block that are executed
    # @yieldparam *args arguments passed from outside
    # @yieldreturn [Object] anything
    # @return [Object] anything returned from the block
    def self.fiber_block(*args)
      Fiber.new { yield(*args) }.resume
    end

    class << self
      private

      # @raise [ArgumentError] if the handler doesn't have a innerclass named Impl
      def check_handler!(handler)
        raise ArgumentError, 'must provide a innerclass of Impl' unless defined? handler::Impl
      end
    end
  end
end