Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

Combining Operations and Stores

Mitch VanDuyn edited this page May 8, 2017 · 5 revisions

The HyperStore wiki page presented an example class called GitHubUserStream

Here is that class:

class GitHubUserStream < HyperLoop::Store
  # This store can have many instances.
  # Each instance provides a stream of unique GitHub user profiles

  # Each instance has a single HyperStore state variable called user
  # user will contain a single hash representing the user profile

  def reload!
    # Reload the state variable. 
    # Note that the syntax is just like in a React::Component
    mutate.user GitHubUserStream.select_random_user
  end

  # extract various attributes from the user hash

  def user_name
    user[:login]
  end

  def user_url
    user[:html_url]
  end

  def avatar
    user[:avatar_url]
  end

  def initialize
    reload!
  end

  def self.select_random_user

    # select_random_user provides a stream of unique user profiles.
    # It will either return a user profile hash, or a promise of one
    # to come.

    return @users.delete_at(rand(@users.length)) unless @users.blank?
    @promise = HTTP.get("https://api.github.com/users?since=#{rand(500)}").then do |response|
      @users = response.json
    end if @promise.nil? || @promise.resolved?
    @promise.then { select_random_user }
  end

  private_state :user

end

Perhaps select_random_user might be better as an Operation.

  • it's a class method (which operations are)
  • it encapsulates access to an outside resource
  • it can return a promise which operations always do

Lets refactor to use an Operation and while we are at it, we will make the class more generic by passing the Operation at initialization and giving the accessor methods generic names. It will be up to the Operation to deliver a hash that has the hash keys we desire.

class UserIconStream < Hyperloop::Store
  # Create a new Icon Stream by passing an Operation that
  # returns a hash with the keys :name, :avatar and :website.
  def initialize(select_random_user_operation)
    @select_random_user = select_random_user_operation
    reload!
  end

  def reload!
    mutate.user @select_random_user.call
  end

  [:name, :avatar, :website].each do |method|
    define_method(method) { state.user[method] }
  end
end

So now we need some Operations that return new unique users.

First, we will define an Operation that pulls users from GitHub. Except for reformatting the attributes the code is the same as the original select_random_user.

class GetRandomGithubUser < UserIconStream::GetRandomUser
  def self.execute
    return @users.delete_at(rand(@users.length)) unless @users.blank?
    @promise = HTTP.get("https://api.github.com/users?since=#{rand(500)}").then do |response|
      @users = response.json.collect do |user| 
        { name: user[:login], website: user[:html_url], avatar: user[:avatar_url] }
      end
    end if @promise.nil? || @promise.resolved?
    @promise.then { execute }
  end
end

Now let's get a random twitter user. The easiest way to do this is to use the secure Twitter sample stream. Because it's secure it must run on the server and here is where the power of Hyperloop's isomorphic Operations shine. All that we need to do is allow the client to invoke the Operation remotely. This is done using the allow_remote_operation method which can be called within the class definition, or in a common Policy file.

class GetRandomTwitterUser < Hyperloop::Operation
  # by default allow any client to access
  # this can be changed in a Policy file if needed
  allow_remote_operation 
  def self.client
    @client ||= Twitter::Streaming::Client.new do |config|
      config.consumer_key        = .. from env variables / yaml / etc ..
      config.consumer_secret     = 
      config.access_token        = 
      config.access_token_secret = 
    end
  end

  def execute
    GetRandomTwitterUser.client.sample do |tweet|
      # note that not all sampled tweets have users
      return {
        html_url: tweet.user.website? ? tweet.user.website.to_s : '', 
        avatar_url: tweet.user.profile_image_url.to_s, 
        login: tweet.user.screen_name
      } if tweet.respond_to? :user
    end
  end
end

Of course with this implementation, we are going to make a remote operation call for each new twitter user. It's simple to restructure the code to bring over a batch to the client if we want to do that:

class GetRandomTwitterUser < Hyperloop::Operation
  def self.execute
    return @users.delete_at(rand(@users.length)) unless @users.blank?
    @promise = GetRandomTwitterUsers.then do |users|
      @users = users
    end if @promise.nil? || @promise.resolved?
    @promise.then { execute }
  end
end

class GetRandomTwitterUsers < Hyperloop::Operation
  allow_remote_operation # by default allow any client to access
  def self.client
    # etc
  end

  def execute
    users = []
    GetRandomTwitterUsers.client.sample do |tweet|
      users << {
        html_url: tweet.user.website? ? tweet.user.website.to_s : '', 
        avatar_url: tweet.user.profile_image_url.to_s, 
        login: tweet.user.screen_name
      } if tweet.respond_to? :user
      return users if users.count == 100
    end
  end
end

Finally, we will do a little more refactoring while we are at it. Notice that both of our GetxxUser Operations have exactly the same structure in the execute method. For convenience we will have the GetUserIconStream class supply a generic class any operation can inherit from.

class UserIconStream < HyperStore::Base
  class GetRandomUserOperation < Hyperloop::Operation
    def self.get_more_users
      raise "must implement!"
    end
    def self.execute
      return @users.delete_at(rand(@users.length)) unless @users.blank?
        @promise = get_more_users.then do |users|
          @users = users
        end if @promise.nil? || @promise.resolved?
        @promise.then { execute }
      end 
    end
  end
end

Now our GetRandomGithubUser class looks like this

class GetRandomGithubUser < UserIconStream::GetRandomUserOperation
  def self.get_more_users
    HTTP.get("https://api.github.com/users?since=#{rand(500)}").then do |response|
      response.json.collect do |user| 
        { name: user[:login], website: user[:html_url], avatar: user[:avatar_url] }
      end
    end
  end
end

and our GetRandomTwitterUser class looks like this

class GetRandomTwitterUser < UserIconStream::GetRandomUserOperation
  def self.get_more_users
    GetRandomTwitterUsers()
  end
end
Clone this wiki locally