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 => 1y 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-1
is illegal as far asRack::Lint
is concerned.throw :async
is 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.callback
makes 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::Deferrable
as body object directly without having to use throw:async
or 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 * ![breaking](breaking.png) .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 ![web](ie.png) .notes Next: ajax !SLIDE center ![ajax](ajax.png) .notes Next: Comet !SLIDE center ![comet](comet.png) .notes Next: Real Time !SLIDE bullets * ![real_time](real_time.jpg) .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 ![rack](rack_stack.png) .notes Next: simple rack app !SLIDE smallish ![working_code](working_code.png) ![stack](endpoint.png) @@@ ruby welcome_app = proc do |env| [200, {'Content-Type' => 'text/html'}, ['Welcome!']] end .notes Next: with any object !SLIDE smallish ![working_code](working_code.png) ![stack](endpoint.png) @@@ ruby welcome_app = Object.new def welcome_app.call(env) [200, {'Content-Type' => 'text/html'}, ['Welcome!']] end .notes Next: in sinatra !SLIDE ![working_code](working_code.png) ![stack](endpoint.png) @@@ ruby get('/') { 'Welcome!' } .notes Next: pseudo handler !SLIDE smallish ![pseudo_code](pseudo_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](middleware.png) @@@ 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 ![working_code](working_code.png) ![stack](something_else.png) @@@ ruby # set up middleware use UpperCase # set endpoint run welcome_app .notes Next: call app (from before) !SLIDE ![working_code](working_code.png) ![stack](handler.png) @@@ ruby status, headers, body = welcome_app.call(env) .notes Next: wrap in middleware !SLIDE smallish ![working_code](working_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![event loop](eventloop1.png) .notes Next: webscale !SLIDE center ![event loop - webscale](eventloop2.png) .notes Next: without eventloop !SLIDE ![working_code](working_code.png) ![stack](something_else.png) @@@ ruby sleep 10 puts "10 seconds are over" puts Redis.new.get('foo') .notes Next: with eventloop !SLIDE smallish ![working_code](working_code.png) ![stack](something_else.png) @@@ 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 ![pseudo_code](pseudo_code.png) ![stack](endpoint.png) @@@ 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 # ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 # ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![pseudo_code](pseudo_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](something_else.png) @@@ 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 ![pseudo_code](pseudo_code.png) ![stack](handler.png) @@@ 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 ![pseudo_code](pseudo_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![pseudo_code](pseudo_code.png) ![stack](handler.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](client.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](client.png) @@@ javascript var src = new WebSocket('ws://127.0.0.1/'); src.onmessage = function (event) { alert(event.data); }; .notes Next: JS EventSource !SLIDE smallish ![working_code](working_code.png) ![stack](client.png) @@@ javascript var src = new EventSource('/updates'); src.onmessage = function (event) { alert(event.data); }; .notes Next: JS WebSocket !SLIDE smallish ![working_code](working_code.png) ![stack](client.png) @@@ 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 ![working_code](working_code.png) ![stack](client.png) @@@ 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 ![working_code](working_code.png) ![stack](something_else.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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 ![working_code](working_code.png) ![stack](endpoint.png) @@@ 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