mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-09 15:08:04 -05:00
IT'S ALIVE
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
vendor/ruby
|
||||
\.bundle/
|
||||
4
Gemfile
4
Gemfile
@@ -1,5 +1,5 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'em-websocket'
|
||||
gem 'rack'
|
||||
gem 'webrick'
|
||||
gem 'eventmachine_httpserver'
|
||||
gem 'json'
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
react.rb
2
react.rb
@@ -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
|
||||
|
||||
27
server.rb
27
server.rb
@@ -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
140
shiny.rb
Normal 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
30
www/index.html
Normal 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
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
131
www/shiny.js
Normal 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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user