-
Notifications
You must be signed in to change notification settings - Fork 18
Combining Operations and Stores
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