IT'S ALIVE

This commit is contained in:
Joe Cheng
2012-06-21 18:13:55 -07:00
parent 4086392236
commit af29721f5b
10 changed files with 9735 additions and 26 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
vendor/ruby
\.bundle/

View File

@@ -1,5 +1,5 @@
source 'https://rubygems.org'
gem 'em-websocket'
gem 'rack'
gem 'webrick'
gem 'eventmachine_httpserver'
gem 'json'

View File

@@ -6,13 +6,13 @@ GEM
addressable (>= 2.1.1)
eventmachine (>= 0.12.9)
eventmachine (0.12.10)
rack (1.4.1)
webrick (1.3.1)
eventmachine_httpserver (0.2.1)
json (1.7.3)
PLATFORMS
ruby
DEPENDENCIES
em-websocket
rack
webrick
eventmachine_httpserver
json

View File

@@ -2,14 +2,22 @@ require './react'
include React
def print_observable_value(obsVal)
Observer.new { puts obsVal.value }
end
sess = Session.new
sess.set('user', '')
user = ObservableValue.new { sess.get('user') }
upUser = ObservableValue.new { user.get.upcase }
Observer.new { puts upUser.get }
user_caps = ObservableValue.new { user.value.upcase }
# This will print the value not just once, but every
# time the value changes
print_observable_value(user_caps)
sess.set('user', 'jcheng')
Context.flush
Context.flush # pay no attention to the man behind the curtain
sess.set('user', 'jjallaire')
sess.set('user', 'jwpaulson')
Context.flush

View File

@@ -124,7 +124,7 @@ module React
update_value
end
def get
def value
cur_ctx = React::Context.current!
@dependencies[cur_ctx.id] = cur_ctx
cur_ctx.on_invalidate do

View File

@@ -1,22 +1,17 @@
require 'em-websocket'
require 'rack'
require './shiny'
require 'digest/sha1'
require 'digest/md5'
class RapportApp
shinyapp = ShinyApp.new
# Rack entry point
def call(env)
return [
200,
{'Content-Type' => 'text/html'},
["Hi"]
]
end
input1 = React::ObservableValue.new { shinyapp.session.get('input1') }
shinyapp.define_output('md5_hash') do
Digest::MD5.hexdigest(input1.value)
end
rapp = RapportApp.new
shinyapp.define_output('sha1_hash') do
Digest::SHA1.hexdigest(input1.value)
end
Rack::Server.new(
:app => rapp,
:Port => 8113,
:server => 'webrick').start
shinyapp.run

140
shiny.rb Normal file
View File

@@ -0,0 +1,140 @@
require 'eventmachine'
require 'evma_httpserver'
require 'em-websocket'
require 'pathname'
require 'json'
require './react'
class WebServer < EM::Connection
include EM::HttpServer
def post_init
super
no_environment_strings
@basepath = File.join(Dir.pwd, 'www')
end
def resolve_path(path)
# It's not a valid path if it doesn't start with /
return nil if path !~ /^\//
abspath = File.join(@basepath, "./#{path}")
# Resolves '..', etc.
abspath = Pathname.new(abspath).cleanpath.to_s
return false if abspath[0...(@basepath.size + 1)] != @basepath + '/'
return false if !File.exist?(abspath)
return abspath
end
def process_http_request
# the http request details are available via the following instance variables:
# @http_protocol
# @http_request_method
# @http_cookie
# @http_if_none_match
# @http_content_type
# @http_path_info
# @http_request_uri
# @http_query_string
# @http_post_content
# @http_headers
response = EM::DelegatedHttpResponse.new(self)
path = @http_path_info
path = '/index.html' if path == '/'
resolved_path = resolve_path(path)
if !resolved_path
response.status = 404
response.content_type 'text/html'
response.content = '<h1>404 Not Found</h1>'
else
response.status = 200
response.content_type case resolved_path
when /\.html?$/
'text/html'
when /\.js$/
'text/javascript'
when /\.png$/
'image/png'
when /\.jpg$/
'image/jpeg'
when /\.gif$/
'image/gif'
end
response.content = File.read(resolved_path)
end
response.send_response
end
end
def run_shiny_app(shinyapp)
EventMachine.run do
EventMachine.start_server '0.0.0.0', 8100, WebServer
EventMachine::WebSocket.start(:host => '0.0.0.0', :port => 8101) do |ws|
shinyapp.websocket = ws
ws.onclose { exit(0) }
ws.onmessage do |msg|
puts "RECV: #{msg}"
msg_obj = JSON.parse(msg)
case msg_obj['method']
when 'init'
msg_obj['data'].each do |k, v|
shinyapp.session.set(k, v)
end
React::Context.flush
shinyapp.instantiate_outputs
when 'update'
msg_obj['data'].each do |k, v|
shinyapp.session.set(k, v)
end
end
React::Context.flush
end
end
end
end
class ShinyApp
attr_reader :session
def initialize
@session = React::Session.new
@outputs = {}
end
def websocket=(value)
@websocket = value
end
def define_output(name, &proc)
@outputs[name] = proc
end
def instantiate_outputs
@outputs.keys.each do |name|
proc = @outputs.delete(name)
React::Observer.new do
value = proc.call
msg = {}
msg[name] = value
puts "SEND: #{JSON.generate(msg)}"
@websocket.send(JSON.generate(msg))
end
end
end
def run
run_shiny_app self
end
end

