[Valid Atom 1.0] This is a valid Atom 1.0 feed.


This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.


  1. <?xml version="1.0" encoding="utf-8"?>
  2. <feed xmlns=""
  3.  xmlns:thr="">
  4.  <link rel="self" href=""/>
  5.  <link rel="hub" href=""/>
  6.  <id></id>
  7.  <icon>../favicon.ico</icon>
  9.  <title>Sam Ruby</title>
  10.  <subtitle>It’s just data</subtitle>
  11.  <author>
  12.    <name>Sam Ruby</name>
  13.    <email>[email protected]</email>
  14.    <uri>/blog/</uri>
  15.  </author>
  16.  <updated>2020-07-04T06:14:08-07:00</updated>
  17.  <link href="/blog/"/>
  18.  <link rel="license" href=""/>
  20.  <entry>
  21.    <id>,2004:3359</id>
  22.    <link href="/blog/2017/12/29/Realtime-Updates-of-Web-Content-Using-WebSockets"/>
  23.    <link rel="replies" href="3359.atom" thr:count="4" thr:updated="2020-07-04T03:42:52-07:00"/>
  24.    <title>Realtime Updates of Web Content Using WebSockets</title>
  25.    <summary type="xhtml"><div xmlns=""><p>Three mini-demos showing how to implement realtime updates of web pages using WebSockets.</p></div></summary>
  26.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="175" height="57" viewBox="0 0 175 57">
  27.  <path d='M60,2l12,12l-12,12l-12-12zM88,2l12,12l-12,12l-12-12zM116,2l12,12l-12,12l-12-12zM18,16l12,12l-12,12l-12-12zM46,16l12,12l-12,12l-12-12zM74,16l12,12l-12,12l-12-12zM102,16l12,12l-12,12l-12-12zM130,16l12,12l-12,12l-12-12zM158,16l12,12l-12,12l-12-12zM60,30l12,12l-12,12l-12-12zM88,30l12,12l-12,12l-12-12zM116,30l12,12l-12,12l-12-12z' fill='#bdbdc5'></path>
  28.  <path stroke='#000' stroke-width='0.5' d='M7,27h26l13,13l14-14l14,14l28-28l14,14l14-14l15,15h26v3h-27l-14-14l-14,14l-14-14l-28,28l-14-14l-14,14l-14-14h-25z' fill='#ffd652'></path>
  29.  <path d='M3,26h5v5h-5zM168,26h5v5h-5z'></path>
  30. </svg>
  31. <h3 id="preface">Preface</h3>
  33. <p>You've seen web sites with stock prices or retweet counts that update in real time.  However, such sites are more the exception rather than the norm.  <a href="">WebSockets</a> make it easy, and are <a href="">widely supported</a>, but not used as much as they could be.</p>
  35. <p>Examples provided for WebSockets typically don't focus on the &quot;pubsub&quot; use case; instead they tend to focus on echo servers and the occasional chat server.  These are OK as far as they go.</p>
  37. <p>This post provides three mini-demos that implement the same design pattern in JavaScript on both the client and server.  </p>
  39. <h3 id="quick_start">Quick Start</h3>
  41. <p>For the impatient who want to see running code, </p>
  43. <div><pre>git clone
  44. cd websocket-demos
  45. npm install
  46. node server.js</pre></div>
  48. <p>After running this, visit <a href="http://localhost:8080/"><code>http://localhost:8080/</code></a> in a browser, and you should see something like this:</p>
  50. <style>
  51.  .screenshot {display: flex; width: 100%; border: 1px solid black}
  52.  .screenshot textarea, .screenshot div {flex: 1}
  53.  h3 {margin-top: 0}
  54. </style>
  56. <div class="screenshot">
  57.  <textarea disabled="disabled"># header
  59.  * one
  60.  * two
  61.  * three</textarea>
  62.  <div>
  63.    <h3>header</h3>
  64.    <ul>
  65.      <li>one</li>
  66.      <li>two</li>
  67.      <li>three</li>
  68.    </ul>
  69.  </div>
  70. </div>
  72. <h3 id="server_support">Server support</h3>
  74. <p>The primary responsibility of the server is to maintain a list of active websocket connections.  The code below will maintain three such sets, one for each of the demos provided.</p>
  76. <div><pre><span>// attach to web server</span>
  77. <span>var</span> wsServer = <span>new</span> websocket.server({<span>httpServer</span>: httpServer});
  79. <span>// three sets of connections</span>
  80. <span>var</span> connections = {
  81.  <span>text</span>: <span>new</span> Set(),
  82.  <span>html</span>: <span>new</span> Set(),
  83.  <span>json</span>: <span>new</span> Set()
  84. };
  86. <span>// when a request comes in for one of these streams, add the websocket to the</span>
  87. <span>// appropriate set, and upon receipt of close events, remove the websocket</span>
  88. <span>// from that set.</span>
  89. wsServer.on(<span><span>'</span><span>request</span><span>'</span></span>, (request) =&gt; {
  90.  <span>var</span> url = request.httpRequest.url.slice(<span>1</span>);
  92.  <span>if</span> (!connections[url]) {
  93.    <span>// reject request if not for one of the pre-identified paths</span>
  94.    request.reject();
  95.    console.log((<span>new</span> Date()) + <span><span>'</span><span> </span><span>'</span></span> + url + <span><span>'</span><span> connection rejected.</span><span>'</span></span>);
  96.    <span>return</span>;
  97.  };
  99.  <span>// accept request and add to the connection set based on the request url</span>
  100.  <span>var</span> connection = request.accept(<span><span>'</span><span>ws-demo</span><span>'</span></span>, request.origin);
  101.  console.log((<span>new</span> Date()) + <span><span>'</span><span> </span><span>'</span></span> + url + <span><span>'</span><span> connection accepted.</span><span>'</span></span>);
  102.  connections[url].add(connection);
  104.  <span>// whenever the connection closes, remove connection from the relevant set</span>
  105.  connection.on(<span><span>'</span><span>close</span><span>'</span></span>, (reasonCode, description) =&gt; {
  106.    console.log((<span>new</span> Date()) + <span><span>'</span><span> </span><span>'</span></span> + url + <span><span>'</span><span> connection disconnected.</span><span>'</span></span>);
  107.    connections[url].<span>delete</span>(connection)
  108.  })
  109. });</pre></div>
  111. <p>The code is fairly straightforward.  Three sets are defined; and when a request comes in it is either accepted or rejected based on the path part of the URL of the request.  If accepted, the connection is added to the appropriate set.  When a connection is closed, the connection is removed from the set.</p>
  113. <p>EZPZ!</p>
  115. <h3 id="client_support">Client Support</h3>
  117. <p>The client's responsibitlity is to open the socket, and to keep it open.</p>
  119. <div><pre><span>function</span> <span>subscribe</span>(path, callback) {    
  120.  <span>var</span> ws = <span>null</span>;
  121.  <span>var</span> base =
  123.  <span>function</span> <span>openchannel</span>() {
  124.    <span>if</span> (ws) <span>return</span>;
  125.    <span>var</span> url = <span>new</span> URL(path, base.replace(<span><span>'</span><span>http</span><span>'</span></span>, <span><span>'</span><span>ws</span><span>'</span></span>));
  126.    ws = <span>new</span> WebSocket(url.href, <span><span>'</span><span>ws-demo</span><span>'</span></span>);
  128.    ws.onopen = (event) =&gt; {
  129.      console.log(path + <span><span>'</span><span> web socket opened!</span><span>'</span></span>);
  130.    };
  132.    ws.onmessage = (event) =&gt; {
  133.      callback(;
  134.    };
  136.    ws.onerror = (event) =&gt; {
  137.      console.log(path + <span><span>'</span><span> web socket error:</span><span>'</span></span>);
  138.      console.log(event);
  139.      ws = <span>null</span>;
  140.    };
  142.    ws.onclose = (event) =&gt; {
  143.      console.log(path + <span><span>'</span><span> web socket closed</span><span>'</span></span>);
  144.      ws = <span>null</span>;
  145.    }
  146.  }
  148.  <span>// open (and keep open) the channel</span>
  149.  openchannel();
  150.  setInterval(() =&gt; openchannel(), <span>2000</span>);
  151. }</pre></div>
  153. <p>A subscribe method is defined that accepts a path and a callback.  The path is used to construct the URL to open.  The callback is called whenever a message is received.  Errors and closures cause the <code>ws</code> variable to be set to <code>null</code>.  Every two seconds, the <code>ws</code> variable is checked, and an attempt is made to reestablish the socket connection when this value is <code>null</code>.</p>
  155. <h3 id="textarea">First example - textarea</h3>
  157. <p>Now it is time to put the sets of server <code>connections</code>, and client <code>subscribe</code> function to use.</p>
  159. <p>Starting with the client:</p>
  161. <div><pre><span>var</span> textarea = document.querySelector(<span><span>'</span><span>textarea</span><span>'</span></span>);
  163. <span>// initially populate the textarea with the contents of data.txt from the</span>
  164. <span>// server</span>
  165. fetch(<span><span>&quot;</span><span>/data.txt</span><span>&quot;</span></span>).then((response) =&gt; {
  166.  response.text().then((body) =&gt; { textarea.value = body })
  167. });
  169. <span>// whenever the textarea changes, send the new value to the server</span>
  170. textarea.addEventListener(<span><span>'</span><span>input</span><span>'</span></span>, (event) =&gt; {
  171.  fetch(<span><span>&quot;</span><span>/data.txt</span><span>&quot;</span></span>, {<span>method</span>: <span><span>'</span><span>POST</span><span>'</span></span>, <span>body</span>: textarea.value});
  172. });
  174. <span>// whenever data is received, update textarea with the value</span>
  175. subscribe(<span><span>'</span><span>text</span><span>'</span></span>, (data) =&gt; { textarea.value = data });</pre></div>
  177. <p>The value of the textarea is fetched from the server on page load.  Changes made to the textarea are posted to the server as they occur.  Updates received from the server are loaded into the textarea.  Nothing to it!</p>
  179. <p>Now, onto the server:</p>
  181. <div><pre><span>// Return the current contents of data.txt</span>
  182. app.get(<span><span>'</span><span>/data.txt</span><span>'</span></span>, (request, response) =&gt; {
  183. response.sendFile(dirname + <span><span>'</span><span>/data.txt</span><span>'</span></span>);
  184. });
  186. <span>// Update contents of data.txt</span>
  187.<span><span>'</span><span>/data.txt</span><span>'</span></span>, (request, response) =&gt; {
  188. <span>var</span> fd = fs.openSync(dirname + <span><span>'</span><span>/data.txt</span><span>'</span></span>, <span><span>'</span><span>w</span><span>'</span></span>);
  189. request.on(<span><span>'</span><span>data</span><span>'</span></span>, (data) =&gt; fs.writeSync(fd, data));
  190. request.on(<span><span>'</span><span>end</span><span>'</span></span>, () =&gt; {
  191.   fs.closeSync(fd);
  192.   response.sendFile(dirname + <span><span>'</span><span>/data.txt</span><span>'</span></span>);
  193. })
  194. })
  196. <span>// watch for file system changes.  when data.txt changes, send new raw</span>
  197. <span>// contents to all /text connections.</span>
  198., {}, (event, filename) =&gt; {
  199.  <span>if</span> (filename == <span><span>'</span><span>data.txt</span><span>'</span></span>) {
  200.    fs.readFile(filename, <span><span>'</span><span>utf8</span><span>'</span></span>, (err, data) =&gt; {
  201.      <span>if</span> (data &amp;&amp; !err) {
  202.        <span>for</span> (connection of connections.text) {
  203.          connection.sendUTF(data)
  204.        };
  205.      }
  206.    })
  207.  }
  208. })</pre></div>
  210. <p>Requests to get <code>data.txt</code> cause the contents of the file to be returned.  Post requests cause the contents to be updated.  It is the last block of code that we are most interested in here: the file system is watched for changes, and whenever <code>data.txt</code> is updated, it is read and the results are sent to each <code>text</code> connection.  Pretty straightforward!</p>
  212. <p>If you visit <a href="http://localhost:8080/textarea"><code>http://localhost:8080/textarea</code></a> in multiple browser windows, you will see a textarea in each.  Updating any one window will update all.  What you have is the beginning of a collaborative editing application, though there would really need to be more logic put in place to properly serialize concurrent updates.</p>
  214. <h3 id="markdown">Second example - markdown</h3>
  216. <p>The first example has the server sending plain text content.  This next example deals with HTML.  The <a href="">marked</a> package is used to convert text to HTML on the server.</p>
  218. <p>This client is simpler in that it doesn't have to deal with sending updates to the server:</p>
  220. <div><pre><span>// initially populate the textarea with the converted markdown obtained</span>
  221. <span>// from the server</span>
  222. fetch(<span><span>&quot;</span><span>/data.html</span><span>&quot;</span></span>).then((response) =&gt; {
  223.  response.text().then((body) =&gt; { document.body.innerHTML = body })
  224. });
  226. <span>// whenever data is received, update body with the data</span>
  227. subscribe(<span><span>'</span><span>html</span><span>'</span></span>, (data) =&gt; { document.body.innerHTML = data });</pre></div>
  229. <p>The primary difference between this example and the previous one is that the content is placed into <code>document.body.innerHTML</code> instead of <code>textarea.value</code>.</p>
  231. <p>Like the client, the server portion of this demo consists of two blocks of code:</p>
  233. <div><pre>app.get(<span><span>'</span><span>/data.html</span><span>'</span></span>, (request, response) =&gt; {
  234.  fs.readFile(<span><span>'</span><span>data.txt</span><span>'</span></span>, <span><span>'</span><span>utf8</span><span>'</span></span>, (error, data) =&gt; {
  235.    <span>if</span> (error) {
  236.      response.status(<span>404</span>).end();
  237.    } <span>else</span> {
  238.      marked(data, (error, content) =&gt; {
  239.        <span>if</span> (error) {
  240.          console.log(error);
  241.          response.status(<span>500</span>).send(error);
  242.        } <span>else</span> {
  243.          response.send(content);
  244.        }
  245.      })
  246.    }
  247.  })
  248. });
  250. <span>// watch for file system changes.  when data.txt changes, send converted</span>
  251. <span>// markdown output to all /html connections.</span>
  252., {}, (event, filename) =&gt; {
  253.  <span>if</span> (filename == <span><span>'</span><span>data.txt</span><span>'</span></span>) {
  254.    fs.readFile(filename, <span><span>'</span><span>utf8</span><span>'</span></span>, (err, data) =&gt; {
  255.      <span>if</span> (data &amp;&amp; !err) {
  256.        marked(data, (err, content) =&gt; {
  257.          <span>if</span> (!err) {
  258.            <span>for</span> (connection of connections.html) {
  259.              connection.sendUTF(content);
  260.            }
  261.          }
  262.        })
  263.      }
  264.    })
  265.  }
  266. })</pre></div>
  268. <p>The salient difference between this example and the previous example is call to the <code>marked</code> function to perform the conversion.</p>
  270. <p>If you visit <a href="http://localhost:8080/markdown"><code>http://localhost:8080/markdown</code></a>, you will see the text converted to markdown.  You can also visit <a href="http://localhost:8080/"><code>http://localhost:8080/</code></a> to see both of these demos side by side, in separate frames.  Updates make in the window on the left will be reflected on the right.</p>
  272. <p>No changes were required to the first demo to make this happen as both demos watch for file system changes.  In fact, you can edit <code>data.txt</code> on the server with your favorite text area and whenever you save your changes all clients will be updated.</p>
  274. <h3 id="json">Final example - JSON</h3>
  276. <p>In this final example, the server will be sending down a recursive directory listing, complete with file names, sizes, and last modified dates.  On the client, <a href="">Vue.js</a> will be used to present the data.  We start with a template:</p>
  278. <div><pre>&lt;tbody&gt;
  279.  &lt;tr v-for=&quot;file in filelist&quot;&gt;
  280.    &lt;td&gt;{{ }}&lt;/td&gt;
  281.    &lt;td&gt;{{ file.size }}&lt;/td&gt;
  282.    &lt;td&gt;{{ file.mtime }}&lt;/td&gt;
  283.  &lt;/tr&gt;
  284. &lt;/tbody&gt;</pre></div>
  286. <p>And add a bit of code:</p>
  288. <div><pre><span>var</span> app = <span>new</span> Vue({<span>el</span>: <span><span>'</span><span>tbody</span><span>'</span></span>, <span>data</span>: {<span>filelist</span>: []}});
  290. fetch(<span><span>'</span><span>filelist.json</span><span>'</span></span>).then((response) =&gt; {
  291.  response.json().then((json) =&gt; { app.filelist = json });
  292. });
  294. subscribe(<span><span>'</span><span>json</span><span>'</span></span>, (data) =&gt; { app.filelist = JSON.parse(data) });</pre></div>
  296. <p>The first line associates some data (initially an empty array) with an HTML element (in this case <code>tbody</code>).  The remaining code should look very familiar by now.  Because of the way Vue.js works, all that is required to update the display is to update the data.</p>
  298. <p>The server side should also seem pretty familiar:</p>
  300. <div><pre>app.get(<span><span>'</span><span>/dir.json</span><span>'</span></span>, (request, response) =&gt; {
  301.  response.json(stats(dirname));
  302. });
  304., {<span>recursive</span>: <span>true</span>}, (event, filename) =&gt; {
  305.  <span>var</span> data = JSON.stringify(stats(dirname));
  306.  <span>for</span> (connection of connections.json) {
  307.    connection.sendUTF(data)
  308.  }
  309. })</pre></div>
  311. <p>Not shown is the code that extracts the information from the filesystem, the rest is the same basic pattern that has been used for each of these demos.</p>
  313. <p>If you visit <a href="http://localhost:8080/filelist"><code>http://localhost:8080/filelist</code></a>, you will see a table showing each of the files on the server.  This list will be updated whenever you create, delete, or update any file.  The server will push a new (and complete) set of data, and Vue.js will determine what needs to be changed in the browser window.  All this generally takes place in a fraction of a second.</p>
  315. <p>Vue.js is only one such framework that can be used in this way.  <a href="">Angular</a>, <a href="">Ember.js</a>, and <a href="">React</a> are additional frameworks that are worth exploring.</p>
  317. <h3 id="recap">Recap</h3>
  319. <p>By focusing on file system modified events, these demos have tried to demonstrate server initiated updates.</p>
  321. <p>With comparatively little code, web sites can be prepared to receive and apply unsolicited updates from the server.  The granularity of the updates can be as little as a single string, can be a HTML fragment, or can be arbitrary data encoded in JSON.</p>
  323. <p>Reserving web sockets for server initiated broadcast operations can keep your code small and understandable. Traditional HTTP GET and POST requests can be used for all client initiated retrieval and update operations.</p>
  325. <p>This makes the division of labor between the client and server straightforward: the server is responsible for providing state -- both on demand and as the state changes.  The client is responsible for updating the view to match the state.</p></div></content>
  326.    <updated>2017-12-29T09:18:24-08:00</updated>
  327.  </entry>
  329.  <entry>
  330.    <id>,2004:3358</id>
  331.    <link href="/blog/2017/12/06/Achieving-Response-Time-Goals-with-Service-Workers"/>
  332.    <link rel="replies" href="3358.atom" thr:count="0"/>
  333.    <title>Achieving Response Time Goals with Service Workers</title>
  334.    <summary type="xhtml"><div xmlns=""><p>Blending cache and live responses in order to achieve response time goals.</p></div></summary>
  335.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  336. <g transform="translate(-5,100) scale(0.035,-0.035)" fill="#000" stroke="none">
  337. <path d="M1041 2503 c-32 -92 -63 -139 -111 -163 -61 -31 -120 -26 -213 17
  338. l-78 37 -79 -79 c-44 -44 -80 -85 -80 -91 0 -7 16 -45 35 -85 40 -84 44 -133
  339. 16 -195 -25 -54 -57 -77 -152 -111 l-79 -28 0 -120 0 -120 79 -28 c95 -34 127
  340. -57 152 -111 28 -62 24 -111 -16 -195 -19 -40 -35 -78 -35 -85 0 -6 36 -47 80
  341. -91 l79 -79 78 37 c93 43 152 48 213 17 48 -24 79 -71 111 -163 l23 -68 119 3
  342. 119 3 28 78 c59 166 152 205 313 130 l78 -37 85 86 86 85 -35 69 c-44 87 -51
  343. 122 -37 174 18 68 68 114 160 146 l80 27 0 122 0 122 -80 27 c-92 32 -142 78
  344. -160 146 -14 52 -7 87 37 174 l35 69 -86 85 -85 86 -78 -37 c-161 -75 -254
  345. -36 -313 130 l-28 78 -119 3 -119 3 -23 -68z m286 -533 c63 -31 112 -80 149
  346. -150 27 -50 27 -220 0 -270 -38 -71 -86 -119 -153 -152 -132 -65 -274 -38
  347. -375 70 -113 121 -116 309 -5 429 106 115 245 141 384 73z"></path>
  348. <path d="M2306 1346 c-13 -30 -33 -59 -44 -65 -31 -17 -78 -13 -121 8 l-38 20
  349. -43 -44 -43 -44 16 -43 c35 -91 12 -144 -72 -169 l-42 -12 3 -65 3 -65 40 -14
  350. c60 -20 88 -57 82 -107 -2 -23 -10 -56 -18 -73 -13 -32 -12 -34 26 -73 21 -22
  351. 44 -40 51 -40 6 0 31 9 55 19 74 32 124 8 152 -74 l14 -40 61 -3 62 -3 25 56
  352. c19 44 32 59 59 70 32 14 40 13 92 -6 l56 -21 43 42 44 43 -20 38 c-40 79 -18
  353. 138 62 169 l44 18 3 64 3 65 -42 12 c-85 26 -111 89 -70 170 l20 38 -47 46
  354. -47 46 -35 -19 c-79 -43 -145 -16 -171 69 l-12 41 -64 0 -63 0 -24 -54z m161
  355. -262 c86 -41 119 -153 69 -238 -60 -103 -193 -114 -273 -23 -120 137 38 339
  356. 204 261z"></path>
  357. <path d="M1586 820 c-7 -40 -31 -48 -60 -21 -27 25 -34 26 -64 3 -19 -15 -21
  358. -22 -13 -43 14 -38 0 -53 -43 -46 -33 5 -38 3 -52 -25 -15 -28 -14 -31 6 -54
  359. 28 -29 20 -52 -20 -61 -28 -5 -31 -9 -28 -41 3 -31 7 -37 36 -44 38 -10 42
  360. -32 11 -64 -19 -21 -20 -24 -5 -52 15 -28 18 -30 48 -21 42 12 62 -9 47 -49
  361. -10 -26 -8 -29 34 -53 15 -8 24 -6 41 10 31 29 45 26 62 -14 12 -29 19 -35 44
  362. -35 25 0 32 6 44 35 17 40 31 43 62 14 21 -19 24 -20 52 -5 27 14 30 19 25 54
  363. -6 46 3 55 46 42 29 -8 32 -6 47 22 15 29 14 31 -7 54 -27 29 -18 55 22 64 19
  364. 5 25 13 27 42 3 32 1 36 -28 41 -41 9 -48 32 -19 63 22 24 22 26 7 52 -15 24
  365. -22 27 -58 24 l-42 -4 4 42 c3 36 0 43 -24 58 -26 15 -28 15 -52 -7 -31 -29
  366. -54 -22 -62 19 -6 27 -10 30 -44 30 -34 0 -38 -3 -44 -30z m91 -175 c12 -3 34
  367. -20 48 -36 30 -36 35 -111 9 -147 -49 -71 -170 -67 -209 8 -36 70 -3 159 68
  368. 179 32 9 41 8 84 -4z"></path>
  369. </g>
  370. </svg>
  371. <p>Service Workers enable a web application to be responsive even if the network isn't.  Frameworks like <a href="">AngularJS</a>, <a href="">React</a> and <a href="">Vue.js</a> enable web applications to efficiently update and render web pages as data changes.</p>
  373. <p>The Apache Software Foundation's <a href="">Whimsy board agenda</a> application uses both in combination to achieve a responsive user experience - both in terms of quick responses to user requests and quick updates based on changes made on the server.</p>
  375. <p>From a performance perspective, the two cases easiest to optimize for are (1) the server fully up and running accessed across a fast network with all possible items cached, and (2) the application fully offline as once you make offline possible at all, it will be fast.</p>
  377. <p>The harder cases ones where the server has received a significant update and needs to get that information to users, and even harder is when the server has no instances running and needs to spin up a new instance to process a request.  While it is possible to do <a href="">blue/green</a> deployment for applications that are &quot;always on&quot;, this isn't practical or appropriate for applications which only used in periodic bursts.  The board agenda tool is one such application.</p>
  379. <p>This article describes how a goal of sub-second response time is achieved in such an environment.  There are plenty of articles on the web that show snippets or sanitized approaches, this one focuses on real world usage.</p>
  381. <h3 id="introduction">Introduction to Service Workers</h3>
  383. <p><a href="">Service Workers</a> are JavaScript files that can intercept and provide responses to navigation and resource requests.  Service Workers <a href="">are supported</a> today by Chrome and FireFox, and are under development in Microsoft Edge and WebKit/Safari.</p>
  385. <p>Service Workers are part of a larger effort dubbed &quot;<a href="">Progressive Web Apps</a>&quot; that aim to make web applications reliable and fast, no matter what the state of the network may happen to be.  The word &quot;progressive&quot; in this name is there to indicate that these applications will work with any browser to the best of that browser's ability.</p>
  387. <p>The signature or premier feature of Service Workers is <a href="">offline</a> applications.  Such web applications are loaded normally the first time, and cached.  When offline, requests are served by the cache, and any input made by users can be stored in <a href="">local storage</a> or in an <a href="">index db</a>.</p>
  389. <p><a href=""></a> and <a href="">The Offline Cookbook</a> provide a number of recipes that can be used.  </p>
  391. <h3 id="board-agenda-overview">Overview of the Board Agenda Tool</h3>
  393. <p>This information is for background purposes only.  Feel free to skim or skip.</p>
  395. <p>The ASF Board meets monthly, and <a href="">minutes</a> are published publicly on the web.  A typical meeting has over one hundred agenda items, though the board agenda tool assists in resolving most off them offline, leaving a manageable 9 officer reports, around 20 PMC reports that may or may not require action, and a handful of special orders.</p>
  397. <p>While the full agenda is several thousand lines long, this file size is only a quarter of a megabyte or the size of a small image.  The server side of this application parses the agenda and presents it to the client in JSON format, and the result is roughly the same size as the original.</p>
  399. <p>To optimize the response of the first page access, the server is structured to do <a href="">server side rendering</a> of the page that is requested, and the resulting response starts with links to stylesheets, then contains the rendered HTML, and finally any scripts and data needed.  This allows the browser to incrementally render the page as it is received.  This set of scripts includes a script that can render any page (or component) that the board agenda tool can produce, and the data includes all the information necessary to do so.  The current implementation is based on Vue.js.</p>
  401. <p>Once loaded, traversals between pages is immeasurably quick.  By that I mean that you can go to the first page and lean on the right arrow button and pages will smoothly scroll through the pages by at roughly the rate at which you can see the faces in a deck of cards shuffled upside down.</p>
  403. <p>The pages generally contain buttons and hidden forms; which buttons appear often depends on the user who requests the page.  For example, only Directors will see approve and unapprove buttons; and individual directors will only see one of these two buttons based on whether or not they have already approved the report.</p>
  405. <p>A <a href="">WebSocket</a> between the server and client is made mostly so the server can push changes to each client; changes that then cause re-rendering and updated displays.  Requests from the client to the server generally are done via <a href="">XMLHttpRequest</a> as it wasn't until very recently that Safari <a href="">supported fetch</a>.  IE still doesn't, but Edge does.</p>
  407. <p>Total (uncompressed) size of the application script is another quarter of a megabyte, and dependencies include Vue.js and <a href="">Bootstrap</a>, the latter being the biggest requiring over a half a megabyte of minimized CSS.</p>
  409. <p>All scripts and stylesheets are served with a <a href="">Cache-Control: immutable</a> header as well as an expiration date a year from when the request was made.  This is made possible by the expedient of utilizing a cache <a href="">busting query string</a> that contains the last modified date.  <a href="">Etag</a> and <a href="">304</a> responses are also supported.</p>
  411. <p>Offline support was added recently.  Updates made when offline are stored in an <a href="">IndexDB</a> and sent as a batch when the user returns online.  Having all of the code and data to render any page made this support very straightforward.</p>
  413. <h3 id="performance-observations">Performance observations (pre-optimization)</h3>
  415. <p>As mentioned at the top of this article, offline operations are virtually instantaneous.  Generally, immeasurably so.  As described above, this also applies to transitions between pages.</p>
  417. <p>This leaves the initial visit, and returning visits, the latter includes opening the application in new tabs.</p>
  419. <p>Best case response times for these cases is about a second.  This may be due to the way that server side rendering is done or perhaps due to the fact that each page is customized to the individual.  Improving on this is not a current priority, though the solution described later in this article addresses this.</p>
  421. <p>Worst case response times are when there are no active server processes and all caches (both server side and client side) are either empty or stale.  It is hard to get precise numbers for this, but it is on the order of eight to ten seconds.  Somewhere around four is the starting of the server.  Building the JSON form of the agenda can take another two given all of the validation (involving things like LDAP queries) involved in the process.  Regenerating the ES5 JavaScript from sources can take another second or so.  Producing the custom rendered HTML is another second.  And then there is all of the client side processing.</p>
  423. <p>In all, probably just under ten seconds if the server is otherwise idle.  It can be a little more if the server is under moderate to heavy load.</p>
  425. <p>The worst parts of this:</p>
  427. <ol>
  428. <li>No change is seen on the browser window until the last second or so.</li>
  429. <li>While the worst case scenario is comparatively rare in production, it virtually precisely matches what happens in development.</li>
  430. </ol>
  432. <h3 id="selecting-approach">Selecting an approach</h3>
  434. <p>Given that the application can be brought up quickly in an entirely offline mode, one possibility would be to show the last cached status and then request updated information and process that information when received.  This approach works well if the only change is to agenda data, but doesn't work so well in production whenever a script change is involved.</p>
  436. <p>This can be solved with a <a href="">window.location.reload()</a> call, which is described (and somewhat discouraged) as <a href="">approach #2 in Dan Fabulic's &quot;How to Fix the Refresh Button When Using Service Workers&quot;</a>.  Note the code below was written before Dan's page was published, but in any case, Dan accurately describes the issue.</p>
  438. <p>Taking some measurements on this produces interesting results.  What is needed to determine if a script or stylesheet has changed is a current inventory from the server.  This can consistently be provided quickly and is independent of the user requesting the data, so it can be cached.  But since the data size is small enough, caching (in the sense of HTTP 304 reponses) isn't all that helpful.</p>
  440. <p>Response time for this request in realistic network conditions when there is an available server process is around 200 milliseconds, and doesn't tend to vary very much.</p>
  442. <p>The good news is that this completely addresses the &quot;reload flash&quot; problem.</p>
  444. <p>Unfortunately, the key words here are &quot;available server process&quot; as that was the original problem to solve.</p>
  446. <p>Fortunately, a combination approach is possible:</p>
  448. <ol>
  449. <li>Attempt to fetch the inventory page from the network, but give it a deadline that it should generally beat.  Say, 500 milliseconds or a half a second.</li>
  450. <li>If the deadline isn't met, load potentially stale data from the cache, and request newer data.  Once the network response is received (which had a 500 millisecond head start), determine if any scripts or stylesheets changed.  If not, we are done.</li>
  451. <li>Only if the deadline wasn't met AND there was a change to a stylesheet or more commonly a script, perform a reload; and figure out a way to address the poor user experience associated with a reload.</li>
  452. </ol>
  454. <p>Additional exploration lead to the solution where the inventory page mentioned below could be formatted in HTML and, in fact, be the equivalent to a blank agenda page.  Such a page would still be less than 2K bytes, and performance would be equivalent to loading a blank page and then navigating to the desired page, in other words, immeasurably fast.</p>
  456. <h3 id="implementation">Implementation</h3>
  458. <p>If you look at existing <a href="">recipes</a>, <a href="">Network or Cache</a> is pretty close; the problem is that it leaves the user with stale data if the network is slow.  It can be improved upon.</p>
  460. <p>Starting with the fetch from the network:</p>
  462. <div><pre><code class="language-none">  // attempt to fetch bootstrap.html from the network
  463.  fetch(request).then(function(response) {
  464.    // cache the response if OK, fulfill the response if not timed out
  465.    if (response.ok) {
  466.      cache.put(request, response.clone());
  468.      // preload stylesheets and javascripts
  469.      if (/bootstrap\.html$/.test(request.url)) {
  470.        response.clone().text().then(function(text) {
  471.          var toolate = !timeoutId;
  473.          setTimeout(
  474.            function() {
  475.              preload(cache, request.url, text, toolate)
  476.            },
  478.            (toolate ? 0 : 3000)
  479.          )
  480.        })
  481.      };
  483.      if (timeoutId) {
  484.        clearTimeout(timeoutId);
  485.        resolve(response)
  486.      }
  487.    } else {
  488.      // bad response: use cache instead
  489.      replyFromCache(true)
  490.    }
  491.  }).catch(function(failure) {
  492.    // no response: use cache instead
  493.    replyFromCache(true)
  494.  })</code></pre></div>
  496. <p>This code needs to be wrapped in a Promise that provides a <code>resolve</code> function, and needs access to a <code>cache</code> as well as a variable named <code>timeoutid</code> and that determines whether or not the response has timed out.</p>
  498. <p>If the response is ok, it and will be cached and a <code>preload</code> method will be called to load resources mentioned in the page.  That will either be done immediately if not <code>toolate</code>, or after a short delay the timer expired to allow updates to be processed.  Finally, if such a response was received in time, the timer will be cleared, and the promise will be resolved.</p>
  500. <p>If either a bad response or no response was received (typically, this represents a network failure), the cache will be used instead.</p>
  502. <p>Next the logic to reply from the cache:</p>
  504. <div><pre><code class="language-none">  // common logic to reply from cache
  505.  var replyFromCache = function(refetch) {
  506.    return cache.match(request).then(function(response) {
  507.      clearTimeout(timeoutId);
  509.      if (response) {
  510.        resolve(response);
  511.        timeoutId = null
  512.      } else if (refetch) {
  513.        fetch(event.request).then(resolve, reject)
  514.      }
  515.    })
  516.  };
  518.  // respond from cache if the server isn't fast enough
  519.  timeoutId = setTimeout(function() {replyFromCache(false)}, timeout);</code></pre></div>
  521. <p>This code looks for a cache match, and if it finds one, it will <code>resolve</code> the response, and clear the <code>timeoutId</code> enabling the fetch code to detect if it was too late.</p>
  523. <p>If no response is found, the action taken will be determined by the <code>refetch</code> argument.  The fetch logic above passes <code>true</code> for this, and the timeout logic passes <code>false</code>.  If true, it will retry the original request (which presumably will fail) and return that result to the user.  This is handling a <em>never should happen</em> scenario where the cache doesn't contain the bootstrap page.</p>
  525. <p>The above two snippets of code are then wrapped by a function, providing the <code>event</code>, <code>resolve</code>, <code>reject</code>, and <code>cache</code> variables, as well as declaring and initializing the <code>timeoutId</code> variable:</p>
  527. <div><pre><code class="language-none">// Return a bootstrap.html page within 0.5 seconds.  If the network responds
  528. // in time, go with that response, otherwise respond with a cached version.
  529. function bootstrap(event, request) {
  530.  return new Promise(function(resolve, reject) {
  531.    var timeoutId = null;
  533.;board/agenda&quot;).then(function(cache) {
  534.        ...
  535.    }
  536. })</code></pre></div>
  538. <p>Next, we need to implement the <code>preload</code> function:</p>
  540. <div><pre><code class="language-none">// look for css and js files and in HTML response ensure that each are cached
  541. function preload(cache, base, text, toolate) {
  542.  var pattern = /&quot;[-.\w+/]+\.(css|js)\?\d+&quot;/g;
  543.  var count = 0;
  544.  var changed = false;
  546.  while (match = pattern.exec(text)) {
  547.    count++;
  548.    var path = match[0].split(&quot;\&quot;&quot;)[1];
  549.    var request = new Request(new URL(path, base));
  551.    cache.match(request).then(function(response) {
  552.      if (response) {
  553.        count--
  554.      } else {
  555.        fetch(request).then(function(response) {
  556.          if (response.ok) cacheReplace(cache, request, response);
  557.          count--;
  558.          if (count == 0 &amp;&amp; toolate) {
  559.            clients.matchAll().then(function(clients) {
  560.              clients.forEach(function(client) {
  561.                client.postMessage({type: &quot;reload&quot;})
  562.              })
  563.            })
  564.          }
  565.        })
  566.      }
  567.    })
  568.  }
  569. };</code></pre></div>
  571. <p>This code parses the HTML response, looking for <code>.css</code>, and <code>.js</code> files, based on a knowledge as to how this particular server will format the HTML.  For each such entry in the HTML, the cache is searched for a match.  If one is found, nothing more needs to be done.  Otherwise, the resource is fetched and placed in the cache.</p>
  573. <p>Once all requests are processed, and if this involved requesting a response from the network, then a check is made to see if this was a late response, and if so, a <code>reload</code> request is sent to all client windows.</p>
  575. <p><code>cacheReplace</code> is another application specific function:</p>
  577. <div><pre><code class="language-none">// insert or replace a response into the cache.  Delete other responses
  578. // with the same path (ignoring the query string).
  579. function cacheReplace(cache, request, response) {
  580.  var path = request.url.split(&quot;?&quot;)[0];
  582.  cache.keys().then(function(keys) {
  583.    keys.forEach(function(key) {
  584.      if (key.url.split(&quot;?&quot;)[0] == path &amp;&amp; key.url != path) {
  585.        cache.delete(key).then(function() {})
  586.      }
  587.    })
  588.  });
  590.  cache.put(request, response)
  591. };</code></pre></div>
  593. <p>The purpose of this method is as stated: to delete from the cache other responses that differ only in the query string.  It also adds the response to the cache.</p>
  595. <p>The remainder is either straightforward or application specific in a way that has no performance relevance.  The scripts and stylesheets are served with a <a href="">cache falling back to network</a> strategy.  The initial preloading which normally could be as simple as a call to <a href="">cache.addAll</a> needs to be aware of query strings and for this application it turns out that a different bootstrap HTML file is needed for each meeting.</p>
  597. <p>Finally, here is the client side logic which handles reload messages from the service worker:</p>
  599. <div><pre><code class="language-none">navigator.serviceWorker.register(scope + &quot;sw.js&quot;, scope).then(function() {
  600.  // watch for reload requests from the service worker
  601.  navigator.serviceWorker.addEventListener(&quot;message&quot;, function(event) {
  602.    if ( == &quot;reload&quot;) {
  603.      // ignore reload request if any input or textarea element is visible
  604.      var inputs = document.querySelectorAll(&quot;input, textarea&quot;);
  606.      if (Math.max.apply(
  607.        Math,
  609. {
  610.          return element.offsetWidth
  611.        })
  612.      ) &lt;= 0) window.location.reload()
  613.    }
  614.  });
  615. }</code></pre></div>
  617. <p>This code watches for <code>type: &quot;reload&quot;</code> messages from the service worker and invokes <code>window.location.reload()</code> only if there are no input or text area elements visible, which is determined using the <a href=""><code>offsetWidth</code></a> property of each element.  Very few board agenda pages have visible input fields by default; many, however, have <a href="">bootstrap modal dialog boxes</a> containing forms.</p>
  619. <h3 id="performance-results">Performance Results</h3>
  621. <p>In production when using a browser that supports Service Workers, requests for the bootstrap page now typically range from 100 to 300 milliseconds, with the resulting page fully loaded in 400 to 600 milliseconds.  Generally, this includes the time it takes to fetch and render updated data, but in rare cases that may take up to an additional 200 milliseconds.</p>
  623. <p>In development, and in production when there are no server processes available and when accessed using a browser that supports Service Workers, the page initially loads in 700 to 1200 milliseconds.  It is not clear to me why this sees a greater range of response times; but in any case, this is still a notable improvement.  Often in development, and in rare cases in production, there may be a noticeable refresh that occurs one to five seconds later.</p>
  625. <p>Visitations by browsers that do not support service workers, and for that matter the first time a new user visits the board agenda tool, do not see any performance improvement or degradation with these changes.</p>
  627. <p>Not a bad result from less than 100 lines of code.</p></div></content>
  628.    <updated>2017-12-06T13:23:55-08:00</updated>
  629.  </entry>
  631.  <entry>
  632.    <id>,2004:3357</id>
  633.    <link href="/blog/2017/09/11/Converting-to-Vue-js"/>
  634.    <link rel="replies" href="3357.atom" thr:count="3" thr:updated="2017-09-29T05:20:59-07:00"/>
  635.    <title>Converting to Vue.js</title>
  636.    <summary type="xhtml"><div xmlns=""><p>I’m in the process of converting four <a href="">Whimsy</a> applications from React.js to Vue; and I’m taking a moment to jot down a list of things I like a lot, things I find valuable, things I dislike (but can work around), and things I’m not using.</p>
  637. <p>On balance, so far I like Vue better than React.js (even ignoring licensing issues) or Angular.js, and am optimistic that Vue will continue to improve.</p></div></summary>
  638.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  639.  <path d="M0,6h38l12,20l12-20h38l-50,87Z" fill="#4dba87"></path>
  640.  <path d="M20,6h18l12,20l12-20h18l-30,52Z" fill="#435466"></path>
  641. </svg>
  642. <p><a href="">Whimsy</a> had four applications which made use of React.js; two of which previously were written using Angular.js.  One of these applications has already been converted to Vue, conversion of a second one is in progress.</p>
  643. <p>The reason for the conversion was the decision by Facebook not to change their <a href="">license</a>.</p>
  644. <p>Selection of Vue was based on two criteria: community size and the ability to support a React-like development model.  As a bonus, Vue supports an Angular-like development model too, is smaller in download size than either, and has a few additional features.  It is also <a href="">fast</a>, though I haven’t done any formal measurements.</p>
  645. <p>Note that the API is different than React.js’s, in particular lifecycle methods and event names.  Oh, and the parameters to createElement are completely different.  Much of my conversion was made easier by the fact that I was already using a <a href="">ruby2js filter</a>, so all I needed to do was to write a new filter.</p>
  646. <p>Things I like a lot:</p>
  647. <ul>
  648. <li>Setters actually change the values synchronously.  This has been a source of subtle bugs and surprises when implementing a React.js application.</li>
  649. <li>Framework can be used without preprocessors.  This is mostly true for React, but React.createClass is now <a href="">deprecated</a>.</li>
  650. </ul>
  652. <p>Things I find valuable:</p>
  653. <ul>
  654. <li><a href="">Mixins</a>.  And probably in the near future <a href="">extends</a>.  These make components true building blocks, not mere means of encapsulation.</li>
  655. <li><a href="">Computed values</a>.  Better than Angular’s watchers, and easier than React’s componentWillReceiveProps.</li>
  656. <li><a href="">Events</a>.  I haven’t made much use of these yet, but this looks promising.</li>
  657. </ul>
  659. <p>Things I dislike (but can work around):</p>
  660. <ul>
  661. <li>Warnings are issued if property and data values are named the same.  I can understand <a href="">why</a> this was done; but I can access properties and data separately, and I’m migrating a codebase which often uses properties to define the initial values for instance data. It would be fine if there were a way to silence this one warning, but the only option available is to silence <a href="">all warnings</a>.</li>
  662. <li>If I have a logic error in my application (it happens :-)), the stack traceback on Chrome doesn’t show my application.  On firefox, it does, but it is formatted oddly, and doesn’t make use of source maps so I can’t directly navigate to either the original source or the downloaded code.</li>
  663. <li>Mounting an element replaces the entire element instead of just its children.  In my case, I’m doing server side rendering followed by client side updates.  Replacing the element means that the client can’t find the mount point.  My workaround is to add the enclosing element to the render.</li>
  664. <li>Rendering on both the server and client can create a timing problem for forms.  At times, there can be just enough of a delay where the user can check a box or start to input data only to have Vue on the client wipe out the input.  I’m not sure why this wasn’t a problem with React.js, but for now I’m rendering the input fields as disabled until mounted on the client.</li>
  665. </ul>
  667. <p>Things I’m not using:</p>
  668. <ul>
  669. <li>templates, directives, and filters.  Mostly because I’m migrating from React instead of Angular.  But also because I like components better than those three.</li>
  670. </ul>
  672. <p>On balance, so far I like Vue best of the three (even ignoring licensing issues), and am optimistic that Vue will continue to improve.</p></div></content>
  673.    <updated>2017-09-11T11:35:11-07:00</updated>
  674.  </entry>
  676.  <entry>
  677.    <id>,2004:3356</id>
  678.    <link href="/blog/2017/04/07/Badges-We-dont-need-no-stinkin-badges"/>
  679.    <link rel="replies" href="3356.atom" thr:count="3" thr:updated="2020-07-04T04:32:33-07:00"/>
  680.    <title>Badges? We don't need no stinkin' badges!</title>
  681.    <summary type="xhtml"><div xmlns="">I found myself included in an IBM Resource Action ("RA").  I’m fine, nothing has changed.  I’m already working with a non-profit, namely the <a href="">Apache Software Foundation</a>, and find my work there to be very rewarding.</div></summary>
  682.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="158" height="61" viewBox="0 0 158 61">
  683.  <path d="M0,0v5h31v-5M35,0v5h45c0,0-4-4-9-5M88,0v5h27l-2-5M133,0l-2,5h27v-5M0,8v5h31v-5M35,8v5h49c0,0,0-3-2-5M88,8v5h30l-2-5M130,8l-2,5h30v-5M9,16v5h13v-5M44,16v5h13v-5M70,16v5h13s1,-2,1,-5M96,16v5h25l-2-5M127,16l-2,5h24v-5M9,24v5h13v-5M44,24v5h34s3-3,4-5M96,24v5h14v-3l1,3h24l1-3v3h13v-5h-25l-1,3l-1-3M9,32v5h13v-5M44,32v5h39s-2-4-4-5M96,32v5h14v-5M112,32l2,5h18l2-5M136,32v5h13v-5M9,40v5h13v-5M44,40v5h13v-5M70,40v5h15s0-3-1-5M96,40v5h14v-5M115,40l2,5h12l2-5M136,40v5h13v-5M0,48v5h31v-5M35,48v5h47s1-0,2.5-5M88,48v5h22v-5M118,48l2,5h6l2-5M136,48v5h22v-5M0,56v5h31v-5M35,56v5h38s4-1,7-5M88,56v5h22v-5M121,56l2,5l2-5M136,56v5h22v-5" fill="#1f70c1"></path>
  684. </svg>
  685. <p>I’ve worked from home since the late 90s.  When IBM made me go in a few years back to replace my badge, I joked that the next time I would need it was when it was time for me to turned it in.</p>
  686. <p>Well, I was close.  I used it for the first time yesterday to go to a seminar describing what options are available to those like me who are part of an IBM Resource Action ("RA").  Which is IBM’s way of saying that my job no longer exists, and I have until June 29th to find another job within IBM or I will be offered a modest severance package, and can pick from an array of options varying from helping me find a new job, connecting me with a non-profit organization, and retraining.</p>
  687. <p>TL;DR: I’m fine, nothing has changed.  I’m already working with a non-profit, namely the <a href="">Apache Software Foundation</a>, and find my work there to be very rewarding.</p>
  688. <p>And, by the way, the key advice from the seminar is to network. That happens to be something that I’m fairly good at.</p>
  689. <p>In fact, now that I’ve told my family, my book editor, many people within IBM, and several hundred of my closest friends at the ASF — many of which want to spread the word and help me out — the inescapable conclusion is that I can’t tell all of these people without the word getting out.  So I might as well do it myself, in order to ensure that everybody gets the correct message.</p>
  690. <p>For starters, the most likely outcome is that I’m going to simply retire.  My wife and I have planned for this for several years. This may be the nudge that was needed to make it happen.  And like many retirees, I will donate my time to work for a non-profit. I’m just ahead of the curve as I am already doing that.</p>
  691. <p>The second most likely outcome is that I will find an equivalent job within IBM.  By equivalent, I mean an opportunity that lets me work full time on open source and open standards in general; and in particular lets me devote the time I feel necessary to the role of ASF President.  I would need to feel comfortable about that before accepting, as retiring later would mean that I would have lost the opportunity for the severance package.  The good news for those who are predisposed to root for this option is that that job has already been identified, and the management team there is working through what it takes to make it happen.  There is no guarantee that they will get HR approval, however, which is why this is listed as the second most likely outcome rather than the first.</p>
  692. <p>And finally, the third most likely outcome is that I take a job outside of IBM.  I have a number of people saying that they will shop my résumé around.  Based on these requests, I have now produced <a href="">one</a>.  I am <b>not</b> looking for a headhunter, but if somebody feels that they have a perfect opportunity for me, I am willing to listen.</p>
  693. <p>Again, whatever happens, I’m fine and nothing has changed.</p></div></content>
  694.    <updated>2017-04-07T05:07:22-07:00</updated>
  695.  </entry>
  697.  <entry>
  698.    <id>,2004:3355</id>
  699.    <link href="/blog/2016/07/11/Service-Workers-First-Impressions"/>
  700.    <link rel="replies" href="3355.atom" thr:count="1" thr:updated="2020-07-04T00:11:20-07:00"/>
  701.    <title>Service Workers - First Impressions</title>
  702.    <summary type="xhtml"><div xmlns="">Cache <code>put</code> and <code>match</code> worked right
  703. the first time; cache <code>keys</code> not so much.  Authentication is a mystery.  Outline of future plans, and a call for help.</div></summary>
  704.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  705. <metadata>
  706. Created by potrace 1.13, written by Peter Selinger 2001-2015
  707. </metadata>
  708. <g transform="translate(-5,100) scale(0.035,-0.035)" fill="#000" stroke="none">
  709. <path d="M1041 2503 c-32 -92 -63 -139 -111 -163 -61 -31 -120 -26 -213 17
  710. l-78 37 -79 -79 c-44 -44 -80 -85 -80 -91 0 -7 16 -45 35 -85 40 -84 44 -133
  711. 16 -195 -25 -54 -57 -77 -152 -111 l-79 -28 0 -120 0 -120 79 -28 c95 -34 127
  712. -57 152 -111 28 -62 24 -111 -16 -195 -19 -40 -35 -78 -35 -85 0 -6 36 -47 80
  713. -91 l79 -79 78 37 c93 43 152 48 213 17 48 -24 79 -71 111 -163 l23 -68 119 3
  714. 119 3 28 78 c59 166 152 205 313 130 l78 -37 85 86 86 85 -35 69 c-44 87 -51
  715. 122 -37 174 18 68 68 114 160 146 l80 27 0 122 0 122 -80 27 c-92 32 -142 78
  716. -160 146 -14 52 -7 87 37 174 l35 69 -86 85 -85 86 -78 -37 c-161 -75 -254
  717. -36 -313 130 l-28 78 -119 3 -119 3 -23 -68z m286 -533 c63 -31 112 -80 149
  718. -150 27 -50 27 -220 0 -270 -38 -71 -86 -119 -153 -152 -132 -65 -274 -38
  719. -375 70 -113 121 -116 309 -5 429 106 115 245 141 384 73z"></path>
  720. <path d="M2306 1346 c-13 -30 -33 -59 -44 -65 -31 -17 -78 -13 -121 8 l-38 20
  721. -43 -44 -43 -44 16 -43 c35 -91 12 -144 -72 -169 l-42 -12 3 -65 3 -65 40 -14
  722. c60 -20 88 -57 82 -107 -2 -23 -10 -56 -18 -73 -13 -32 -12 -34 26 -73 21 -22
  723. 44 -40 51 -40 6 0 31 9 55 19 74 32 124 8 152 -74 l14 -40 61 -3 62 -3 25 56
  724. c19 44 32 59 59 70 32 14 40 13 92 -6 l56 -21 43 42 44 43 -20 38 c-40 79 -18
  725. 138 62 169 l44 18 3 64 3 65 -42 12 c-85 26 -111 89 -70 170 l20 38 -47 46
  726. -47 46 -35 -19 c-79 -43 -145 -16 -171 69 l-12 41 -64 0 -63 0 -24 -54z m161
  727. -262 c86 -41 119 -153 69 -238 -60 -103 -193 -114 -273 -23 -120 137 38 339
  728. 204 261z"></path>
  729. <path d="M1586 820 c-7 -40 -31 -48 -60 -21 -27 25 -34 26 -64 3 -19 -15 -21
  730. -22 -13 -43 14 -38 0 -53 -43 -46 -33 5 -38 3 -52 -25 -15 -28 -14 -31 6 -54
  731. 28 -29 20 -52 -20 -61 -28 -5 -31 -9 -28 -41 3 -31 7 -37 36 -44 38 -10 42
  732. -32 11 -64 -19 -21 -20 -24 -5 -52 15 -28 18 -30 48 -21 42 12 62 -9 47 -49
  733. -10 -26 -8 -29 34 -53 15 -8 24 -6 41 10 31 29 45 26 62 -14 12 -29 19 -35 44
  734. -35 25 0 32 6 44 35 17 40 31 43 62 14 21 -19 24 -20 52 -5 27 14 30 19 25 54
  735. -6 46 3 55 46 42 29 -8 32 -6 47 22 15 29 14 31 -7 54 -27 29 -18 55 22 64 19
  736. 5 25 13 27 42 3 32 1 36 -28 41 -41 9 -48 32 -19 63 22 24 22 26 7 52 -15 24
  737. -22 27 -58 24 l-42 -4 4 42 c3 36 0 43 -24 58 -26 15 -28 15 -52 -7 -31 -29
  738. -54 -22 -62 19 -6 27 -10 30 -44 30 -34 0 -38 -3 -44 -30z m91 -175 c12 -3 34
  739. -20 48 -36 30 -36 35 -111 9 -147 -49 -71 -170 -67 -209 8 -36 70 -3 159 68
  740. 179 32 9 41 8 84 -4z"></path>
  741. </g>
  742. </svg>
  743. <p>
  744.  Successes, progress, and stumbling blocks encountered while exploring
  745.  Service Workers.
  746. </p>
  748. <h3 id="preface">Preface</h3>
  750. <p>
  751.  The <a href="">Apache
  752.  Whimsy Board Agenda tool</a> is designed to make ASF Board meetings run
  753.  more smoothly.  It does this by downloading all of the provided reports and
  754.  collating them with comments, prior comments, action items, minutes, links to
  755.  prior reports, links to committee information, and the like.  It provides a UI
  756.  to allow Directors and guests to enter comments.  It provides a UI to allow
  757.  the Secretary to take minutes.
  758. </p>
  760. <p>
  761.  The tool itself is built using
  762.  <a href="">React.JS</a>.  It starts by
  763.  downloading all of the reports.  Navigation between reports can be done via
  764.  mouse clicks or cursor keys and doesn't involve any server interaction.  As
  765.  new data is received, the window is updated.
  766. </p>
  768. <p>
  769.  Finally, I'm new to Service Workers so I may be doing things in a profoundly
  770.  dumb way.  Any pointers would be appreciated.  I am capable of RTFM and
  771.  following examples.
  772. </p>
  774. <h3 id="caching-json">First step - caching JSON</h3>
  776. <p>
  777.  Some of the data (e.g., the list of ASF JIRA projects) is fetched on demand.
  778.  Generally the page is first rendered using an empty list, and then updated
  779.  once the actual list is received.
  780. </p>
  782. <p>
  783.  This process could be improved by caching the results received and using that
  784.  data until fresh data arrives.  As the Cache API is built on promises, and
  785.  therefore asynchronous, this generally means rendering three times: once with
  786.  a empty list, then with the cache, and finally with live data.
  787. </p>
  789. <pre><span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> retrieve an cached object.  Note: block may be dispatched twice,
  790. <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> once with slightly stale data <span style="color:#080;font-weight:bold">and</span> once with current data
  791. <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span>
  792. <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> <span style="color:#606">Note</span>: caches only work currently on <span style="color:#036;font-weight:bold">Firefox</span> <span style="color:#080;font-weight:bold">and</span> <span style="color:#036;font-weight:bold">Chrome</span>.  All
  793. <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> other browsers fall back to <span style="color:#036;font-weight:bold">XMLHttpRequest</span> (<span style="color:#036;font-weight:bold">AJAX</span>).
  794. JSONStorage.fetch = function(name, block) {
  795.  <span style="color:#080;font-weight:bold">if</span> (typeof fetch !== <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">'</span><span style="color:#D20">undefined</span><span style="color:#710">'</span></span> &amp;&amp; typeof caches !== <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">'</span><span style="color:#D20">undefined</span><span style="color:#710">'</span></span> &amp;&amp;
  796.     (location.protocol == <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">https:</span><span style="color:#710">"</span></span> || location.hostname == <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">localhost</span><span style="color:#710">"</span></span>)) {
  798.<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">board/agenda</span><span style="color:#710">"</span></span>).then(function(cache) {
  799.      var fetched = null;
  800.      clock_counter++;
  802.      <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> construct arguments to fetch
  803.      var args = {
  804.        <span style="color:#606">method</span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">get</span><span style="color:#710">"</span></span>,
  805.        <span style="color:#606">credentials</span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">include</span><span style="color:#710">"</span></span>,
  806.        <span style="color:#606">headers</span>: {<span style="color:#606">Accept</span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">application/json</span><span style="color:#710">"</span></span>}
  807.      };
  809.      <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> dispatch request
  810.      fetch(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">../json/</span><span style="color:#710">"</span></span> + name, args).then(function(response) {
  811.        cache.put(name, response.clone());
  813.        response.json().then(function(json) {
  814.          <span style="color:#080;font-weight:bold">if</span> (!fetched || <span style="color:#036;font-weight:bold">JSON</span>.stringify(fetched) != <span style="color:#036;font-weight:bold">JSON</span>.stringify(json)) {
  815.            <span style="color:#080;font-weight:bold">if</span> (!fetched) clock_counter--;
  816.            fetched = json;
  817.            <span style="color:#080;font-weight:bold">if</span> (json) block(json);
  818.            <span style="color:#036;font-weight:bold">Main</span>.refresh()
  819.          }
  820.        })
  821.      });
  823.      <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> check cache
  824.      cache.match(name).then(function(response) {
  825.        <span style="color:#080;font-weight:bold">if</span> (response &amp;&amp; !fetched) {
  826.          response.json().then(function(json) {
  827.            clock_counter--;
  828.            fetched = json;
  829.            <span style="color:#080;font-weight:bold">if</span> (json) block(json);
  830.            <span style="color:#036;font-weight:bold">Main</span>.refresh()
  831.          })
  832.        }
  833.      })
  834.    })
  835.  } <span style="color:#080;font-weight:bold">else</span> <span style="color:#080;font-weight:bold">if</span> (typeof <span style="color:#036;font-weight:bold">XMLHttpRequest</span> !== <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">'</span><span style="color:#D20">undefined</span><span style="color:#710">'</span></span>) {
  836.    <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> retrieve from the network only
  837.    retrieve(name, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">json</span><span style="color:#710">"</span></span>, function(item) {<span style="color:#080;font-weight:bold">return</span> item.block})
  838.  }
  839. }</pre>
  840. <p>
  841.  All in all remarkably painless and completely transparent to the calling
  842.  application.  Doesn't involve the activation of Service Workers, but it
  843.  doesn't have to.
  844. </p>
  846. <h3 id="caching-html">Second step - caching HTML</h3>
  848. <p>
  849.  What's true for JSON should also be true for HTML.  Prior to the caching
  850.  logic introduced above, and continuing to be true for browsers that don't
  851.  support the service workers caching interface, data that should appear on the
  852.  page would be missing briefly and show up a second or two later.  In the case
  853.  of HTML, that data would be the entire page.  This would typically be seen
  854.  both on the initial page load as well as any time a link is opened in a new
  855.  tab.
  856. </p>
  858. <p>
  859.  The HTML case is both simpler and more difficult.  Fetching the HTML from
  860.  cache and then replacing it wholesale from the network, while possible, would
  861.  be jarring.  Fortunately, there already is logic in place to update the
  862.  content of the pages based on updates received by XHR.  So initially
  863.  displaying where the user last left off, as well as updating the cache,
  864.  is sufficient.
  865. </p>
  867. <p>
  868.  Unfortunately, it isn't quite so simple.  I've included the current code below
  869.  complete with log statements and dead ends.
  870. </p>
  872. <pre><span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> simple hashcode to prevent authorization from leaking
  873. var hashcode = function(s) {
  874.  <span style="color:#080;font-weight:bold">return</span> s &amp;&amp; s.split(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#710">"</span></span>).reduce(
  875.    function(a, b) {
  876.      <span style="color:#080;font-weight:bold">return</span> ((a &lt;&lt; <span style="color:#00D">5</span>) - a) + b.charCodeAt(<span style="color:#00D">0</span>)
  877.    },
  879.    <span style="color:#00D">0</span>
  880.  )
  881. };
  883. var status = {<span style="color:#606">auth</span>: null};
  885. this.addEventListener(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">fetch</span><span style="color:#710">"</span></span>, function(event) {
  886.  var scope = this.registration.scope;
  887.  var url = event.request.url;
  888.  var path = url.slice(scope.length);
  889.  var auth = hashcode(event.request.headers.get(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">Authorization</span><span style="color:#710">"</span></span>));
  891.  <span style="color:#080;font-weight:bold">if</span> (<span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#808">^</span><span style="color:#D20">\d</span><span style="color:#D20">\d</span><span style="color:#D20">\d</span><span style="color:#D20">\d</span><span style="color:#808">-</span><span style="color:#D20">\d</span><span style="color:#D20">\d</span><span style="color:#808">-</span><span style="color:#D20">\d</span><span style="color:#D20">\d</span><span style="color:#404">/</span></span>/.test(path) &amp;&amp; event.request.method == <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">GET</span><span style="color:#710">"</span></span>) {
  892.    console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">Handling fetch event for</span><span style="color:#710">"</span></span>, event.request.url);
  894.    event.respondWith(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">board/agenda</span><span style="color:#710">"</span></span>).then(function(cache) {
  895.      <span style="color:#080;font-weight:bold">return</span> cache.match(path).then(function(cached) {
  896.        <span style="color:#080;font-weight:bold">if</span> (cached) console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">matched</span><span style="color:#710">"</span></span>);
  897.        console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">auth</span><span style="color:#710">"</span></span>, auth, status.auth);
  899.        <span style="color:#080;font-weight:bold">if</span> (!auth || auth != status.auth) {
  900.          <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> the following doesn't work
  901.          cached = new Response(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">Unauthorized</span><span style="color:#710">"</span></span>, {
  902.            <span style="color:#606">status</span>: <span style="color:#00D">401</span>,
  903.            <span style="color:#606">statusText</span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">Unauthorized</span><span style="color:#710">"</span></span>,
  904.            <span style="color:#606">headers</span>: {<span style="color:#606"><span style="color:#404">"</span><span>WWW-Authenticate</span><span style="color:#404">"</span></span>: <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">Basic realm=</span><span style="color:#710">"</span></span><span style="color:#036;font-weight:bold">ASF</span> <span style="color:#036;font-weight:bold">Members</span> <span style="color:#080;font-weight:bold">and</span> <span style="color:#036;font-weight:bold">Officers</span><span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#710">"</span></span>}
  905.          });
  907.          <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> <span style="color:#606">fallback</span>: ignore cache <span style="color:#080;font-weight:bold">unless</span> authorized
  908.          cached = null
  909.        };
  911.        <span style="color:#080;font-weight:bold">if</span> (cached) console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">serving from cache</span><span style="color:#710">"</span></span>);
  913.        var network = fetch(event.request).then(function(response) {
  914.          <span style="color:#080;font-weight:bold">if</span> (!cached) console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">fetching from network</span><span style="color:#710">"</span></span>);
  915.          <span style="color:#080;font-weight:bold">if</span> (cached) console.log(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">updating cache</span><span style="color:#710">"</span></span>);
  916.          console.log(response);
  917.          <span style="color:#080;font-weight:bold">if</span> (response.ok) cache.put(path, response.clone());
  918.          status.auth = auth;
  919.          <span style="color:#080;font-weight:bold">return</span> response
  920.        });
  922.        <span style="color:#080;font-weight:bold">return</span> cached || network
  923.      })
  924.    }))
  925.  } <span style="color:#080;font-weight:bold">else</span> <span style="color:#080;font-weight:bold">if</span> (auth) {
  926.    <span style="background-color:hsla(300,100%,50%,0.06)"><span style="color:#404">/</span><span style="color:#404">/</span></span> capture authorization from other pages, <span style="color:#080;font-weight:bold">if</span> provided
  927.    status.auth = auth
  928.  }
  929. })</pre>
  930. <p>
  931.  The primary problem is that the board agenda tool requires authentication to
  932.  use as the data presented may contain Apache Software Foundation confidential
  933.  information.
  934. </p>
  935. <p>
  936.  Without accounting for this, what often would be placed into the cache would
  937.  be the HTTP <code>401</code> challenge response.  That's not desirable.
  938. </p>
  939. <p>
  940.  Attempting to force the return of a challenge when an Authorization header is not present results in the display of the challenge response.  Again, not what we want.
  941. </p>
  942. <p>
  943.  Falling back to only providing the cached data when the Authorization header
  944.  is present (and matches the one used for the cache) results in the cache being
  945.  used sometimes with Firefox.  And, unfortunately, never with Chrome.
  946. </p>
  947. <p>
  948.  A secondary problem, of lesser importance, is that the cache never gets
  949.  updated if the service worker responds with a cache copy.  Of if it does,
  950.  the <code>console.log</code> messages aren't getting executed or aren't
  951.  producing output.
  952. </p>
  954. <h3 id="monitoring">Third step - monitoring</h3>
  956. <p>
  957.  To help with debugging, it occurred to me that it would make sense to produce
  958.  a page that shows Service Worker and Cache status.
  959. </p>
  961. <ul>
  962.  <li>
  963.    <p>
  964.      For service workers, there was no problems, but the results were
  965.      underwhelming.  I only got information back about my service worker even
  966.      though I had several activated by this point by virtue of running
  967.      various demos.  That's not a problem, as that's all I needed.  The only
  968.      information I could get was the state of the service worker.  But even
  969.      so, I could use this as a building block to enable users to send a
  970.      message to the service worker and/or unregister it.  See plans below for
  971.      more details.
  972.    </p>
  973.  </li>
  975.  <li>
  976.    <p>
  977.      For caches, I simply couldn't get it to work.  For example, I tried
  978.      adding the following line immediate after the <code>cache.put</code>
  979.      line in the first code snippet:
  980.    </p>
  981.    <pre>console.log cache.keys()</pre>
  982.    <p>
  983.      The result was an empty list (<code>[]</code>) on both Firefox and
  984.      Chrome.  This is problematic on a number of levels, not the least of
  985.      which being that the interface is defined to return a promise and Arrays
  986.      in JavaScript don't respond to then.
  987.    </p>
  988.    <p>References:</p>
  989.    <ul>
  990.      <li>
  991.        <a href="">Service Workers Nightly</a>
  992.      </li>
  993.      <li>
  994.        <a href="">Cache.keys() - Web APIs | MDN</a>
  995.      </li>
  996.    </ul>
  997.  </li>
  998. </ul>
  1000. <h3 id="plans">Plans</h3>
  1002. <p>
  1003.  One thing I haven't explored yet is replacing the fetch call with one with
  1004.  different values for the request mode and credentials mode.  I figured I would
  1005.  ask for guidance before proceeding down that path.
  1006. </p>
  1008. <p>
  1009.  Once caching HTML is mastered, caching related artifacts like stylesheets and
  1010.  javascripts would be in order.  An online fallfack approach would likely be
  1011.  the best match.
  1012. </p>
  1014. <p>
  1015.  After that, the next order of business would be queuing of updates while
  1016.  offline.  While in general, that would be a hard problem, in this case as user
  1017.  operations are limited by role and generaally to editing their own changes,
  1018.  it should be manageable.
  1019. </p></div></content>
  1020.    <updated>2016-07-11T11:27:29-07:00</updated>
  1021.  </entry>
  1023.  <entry>
  1024.    <id>,2004:3354</id>
  1025.    <link href="/blog/2015/09/24/FacePalm"/>
  1026.    <link rel="replies" href="3354.atom" thr:count="1" thr:updated="2020-07-03T22:11:06-07:00"/>
  1027.    <title>FacePalm</title>
  1028.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1029.  <rect fill="#D22" x="0" y="3" height="95" width="95" rx="15"></rect>
  1030.  <circle cx="18" cy="81" r="9" fill="#FFF"></circle>
  1031.  <path d="M48,84s0-33-33-33 M75,84s0-60-60-60"
  1032.    stroke-linecap="round" stroke-width="15" stroke="#FFF" fill="none"></path>
  1033. </svg>
  1034. <p><a href="">Automated Publishing with Instant Articles</a></p>
  1035. <p><code>&lt;description&gt;</code> A summary of your article, in <b>plain text</b> form.</p>
  1036. <p><code>&lt;pubDate&gt;</code> The date of the article’s publication, in <a href="">ISO-8601 format.</a></p>
  1037. <p>Related: <a href="">plaintext</a>, <a href="">May Day</a>, <a href="">June Bug</a>, <a href="">Another Month</a>, and numerous others.</p></div></content>
  1038.    <updated>2015-09-24T08:44:23-07:00</updated>
  1039.  </entry>
  1041.  <entry>
  1042.    <id>,2004:3353</id>
  1043.    <link href="/blog/2015/05/18/Brief-history-of-the-ASF-Board-Agenda-tool"/>
  1044.    <link rel="replies" href="3353.atom" thr:count="0"/>
  1045.    <title>Brief history of the ASF Board Agenda tool</title>
  1046.    <summary type="xhtml"><div xmlns=""><p>the current implementation is a lot more fun to develop and easier to maintain than prior versions.  As an example, if it were decided that the moment the secretary clicked the ‘timestamp` button on the 'Call to order’ page, all comment buttons are to be removed from all windows and all comment modal dialogs are to be closed, this could be implemented using a single if statement as the event is already propagated, and a re-render is already triggered.  All that would be required is to change the conditions under which the comment button appears.</p>
  1047. <p>The <a href="">board agenda tool</a> has been tested on Linux, Mac OS/X, Vagrant, and Docker.  It contains a suite of tests.</p></div></summary>
  1048.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="100" height="100" viewBox="0 0 100 100">
  1049.  <path d="M34,38a16,16,0,1,0,0,24l32-24a16,16,0,1,1,0,24M40,43l20,15"
  1050.    stroke="#44B74A" stroke-width="4" fill="none"></path>
  1051. </svg>
  1052. <p>The gold standard of server side web applications is Model, View, Controller.  Early versions of this tool was not written that way: it was a CGI script that grew like a weed.  Over time, some <a href="">JQuery</a> effects were added.</p>
  1053. <p>The first major rewrite was done using <a href="">Angular.js</a> and <a href="">Bootstrap</a>.  These frameworks enabled me to do things I had never done before.  It also required me to write code that watched for changes, and to ensure that changes were applied in place (specifically arrays and hashes could not be replaced, they had to be updated).</p>
  1054. <p>While Angular.js used terms like Directives, Filters, and Services, the overall effect was to impose a structure on the client side application.  As with most things, this structure was both constraining and freeing.</p>
  1055. <p>The current rewrite replaces Angular.js with <a href="">React.js</a>.  Gone is all watches and the need to update things in place.  In its place is a policy of “rerender everything” whenever an event (a keystroke, a mouse click, a server side event) occurs.  With React.JS, rerendering everything is efficient as React computes a delta and then only applies the delta to the DOM.  React.JS does provide a suggested architecture, namely Flux, that minimizes the need to rerender everything, but in practice I have not found that necessary.</p>
  1056. <p>To illustrate, if you bring up the “Call to order” page and press and hold down the right arrow key, every page of the agenda will be flashed up and promptly replaced.</p>
  1057. <p>The overall resulting flow is as follows: when a page is fetched the response starts out with a pre-rendered representation (simple HTML), followed by the scripts needed to produce that page, followed by the data used by those scripts.  This ensures that the data is presented promptly, then become reactive to input and events.</p>
  1058. <p>The resulting architecture isn’t MVC on either the client or the server.  Instead, V and C get mushed together, and a unified client/server event stream is added.</p>
  1059. <p>Events are received from the server using <a href="">Server Sent Events</a>.  This is <a href="">widely implemented</a>, and has a solid <a href="">polyfill</a> for browsers (most notably, IE) that haven’t implemented this standard.  Its one way data flow is a good fit for React.js.</p>
  1060. <p>Events are generally triggered by actions on a client browser window somewhere (typically a mouse click) resulting in a HTTP GET or POST request being sent to the server, but can also be triggered by file system changes on the server (example: a cron job does a svn update, which causes the agenda to contain new data).</p>
  1061. <p>A single event-stream is maintained per browser, and that process is responsible for propagating updates to all tabs and windows.  Events can be sent to all clients, or only clients authenticated with a given user id.  This enables my pending updates to be immediately reflected on all of my tabs and windows but not affect others.  The result of an event is to update one or more models, and then trigger a re-render.</p>
  1062. <p>Models on both the client and server are simple classes.  Class methods operate on the entity as a whole (example: write the whole agenda to disk on the server, or provide an index for the agenda on the client).  Instance methods refer to an individual item (example: an agenda item).</p>
  1063. <p>What’s left is React Components on the client and actions on the server.</p>
  1064. <p>React components have a render method.  That method has full (read-only) access to client models, and can do if statements, iterate over result, and (generally minor) computations.  More extensive computations should be refactored to other methods in the component when limited in scope to a single component, or to the client model otherwise.  The one limitation that is enforced is that render methods can not directly or indirectly change state.  A predefined <a href="">life-cycle</a> is defined.  Other methods can be added, for example methods to handle onClick events.</p>
  1065. <p>These methods can trigger HTTP POST and GET requests (the convenience method I provide for the latter is called fetch instead).  These run small scripts on the server that may update models, generate events, and return JSON.</p>
  1066. <p>Taken together, the current implementation is a lot more fun to develop and easier to maintain than prior versions.  As an example, if it were decided that the moment the secretary clicked the ‘timestamp` button on the 'Call to order’ page, all comment buttons are to be removed from all windows and all comment modal dialogs are to be closed, this could be implemented using a single if statement as the event is already propagated, and a re-render is already triggered.  All that would be required is to change the conditions under which the comment button appears.</p>
  1067. <p>The <a href="">board agenda tool</a> has been tested on Linux, Mac OS/X, Vagrant, and Docker.  It contains a suite of tests.</p></div></content>
  1068.    <updated>2015-05-18T09:15:15-07:00</updated>
  1069.  </entry>
  1071.  <entry>
  1072.    <id>,2004:3352</id>
  1073.    <link href="/blog/2015/04/02/Spartan-Test-Results"/>
  1074.    <link rel="replies" href="3352.atom" thr:count="0"/>
  1075.    <title>Spartan Test Results</title>
  1076.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1077.  <g stroke-width="5" stroke="#030092" fill="none">
  1078.    <circle cx="50" cy="50" r="46"></circle>
  1079.    <path d="M8,35h84M8,65h84"></path>
  1080.    <ellipse cx="50" cy="50" rx="20" ry="43"></ellipse>
  1081.  </g>
  1082. </svg>
  1083. <p>I replaced IE results with Spartan results in my <a href="">urltests</a>.  Other than the user agent string, nothing changed.</p>
  1084. <p>Following are selected examples where three out of four of the top browsers agree, identified by the odd browser out:</p>
  1085. <ul>
  1086. <li><a href=";baseline=chrome">Chrome</a></li>
  1087. <li><a href=";baseline=firefox">Firefox</a></li>
  1088. <li><a href=";baseline=safari">Safari</a></li>
  1089. <li><a href=";baseline=spartan">Spartan</a></li>
  1090. </ul></div></content>
  1091.    <updated>2015-04-02T16:54:22-07:00</updated>
  1092.  </entry>
  1094.  <entry>
  1095.    <id>,2004:3351</id>
  1096.    <link href="/blog/2015/04/01/Ruby2JS-2-0"/>
  1097.    <link rel="replies" href="3351.atom" thr:count="1" thr:updated="2020-07-04T04:54:44-07:00"/>
  1098.    <title>Ruby2JS 2.0</title>
  1099.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="100" height="100" viewBox="0 0 100 100">
  1100. <path d='M20,100l74-5l6-75zM61,35l37-2l-29-24z' fill='#b11'></path>
  1101. <path d='M21,100l74-5l-47-4zM98,33c4-12,5-29-14-33l-15,9l29,24z' fill='#811'></path>
  1102. <path d='M7,67l14,33l11-38z' fill='#d44'></path>
  1103. <path d='M29,61l42,13l-10-42zM56,0h28l-16,10zM1,51l-1,29l7-13z' fill='#c22'></path>
  1104. <path d='M32,61l39,13c-14,13-30,24-50,26z' fill='#a00'></path>
  1105. <path d='M61,35l10,39l17-23zM32,61l16,30c9-5,16-11,23-17l-39-13z' fill='#900'></path>
  1106. <path d='M61,35l27,17l10-20l-37,3z' fill='#800'></path>
  1107. <path d='M71,74l23,21l-6-44zM0,80c1,19,15,20,21,20l-14-33l-7,13zM7,67l-2,26c4,6,9,7,15,6c-4-11-13-32-13-32zM69,9l30,4c-1-7-6-11-15-13l-15,9z' fill='#911'></path>
  1108. <path
  1109. d='M1,51l6,16l25-5l29-27l8-26l-13-9l-22,8c-6,7-20,19-20,19c-1,1-9,16-13,24z'
  1110. fill='#f84'></path>
  1111. <path d='M21,21c15-14,34-23,42-16c7,8-1,26-16,40c-14,15-33,24-41,17c-7-7,1-26,15-41z' fill='#F0DB4F'></path>
  1112. <g transform="rotate(307,33,12),scale(0.45)">
  1113. <path d='M26,84l8-5c1,3,3,5,6,5c3,0,5-1,5-6v-32h9v32c0,10-5,14-14,14c-7,0-11-4-14-8' id='j'></path>
  1114. <path d='M60,83l7-5c2,3,5,6,9,6c4,0,7-2,7-5c0-3-3-4-7-6l-2-1c-7-3-12-7-12-14c0-7,6-13,14-13c6,0,10,2,13,8l-7,5c-1-3-3-4-6-4c-3,0-4,1-4,4c0,2,1,4,5,5l3,1c8,4,12,7,12,15c0,9-6,13-15,13c-9,0-15-4-17-9' id='s'></path>
  1115. </g>
  1116. </svg>
  1117. <p>I’ve released <a href="">Ruby2JS</a> version 2.0.  Key new features:</p>
  1118. <ul>
  1119. <li>Line comment support.  More specifically, comments associated with statements are copied to the output.  Comments within statements are still omitted.</li>
  1120. <li><a href="">Source Map</a> support.  This enables debugging of generated JavaScript using the Ruby source.</li>
  1121. </ul>
  1123. <p>The <a href="">Whimsy Agenda</a> rewrite-in-progress (previously based on Angular.js, now being rebased on React.js) can be used to explore both of these features.</p></div></content>
  1124.    <updated>2015-04-01T07:26:31-07:00</updated>
  1125.  </entry>
  1127.  <entry>
  1128.    <id>,2004:3350</id>
  1129.    <link href="/blog/2015/02/11/React-rb-updates"/>
  1130.    <link rel="replies" href="3350.atom" thr:count="0"/>
  1131.    <title>React.rb updates</title>
  1132.    <summary type="xhtml"><div xmlns=""><p>I’ve made a number of updates to the demos.  The <a href="">tutorial</a> demo has been updated to do server side rendering.  This means that it is able to be used by clients which either don’t support or have turned off JavaScript.  </p>
  1133. <p>The second demo is a calendar.  Unlike the tutorial which is a single file, this application is organized in a manner more consistent with how I expect projects to be organized.</p></div></summary>
  1134.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1135.  <g transform="translate(50,50)">
  1136.    <circle fill="#00D8FF" r="8"></circle>
  1137.    <g fill="none" stroke="#00D8FF" stroke-width="4">
  1138.      <ellipse rx="45" ry="17"></ellipse>
  1139.      <ellipse rx="45" ry="17" transform="rotate(60)"></ellipse>
  1140.      <ellipse rx="45" ry="17" transform="rotate(120)"></ellipse>
  1141.    </g>
  1142.  </g>
  1143. </svg>
  1144. <p>I’ve made a number of updates to the demos.  The <a href="">tutorial</a> demo has been updated to do server side rendering.  This means that it is able to be used by clients which either don’t support or have turned off JavaScript.  To run:</p>
  1145. <pre class="code">git clone
  1146. cd ruby2js/demo
  1147. bundle update
  1148. ruby react-tutorial.rb</pre>
  1149. <p>Visit the URL (typically <a href="http://localhost:4567/">http://localhost:4567/</a>) and enter a comment.  Visit the same URL in a different tab or a different browser and enter another comment.  Switch back to the original browser/tab.  If you have client side JavaScript disabled, you will need to hit refresh.</p>
  1150. <p>The second demo is a calendar.  To get started:</p>
  1151. <pre class="code">git clone
  1152. cd wunderbar/demo/calendar
  1153. bundle update
  1154. rackup</pre>
  1155. <p>Visit the URL (typically <a href="http://localhost:9292/">http://localhost:9292/</a>). This will take you to the current month.  Left and right arrows will take you different months (and update the URL).  Unlike the tutorial which is a single file, this application is organized in a manner more consistent with how I expect projects to be organized.</p></div></content>
  1156.    <updated>2015-02-11T15:10:31-08:00</updated>
  1157.  </entry>
  1159.  <entry>
  1160.    <id>,2004:3349</id>
  1161.    <link href="/blog/2015/02/03/DSL-for-JavaScript"/>
  1162.    <link rel="replies" href="3349.atom" thr:count="3" thr:updated="2020-07-04T03:12:29-07:00"/>
  1163.    <title>DSL for JavaScript</title>
  1164.    <summary type="xhtml"><div xmlns=""><p><a href="">Jeremy Ashkenas</a>: <em>“work towards building a language that is to ES6 as CoffeeScript is to ES5”… close, but—do it for [ES6+HTML+CSS], and you’ll win ;)</em></p>
  1165. <p>It occurs to me that there is a shortcut available.  Let a library like React replace [ES6+HTML+CSS].  Then build a <a href="">DSL</a> for that library.</p></div></summary>
  1166.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1167.  <path d="M4,14h92" stroke="#4682b4" stroke-width="5"></path>
  1168.  <text x="50" y="90" font-size="90" fill="#5f9ea0" font-family="serif" text-anchor="middle"><![CDATA[W]]></text>
  1169. </svg>
  1170. <p><a href=""><cite>Jeremy Ashkenas</cite></a>: <em>“work towards building a language that is to ES6 as CoffeeScript is to ES5”… close, but—do it for [ES6+HTML+CSS], and you’ll win ;)</em></p>
  1171. <p>It occurs to me that there is a shortcut available.  Let a library like React replace [ES6+HTML+CSS].  Then build a <a href="">DSL</a> for that library.</p>
  1172. <p>JavaScript isn’t exactly known for its ability to build DSLs.  Ruby, however, <a href="">is</a>.  And has an excellent <a href="">parser</a> library.  By <a href="">transforming</a> the <a href="">AST</a>, I can convert <a href="">calendar.js.rb</a> into <a href="">calendar.js</a>.</p>
  1173. <p>In the process, I start by replacing <a href="">JSX</a> with a <a href="">library</a> which was inspired by <a href="">Builder</a>, <a href="">Markaby</a>, and <a href="">Tagz</a>.  These libraries, in turn were presumably inspired by earlier works like <a href="">Perl’s CGI</a>.</p>
  1174. <p>But there is more.  JSX can’t directly express iteration.  Look at <a href="">CommentList</a> from the <a href="">React tutorial</a>.  Instead you build up a list, and then subsequently wrap that list.  For nested lists, it appears worthwhile to split out separate components.  There is nothing wrong with doing that, but I will suggest that the primary reason to split out a component shouldn’t be to pander to the limitations of the programming language syntax.</p>
  1175. <p>In Ruby you <b>can</b> directly express iteration.  So where a comment box in the tutorial takes four classes, an entire calendar month can be expressed in one.</p>
  1176. <p>And there is even more.  <a href="">Functions</a> in JavaScript are the swiss army knives of programming language features.  The can be used to express classes, blocks, lambdas, procs.  But this flexiblity comes at a <a href="">price</a>.  Ruby2JS can detect when idioms like <a href="">var self=this</a> are needed and automatically apply them.</p>
  1177. <p>The net is that I can write smaller, more understandable code.  And in the process focus more on the problem I’m trying to solve.</p>
  1178. <p>Like with <a href="">CoffeeScript</a>, <em>"It’s just JavaScript"</em>. The code compiles one-to-one into the equivalent JS, and there is no interpretation at runtime.  You can use any existing JavaScript library seamlessly from Ruby2JS (and vice-versa). The compiled output is readable and pretty-printed, will work in every JavaScript runtime, and tends to run as fast or faster than the equivalent handwritten JavaScript.</p>
  1179. <p>Now I don’t expect to have the success or <a href="">impact</a> that CoffeeScript has had.  But I can say that I’m having fun.  And in the process, I’m seeing the benefits with applications I write.</p></div></content>
  1180.    <updated>2015-02-03T16:50:18-08:00</updated>
  1181.  </entry>
  1183.  <entry>
  1184.    <id>,2004:3348</id>
  1185.    <link href="/blog/2015/02/02/Web-Components"/>
  1186.    <link rel="replies" href="3348.atom" thr:count="3" thr:updated="2015-02-02T20:11:06-08:00"/>
  1187.    <title>Web Components</title>
  1188.    <summary type="xhtml"><div xmlns=""><p><a href="">Brian Leroux</a>: <em>ES6 and Web Components</em></p>
  1189. <p>My take is that this talk lumps React in with others based on when it was introduced; but that it is fundamentally different from, say Angular.js as Angular.js is from jQuery.</p></div></summary>
  1190.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1191.  <g transform="translate(50,50)">
  1192.    <circle fill="#00D8FF" r="8"></circle>
  1193.    <g fill="none" stroke="#00D8FF" stroke-width="4">
  1194.      <ellipse rx="45" ry="17"></ellipse>
  1195.      <ellipse rx="45" ry="17" transform="rotate(60)"></ellipse>
  1196.      <ellipse rx="45" ry="17" transform="rotate(120)"></ellipse>
  1197.    </g>
  1198.  </g>
  1199. </svg>
  1200. <p><a href=""><cite>Brian Leroux</cite></a>: <em>ES6 and Web Components</em></p>
  1201. <p>Good overview.  Issues:</p>
  1202. <ul>
  1203. <li>YUI is an example of a key problem w/ corp stewardship; Angular, Polymer, React all OK though?</li>
  1204. <li>HTML Imports in trouble as Mozilla doesn’t want to implement; Custom Elements OK even though Chrome is the only implementation?</li>
  1205. <li>Overall, Brian mentions four specifications, and crosses off three.  Why not all four?</li>
  1206. </ul>
  1208. <p>My take is that this talk lumps React in with others based on when it was introduced; but that it is fundamentally different from, say Angular.js as Angular.js is from jQuery.  Compared to the alternatives, react is more imperative, and is based on a virtual DOM.  It also can run in both the server and the client.</p>
  1209. <p>Brian suggests that you view source on <a href=""></a>.  What you don’t see when you do that is today’s date.  I’d suggest that the ideal would be a page where you do see today’s date — even if JavaScript is disabled.  And for you to be able to interact with that page in ways that involve the server.</p>
  1210. <p>I have my own page on which I would suggest that you view source: <del><a href="">calendar-demo</a></del> (<strong>Update:</strong> that site is down, try <a href="">this static snapshot</a>).  Use the left and right arrow buttons to go to the previous and next months.  Viewing source reveals that the page is delivered pre-rendered, and only after the content is delivered are script libraries loaded.  Traversing to the next and previous months are pretty snappy despite the fact that there has been no optimization: in particular, there are no anticipatory prefetches.  Nor is data retained should you go back to a previous month.  Neither of these changes would be hard to implement.</p>
  1211. <p>Source is available in <a href="">svn</a>.  Check it out, do a bundle update to get the dependencies, run rake if you want to run a few tests, and run rackup to start a local server.</p>
  1212. <p>I must say that being able to define a component with all of the rendering, client, and server logic in one place is very appealing to me.</p>
  1213. <p>Brian suggests authoring source in ES6, and targeting ES5.  My preference would be to work towards building a language that is to ES6 as CoffeeScript is to ES5.  At the moment, my experimentation along those lines is happening in <a href="">Ruby2JS</a>.</p>
  1214. <p><a href="">React Native</a> looks worth watching.  Perhaps as my calendar is using flexbox, I will be able to quickly build an Android or IOS equivalent.</p></div></content>
  1215.    <updated>2015-02-02T14:28:32-08:00</updated>
  1216.  </entry>
  1218.  <entry>
  1219.    <id>,2004:3347</id>
  1220.    <link href="/blog/2015/01/28/Email-addresses"/>
  1221.    <link rel="replies" href="3347.atom" thr:count="0"/>
  1222.    <title>Email addresses</title>
  1223.    <summary>I have been telling all non-IBMers to not use my email address for years, but this advice is routinely ignored.  I’ve repeated the reaons behind why I ask this enough times that it makes sense for me to post the reasons in one place so that I can point to it.</summary>
  1224.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="167" height="60" viewBox="0 0 167 60">
  1225.  <rect x='32' y='15' fill='#f3b457' rx='3' height='37' width='113'></rect>
  1226.  <g stroke='#FFF' stroke-width='2'>
  1227.    <path d='M38,9c-3,17-11,31-12,33c11,4,21,9,31,15c3-8,5-16,7-24-8-11-17-17-26-24' fill='#64a15a'></path>
  1228.    <path d='M38,9c5,12,8,20,11,30l15-6' fill='#64a15a'></path>
  1229.     <path d='M53,14c10,12,20,24,24,38c10-8,20-16,29-22-1-15-8-23-17-29z' fill='#57a295'></path>
  1230.     <path d='M53,14c13,0,26,2,38,6c0-6,1-13-2-20' fill='#57a295'></path>
  1231.     <path d='M91,33c11-7,22-13,38-15c17,6,16,11,21,17-14,3-25,14-35,23-7-16-16-18-24-25z' fill='#d37736'></path>
  1232.     <path d='M91,33c14-2,26-1,39,0v-14' fill='#d37736'></path>
  1233.   </g>
  1234.   <path d='M4,24l5,4-5,4h7v-8z' fill='#FFF200'></path>
  1235.   <path d='M25,27l-5-3h-16l9,4-9,4h16l5-3z' fill='#d4477e'></path>
  1236.   <path d='M27,28l-4-2h-14l4,2-4,2h14l5-3z' fill='#e55d9c'></path>
  1237.   <path d='M61,27h38l-4,2h-32zM31,27h-28l-3,1l4,1h27zM122,27h33v2h-31z' fill='#303f7a'></path>
  1238.   <path d='M151,31l17-3-17-3c4,2,4,4,0,6' fill='#303f7a'></path>
  1239. </svg>
  1240. <p>I have been telling all non-IBMers to not use my email address for years, but this advice is routinely ignored.  I’ve repeated the reaons behind why I ask this enough times that it makes sense for me to post the reasons in one place so that I can point to it.</p>
  1241. <p>The back story is that 15 years ago I wrote some open source code in a programming language called Java.  I don’t use that language much any more, but I understand that it remains popular in some circles.  In any case, javadoc style comments encouraged sharing your email address, and my employer discouraged me from doing anything that would hide my relationship with them, so my email address was put out on the web.</p>
  1242. <p>The inevitable result is that I’m deluged with spam, most in languages I am not familiar with.</p>
  1243. <p>My personal email I have control over and the spam tools (all open source) I use are largely effective.  I don’t have that option with my corporate email.  As others within IBM don’t have this problem, I am clearly an outlier.</p>
  1244. <p>Over time, I was missing enough important work-related emails that I tought myself enough LotusScript to write a script that I can invoke as an ‘Action’.  This script identifies emails that were sent from outside of Lotus Notes and places them into a separate folder.  If I am alerted to the presence of a single email, and given enough information (like senders name and time it was sent) I can generally find the email; but in general people should assume that emails sent to my corporate email address from outside of IBM are never seen by me.</p>
  1245. <p>Another downside of this is that some of my IBM email is sent from service machines that don’t interface directly with Lotus Notes.  That means that I miss some important updates.  And important reminders.  Eventually such reminders copy my manager, who sends them on to me.</p>
  1246. <p>Apparently there is plans in the works to migrate corporate email to the “cloud”.  Perhaps that will be better.  Perhaps I will need to find a way to reimplement my filter or equivalent.  Or perhaps it won’t be something that I <a href="">won’t need to worry about any more</a>.</p></div></content>
  1247.    <updated>2015-01-28T08:48:39-08:00</updated>
  1248.  </entry>
  1250.  <entry>
  1251.    <id>,2004:3346</id>
  1252.    <link href="/blog/2015/01/22/React-rb"/>
  1253.    <link rel="replies" href="3346.atom" thr:count="1" thr:updated="2015-01-23T06:22:34-08:00"/>
  1254.    <title>React.rb</title>
  1255.    <summary type="xhtml"><div xmlns="">Having determined that Angular.js is overkill for my <a href="">blog rewrite</a>, I started looking more closely at <a href="">React</a>.  It occurred to me that I could do better than <a href="">JSX</a>, so I wrote a <a href="">Ruby2JS</a> filter.  Compare for yourself.</div></summary>
  1256.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1257.  <g transform="translate(50,50)">
  1258.    <circle fill="#00D8FF" r="8"></circle>
  1259.    <g fill="none" stroke="#00D8FF" stroke-width="4">
  1260.      <ellipse rx="45" ry="17"></ellipse>
  1261.      <ellipse rx="45" ry="17" transform="rotate(60)"></ellipse>
  1262.      <ellipse rx="45" ry="17" transform="rotate(120)"></ellipse>
  1263.    </g>
  1264.  </g>
  1265. </svg>
  1266. <p>Having determined that Angular.js is overkill for my <a href="">blog rewrite</a>, I started looking more closely at <a href="">React</a>.  It occurred to me that I could do better than <a href="">JSX</a>, so I wrote a <a href="">Ruby2JS</a> filter.  Compare for yourself.  Excerpt from the <a href="">React tutorial</a>:</p>
  1267. <pre class="code">var CommentList = React.createClass({
  1268.  render: function() {
  1269.    var commentNodes = (comment) {
  1270.      return (
  1271.        &lt;Comment author={}&gt;
  1272.          {comment.text}
  1273.        &lt;/Comment&gt;
  1274.      );
  1275.    });
  1276.    return (
  1277.      &lt;div className="commentList"&gt;
  1278.        {commentNodes}
  1279.      &lt;/div&gt;
  1280.    );
  1281.  }
  1282. });</pre>
  1283. <p>Equivalent using the Ruby2JS filter:</p>
  1284. <pre class="code">class CommentList &lt; React
  1285.  def render
  1286.    _div.commentList do
  1287.      @@data.forEach do |comment|
  1288.        _CommentBlock comment.text, author:
  1289.      end
  1290.    end
  1291.  end
  1292. end</pre>
  1293. <p>Note: I renamed the <code>Comment</code> class to <code>CommentBlock</code> to avoid a conflict with the existing <a href="">Comment</a> API.  I wouldn’t have thought that would be necessary, but things didn’t work until I made this change.</p>
  1294. <p><a href="">Full source</a> for the tutorial reimplemented in Ruby is available.</p></div></content>
  1295.    <updated>2015-01-22T17:54:56-08:00</updated>
  1296.  </entry>
  1298.  <entry>
  1299.    <id>,2004:3345</id>
  1300.    <link href="/blog/2015/01/17/RFC-3986bis"/>
  1301.    <link rel="replies" href="3345.atom" thr:count="0"/>
  1302.    <title>RFC 3986bis</title>
  1303.    <summary type="xhtml"><div xmlns="">URL parsers consume URLs and generate URIs.  Such URIs are not <a href="">RFC 3986</a> complaint.  I’d like to fix that.</div></summary>
  1304.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="132" height="76" viewBox="0 0 132 76">
  1305.  <path d='M57,29c0-9,18-12,17-2c0,7-12,10-12,18v5h8v-3c0-7,13-9,13-21c-1-16-34-16-34,3zM62,53h8v8h-8z' fill='#371'></path>
  1306.  <circle cy='38' stroke='#371' fill='none' r='33' stroke-width='10' cx='66'></circle>
  1307.  <path d='M45,17l9,9l-9,10l-9-10zM67,17l9,9l-9,10l-10-10zM88,17l9,9l-9,10l-9-10zM14,28l9,9l-9,9l-9-9zM35,28l9,9l-9,9l-9-9zM56,28l9,9l-9,9l-9-9zM77,28l9,9l-9,9l-9-9zM98,28l9,9l-9,9l-9-9zM119,28l10,9l-10,9l-9-9zM45,39l9,9l-9,9l-9-9zM67,39l9,9l-9,9l-10-9zM88,39l9,9l-9,9l-9-9z' fill='#bdbdc5'></path>
  1308.  <path d='M44,13l9,31l9-31h25v3l-10,16c23,7,2,52-16,21l6-2c11,21,24-16,2-14v-3l9-15h-11l-13,44h-1l-10-31l-9,31h-1l-15-50h7l9,31l6-21l-3-10z' fill='#005A9C'></path>
  1309.  <path stroke='#000' d='M5,36h20l10,10l10-10l11,10l21-21l11,11l10-11l12,11h19v3h-20l-11-11l-10,11l-11-11l-21,21l-11-10l-10,10l-11-10h-19z' fill='#ffd652' stroke-width='0.5'></path>
  1310.  <path d='M88,49c11,24,22,11,26,5l-1-5c-12,20-24,2-25,0M109,21c-8-16-26,0-16,23c-4-23,12-29,17-16l4-8l-1-6'></path>
  1311.  <path d='M2,35h5v5h-5zM127,35h5v5h-5z'></path>
  1312.  <path d='M57,29c0-9,18-12,17-2c0,7-12,10-12,18v5h8v-3c0-7,13-9,13-21c-1-16-34-16-34,3zM62,53h8v8h-8z' fill='#371'></path>
  1313. </svg>
  1314. <p>TL;DR: URL parsers consume URLs and generate URIs.  Such URIs are not <a href="">RFC 3986</a> complaint.  I’d like to fix that.</p>
  1315. <p> - - -</p>
  1316. <p>Let’s talk a bit about nomenclature.</p>
  1317. <p>On the web, particularly in places like values of attributes named <a href="">href</a>, there are things that people have, at various times, attempted to call <a href="">web addresses</a> or <a href="">IRIs</a>.  Neither term has stuck.  In common uses these are called <a href="">URLs</a>.</p>
  1318. <p>In between the markup and servers, there are user agents.  One such user agent is a browser.  Browsers don’t passively send URLs along, they reject some outright, and transform others.  There should be a name for the set of outputs of the various cleanups that browsers perform.</p>
  1319. <p>Since browsers are programmable, you can directly observe this transformation.  The WHATWG URL specification defines an <a href="">API</a> which has already been implemented by Firefox and Chrome, and is being evaluated by Microsoft and Apple.  Create a JavaScript console and enter the following:</p>
  1320. <pre class="code">new URL("hTtP:/EXamPLe.COM/").href</pre>
  1321. <p>The output you will see is:</p>
  1322. <pre class="code">""</pre>
  1323. <p>The output is clearly much cleaner and more consistent than the input.  In fact, in this case the output is RFC 3986 compliant.</p>
  1324. <p>Unfortunately, in the general case, this isn’t true.  Browsers (and more generally: other libraries like the ones found in pretty much every modern programming language) can produce things that aren’t RFC 3986 compliant.</p>
  1325. <p>I’m <a href="">looking</a> at every browser and every library I can.  I’m specifically looking for differences.  In some cases, I’m pointing out where such outputs are clearly wrong and need to be fixed.</p>
  1326. <p>In other cases, the output may not be RFC 3986 compliant, but actually are useful and actually work.  What this means in practice is that the set of things that consumers need to be able to correctly process is not defined by RFC 3986 but by these tools.</p>
  1327. <p>People can learn this the hard way by starting out to implement RFC 3986 and then finding that they need to reverse engineer other tools.  We can do better.  We can set out to update RFC 3986 or otherwise document what the actual set of inputs that can be expected to interoperably process is.</p>
  1328. <p>In general, I have found that it isn’t difficult to talk about places where RFC 3986 can be tightened up.  Where there has been push-back is exploring any notion of loosening the definition.  The reaction generally is expressed along the lines of “doing so would break things”.</p>
  1329. <p>I can see how some see such a position as reasonable.  I don’t, and I’ll tell you why.  What is effectively being said is that documenting how things actually work will break things, which is clearly untrue.</p>
  1330. <p>What such an effort will do is not break things, but uncover uncomfortable truths.  To build upon an <a href="">example</a> from Dave Cridland, one such uncomfortable truth may be that the sets of things that everybody except LDAP schemas can handle is different than the sets of things LDAP schemas can handle.</p>
  1331. <p>There are three ways to handle that.  One would be to change everybody to conform to what LDAP can handle.  One would be to change LDAP.  And one would be to document clearly that the set of things LDAP can handle and the set of things that everybody else expects to be handled are separate sets.  Largely overlapping, yes, but not identical sets.</p>
  1332. <p>While documenting three sets (the set of things Chrome and other browser supports, the set of things HTTP and other protocols support, and the set of things LDAP supports) would not be my first choice, but it may be the only option available given the constraints.</p>
  1333. <p>If you look at those three sets, ideally each would be a proper subset of these that precede it.  That’s not currently the case at the moment, but as I mentioned proposals made with clear rationale provided to tighten up RFC 3986 don’t seem to be getting much push-back.</p>
  1334. <p>What we need then it three names.  URIs seem to be the obvious choice for name of the set of “things LDAP schemas support”.  For better or worse, URLs seem to be the name that has stuck for the first set.</p>
  1335. <p>At this point, a number of people seeing an opening suggest IRIs as the name for the set in the middle.  Um, no.  Except for fragments, this set is 100% pure ASCII.  The name for what IRIs attempted to define is URLs.</p>
  1336. <p>So this means that we need to define a new name.  That’s not so bad, is it?  It could be worse, at least we don’t have to define a <a href="">cache invalidation</a> strategy.</p></div></content>
  1337.    <updated>2015-01-17T10:55:26-08:00</updated>
  1338.  </entry>
  1340.  <entry>
  1341.    <id>,2004:3344</id>
  1342.    <link href="/blog/2015/01/11/URL-Work-Status"/>
  1343.    <link rel="replies" href="3344.atom" thr:count="1" thr:updated="2020-07-04T01:09:27-07:00"/>
  1344.    <title>URL Work Status</title>
  1345.    <summary type="xhtml"><div xmlns=""><p>I have <a
  1346. href="">test results</a> that
  1347. show that there is much work to be done.</p> <p>The most likely path forward
  1348. at this point is to get representatives from browser vendors into a room and
  1349. go through these results and make recommendations.  This likely will happen in
  1350. the spring, and in the SF Bay Area.  With that in place, I can work with
  1351. authors of libraries in popular programming languages to produce
  1352. web-compatible versions.  This work will take the form of bug reports,
  1353. patches, or — when required — authoring new libraries.</p></div></summary>
  1354.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="132" height="76" viewBox="0 0 132 76">
  1355.  <path d='M57,29c0-9,18-12,17-2c0,7-12,10-12,18v5h8v-3c0-7,13-9,13-21c-1-16-34-16-34,3zM62,53h8v8h-8z' fill='#371'></path>
  1356.  <circle cy='38' stroke='#371' fill='none' r='33' stroke-width='10' cx='66'></circle>
  1357.  <path d='M45,17l9,9l-9,10l-9-10zM67,17l9,9l-9,10l-10-10zM88,17l9,9l-9,10l-9-10zM14,28l9,9l-9,9l-9-9zM35,28l9,9l-9,9l-9-9zM56,28l9,9l-9,9l-9-9zM77,28l9,9l-9,9l-9-9zM98,28l9,9l-9,9l-9-9zM119,28l10,9l-10,9l-9-9zM45,39l9,9l-9,9l-9-9zM67,39l9,9l-9,9l-10-9zM88,39l9,9l-9,9l-9-9z' fill='#bdbdc5'></path>
  1358.  <path d='M44,13l9,31l9-31h25v3l-10,16c23,7,2,52-16,21l6-2c11,21,24-16,2-14v-3l9-15h-11l-13,44h-1l-10-31l-9,31h-1l-15-50h7l9,31l6-21l-3-10z' fill='#005A9C'></path>
  1359.  <path stroke='#000' d='M5,36h20l10,10l10-10l11,10l21-21l11,11l10-11l12,11h19v3h-20l-11-11l-10,11l-11-11l-21,21l-11-10l-10,10l-11-10h-19z' fill='#ffd652' stroke-width='0.5'></path>
  1360.  <path d='M88,49c11,24,22,11,26,5l-1-5c-12,20-24,2-25,0M109,21c-8-16-26,0-16,23c-4-23,12-29,17-16l4-8l-1-6'></path>
  1361.  <path d='M2,35h5v5h-5zM127,35h5v5h-5z'></path>
  1362.  <path d='M57,29c0-9,18-12,17-2c0,7-12,10-12,18v5h8v-3c0-7,13-9,13-21c-1-16-34-16-34,3zM62,53h8v8h-8z' fill='#371'></path>
  1363. </svg>
  1364. <p>I have <a href="">test
  1365. results</a> that show that there is much work to be done.</p>
  1366. <p>The most likely path forward at this point is to get representatives from
  1367. browser vendors into a room and go through these results and make
  1368. recommendations.  This likely will happen in the spring, and in the SF Bay
  1369. Area.  With that in place, I can work with authors of libraries in popular
  1370. programming languages to produce web-compatible versions.  This work will take
  1371. the form of bug reports, patches, or — when required — authoring new
  1372. libraries.</p>
  1373. <p>Status by venue:</p>
  1374. <dl>
  1375. <dt><b>WHATWG</b></dt>
  1376. <dd><p>At the WHATWG, I’m limited only by my own ability to do the work
  1377. required.  My biggest complaint remains that that the barrier to entry to
  1378. participate is too high.  This. however, is something entirely under my
  1379. control to fix for the specifications I’m working on.  I’m hopeful that
  1380. leading by example will cause others in the WHATWG to do likewise.</p></dd>
  1382. <dt><b>WebPlatform</b></dt>
  1383. <dd><p>I’ve had <a href="">some success</a>,
  1384. but virtually all of this is attributable to GitHub, not WebPlatform.  At the
  1385. moment, technical issues prevent me from updating the spec there.  These
  1386. issues started on December 24th and were promptly reported.  If this
  1387. continues, I’ll push the webspecs develop branch to a whatwg develop branch
  1388. and <a href="">migrate the
  1389. issues</a>.</p></dd>
  1391. <dt><b>W3C</b></dt>
  1392. <dd><p>There has been no demonstrable progress in the WebApps WG.  The <a
  1393. href="">TAG</a> seems generally supportive.  I
  1394. briefed the <a href="">AB</a>, but nothing has come
  1395. of that.  Same is <a
  1396. href="">true</a> for the
  1397. process CG.  I’m willing to try proposing a <a
  1398. href="">new
  1399. working group</a>.  Failing this, I believe that I have all the evidence I
  1400. need to convince the W3C Director that <a
  1401. href="">normative references</a>
  1402. to the Living Standard are the only viable alternative.  As Sherlock Holmes
  1403. was known to say: <em>when you have eliminated the impossible, whatever
  1404. remains, however improbable, must be the truth</em>.</p></dd>
  1406. <dt><b>IETF</b></dt>
  1407. <dd><p>I’ve <a
  1408. href="">met
  1409. with</a> Area Directors.  I’ve participated on the <a
  1410. href="">apps-discuss
  1411. mailing list</a>.  With the help of <a href="">Larry
  1412. Masinter</a>, I’ve produced and published a <a
  1413. href=";modeAsFormat=html%2Fascii">problem
  1414. statement</a>.  Sadly, this seems like a clear case of <em>you can lead a
  1415. horse to water, but you can’t make it drink</em>.  Should this change, I have
  1416. until <a href="">February
  1417. 5th</a> to propose a BOF.</p></dd>
  1418. </dl>
  1420. <p>More details and links are available in the
  1421. <a href="">README</a>.</p></div></content>
  1422.    <updated>2015-01-11T06:46:06-08:00</updated>
  1423.  </entry>
  1425.  <entry>
  1426.    <id>,2004:3343</id>
  1427.    <link href="/blog/2015/01/08/Ununzippable-Modern-IE"/>
  1428.    <link rel="replies" href="3343.atom" thr:count="5" thr:updated="2016-01-07T02:00:58-08:00"/>
  1429.    <title>Ununzippable Modern.IE</title>
  1430.    <summary type="xhtml"><div xmlns="">I’ve downloaded the multi-part zip archive for IE11 on Win10 for VirtualBox on OS/X from <a href=""></a>.  I’ve downloaded the single-file archive on both OS/X and Linux.  I’ve verified the md5 signatures for each.  Yet each time, when I try to unzip the result, I fail.</div></summary>
  1431.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="100" height="100" viewBox="0 0 100 100">
  1432.  <path d="M57,11c40-22,42-2,35,12c8-27-15-20-30-11z" fill="#47b"></path>
  1433.  <path d="M36,56h56c4-60-83-60-86-6c13-16,26-26,36-30l-29,53c20,23,64,26,79-12h-30c0,14-26,12-25-5zM37,43c0-17,26-17,26,0zM39,89c-10,7-42,15-26-16l29-52c-15,6-36,40-37,48c-12,35,14,37,37,20" fill="#47b"></path>
  1434. </svg>
  1435. <p>I’ve downloaded the multi-part zip archive for IE11 on Win10 for VirtualBox on OS/X from <a href=""></a>.  I’ve downloaded the single-file archive on both OS/X and Linux.  I’ve verified the md5 signatures for each.  Yet each time, when I try to unzip the result, I get the following:</p>
  1436. <pre class="code">$ unzip
  1437. Archive:
  1438. warning []:  4294967296 extra bytes at beginning or within zipfile
  1439.  (attempting to process anyway)
  1440. file #1:  bad zipfile offset (local header sig):  4294967296
  1441.  (attempting to re-compensate)
  1442.  inflating: IE11 - Win10.ova        
  1443.  error:  invalid compressed data to inflate</pre>
  1444. <p>I’ve even tried <a href="">jar xf</a> with no luck:</p>
  1445. <pre class="code">$ jar xf
  1446. invalid entry size (expected 5632888297048912 but got 4801961472 bytes)
  1447. at
  1448. at
  1449. at
  1450. at
  1451. at
  1452. at
  1453. at</pre>
  1454. <p>This shows signs of <a href="">integer overflow</a>, so it seems likely that the problem is client side.  Even with that said, choosing to make a this content available in a format for which there isn’t working client libraries available to unpack it isn’t helpful.</p>
  1455. <p>I’m submitting this link as <a href="">feedback</a>.</p></div></content>
  1456.    <updated>2015-01-08T03:55:41-08:00</updated>
  1457.  </entry>
  1459.  <entry>
  1460.    <id>,2004:3342</id>
  1461.    <link href="/blog/2015/01/06/New-PhantomJS-and-Capybara-fan"/>
  1462.    <link rel="replies" href="3342.atom" thr:count="2" thr:updated="2020-07-04T04:55:46-07:00"/>
  1463.    <title>New PhantomJS and Capybara fan</title>
  1464.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="96" height="104" viewBox="0 0 96 104">
  1465.  <path d='M4,88c4,3,10,1,16,3c4,1,6,11,11,9c5-1,14-6,20-4c6,1,13,12,17,7c4-5,6-16,16-15c10,1,12,1,11-12c-1-7,5-13-3-18c-13-9-34-3-46,5c-14,10-47,20-42,25' fill='#000' fill-opacity='0.23'></path>
  1466.  <path d='M82,43c0,22,9,27,9,37c0,5-12,1-17,4c-4,3-2,7-9,9c-4,2-10-6-17-6c-5,0-14,7-19,4c-4-2-4-9-10-10c-6,0-19,7-19,1c0-7,8-14,8-39c0-23,17-43,37-42c21,0,37,20,37,44' fill='#ccc' fill-opacity='0.63' stroke='#000'></path>
  1467.  <path d='M33,22c-5,0-9,4-9,10c0,6,4,10,9,10c4,0,7-2,9-6c1,4,4,6,8,6c5,0,9-4,9-10c0-6-4-10-9-10c-4,0-7,2-8,6c-2-4-5-6-9-6' fill='#fff' stroke='#000'></path>
  1468.  <circle cx="36" cy="34" r="4"></circle>
  1469.  <circle cx="48" cy="34" r="4"></circle>
  1470.  <path d='M69,15c2,0,9,9,10,19l2,23c1,6,10,22,6,21c-8,0-13-13-12-33c3-21-9-29-6-30M73,82c-2,2-6,11-7,7c-2-4-1-11-2-22c-1-8-5-18-2-16c3,1,5,5,7,13c3,7,6,16,4,18M45,85c-2,2-6,4-9,3c-3,0-3-10-2-17c1-6,4-16,6-17c2-2,0,6,2,18c2,8,5,11,3,13
  1471. M20,79c-2,0-5,0-7,1c-3,1,0-4,4-11c3-6,4-12,5-14c2-2,0,6-1,13c0,7,1,11-1,11' fill-opacity='0.12'></path>
  1472. </svg>
  1473. <p>While I’m clearly late to the party, I’ve already become a huge fan of <a href="">capybara</a> and <a href="">phantomjs</a>.  I’m now using both with my <a href="">previously mentioned</a> <a href="">blogging software</a> rewrite.</p>
  1474. <p>My original intent was to aggressively prune unnecessary function with the intent of producing a more maintainable result, but with the ability to have automated acceptance tests, this is now less of a concern.</p></div></content>
  1475.    <updated>2015-01-06T11:47:40-08:00</updated>
  1476.  </entry>
  1478.  <entry>
  1479.    <id>,2004:3341</id>
  1480.    <link href="/blog/2015/01/05/Apple-Apostasy"/>
  1481.    <link rel="replies" href="3341.atom" thr:count="8" thr:updated="2015-01-06T16:33:30-08:00"/>
  1482.    <title>Apple Apostasy</title>
  1483.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns='' width="90" height="100" viewBox="0 0 90 100">
  1484.  <path d='M62,0c2,10-9,24-20,24c-3-14,9-22,20-24M5,36c5-8,13-12,21-12c7,0,12,4,19,4c6,0,10-4,19-4c6,0,14,3,19,10c-16,4-15,35,3,39c-7,17-18,27-24,27c-7,0-8-5-17-5c-9,0-11,5-17,5c-7-1-13-7-17-13c-9-10-15-40-6-51' fill='#AAA'></path>
  1485. </svg>
  1486. <p>Looks like <a href="">Why I quit OS X</a> struck a nerve — it is currently down (see <a href="">web archive</a>).  Also good: <a href="">Apple has lost the functional high ground</a>.</p>
  1487. <p>I particularly like the comment that <em>“It just works” was never completely true</em>.  My experience is that when working with open source codebases, doing so on an Linux operating system comes much closer to “It just works” than doing so on any other.</p></div></content>
  1488.    <updated>2015-01-05T12:09:46-08:00</updated>
  1489.  </entry>
  1491.  <entry>
  1492.    <id>,2004:3340</id>
  1493.    <link href="/blog/2015/01/03/Rack-broke-Sinatra"/>
  1494.    <link rel="replies" href="3340.atom" thr:count="3" thr:updated="2020-07-04T02:23:48-07:00"/>
  1495.    <title>Rack broke Sinatra</title>
  1496.    <content type="xhtml"><div xmlns=""><svg style="float:right" xmlns="" width="90" height="111" viewBox="0 0 90 111">
  1497.  <g stroke-linejoin="bevel" stroke-linecap="square" fill="none" stroke="#000">
  1498.    <path d="M6,15l30-10l49,11v82v-82l-24,8v83v-83l-55-9v83l56,8l23-8" stroke-width="4"></path>
  1499.    <path d="M6,98l27-9v-13l-26-4l27-8v-20l-27-5l28-8v-11v11l49,10l-24,8l-26-5v20l50,8l-24,9l-27-5v13l51,8" stroke-width="2"></path>
  1500.  </g>
  1501. </svg>
  1502. <p>Not rack’s fault, but Sinatra hasn’t released in a while.  Problem has been known since <a href="">July</a>, and a fix was merged into master in <a href="">August</a>.  One <a href="">possible workaround</a> has been posted.  An alternate workaround:</p>
  1503. <pre class="code">module Rack
  1504.  class ShowExceptions
  1505.    alias_method :old_pretty, :pretty
  1506.    def pretty(*args)
  1507.      result = old_pretty(*args)
  1508.      def result.join; self; end
  1509.      def result.each(&amp;block);; end
  1510.      result
  1511.    end
  1512.  end
  1513. end</pre></div></content>
  1514.    <updated>2015-01-03T17:31:33-08:00</updated>
  1515.  </entry>
  1517. </feed>

If you would like to create a banner that links to this page (i.e. this validation result), do the following:

  1. Download the "valid Atom 1.0" banner.

  2. Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)

  3. Add this HTML to your page (change the image src attribute if necessary):

If you would like to create a text link instead, here is the URL you can use:

Copyright © 2002-9 Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda