Ejemplo de Server Sent Events: irb en el navegador

Véase

  1. Real Time Rack presentation by Konstantin Haase en GitHub using Scott Chacon showoff
  2. The corresponding talk "Real Time Rack" by Konstantin Haase 2011. Confreaks videos.
  3. La sección Showoff 106 en estos apuntes

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: Rack
The <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 ShowOff
Obsé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
      | &gt;&gt;&nbsp;
      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 "&gt;&gt; #{value}"
    input.val ""
    input.focus()
    e.preventDefault()

  src = new EventSource('/events.es')
  src.onmessage = (e) -> output e.data
  1. La llamada 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")
    
  2. El método JQuery .bind( eventType [, eventData ], handler(eventObject) ) Attaches a handler to an event for the elements. En este caso eventType es keydown.
  3. La llamada:
      src = new EventSource('/events.es')
    
    Hace que nos suscribamos a los mensajes generados por /events.es
  4. Cada vez que llega un mensaje lo volcamos en la página mediante output:
      src.onmessage = (e) -> output e.data
    

  5. Javascript Char Codes (Key Codes)
  6. Creo que el código de la flecha arriba es 38 y el de abajo 40. Asi pues lo que se suma a 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
    
  7. Cuando introducimos nuestra expresión en el formulario y pulsamos retorno de carro se ejecuta la correspondiente callback. Mediante $.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 "&gt;&gt; #{value}"
        input.val ""
        input.focus()
        e.preventDefault()
    
  8. The live method attaches an event handler for all elements which match the current selector, now and in the future.

    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
    
    1. El método eval tiene estos argumentos:
      eval(string [, binding [, filename [,lineno]]])
      
      1. Evaluates the Ruby expression(s) in string.
      2. binding is a Binding object: the evaluation is performed in its context.
      3. filename and lineno are used when reporting syntax errors.
    2. El método 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"
      
  9. En el módulo 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
    
    1. El método 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
    2. El método 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
      
  10. 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
      ...
    
  11. 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
    
  12. Este es el código de la ruta 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
    
    1. Como se ha mencionado rack espera que el cuerpo de la respuesta sea un objeto que disponga de un método 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
    2. El método
       (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 run
      
      Raises:
      (ArgumentError)
      
    3. El siguiente texto esta tomado de Asynchronous responses in Rack por Patrick April 15, 2012.

      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 provides env['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 as Rack::Lint is concerned. throw :async is not flagged as an error.
      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.

    4. Véase en StakOverflow la pregunta: Rack concurrency - rack.multithread, async.callback, or both?

      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.

      Using async.callback makes sense when only using eventmachine libraries for IO. Thin will send the response to the client once env['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 an EM::Deferrable as body object directly without having to use throw :async or a status code of -1.

  13. Un objeto 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
    

example.rb

[~/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(" ", "&nbsp;")
  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 "&gt;&gt; #{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
      | &gt;&gt;&nbsp;
      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

showoff.json

[~/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"  }
  ]
}

slides/01_slides.md

[~/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



Subsecciones
Casiano Rodriguez León 2015-06-18