30
www/index.html Normal file
View File

@@ -0,0 +1,30 @@
<html>
<head>
<script src="jquery-1.7.2.js" type="text/javascript"></script>
<script src="shiny.js" type="text/javascript"></script>
<style type="text/css">
body.disconnected {
background-color: #999;
opacity: 0.5;
}
</style>
</head>
<body>
<h1>Example 1: Echo</h1>
<p>
<label>Input:</label><br />
<input name="input1" value="Hello World!"/>
</p>
<p>
<label>MD5:</label><br />
<pre id="md5_hash" class="live-text"></pre>
</p>
<p>
<label>SHA-1:</label><br />
<pre id="sha1_hash" class="live-text"></pre>
</p>
</body>
</html>

9404
www/jquery-1.7.2.js vendored Normal file

File diff suppressed because it is too large Load Diff

131
www/shiny.js Normal file
View File

@@ -0,0 +1,131 @@
(function() {
var $ = jQuery;
var ShinyApp = function() {
this.$socket = null;
this.$bindings = {};
this.$values = {};
this.$pendingMessages = [];
};
(function() {
this.connect = function(initialInput) {
if (this.$socket)
throw "Connect was already called on this application object";
this.$socket = this.createSocket();
this.$initialInput = initialInput;
};
this.createSocket = function () {
var self = this;
var socket = new WebSocket('ws://' + window.location.hostname + ':8101/events');
socket.onopen = function() {
console.log('connected to websocket');
socket.send(JSON.stringify({
method: 'init',
data: self.$initialInput
}))
while (self.$pendingMessages.length) {
var msg = self.$pendingMessages.shift();
socket.send(msg);
}
};
socket.onmessage = function(e) {
self.dispatchMessage(e.data);
};
socket.onclose = function() {
$(document.body).addClass('disconnected');
};
return socket;
};
this.sendInput = function(values) {
var msg = JSON.stringify({
method: 'update',
data: values
});
if (this.$socket.readyState == WebSocket.CONNECTING) {
this.$pendingMessages.push(msg);
}
else {
this.$socket.send(msg);
}
};
this.receiveOutput = function(name, value) {
var oldValue = this.$values[name];
this.$values[name] = value;
if (oldValue === value)
return;
var binding = this.$bindings[name];
if (binding) {
binding.onValueChange(value);
}
return value;
};
this.dispatchMessage = function(msg) {
var msgObj = JSON.parse(msg);
for (key in msgObj) {
this.receiveOutput(key, msgObj[key]);
}
};
this.bind = function(id, binding) {
if (!id)
throw "Can't bind an element with no ID";
if (this.$bindings[id])
throw "Duplicate binding for ID " + id;
this.$bindings[id] = binding;
return binding;
};
}).call(ShinyApp.prototype);
var LiveTextBinding = function(el) {
this.el = el;
};
(function() {
this.onValueChange = function(data) {
$(this.el).text(data);
};
}).call(LiveTextBinding.prototype);
$(function() {
var shinyapp = window.shinyapp = new ShinyApp();
$('.live-text').each(function() {
shinyapp.bind(this.id, new LiveTextBinding(this));
});
var initialValues = {};
$('input').each(function() {
var input = this;
var name = input.name;
var value = $(input).val();
// TODO: validate name is non-blank, and no duplicates
// TODO: If submit button is present, don't send anything
// until submit button is pressed
initialValues[name] = value;
$(input).keyup(function() {
var data = {};
data[name] = $(input).val();
shinyapp.sendInput(data);
});
});
shinyapp.connect(initialValues);
});
})();