Véase
Este código se encuentra en: https://github.com/rkh/presentations/blob/realtime-rack/example.rb
El javascript y el server se activan desde las trasparencias. En concreto en esta trasparencia
que está en el fichero
slides/01_slides.md:
!SLIDE center # Demo! # <iframe src="/events?" width="980" height="600"></iframe> .notes Next: RackThe
<iframe> tag specifies an inline frame.
An inline frame is used to embed another document within the current HTML document.
src="/events?" hace que se dispare el código correspondiente a la ruta
/events descrita en el fichero example.rb.
La ruta /events
está en el fichero example.rb:
get('/events') { slim :html }
El fichero example.rb es cargado desde el config.ru:
[~/local/src/ruby/sinatra/sinatra-streaming/konstantin_haase/presentations(realtime-rack)]$ cat config.ru require 'showoff' require './example' use Example run ShowOffObsérvese que es cargado por
config.ru mediante use.
El template html contiene:
@@ html
html
head
title brirb
link href="/events.css" rel="stylesheet" type="text/css"
script src="/jquery.min.js" type="text/javascript"
script src="/events.js" type="text/javascript"
body
#log
form#form
| >>
input#input type='text'
Como vemos tenemos identificadores log, form y input para hablar
de los correspondientes elementos implicados.
La carga de /events.js es también manejado por una ruta:
get('/events.js') { coffee :script }
La gema
coffee-script
provee el mecanismo para compilar el javascript y producir el correspondiente
código JavaScript.
Este es el CoffeeScript contenido en
el template script:
$(document).ready ->
input = $("#input")
log = $("#log")
history = []
count = 0
output = (str) ->
log.append str
log.append "<br>"
input.attr scrollTop: input.attr("scrollHeight")
input.bind "keydown", (e) ->
if e.keyCode == 38 or e.keyCode == 40
count += e.keyCode - 39
count = 0 if count < 0
count = input.length + 1 if count > input.length
input.val history[count]
false
else
true
$("#form").live "submit", (e) ->
value = input.val()
history.push value
count++
$.post '/run', code: input.val()
output ">> #{value}"
input.val ""
input.focus()
e.preventDefault()
src = new EventSource('/events.es')
src.onmessage = (e) -> output e.data
output(str) añade en el punto indicado por
#log el texto str. Además se encarga del scrolling:
output = (str) ->
log.append str
log.append "<br>"
input.attr scrollTop: input.attr("scrollHeight")
.bind( eventType [, eventData ], handler(eventObject) )
Attaches a handler to an event for the elements.
En este caso eventType es keydown.
src = new EventSource('/events.es')
Hace que nos suscribamos a los mensajes generados por
/events.es
output:
src.onmessage = (e) -> output e.data
count es 1 o -1.
Parece que navegamos en el histórico de comandos de esta forma:
input.bind "keydown", (e) ->
if e.keyCode == 38 or e.keyCode == 40
count += e.keyCode - 39
count = 0 if count < 0
count = input.length + 1 if count > input.length
input.val history[count]
false
else
true
$.post '/run', code: input.val() enviamos al servidor la petición de que evalúe la entrada:
$("#form").live "submit", (e) ->
value = input.val()
history.push value
count++
$.post '/run', code: input.val()
output ">> #{value}"
input.val ""
input.focus()
e.preventDefault()
La petición es recibida en la correspondiente ruta
post '/run' do
begin
result = nil
stdout = capture_stdout do
result = eval("_ = (#{params[:code]})", settings.scope, "(irb)", settings.line)
settings.line += 1
end
stdout << "=> " << result.inspect
rescue Exception => e
stdout = [e.to_s, *e.backtrace.map { |l| "\t#{l}" }].join("\n")
end
source = escape stdout
Scope.send source
''
end
eval tiene estos argumentos:
eval(string [, binding [, filename [,lineno]]])
binding is
a Binding object: the evaluation is performed in its
context.
filename and lineno are
used when reporting syntax errors.
capture_stdout nos permite capturar la salida por stdout de
una evaluación:
[~/Chapter6MethodsProcsLambdasAndClosures]$ pry
[1] pry(main)> require 'capture_stdout'
=> true
[2] pry(main)> string = 'yeah'
=> "yeah"
[3] pry(main)> output = capture_stdout { print(string) }
=> "yeah"
Scope se define el método Scope.send el cual envía a todos los
clientes el mensaje especificado:
module Scope
def self.send(*args)
Example.subscribers.each { |s| s.send(*args) }
end
def self.puts(*args)
args.each { |str| send str.to_s }
nil
end
def self.binding
Kernel.binding
end
end
send recorre el array subscribers
que es un array de objetos EventSource y delega en el método
send del subscriptor el envío de los datos en *args
binding
delega en el correspondiente método del Kernel.
El método es usado para guardar el binding
en la variable :scope en la clase Example:
class Example < Sinatra::Base
enable :inline_templates, :logging, :static
set :public, File.expand_path('../public', __FILE__)
set :subscribers => [], :scope => Scope.binding, :line => 1
y es posteriormente usado cuando se evalúa la expresión:
stdout = capture_stdout do
result = eval("_ = (#{params[:code]})", settings.scope, "(irb)", settings.line)
settings.line += 1 # número de línea
end
Example.suscribers es un array que es inicializado al comienzo de la clase
Examples:
class Example < Sinatra::Base
enable :inline_templates, :logging, :static
set :public_folder, File.expand_path('../public', __FILE__)
set :subscribers => [], :scope => Scope.binding, :line => 1
...
subscribers se actualiza en el código asociado con la ruta events.es
que es visitada - desde el código CoffeeScript - cada vez que se carga una nueva página:
$(document).ready ->
input = $("#input")
...
output = (str) ->
...
input.bind "keydown", (e) ->
...
$("#form").live "submit", (e) ->
...
src = new EventSource('/events.es')
src.onmessage = (e) -> output e.data
events.es:
get '/events.es' do
content_type request.preferred_type("text/event-stream", "text/plain")
body EventSource.new
settings.subscribers << body
EM.next_tick { env['async.callback'].call response.finish }
throw :async
end
each. Con la llamada body EventSource.new
establecemos el cuerpo de la respuesta. La definición de la clase EventSource
aparece en el item 13
(Object) next_tick(pr = nil, &block)Schedules a Proc for execution immediately after the next turn through the reactor core.
An advanced technique, this can be useful for improving memory management and/or application responsiveness, especially when scheduling large amounts of data for writing to a network connection.
This method takes either a single argument (which must be a callable object) or a block.
Parameters:
pr (#call) (defaults to: nil) — A callable object to runRaises:
(ArgumentError)
While there is not yet an async interface in the Rack specification, several Rack servers have implemented James Tucker's async scheme.
Rather than returning[status, headers, body], the app returns a status of-1, or throws the symbol:async.
The server providesenv['async.callback']which the app saves and later calls with the usual[status, headers, body]to send the response.
Note: returning a status of-1is illegal as far asRack::Lintis concerned.throw :asyncis not flagged as anerror.
class AsyncApp
def call(env)
Thread.new do
sleep 5 # simulate waiting for some event
response = [200, {'Content-Type' => 'text/plain'}, ['Hello, World!']]
env['async.callback'].call response
end
[-1, {}, []] # or throw :async
end
end
In the example above, the request is suspended, nothing is sent back to the client, the connection remains open, and the client waits for a response.
The app returns the special status, and the worker process is able to handle more HTTP requests (i.e. it is not blocked). Later, inside the thread, the full response is prepared and sent to the client.
There is another, more oft discussed means of achieving concurrency, involving EventMachine.defer and throw :async. Strictly speaking, requests are not handled using threads. They are dealt with serially, but pass their heavy lifting and a callback off to EventMachine, which uses async.callback to send a response at a later time. After request A has offloaded its work to EM.defer, request B is begun. Is this correct?
Respuesta de Konstantin:
Using async.callback in conjunction with EM.defer actually makes
not too much sense, as it would basically use the thread-pool,
too, ending up with a similar construct as described in Q1.
Usingasync.callbackmakes sense when only using eventmachine libraries for IO. Thin will send the response to the client onceenv['async.callback']is called with a normal Rack response as argument.
If the body is an EM::Deferrable, Thin will not close the
connection until that deferrable succeeds.
A rather well kept secret: If you want more than just long polling (i.e. keep the connection open after sending a partial response), you can also return anEM::Deferrableas body object directly without having to use throw:asyncor a status code of -1.
EventSource tiene métodos each y send:
class EventSource
include EventMachine::Deferrable
def send(data, id = nil)
data.each_line do |line|
line = "data: #{line.strip}\n"
@body_callback.call line
end
@body_callback.call "id: #{id}\n" if id
@body_callback.call "\n"
end
def each(&blk)
@body_callback = blk
end
end
[~/sinatra/sinatra-streaming/konstantin_haase/presentations(realtime-rack)]$ cat example.rb
require 'sinatra/base'
require 'capture_stdout'
require 'escape_utils'
require 'slim'
require 'sass'
require 'coffee-script'
require 'eventmachine'
class EventSource
include EventMachine::Deferrable
def send(data, id = nil)
data.each_line do |line|
line = "data: #{line.strip}\n"
@body_callback.call line
end
@body_callback.call "id: #{id}\n" if id
@body_callback.call "\n"
end
def each(&blk)
@body_callback = blk
end
end
module Scope
def self.send(*args)
Example.subscribers.each { |s| s.send(*args) }
end
def self.puts(*args)
args.each { |str| send str.to_s }
nil
end
def self.binding
Kernel.binding
end
end
class Example < Sinatra::Base
enable :inline_templates, :logging, :static
set :public_folder, File.expand_path('../public', __FILE__)
set :subscribers => [], :scope => Scope.binding, :line => 1
def escape(data)
EscapeUtils.escape_html(data).gsub("\n", "<br>").
gsub("\t", " ").gsub(" ", " ")
end
get '/events.es' do
content_type request.preferred_type("text/event-stream", "text/plain")
body EventSource.new
settings.subscribers << body
EM.next_tick { env['async.callback'].call response.finish }
throw :async
end
get('/events') { slim :html }
get('/events.js') { coffee :script }
get('/events.css') { sass :style }
post '/run' do
begin
result = nil
stdout = capture_stdout do
result = eval("_ = (#{params[:code]})", settings.scope, "(irb)", settings.line)
settings.line += 1
end
stdout << "=> " << result.inspect
rescue Exception => e
stdout = [e.to_s, *e.backtrace.map { |l| "\t#{l}" }].join("\n")
end
source = escape stdout
Scope.send source
''
end
end
__END__
@@ script
$(document).ready ->
input = $("#input")
log = $("#log")
history = []
count = 0
output = (str) ->
log.append str
log.append "<br>"
input.attr scrollTop: input.attr("scrollHeight")
input.bind "keydown", (e) ->
if e.keyCode == 38 or e.keyCode == 40
count += e.keyCode - 39
count = 0 if count < 0
count = input.length + 1 if count > input.length
input.val history[count]
false
else
true
$("#form").live "submit", (e) ->
value = input.val()
history.push value
count++
$.post '/run', code: input.val()
output ">> #{value}"
input.val ""
input.focus()
e.preventDefault()
src = new EventSource('/events.es')
src.onmessage = (e) -> output e.data
@@ html
html
head
title brirb
link href="/events.css" rel="stylesheet" type="text/css"
script src="/jquery.min.js" type="text/javascript"
script src="/events.js" type="text/javascript"
body
#log
form#form
| >>
input#input type='text'
@@ style
body
font:
size: 200%
family: monospace
input#input
font-size: 100%
font-family: monospace
border: none
padding: 0
margin: 0
width: 80%
&:focus
border: none
outline: none
[~/local/src/ruby/sinatra/sinatra-streaming/konstantin_haase/presentations(realtime-rack)]$ cat showoff.json
{
"name": "Real Time Rack",
"sections": [
{ "section": "intro" },
{ "section": "slides" },
{ "section": "outro" }
]
}
[~/local/src/ruby/sinatra/sinatra-streaming/konstantin_haase/presentations(realtime-rack)]$ cat slides/01_slides.md
!SLIDE bullets
* 
.notes Next: Warning
!SLIDE bullets incremental
# Warning
* There will be a lot of code ...
* A lot!
* Also, this is the *Special Extended Director's Cut*!
.notes Next: good old web
!SLIDE center

.notes Next: ajax
!SLIDE center

.notes Next: Comet
!SLIDE center

.notes Next: Real Time
!SLIDE bullets
* 
.notes Next: come again?
!SLIDE bullets incremental
# Come again? #
* streaming
* server push
.notes streaming, server push. --- Next: decide what to send while streaming, not upfront
!SLIDE bullets
* decide what to send while streaming, not upfront
.notes Next: usage example
!SLIDE bullets
* Streaming APIs
* Server-Sent Events
* Websockets
.notes Next: demo
!SLIDE center
# Demo! #
<iframe src="/events?" width="980" height="600"></iframe>
.notes Next: Rack
!SLIDE bullets incremental
# Rack #
* Ruby to HTTP to Ruby bridge
* Middleware API
* Powers Rails, Sinatra, Ramaze, ...
.notes HTTP bridge, middleware, frameworks. --- Next: rack stack
!SLIDE center

.notes Next: simple rack app
!SLIDE smallish


@@@ ruby
welcome_app = proc do |env|
[200, {'Content-Type' => 'text/html'},
['Welcome!']]
end
.notes Next: with any object
!SLIDE smallish


@@@ ruby
welcome_app = Object.new
def welcome_app.call(env)
[200, {'Content-Type' => 'text/html'},
['Welcome!']]
end
.notes Next: in sinatra
!SLIDE


@@@ ruby
get('/') { 'Welcome!' }
.notes Next: pseudo handler
!SLIDE smallish


@@@ ruby
env = parse_http
status, headers, body =
welcome_app.call env
io.puts "HTTP/1.1 #{status}"
headers.each { |k,v| io.puts "#{k}: #{v}" }
io.puts ""
body.each { |str| io.puts str }
close_connection
.notes Next: middleware
!SLIDE smallish
# Middleware #
.notes Next: upcase example
!SLIDE smallish


@@@ ruby
# foo => FOO
class UpperCase
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
upper = []
body.each { |s| upper << s.upcase }
[status, headers, upper]
end
end
.notes Next: config.ru
!SLIDE large


@@@ ruby
# set up middleware
use UpperCase
# set endpoint
run welcome_app
.notes Next: call app (from before)
!SLIDE


@@@ ruby
status, headers, body =
welcome_app.call(env)
.notes Next: wrap in middleware
!SLIDE smallish


@@@ ruby
app = UpperCase.new(welcome_app)
status, headers, body = app.call(env)
.notes Next: streaming with each
!SLIDE
# Streaming with #each #
.notes Next: custom body object
!SLIDE smallish


@@@ ruby
my_body = Object.new
get('/') { my_body }
def my_body.each
20.times do
yield "<p>%s</p>" % Time.now
sleep 1
end
end
.notes Next: Let's build a messaging service!
!SLIDE bullets
* Let's build a messaging service!
.notes Next: sinatra app
!SLIDE smallish


@@@ ruby
subscribers = []
get '/' do
body = Subscriber.new
subscribers << body
body
end
post '/' do
subscribers.each do |s|
s.send params[:message]
end
end
.notes Next: subscriber object
!SLIDE smallish


@@@ ruby
class Subscriber
def send(data)
@data = data
@thread.wakeup
end
def each
@thread = Thread.current
loop do
yield @data.to_s
sleep
end
end
end
.notes Next: issues with this
!SLIDE bullets incremental
* blocks the current thread
* does not work well with some middleware
* does not work (well) on evented servers <br> (Thin, Goliath, Ebb, Rainbows!)
.notes blocks, middleware, evented servers. --- Next: evented streaming
!SLIDE
# Evented streaming with async.callback #
.notes Next: event loop graphics
!SLIDE center

.notes Next: webscale
!SLIDE center

.notes Next: without eventloop
!SLIDE


@@@ ruby
sleep 10
puts "10 seconds are over"
puts Redis.new.get('foo')
.notes Next: with eventloop
!SLIDE smallish


@@@ ruby
require 'eventmachine'
EM.run do
EM.add_timer 10 do
puts "10 seconds are over"
end
redis = EM::Hiredis.connect
redis.get('foo').callback do |value|
puts value
end
end
.notes Next: async.callback
!SLIDE smallish


@@@ ruby
get '/' do
EM.add_timer(10) do
env['async.callback'].call [200,
{'Content-Type' => 'text/html'},
['sorry you had to wait']]
end
"dear server, I don't have a " \
"response yet, please wait 10 " \
"seconds, thank you!"
end
.notes Next: throw
!SLIDE smallish
# With #throw #


@@@ ruby
get '/' do
EM.add_timer(10) do
env['async.callback'].call [200,
{'Content-Type' => 'text/html'},
['sorry you had to wait']]
end
# will skip right to the handler
throw :async
end
.notes Next: -1
!SLIDE smallish
# Status Code #


@@@ ruby
get '/' do
EM.add_timer(10) do
env['async.callback'].call [200,
{'Content-Type' => 'text/html'},
['sorry you had to wait']]
end
# will go through middleware
[-1, {}, []]
end
.notes Next: async-sinatra
!SLIDE smallish


@@@ ruby
# gem install async-sinatra
require 'sinatra/async'
aget '/' do
EM.add_timer(10) do
body 'sorry you had to wait'
end
end
.notes Next: with redis
!SLIDE smallish


@@@ ruby
redis = EM::Hiredis.connect
aget '/' do
redis.get('foo').callback do |value|
body value
end
end
.notes Next: pseudo handler with callback
!SLIDE smallish


@@@ ruby
env = parse_http
cb = proc do |response|
send_headers(response)
response.last.each { |s| send_data(s) }
close_connection
end
catch(:async) do
env['async.callback'] = cb
response = app.call(env)
cb.call(response) unless response[0] == -1
end
.notes Next: postponing, not streaming
!SLIDE bullets incremental
* that's postponing ...
* ... not streaming
.notes Next: EM::Deferrable
!SLIDE
# EM::Deferrable #
.notes Next: Deferrable explained
!SLIDE smallish


@@@ ruby
require 'eventmachine'
class Foo
include EM::Deferrable
end
EM.run do
f = Foo.new
f.callback { puts "success!" }
f.errback { puts "something went wrong" }
f.succeed
end
.notes Next: pseudo handler - callback from before
!SLIDE smallish


@@@ ruby
cb = proc do |response|
send_headers(response)
response.last.each { |s| send_data(s) }
close_connection
end
.notes Next: pseudo handler - new callback
!SLIDE smallish


@@@ ruby
cb = proc do |response|
send_headers(response)
body = response.last
body.each { |s| send_data(s) }
if body.respond_to? :callback
body.callback { close_connection }
body.errback { close_connection }
else
close_connect
end
end
.notes Next: Evented Messaging System
!SLIDE
# Evented Messaging System #
.notes Next: old messaging system
!SLIDE smallish


@@@ ruby
# THIS IS NOT EVENTED
subscribers = []
get '/' do
body = Subscriber.new
subscribers << body
body
end
post '/' do
subscribers.each do |s|
s.send params[:message]
end
end
.notes Next: new messaging system (sinatra app)
!SLIDE smallish


@@@ ruby
subscribers = []
aget '/' do
body Subscriber.new
subscribers << body
end
post '/' do
subscribers.each do |s|
s.send params[:message]
end
end
.notes Next: new subscriber class
!SLIDE smallish


@@@ ruby
class Subscriber
include EM::Deferrable
def send(data)
@body_callback.call(data)
end
def each(&blk)
@body_callback = blk
end
end
.notes Next: callback again
!SLIDE smallish


@@@ ruby
cb = proc do |response|
send_headers(response)
body = response.last
body.each { |s| send_data(s) }
if body.respond_to? :callback
body.callback { close_connection }
body.errback { close_connection }
else
close_connect
end
end
.notes Next: new subscriber class (again)
!SLIDE smallish


@@@ ruby
class Subscriber
include EM::Deferrable
def send(data)
@body_callback.call(data)
end
def each(&blk)
@body_callback = blk
end
end
.notes Next: delete subscribers
!SLIDE smallish


@@@ ruby
delete '/' do
subscribers.each do |s|
s.send "Bye bye!"
s.succeed
end
subscribers.clear
end
.notes Next: Server-Sent Events
!SLIDE bullets
# Server-Sent Events #
* [dev.w3.org/html5/eventsource](http://dev.w3.org/html5/eventsource/)
.notes Next: explained
!SLIDE bullets incremental
* Think one-way WebSockets
* Simple
* Resumable
* Client can be implemented in JS
* Degrade gracefully to polling
.notes one-way WS, simple, resumable, client in JS, degrade --- Next: js code
!SLIDE smallish


@@@ javascript
var source = new EventSource('/updates');
source.onmessage = function (event) {
alert(event.data);
};
.notes Next: HTTP headers
!SLIDE
HTTP/1.1 200 OK
Content-Type: text/event-stream
.notes Next: HTTP headers + 1
!SLIDE
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: This is the first message.
.notes Next: HTTP headers + 2
!SLIDE
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: This is the first message.
data: This is the second message, it
data: has two lines.
.notes Next: HTTP headers + 3
!SLIDE
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: This is the first message.
data: This is the second message, it
data: has two lines.
data: This is the third message.
.notes Next: with IDs
!SLIDE
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: the client
id: 1
data: keeps track
id: 2
data: of the last id
id: 3
.notes Next: EventSource in Ruby
!SLIDE smallish


@@@ ruby
class EventSource
include EM::Deferrable
def send(data, id = nil)
data.each_line do |line|
line = "data: #{line.strip}\n"
@body_callback.call line
end
@body_callback.call "id: #{id}\n" if id
@body_callback.call "\n"
end
def each(&blk)
@body_callback = blk
end
end
.notes Next: WebSockets
!SLIDE bullets
# WebSockets #
* Think two-way EventSource
.notes Next: JS WebSockets
!SLIDE smallish


@@@ javascript
var src = new WebSocket('ws://127.0.0.1/');
src.onmessage = function (event) {
alert(event.data);
};
.notes Next: JS EventSource
!SLIDE smallish


@@@ javascript
var src = new EventSource('/updates');
src.onmessage = function (event) {
alert(event.data);
};
.notes Next: JS WebSocket
!SLIDE smallish


@@@ javascript
var src = new WebSocket('ws://127.0.0.1/');
src.onmessage = function (event) {
alert(event.data);
};
.notes Next: JS WebSocket with send
!SLIDE smallish


@@@ javascript
var src = new WebSocket('ws://127.0.0.1/');
src.onmessage = function (event) {
alert(event.data);
};
src.send("ok, let's go");
.notes Next: Ruby WebSocket
!SLIDE smallish


@@@ ruby
options = { host: '127.0.0.1', port: 8080 }
EM::WebSocket.start(options) do |ws|
ws.onmessage { |msg| ws.send msg }
end
.notes Next: WebSockets are hard to use
!SLIDE bullets incremental
# WebSockets are hard to use #
* Protocol upgrade (not vanilla HTTP)
* Specification in flux
* Client support incomplete
* Proxies/Load Balancers have issues
* Rack can't do it
.notes Protocol upgrade, in flux, client support, proxies, rack --- Next: sinatra streaming
!SLIDE bullets
# Sinatra Streaming API #
* introduced in Sinatra 1.3
.notes Next: example
!SLIDE smallish


@@@ ruby
get '/' do
stream do |out|
out << "It's gonna be legen -\n"
sleep 0.5
out << " (wait for it) \n"
sleep 1
out << "- dary!\n"
end
end
.notes Next: keep open
!SLIDE smallish


@@@ ruby
connections = []
get '/' do
# keep stream open
stream(:keep_open) do |out|
connections << out
end
end
post '/' do
# write to all open streams
connections.each do |out|
out << params[:message] << "\n"
end
"message sent"
end
.notes Next: sinatra chat
!SLIDE bullets
* Let's build a Chat!
* Code: [gist.github.com/1476463](https://gist.github.com/1476463)
* Demo: [sharp-night-9421.herokuapp.com](http://sharp-night-9421.herokuapp.com/)
.notes Next: go there now
!SLIDE bullets
* Yes, go there now!
* Here's the link again:<br>[**sharp-night-9421.herokuapp.com**](http://sharp-night-9421.herokuapp.com/)
* Yes, there is no CSS. Sorry.
.notes Next: demo
!SLIDE
## [**sharp-night-9421.herokuapp.com**](http://sharp-night-9421.herokuapp.com/)
<iframe src="http://sharp-night-9421.herokuapp.com/?showoff=1" width="980" height="600"></iframe>
.notes Next: code
!SLIDE small
## Ruby Code
@@@ ruby
set server: 'thin', connections: []
get '/stream', provides: 'text/event-stream' do
stream :keep_open do |out|
settings.connections << out
out.callback { settings.connections.delete(out) }
end
end
post '/' do
settings.connections.each { |out| out << "data: #{params[:msg]}\n\n" }
204 # response without entity body
end
## JavaScript Code
@@@ javascript
var es = new EventSource('/stream');
es.onmessage = function(e) { $('#chat').append(e.data) };
$("form").live("submit", function(e) {
$.post('/', {msg: "<%= params[:user] %>: " + $('#msg').val()});
e.preventDefault();
});
## HTML
@@@ html
<pre id='chat'></pre> <form><input id='msg' /></form>
Code: [**gist.github.com/1476463**](https://gist.github.com/1476463) -
Demo: [**sharp-night-9421.herokuapp.com**](http://sharp-night-9421.herokuapp.com/)
.notes Next: javascript
!SLIDE small
.notes Next: done