Begin main content

Opportunistically streaming html output with Ruby lambdas

In my last post (Ruby, HTML, s-expressions, lambdas?) I said I would show how we can modify the html generation to allow streaming.

Of course I was making it out to be a lot more complicated in my head than it really is - all the hard work is done already by the lambdas enforcing correct execution order. All we need to do is add a few judicious STDOUT.flush calls before anything that might take some time (ie. evaluating the tag content which could include a database lookup etc.)

I've done that in the new htm.rb below, but first, here's some demo code with some delays:

engines = [["http://google.com/", "Google"],
           ["http://yahoo.com/", "Yahoo!"],
           ["http://webcrawler.com/", "Showing my age"]]

Htm.htm(:ul, engines.map \
        { |e| Htm.htm(:li,
                      [:a, :href, e[0], [:b, lambda { sleep 3; Htm.str(e[1])}]])}).call

Notice also how we can use a lambda anywhere we can put content. Instead of a sleep, perhaps it's an external ImageMagick command. Or perhaps instead of the map iterator our loop is iterating over results being streamed from a slow database query.

Our output will look the same, but I've indicated with !!! where the output pauses for 3 seconds:

<ul><li><a href="http://google.com/"><b> !!! Google</b></a></li><li><a href="http://yahoo.com/"><b> !!! Yahoo!</b></a></li><li><a href="http://webcrawler.com/"><b> !!! Showing my age</b></a></li></ul>

You can see that the http stream will receive all the output it possibly can while waiting for the expensive operation, and the tags are still automagically closed. Job done!

Here's the up to date htm.rb:

require 'cgi'

module Htm
  def Htm.str(content)
    lambda { print CGI.escapeHTML(content.to_s) }
  end

  def Htm._build_tag(tag, args)

    # open the tag
    print '<' + tag.to_s

    tok = args.shift

    # find any tag attribute key/value pairs
    while tok.kind_of? Symbol
      print ' ' + tok.to_s + '="' +
        CGI.escapeHTML(args.shift.to_s) + '"'
      tok = args.shift
    end

    if tok.nil?
      # self-close tag if no content
      print ' />'
    else
      # finish open tag
      print '>'

      # output tag content
      Htm._eval_content(
                        tok.kind_of?(Symbol) ? Htm.htm(tok, args) : tok )

      # close tag
      print '</' + tag.to_s + '>'
    end
  end

  def Htm._eval_content(content)
    while content.kind_of? Proc
      STDOUT.flush
      content = content.call
    end

    if content.kind_of? Array
      Htm._eval_content(Htm.htm( *content ))
    elsif ! content.nil?
      print content
    end
  end

  def Htm.htm( *args )
    lambda do

      tok = args.shift

      while ! tok.nil?
        if tok.kind_of? Symbol
          Htm._build_tag(tok, args)
        else
          Htm._eval_content(tok)
        end
        tok = args.shift
      end
    end
  end
end

10:02 AM, 09 May 2009 by Mark Aufflick Permalink | Short Link

Add comment