browserchannel
Version:
Google BrowserChannel server for NodeJS
266 lines (215 loc) • 126 kB
HTML
<!DOCTYPE html> <html> <head> <title>server.coffee</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link rel="stylesheet" media="all" href="docco.css" /> </head> <body> <div id="container"> <div id="background"></div> <table cellpadding="0" cellspacing="0"> <thead> <tr> <th class="docs"> <h1> server.coffee </h1> </th> <th class="code"> </th> </tr> </thead> <tbody> <tr id="section-1"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-1">¶</a> </div> <h1>A BrowserChannel server.</h1>
<ul>
<li>Its still pretty young, so there's probably bugs lurking around and the API
will still change quickly.</li>
<li>Its missing integration tests</li>
</ul>
<p>It works in all the browsers I've tried.</p>
<p>I've written this using the literate programming style to try it out. So, thats why
there's a million comments everywhere.</p>
<p>The server is implemented as connect middleware. Its intended to be used like this:</p>
<p><code>
server = connect(
browserChannel (client) -> client.send 'hi'
)
</code></p> </td> <td class="code"> <div class="highlight"><pre></pre></div> </td> </tr> <tr id="section-2"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-2">¶</a> </div> <h2>Dependancies, helper methods and constant data</h2> </td> <td class="code"> <div class="highlight"><pre></pre></div> </td> </tr> <tr id="section-3"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-3">¶</a> </div> <p><code>parse</code> helps us decode URLs in requests</p> </td> <td class="code"> <div class="highlight"><pre><span class="p">{</span><span class="nx">parse</span><span class="p">}</span> <span class="o">=</span> <span class="nx">require</span> <span class="s1">'url'</span></pre></div> </td> </tr> <tr id="section-4"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-4">¶</a> </div> <p><code>querystring</code> will help decode the URL-encoded forward channel data</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">querystring = </span><span class="nx">require</span> <span class="s1">'querystring'</span></pre></div> </td> </tr> <tr id="section-5"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-5">¶</a> </div> <p><code>fs</code> is used to read & serve the client library</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">fs = </span><span class="nx">require</span> <span class="s1">'fs'</span></pre></div> </td> </tr> <tr id="section-6"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-6">¶</a> </div> <p>Client sessions are `EventEmitters</p> </td> <td class="code"> <div class="highlight"><pre><span class="p">{</span><span class="nx">EventEmitter</span><span class="p">}</span> <span class="o">=</span> <span class="nx">require</span> <span class="s1">'events'</span></pre></div> </td> </tr> <tr id="section-7"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-7">¶</a> </div> <p>Client session Ids are generated using <code>node-hat</code></p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">hat = </span><span class="nx">require</span><span class="p">(</span><span class="s1">'hat'</span><span class="p">).</span><span class="nx">rack</span><span class="p">(</span><span class="mi">40</span><span class="p">,</span> <span class="mi">36</span><span class="p">)</span></pre></div> </td> </tr> <tr id="section-8"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-8">¶</a> </div> <p><code>randomInt(n)</code> generates and returns a random int smaller than n (0 <= k < n)</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">randomInt = </span><span class="nf">(n) -></span> <span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="nx">n</span><span class="p">)</span></pre></div> </td> </tr> <tr id="section-9"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-9">¶</a> </div> <p><code>randomArrayElement(array)</code> Selects and returns a random element from <em>array</em></p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">randomArrayElement = </span><span class="nf">(array) -></span> <span class="nx">array</span><span class="p">[</span><span class="nx">randomInt</span><span class="p">(</span><span class="nx">array</span><span class="p">.</span><span class="nx">length</span><span class="p">)]</span></pre></div> </td> </tr> <tr id="section-10"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-10">¶</a> </div> <p>For testing we'll override <code>setInterval</code>, etc with special testing stub versions (so
we don't have to actually wait for actual <em>time</em>. To do that, we need local variable
versions (I don't want to edit the global versions). ... and they'll just point to the
normal versions anyway.</p> </td> <td class="code"> <div class="highlight"><pre><span class="p">{</span><span class="nx">setInterval</span><span class="p">,</span> <span class="nx">clearInterval</span><span class="p">,</span> <span class="nx">setTimeout</span><span class="p">,</span> <span class="nx">clearTimeout</span><span class="p">,</span> <span class="nb">Date</span><span class="p">}</span> <span class="o">=</span> <span class="nx">global</span></pre></div> </td> </tr> <tr id="section-11"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-11">¶</a> </div> <p>The module is configurable</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">defaultOptions =</span></pre></div> </td> </tr> <tr id="section-12"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-12">¶</a> </div> <p>An optional array of host prefixes. Each browserchannel client will randomly pick
from the list of host prefixes when it connects. This reduces the impact of per-host
connection limits.</p>
<p>All host prefixes should point to the same server. Ie, if your server's hostname
is <em>example.com</em> and your hostPrefixes contains ['a', 'b', 'c'],
a.example.com, b.example.com and c.example.com should all point to the same host
as example.com.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">hostPrefixes: </span><span class="kc">null</span></pre></div> </td> </tr> <tr id="section-13"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-13">¶</a> </div> <p>You can specify the base URL which browserchannel connects to. Change this if you want
to scope browserchannel in part of your app, or if you want /channel to mean something
else, or whatever.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">base: </span><span class="s1">'/channel'</span></pre></div> </td> </tr> <tr id="section-14"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-14">¶</a> </div> <p>We'll send keepalives every so often to make sure the http connection isn't closed by
eagar clients. The standard timeout is 30 seconds, so we'll default to sending them
every 20 seconds or so.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">keepAliveInterval: </span><span class="mi">20</span> <span class="o">*</span> <span class="mi">1000</span></pre></div> </td> </tr> <tr id="section-15"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-15">¶</a> </div> <p>After awhile (30 seconds or so) of not having a backchannel connected, we'll evict the
session completely. This will happen whenever a user closes their browser.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">sessionTimeoutInterval: </span><span class="mi">30</span> <span class="o">*</span> <span class="mi">1000</span></pre></div> </td> </tr> <tr id="section-16"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-16">¶</a> </div> <p>All server responses set some standard HTTP headers.
To be honest, I don't know how many of these are necessary. I just copied
them from google.</p>
<p>The nocache headers in particular seem unnecessary since each client
request includes a randomized <code>zx=junk</code> query parameter.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">standardHeaders =</span>
<span class="s1">'Content-Type'</span><span class="o">:</span> <span class="s1">'text/plain'</span>
<span class="s1">'Cache-Control'</span><span class="o">:</span> <span class="s1">'no-cache, no-store, max-age=0, must-revalidate'</span>
<span class="s1">'Pragma'</span><span class="o">:</span> <span class="s1">'no-cache'</span>
<span class="s1">'Expires'</span><span class="o">:</span> <span class="s1">'Fri, 01 Jan 1990 00:00:00 GMT'</span>
<span class="s1">'X-Content-Type-Options'</span><span class="o">:</span> <span class="s1">'nosniff'</span></pre></div> </td> </tr> <tr id="section-17"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-17">¶</a> </div> <p>Gmail also sends this, though I'm not really sure what it does...
'X-Xss-Protection': '1; mode=block'</p> </td> <td class="code"> <div class="highlight"><pre></pre></div> </td> </tr> <tr id="section-18"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-18">¶</a> </div> <p>The one exception to that is requests destined for iframes. They need to
have content-type: text/html set for IE to process the juicy JS inside.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">ieHeaders = </span><span class="nb">Object</span><span class="p">.</span><span class="nx">create</span> <span class="nx">standardHeaders</span>
<span class="nx">ieHeaders</span><span class="p">[</span><span class="s1">'Content-Type'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'text/html'</span></pre></div> </td> </tr> <tr id="section-19"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-19">¶</a> </div> <p>Google's browserchannel server adds some junk after the first message data is sent. I
assume this stops some whole-page buffering in IE. I assume the data used is noise so it
doesn't compress.</p>
<p>I don't really know why google does this. I'm assuming there's a good reason to it though.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">ieJunk = </span><span class="s2">"7cca69475363026330a0d99468e88d23ce95e222591126443015f5f462d9a177186c8701fb45a6ffe</span>
<span class="s2">e0daf1a178fc0f58cd309308fba7e6f011ac38c9cdd4580760f1d4560a84d5ca0355ecbbed2ab715a3350fe0c47</span>
<span class="s2">9050640bd0e77acec90c58c4d3dd0f5cf8d4510e68c8b12e087bd88cad349aafd2ab16b07b0b1b8276091217a44</span>
<span class="s2">a9fe92fedacffff48092ee693af\n"</span></pre></div> </td> </tr> <tr id="section-20"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-20">¶</a> </div> <p>If the user is using IE, instead of using XHR backchannel loaded using
a forever iframe. When data is sent, it is wrapped in <script></script> tags
which call functions in the browserchannel library.</p>
<p>This method wraps the normal <code>.writeHead()</code>, <code>.write()</code> and <code>.end()</code> methods by
special versions which produce output based on the request's type.</p>
<p>This <strong>is not used</strong> for:</p>
<ul>
<li>The first channel test</li>
<li>The first <em>bind</em> connection a client makes. The server sends arrays there, but the
connection is a POST and it returns immediately. So that request happens using XHR/Trident
like regular forward channel requests.</li>
</ul> </td> <td class="code"> <div class="highlight"><pre><span class="nv">messagingMethods = </span><span class="nf">(query, res) -></span>
<span class="nv">type = </span><span class="nx">query</span><span class="p">.</span><span class="nx">TYPE</span>
<span class="k">if</span> <span class="nx">type</span> <span class="o">==</span> <span class="s1">'html'</span>
<span class="nv">junkSent = </span><span class="kc">false</span>
<span class="nv">methods =</span>
<span class="nv">writeHead: </span><span class="o">-></span>
<span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span> <span class="mi">200</span><span class="p">,</span> <span class="s1">'OK'</span><span class="p">,</span> <span class="nx">ieHeaders</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="s1">'<html><body>'</span>
<span class="nv">domain = </span><span class="nx">query</span><span class="p">.</span><span class="nx">DOMAIN</span></pre></div> </td> </tr> <tr id="section-21"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-21">¶</a> </div> <p>If the iframe is making the request using a secondary domain, I think we need
to set the <code>domain</code> to the original domain so that we can call the response methods.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="k">if</span> <span class="nx">domain</span> <span class="o">and</span> <span class="nx">domain</span> <span class="o">!=</span> <span class="s1">''</span></pre></div> </td> </tr> <tr id="section-22"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-22">¶</a> </div> <p>Make sure the domain doesn't contain anything by naughty by <code>JSON.stringify()</code>-ing
it before passing it to the client. There are XSS vulnerabilities otherwise.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="s2">"<script>try{document.domain=#{JSON.stringify domain};}catch(e){}</script>\n"</span>
<span class="nv">write: </span><span class="nf">(data) -></span></pre></div> </td> </tr> <tr id="section-23"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-23">¶</a> </div> <p>The data is passed to <code>m()</code>, which is bound to <em>onTridentRpcMessage_</em> in the client.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="s2">"<script>try {parent.m(#{JSON.stringify data})} catch(e) {}</script>\n"</span>
<span class="nx">unless</span> <span class="nx">junkSent</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="nx">ieJunk</span>
<span class="nv">junkSent = </span><span class="kc">true</span>
<span class="nv">end: </span><span class="o">-></span></pre></div> </td> </tr> <tr id="section-24"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-24">¶</a> </div> <p>Once the data has been received, the client needs to call <code>d()</code>, which is bound to
<em>onTridentDone_</em> with success=<em>true</em>.
The weird spacing of this is copied from browserchannel. Its really not necessary.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">res</span><span class="p">.</span><span class="nx">end</span> <span class="s2">"<script>try {parent.d(); }catch (e){}</script>\n"</span></pre></div> </td> </tr> <tr id="section-25"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-25">¶</a> </div> <p>This is a helper method for signalling an error in the request back to the client.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">writeError: </span><span class="nf">(statusCode, message) -></span></pre></div> </td> </tr> <tr id="section-26"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-26">¶</a> </div> <p>The HTML (iframe) handler has no way to discover that the embedded script tag
didn't complete successfully. To signal errors, we return <strong>200 OK</strong> and call an
exposed rpcClose() method on the page.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">methods</span><span class="p">.</span><span class="nx">writeHead</span><span class="p">()</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">end</span> <span class="s2">"<script>try {parent.rpcClose(#{JSON.stringify message})} catch(e){}</script>\n"</span></pre></div> </td> </tr> <tr id="section-27"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-27">¶</a> </div> <p>For some reason, sending data during the second test (111112) works slightly differently for
XHR, but its identical for html encoding. We'll use a writeRaw() method in that case, which
is copied in the case of html.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">methods.writeRaw = </span><span class="nx">methods</span><span class="p">.</span><span class="nx">write</span>
<span class="nx">methods</span>
<span class="k">else</span></pre></div> </td> </tr> <tr id="section-28"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-28">¶</a> </div> <p>For normal XHR requests, we send data normally.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">writeHead: </span><span class="o">-></span> <span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span> <span class="mi">200</span><span class="p">,</span> <span class="s1">'OK'</span><span class="p">,</span> <span class="nx">standardHeaders</span>
<span class="nv">write: </span><span class="nf">(data) -></span> <span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="s2">"#{data.length}\n#{data}"</span>
<span class="nv">writeRaw: </span><span class="nf">(data) -></span> <span class="nx">res</span><span class="p">.</span><span class="nx">write</span> <span class="nx">data</span>
<span class="nv">end: </span><span class="o">-></span> <span class="nx">res</span><span class="p">.</span><span class="nx">end</span><span class="p">()</span>
<span class="nv">writeError: </span><span class="nf">(statusCode, message) -></span>
<span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span> <span class="nx">statusCode</span><span class="p">,</span> <span class="nx">standardHeaders</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">end</span> <span class="nx">message</span></pre></div> </td> </tr> <tr id="section-29"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-29">¶</a> </div> <p>For telling the client its done bad.</p>
<p>It turns out google's server isn't particularly fussy about signalling errors using the proper
html RPC stuff, so this is useful for html connections too.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">sendError = </span><span class="nf">(res, statusCode, message) -></span>
<span class="nx">res</span><span class="p">.</span><span class="nx">writeHead</span> <span class="nx">statusCode</span><span class="p">,</span> <span class="nx">message</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">end</span> <span class="s2">"<html><body><h1>#{message}</h1></body></html>"</span>
<span class="k">return</span></pre></div> </td> </tr> <tr id="section-30"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-30">¶</a> </div> <h2>Parsing client maps from the forward channel</h2>
<p>The client sends data in a series of url-encoded maps. The data is encoded like this:</p>
<p><code>
count=2&ofs=0&req0_x=3&req0_y=10&req1_abc=def
</code></p>
<p>First, we need to buffer up the request response and query string decode it.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">bufferPostData = </span><span class="nf">(req, callback) -></span>
<span class="nv">data = </span><span class="p">[]</span>
<span class="nx">req</span><span class="p">.</span><span class="kc">on</span> <span class="s1">'data'</span><span class="p">,</span> <span class="nf">(chunk) -></span>
<span class="nx">data</span><span class="p">.</span><span class="nx">push</span> <span class="nx">chunk</span><span class="p">.</span><span class="nx">toString</span> <span class="s1">'utf8'</span>
<span class="nx">req</span><span class="p">.</span><span class="kc">on</span> <span class="s1">'end'</span><span class="p">,</span> <span class="o">-></span>
<span class="nv">data = </span><span class="nx">data</span><span class="p">.</span><span class="nx">join</span> <span class="s1">''</span>
<span class="nx">callback</span> <span class="nx">data</span></pre></div> </td> </tr> <tr id="section-31"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-31">¶</a> </div> <p>Next, we'll need to decode the incoming client data into an array of objects.</p>
<p>The data could be in two different forms:</p>
<ul>
<li>Classical browserchannel format, which is a bunch of string->string url-encoded maps</li>
<li>A JSON object</li>
</ul>
<p>We can tell what format the data is in by inspecting the content-type header</p>
<h2>URL Encoded data</h2>
<p>Essentially, url encoded the data looks like this:</p>
<p><code>
{ count: '2',
ofs: '0',
req0_x: '3',
req0_y: '10',
req1_abc: 'def'
}
</code></p>
<p>... and we will return an object in the form of <code>[{x:'3', y:'10'}, {abc: 'def'}, ...]</code></p>
<h2>JSON Encoded data</h2>
<p>JSON encoded the data looks like:</p>
<p><code>
{ ofs: 0
, data: [null, {...}, 1000.4, 'hi', ...]
}
</code></p>
<p>or <code>null</code> if there's no data.</p>
<p>This function returns null if there's no data or {ofs, json:[...]} or {ofs, maps:[...]}</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">decodeData = </span><span class="nf">(req, data) -></span>
<span class="k">if</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">]</span> <span class="o">==</span> <span class="s1">'application/json'</span>
<span class="nv">data = </span><span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span> <span class="nx">data</span>
<span class="k">return</span> <span class="kc">null</span> <span class="k">if</span> <span class="nx">data</span> <span class="o">is</span> <span class="kc">null</span> <span class="c1"># There's no data. This is a valid response.</span></pre></div> </td> </tr> <tr id="section-32"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-32">¶</a> </div> <p>We'll restructure it slightly to mark the data as JSON rather than maps.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="p">{</span><span class="nx">ofs</span><span class="p">,</span> <span class="nx">data</span><span class="p">}</span> <span class="o">=</span> <span class="nx">data</span>
<span class="p">{</span><span class="nx">ofs</span><span class="p">,</span> <span class="nx">json</span><span class="o">:</span><span class="nx">data</span><span class="p">}</span>
<span class="k">else</span></pre></div> </td> </tr> <tr id="section-33"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-33">¶</a> </div> <p>Maps. Ugh.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">data = </span><span class="nx">querystring</span><span class="p">.</span><span class="nx">parse</span> <span class="nx">data</span>
<span class="nv">count = </span><span class="nb">parseInt</span> <span class="nx">data</span><span class="p">.</span><span class="nx">count</span>
<span class="k">return</span> <span class="kc">null</span> <span class="k">if</span> <span class="nx">count</span> <span class="o">is</span> <span class="mi">0</span></pre></div> </td> </tr> <tr id="section-34"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-34">¶</a> </div> <p>ofs will be missing if count is zero</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">ofs = </span><span class="nb">parseInt</span> <span class="nx">data</span><span class="p">.</span><span class="nx">ofs</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span> <span class="s1">'invalid map data'</span> <span class="k">if</span> <span class="nb">isNaN</span> <span class="nx">count</span> <span class="o">or</span> <span class="nb">isNaN</span> <span class="nx">ofs</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span> <span class="s1">'Invalid maps'</span> <span class="nx">unless</span> <span class="nx">count</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">or</span> <span class="p">(</span><span class="nx">count</span> <span class="o">></span> <span class="mi">0</span> <span class="o">and</span> <span class="nx">data</span><span class="p">.</span><span class="nx">ofs</span><span class="o">?</span><span class="p">)</span>
<span class="nv">maps = </span><span class="k">new</span> <span class="nb">Array</span> <span class="nx">count</span></pre></div> </td> </tr> <tr id="section-35"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-35">¶</a> </div> <p>Scan through all the keys in the data. Every key of the form:
<code>req123_xxx</code> will be used to populate its map.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">regex = </span><span class="sr">/^req(\d+)_(.+)$/</span>
<span class="k">for</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">val</span> <span class="k">of</span> <span class="nx">data</span>
<span class="nv">match = </span><span class="nx">regex</span><span class="p">.</span><span class="nx">exec</span> <span class="nx">key</span>
<span class="k">if</span> <span class="nx">match</span>
<span class="nv">id = </span><span class="nx">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="nv">mapKey = </span><span class="nx">match</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="nv">map = </span><span class="p">(</span><span class="nx">maps</span><span class="p">[</span><span class="nx">id</span><span class="p">]</span> <span class="o">||=</span> <span class="p">{})</span></pre></div> </td> </tr> <tr id="section-36"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-36">¶</a> </div> <p>The client uses <code>mapX_type=_badmap</code> to signify an error encoding a map.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="k">continue</span> <span class="k">if</span> <span class="nx">id</span> <span class="o">==</span> <span class="s1">'type'</span> <span class="o">and</span> <span class="nx">mapKey</span> <span class="o">==</span> <span class="s1">'_badmap'</span>
<span class="nx">map</span><span class="p">[</span><span class="nx">mapKey</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span>
<span class="p">{</span><span class="nx">ofs</span><span class="p">,</span> <span class="nx">maps</span><span class="p">}</span></pre></div> </td> </tr> <tr id="section-37"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-37">¶</a> </div> <p>This is a helper method to order the handling of messages / requests / whatever.</p>
<p>Use it like this:
inOrder = order 0</p>
<p>inOrder 1, -> console.log 'second'
inOrder 0, -> console.log 'first'</p>
<p>Start is the ID of the first element we expect to receive. If we get data for earlier
elements, we'll play them anyway if playOld is truthy.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">order = </span><span class="nf">(start, playOld) -></span></pre></div> </td> </tr> <tr id="section-38"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-38">¶</a> </div> <p>Base is the ID of the (missing) element at the start of the queue</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">base = </span><span class="nx">start</span></pre></div> </td> </tr> <tr id="section-39"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-39">¶</a> </div> <p>The queue will start with about 10 elements. Elements of the queue are undefined
if we don't have data for that queue element.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">queue = </span><span class="k">new</span> <span class="nb">Array</span> <span class="mi">10</span>
<span class="nf">(seq, callback) -></span></pre></div> </td> </tr> <tr id="section-40"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-40">¶</a> </div> <p>Its important that all the cells of the array are truthy if we have data. We'll use an
empty function instead of null.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">callback</span> <span class="o">or=</span> <span class="o">-></span></pre></div> </td> </tr> <tr id="section-41"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-41">¶</a> </div> <p>Ignore old messages, or play them back immediately if playOld=true</p> </td> <td class="code"> <div class="highlight"><pre> <span class="k">if</span> <span class="nx">seq</span> <span class="o"><</span> <span class="nx">base</span>
<span class="nx">callback</span><span class="p">()</span> <span class="k">if</span> <span class="nx">playOld</span>
<span class="k">else</span>
<span class="nx">queue</span><span class="p">[</span><span class="nx">seq</span> <span class="o">-</span> <span class="nx">base</span><span class="p">]</span> <span class="o">=</span> <span class="nx">callback</span>
<span class="k">while</span> <span class="nx">queue</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="nv">callback = </span><span class="nx">queue</span><span class="p">.</span><span class="nx">shift</span><span class="p">()</span>
<span class="nx">base</span><span class="o">++</span>
<span class="nx">callback</span><span class="p">()</span></pre></div> </td> </tr> <tr id="section-42"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-42">¶</a> </div> <p>We need access to the client's sourcecode. I'm going to get it using a synchronous file call
(it'll be fast anyway, and only happen once).</p>
<p>I'm also going to set an etag on the client data so the browser client will be cached. I'm kind of
uncomfortable about adding complexity here because its not like this code hasn't been written
before, but.. I think a lot of people will use this API.</p>
<p>I should probably look into hosting the client code as a javascript module using that client-side
npm thing.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">clientFile = </span><span class="s2">"#{__dirname}/../dist/bcsocket.js"</span>
<span class="nv">clientStats = </span><span class="nx">fs</span><span class="p">.</span><span class="nx">statSync</span> <span class="nx">clientFile</span>
<span class="k">try</span>
<span class="nv">clientCode = </span><span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span> <span class="nx">clientFile</span><span class="p">,</span> <span class="s1">'utf8'</span>
<span class="k">catch</span> <span class="nx">e</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="s1">'Could not load the client javascript. Run `cake client` to generate it.'</span>
<span class="k">throw</span> <span class="nx">e</span></pre></div> </td> </tr> <tr id="section-43"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-43">¶</a> </div> <p>This is mostly to help development, but if the client is recompiled, I'll pull in a new version.
This isn't tested by the unit tests - but its not a big deal.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nx">fs</span><span class="p">.</span><span class="nx">watchFile</span> <span class="nx">clientFile</span><span class="p">,</span> <span class="nv">persistent: </span><span class="kc">false</span><span class="p">,</span> <span class="nf">(curr, prev) -></span>
<span class="k">if</span> <span class="nx">curr</span><span class="p">.</span><span class="nx">mtime</span><span class="p">.</span><span class="nx">getTime</span><span class="p">()</span> <span class="o">!=</span> <span class="nx">prev</span><span class="p">.</span><span class="nx">mtime</span><span class="p">.</span><span class="nx">getTime</span><span class="p">()</span></pre></div> </td> </tr> <tr id="section-44"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-44">¶</a> </div> <p>Putting a synchronous file call here will stop the whole server while the client is reloaded.
Again, this will only happen during development so its not a big deal.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span> <span class="s2">"Reloading client JS"</span>
<span class="nv">clientCode = </span><span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span> <span class="nx">clientFile</span><span class="p">,</span> <span class="s1">'utf8'</span>
<span class="nv">clientStats = </span><span class="nx">curr</span></pre></div> </td> </tr> <tr id="section-45"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-45">¶</a> </div> <hr />
<h1>The server middleware</h1>
<p>The server module returns a function, which you can call with your configuration
options. It returns your configured connect middleware, which is actually another function.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">module.exports = browserChannel = </span><span class="nf">(options, onConnect) -></span>
<span class="k">if</span> <span class="k">typeof</span> <span class="nx">onConnect</span> <span class="o">==</span> <span class="s1">'undefined'</span>
<span class="nv">onConnect = </span><span class="nx">options</span>
<span class="nv">options = </span><span class="p">{}</span>
<span class="nx">options</span> <span class="o">||=</span> <span class="p">{}</span>
<span class="nx">options</span><span class="p">[</span><span class="nx">option</span><span class="p">]</span> <span class="o">?=</span> <span class="nx">value</span> <span class="k">for</span> <span class="nx">option</span><span class="p">,</span> <span class="nx">value</span> <span class="k">of</span> <span class="nx">defaultOptions</span></pre></div> </td> </tr> <tr id="section-46"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-46">¶</a> </div> <p>Strip off a trailing slash in base.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">base = </span><span class="nx">options</span><span class="p">.</span><span class="nx">base</span>
<span class="nv">base = </span><span class="nx">base</span><span class="p">[...</span> <span class="nx">base</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="k">if</span> <span class="nx">base</span><span class="p">.</span><span class="nx">match</span> <span class="sr">/\/$/</span>
</pre></div> </td> </tr> <tr id="section-47"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-47">¶</a> </div> <p>Add a leading slash back on base</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">base = </span><span class="s2">"/#{base}"</span> <span class="nx">unless</span> <span class="nx">base</span><span class="p">.</span><span class="nx">match</span> <span class="sr">/^\//</span>
<span class="nv">sessions = </span><span class="p">{}</span></pre></div> </td> </tr> <tr id="section-48"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-48">¶</a> </div> <p>Host prefixes provide a way to skirt around connection limits. They're only
really important for old browsers.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">getHostPrefix = </span><span class="o">-></span>
<span class="k">if</span> <span class="nx">options</span><span class="p">.</span><span class="nx">hostPrefixes</span>
<span class="nx">randomArrayElement</span> <span class="nx">options</span><span class="p">.</span><span class="nx">hostPrefixes</span>
<span class="k">else</span>
<span class="kc">null</span></pre></div> </td> </tr> <tr id="section-49"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-49">¶</a> </div> <h1>Create a new client session.</h1>
<p>This method will start a new client session.</p>
<p>Session ids are generated by <a href="https://github.com/substack/node-hat">node-hat</a>. They are guaranteed to be unique.
This method is synchronous, because a database will never be involved in browserchannel
session management. Browserchannel sessions only last as long as the user's browser
is open. If there's any connection turbulence, the client will reconnect and get
a new session id.</p>
<p>Sometimes a client will specify an old session ID and old array ID. In this case, the client
is reconnecting and we should evict the named session (if it exists).</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">createSession = </span><span class="nf">(address, query, headers) -></span>
<span class="p">{</span><span class="nx">RID</span><span class="o">:</span><span class="nx">initialRid</span><span class="p">,</span> <span class="nx">CVER</span><span class="o">:</span><span class="nx">appVersion</span><span class="p">,</span> <span class="nx">OSID</span><span class="o">:</span><span class="nx">oldSessionId</span><span class="p">,</span> <span class="nx">OAID</span><span class="o">:</span><span class="nx">oldArrayId</span><span class="p">}</span> <span class="o">=</span> <span class="nx">query</span>
<span class="k">if</span> <span class="nx">oldSessionId</span><span class="o">?</span> <span class="o">and</span> <span class="p">(</span><span class="nv">oldSession = </span><span class="nx">sessions</span><span class="p">[</span><span class="nx">oldSessionId</span><span class="p">])</span>
<span class="nx">oldSession</span><span class="p">.</span><span class="nx">_acknowledgeArrays</span> <span class="nx">oldArrayId</span>
<span class="nx">oldSession</span><span class="p">.</span><span class="nx">close</span> <span class="s1">'Reconnected'</span></pre></div> </td> </tr> <tr id="section-50"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-50">¶</a> </div> <p>Create a new session. Sessions extend node's <a href="http://nodejs.org/docs/v0.4.12/api/events.html">EventEmitter</a> so they have access to
goodies like <code>session.on(event, handler)</code>, <code>session.emit('paarty')</code>, etc.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">session = </span><span class="k">new</span> <span class="nx">EventEmitter</span></pre></div> </td> </tr> <tr id="section-51"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-51">¶</a> </div> <p>The session's unique ID for this connection</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">session.id = </span><span class="nx">hat</span><span class="p">()</span></pre></div> </td> </tr> <tr id="section-52"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-52">¶</a> </div> <p>The client stores its IP address and headers from when it first opened the session. The
handler can use this information for authentication or something.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">session.address = </span><span class="nx">address</span>
<span class="nv">session.headers = </span><span class="nx">headers</span></pre></div> </td> </tr> <tr id="section-53"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-53">¶</a> </div> <p>The session is a little state machine. It has the following states:</p>
<ul>
<li>