<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sam Thorogood]]></title><description><![CDATA[Hello there. I'm Sam.]]></description><link>https://samthor.au/</link><image><url>https://samthor.au/i128.png</url><title>Sam Thorogood</title><link>https://samthor.au/</link></image><generator>?</generator><lastBuildDate>Sun, 17 May 2026 04:06:53 GMT</lastBuildDate><atom:link href="https://samthor.au/rss.xml" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Base64 Is Fast Now, Actually]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2026%2Fbase64-is-fast%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="base64-is-fast-now%2C-actually">Base64 Is Fast Now, Actually</h1>
<p>This is a short post.</p>
<p>I was wondering recently, if I had to send a binary file across a text stream to a browser - i.e., embedded in JSON - what would be the fastest way to do that.</p>
<p>Clearly, base64'ing the content is a valid approach and always has a 4/3 overhead.</p>
<p>However, one thought I had is that JS strings are fundamentally just arrays of unsigned 16-bit integers (or <code>[]uint16</code>, if you're writing lots of Go, like me).
So we can actually just convert any kind of data to a JS string and then encode it <em>again</em> as UTF-8 without risk of loss.</p>
<h2 id="the-thesis">The Thesis</h2>
<p>What we do is basically take a <code>Uint8Array</code> (&quot;native bytes&quot;), reinterpret it as a <code>Uint16Array</code>, then convert it to a JS string.
The code looks a bit like:</p>
<pre><code class="language-ts"><span class="hljs-keyword">function</span> <span class="hljs-title function_">encode</span>(<span class="hljs-params">raw: <span class="hljs-built_in">Uint8Array</span></span>): <span class="hljs-built_in">string</span> {
  <span class="hljs-keyword">const</span> u16 = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Uint16Array</span>(raw);
  <span class="hljs-keyword">const</span> s = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TextDecoder</span>(<span class="hljs-string">&#x27;utf-16le&#x27;</span>).<span class="hljs-title function_">decode</span>(u16);
  <span class="hljs-keyword">return</span> <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(s);
}
</code></pre>
<p>…this actually doesn't work for two reasons:</p>
<ol>
<li>
<p>our data might have an odd number of bytes</p>
</li>
<li>
<p><code>TextDecoder</code> actually generates <code>U+FFFD</code> for bad data, which random binary data will have - it's not <em>actually string data</em>.</p>
</li>
</ol>
<p>Both can be solved - in Node, using <code>Buffer</code>, and in the browser using a few hacks.</p>
<h2 id="the-results">The Results</h2>
<p>Here is a table of all the results (for a single encode/decode round-trip in ms, but tests run 100s of times).
I used an audio file of about 1.4mb, and a zipped version of the same file (~600k).</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/enc-chart.png" width="1204" height="746" alt="Chart comparing speed differences for encoding data" />
  <figcaption>time in ms; compressed tends to be faster, even ignoring size differences</figcaption>
</figure>
<p>So: native base64 is stupidly fast.
But encoding via UTF-16 string isn't far behind.</p>
<p>To explain the options:</p>
<ul>
<li>
<p>native base64: this is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64"><code>fromBase64</code></a> and friends, available in Node and all browsers <a href="https://caniuse.com/mdn-javascript_builtins_uint8array_tobase64">from September 2025</a></p>
</li>
<li>
<p>utf-16 in Node: this is the above technique, using <code>Buffer</code></p>
</li>
<li>
<p>utf-16 in browser: we can't use <code>Buffer</code> in the browser, so we have to use <code>TextDecoder</code> with a few hacks</p>
</li>
<li>
<p>number array: a baseline of literally converting a <code>Uint8Array</code> to <code>number[]</code> and encoding/decoding it (e.g., <code>[123,0,3,1,255,...]</code>)</p>
</li>
<li>
<p>atob/btoa: the 'classic' way of doing base64.
This is slow because it has to round-trip through as JS string which contains values 0-255.
Most polyfills use this approach.</p>
</li>
</ul>
<p>The code to generate this data <a href="https://github.com/samthor/enc-speed">is here</a>.</p>
<h2 id="analysis">Analysis</h2>
<p>The UTF-16 approach is interesting, and remarkably fast.
It generates a totally valid string, a random excerpt looks like:</p>
<blockquote>
<p>&quot;…弼並于忒퍛孾븭䍗伐뾠䫼藸왬愣邲틫違纝Ꟍ牿\udf00왻﫶星멝ꟁ郧葓놹쑘澓燕㺪枢궅᧧澵춢呪垦ൢួ醝⠷銗䇎\udd2c흅蝗ꟛ퍾慙瘝\udc9d縱쯙꾵\udeef篙䶄Ḛ煩忌೷떵䵺업\ude9f輫\udcdc㾺뤻촸㯆孟\udcb9꡸뉺捸蜹躯액ꌀ瑍幘㕢譎뉛ᡦḝ萿힓ܞꪾඞ䣾闧忓槢\udf8e⭖㹠❎⁼ﷳ봝霗טּ棭\ud8dfጚ᭲ꄝ졷憢뙡뷪憓ä䧻弱ᘋᢃ䘶ر哤樹Ꮴ䮙ᔾᎻꩮꐱ趫\ud875摁狃渣ᅝ쿮꣖곷㰰뗊뼥ꎝ᜷\udbcfﴞᮽ轺稯올熽䝂鏅᧵눎᜾ᐋ퉖鋲\udaee쉼⇅뾠奰᳨詻닞굉\ud82dϓ綋놼☳뎭㳉趽畵ꨨ恫ꬬ洕ꈞ㨷鮊ꨋጦ渚陾翿䂼콦爧섺樼ඓ浧乖\ud872༗ḻ࿀徚ꟽ\udfa1疋庨㼖븖⦊⿧…&quot;</p>
</blockquote>
<p>But it's also not any smaller than base64.
For the two files, the UTF-16 approach is 173% and 154% of the original size, respectively.</p>
<p>Remember, what we're doing is:</p>
<ul>
<li>taking a <code>Uint8Array</code></li>
<li>reading it as a JS string via <code>Uint16Array</code></li>
<li>encoding each code point (or surrogate pair) as UTF-8, so it's valid JSON.</li>
</ul>
<p>But part of the issue is that most data here ends up having no valid representation in UTF-8, so it ends up being escaped - that's the <code>\udc9d</code> and friends we see above.</p>
<p>And yes, we could start inventing our own binary formats… but that's not the point, the theory is that this must still be valid JSON.</p>
<p><em>Whereas</em>, base64 is consistently 4/3, or 133% the size of the original.</p>
<h2 id="so-what%3F">So What?</h2>
<p>This article has a pretty clear outcome, which was literally in the title.</p>
<p><strong>You should probably encode data using the native base64 methods.</strong>
They're really fast and consistent.</p>
<p>For users on old Chrome, - provide a polyfill maybe for the next year or so, which will be slower, but basically penalize them for not upgrading.</p>
<p>Thanks for reading! 🕺</p>
]]></description><link>https://samthor.au/2026/base64-is-fast/</link><guid isPermaLink="true">https://samthor.au/2026/base64-is-fast/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate></item><item><title><![CDATA[Major Node Changes]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2Fnode-releases%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="major-node-changes">Major Node Changes</h1>
<p>This document is a short list of changes in Node.js' major releases.
It does not cover point releases, just e.g., v13, v14 and so on.</p>
<h2 id="v25-(2025)">v25 (2025)</h2>
<ul>
<li>v8 14.1</li>
<li>Supports <code>Uint8Array</code> base64/hex helpers</li>
<li>Type stripping for TS support enabld by default
<ul>
<li>This does not <em>transform</em> types, you may still need <code>--experimental-transform-types</code></li>
</ul>
</li>
</ul>
<h2 id="v24-(2025)">v24 (2025)</h2>
<ul>
<li>v8 13.6
<ul>
<li>Adds <code>Float16Array</code>, <code>RegExp.escape</code></li>
<li>Adds explicit resource management, aka, the <code>using</code> keyword</li>
</ul>
</li>
<li>Improves <code>AsyncLocalStorage</code> (async context tracking) performance</li>
<li>Modifies the test runner to automatically wait for subtests</li>
<li><code>URLPattern</code> is now a global</li>
</ul>
<h2 id="v23-(2024)">v23 (2024)</h2>
<ul>
<li>Better support for <code>require(esm)</code> in more places, for interopability</li>
<li>Mark <code>--run</code> as stable</li>
</ul>
<h2 id="v22-(2024)">v22 (2024)</h2>
<ul>
<li>v8 12.4
<ul>
<li>Including <code>Array.fromAsync</code>, additional <code>Set</code> methods</li>
</ul>
</li>
<li>Experimental support for <code>require()</code> on ESM</li>
<li>Node itself can <code>--run &lt;script&gt;</code> from &quot;package.json&quot;</li>
<li>Native <code>WebSocket</code></li>
<li>Glob functions inside <code>node:fs</code></li>
<li>Support for <code>--experimental-transform-types</code> to <a href="https://samthor.au/2024/node-run-typescript/">run TS inside Node</a></li>
</ul>
<h2 id="v21-(2023)">v21 (2023)</h2>
<ul>
<li>Experimental support (behind a flag) for
<ul>
<li>native <code>WebSocket</code></li>
<li>default ESM support</li>
</ul>
</li>
<li>Added global <code>navigator</code> object</li>
<li>v8 11.8, including array <code>groupBy</code></li>
</ul>
<h2 id="v20-(2023)">v20 (2023)</h2>
<ul>
<li>V8 11.3, including change array by-copy methods such as <code>toSorted</code></li>
<li>Experimental permission model (e.g., restrict FS access)</li>
<li>Marks the test runner as stable</li>
</ul>
<h2 id="v19-(2022)">v19 (2022)</h2>
<ul>
<li>Adds <code>node --watch</code></li>
<li>V8 10.7</li>
</ul>
<h2 id="v18-(2022)">v18 (2022)</h2>
<ul>
<li>Adds built-in Fetch API (<code>fetch</code>)
<ul>
<li>Includes <code>Headers</code>, <code>FormData</code>, <code>Blob</code> and so on</li>
<li>Adds Web Streams API including <code>ReadableStream</code> and friends</li>
</ul>
</li>
<li>Adds <a href="https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel"><code>BroadcastChannel</code></a></li>
<li>Adds experimental <a href="https://nodejs.org/dist/latest-v18.x/docs/api/test.html">test runner</a></li>
<li>V8 10.1
<ul>
<li>Includes <code>Array.findLast()</code> and <code>Array.findLastIndex()</code></li>
</ul>
</li>
<li>Built-in <a href="/2022/builltin-nodejs-test/">test runner</a></li>
</ul>
<h2 id="v17-(2021)">v17 (2021)</h2>
<ul>
<li>V8 9.5</li>
</ul>
<h2 id="v16-(2021)">v16 (2021)</h2>
<ul>
<li>Adds <code>'timers/promises'</code> API</li>
<li>V8 9.0
<ul>
<li>Adds the <code>/d</code> flag to regular expressions</li>
</ul>
</li>
<li>Native support for arm64 on macOS</li>
<li>Adds <code>AbortController</code> as stable</li>
</ul>
<h2 id="v15-(2020)">v15 (2020)</h2>
<ul>
<li>NPM 7 <a href="https://github.com/nodejs/node/pull/35631">#35631</a></li>
<li>Throw On Unhandled Rejections <a href="https://github.com/nodejs/node/pull/33021">#33021</a>
<ul>
<li>If you create a <code>Promise</code> without a catch handler, or use a top-level <code>await</code>, a failure will cause your program to crash by default</li>
</ul>
</li>
<li>V8 8.6
<ul>
<li>Supports new operators <code>&amp;&amp;=</code>, <code>||=</code>, and <code>??=</code></li>
</ul>
</li>
</ul>
<h2 id="v14-(2020%2C-lts-until-2023-04-30)">v14 (2020, LTS until 2023-04-30)</h2>
<ul>
<li>ES Modules no longer generate warnings</li>
<li>Supports top-level <code>await</code></li>
<li>V8 8.1
<ul>
<li>Supports optional chaining, nullish coalescing and friends (the <code>?.</code>, <code>??</code> operators)</li>
<li>Improved <code>Intl</code> support</li>
</ul>
</li>
</ul>
<h2 id="v13-(2019)">v13 (2019)</h2>
<ul>
<li>Support for <a href="https://nodejs.medium.com/announcing-core-node-js-support-for-ecmascript-modules-c5d6dc29b663">ECMAScript modules</a>
<ul>
<li>Includes <code>.mjs</code> extension and setting <code>&quot;type&quot;: &quot;module&quot;</code></li>
</ul>
</li>
<li>Full ICU support, with support for hundreds of locales</li>
</ul>
]]></description><link>https://samthor.au/node-releases/</link><guid isPermaLink="true">https://samthor.au/node-releases/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[Why Will Wikipedia Wither?]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2025%2Fwhy-will-wikipedia-wither%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="why-will-wikipedia-wither%3F">Why Will Wikipedia Wither?</h1>
<p><em>aka: in the age of AI, what is the role of summarizers</em></p>
<p>Wikipedia is the world's free encyclopedia.</p>
<p>Some definitions: an encyclopedia summarizes knowledge—let's call this primary sources—in a useful, digestable format.
And primary sources are, well, primary!
Research papers, quotes, police reports, press releases, survey data, photos, stats on Pokémon, medical records, …the list goes on.</p>
<p>So maybe you can see where this is going. 👀</p>
<p>Wikipedia intends to be an unbiased, representative summary of primary source data.
(Whether it is or not is a different post, but let's go with this.)</p>
<p>I have this theory that, as we as a society head down the infinite slippery slide of all-knowing AI, anything that 'summarizes'—not just Wikipedia, but also literal journalism—has no utility in a world where an AI can <em>just do this for you</em>.</p>
<h2 id="star-trek">Star Trek</h2>
<p>There's a famous memable scene from Star Trek: The Voyage Home, released in 1986, a few months after I was born.
Scotty tries to talk to the computer.
Laughable at the time, but today this is something <em>we can just do</em>.</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/hello-computer.gif" alt="A scene from 1986's Star Trek movie" width="220" height="173" class="round" />
  <figcaption>go ahead, <a href="https://www.youtube.com/watch?v=hShY6xZWVGE">watch the whole scene</a>, as a treat</figcaption>
</figure>
<p>When I think about the future, I have great respect for sci-fi writers of the past: what did they imagine, even and often especially concerning the seemingly inane world-building that purely fills out the scene.</p>
<p>(As a digression, I love that in The Expanse, various scenes feature <a href="https://en.wikipedia.org/wiki/Intermodal_container">intermodal containers</a> but <em>IN SPACE</em>.
Because why would humanity redesign the standard? 📦🚀)</p>
<p>I won't be the first or last person to think about this, but I think that Star Trek predicted this new world aeons ago (and the newer series basically approach it the same way).</p>
<p>In Star Trek, or other sci-fi canon, the primary way our protagonists—or honestly, average citizens—interact with technology is indirect, via what we now know as LLMs or whatever, that summarize and present information derived from primary sources.</p>
<p>These sources may be &quot;is the ship on fire&quot;, but also &quot;what is the population of planet XYZ&quot;, or &quot;who was the victor in the Great War of 2775&quot;.</p>
<p>We don't see our stars sit around reading blog posts trying to glean some insight into the culture of a new race they're meeting, not just because it'd make for pretty lackluster TV, but because this is a 'solved problem' in their universe.</p>
<h2 id="deep-implications">Deep Implications</h2>
<p>Let's apply this to today's world, or say the world we want to build given the advent of LLMs (and LLMs masquerading as &quot;agentic AI&quot;).</p>
<p>I think it basically tells us that the web is dying.</p>
<p>This is a big take.
My job at Google for years was to promote it.</p>
<p>But even in the early days of Gemini or ChatGPT, I found that the best use of AI was basically just to 'summarize' some content, news or otherwise, that was otherwise covered in ads and cookie notices.
And while I do think content sites have realized their hubris here, and generally pulled back on some of the noise, I don't see a world where this business model works.</p>
<p>But to take this point to its logical extremes, why do say, news sites even exist?
I'm aware this is a horrific idea—we're killing off journalism—but at its core, journalism is reporting on <em>what happened</em>, i.e., primary sources.</p>
<p>Instead, why not simply ask an all-knowing AI to summarize those same primary sources?
Isn't that what journalists and Wikipedia do?</p>
<p>Again, it goes without saying that this (as it stands today) is a terrible idea:</p>
<ul>
<li>
<p>journalists often (but not always) collect/prepare primary sources: they conduct interviews, go on-site to take photos, investigate and reveal hidden data, etc</p>
</li>
<li>
<p>the bias of journalists is (sometimes) known; for example, I don't believe articles on The Australian (despite being behind a paywall) are written with good intentions; similarly, the ABC often promotes deeply unscientific views in the name of showing 'both sides'</p>
</li>
<li>
<p>more fundamentally, what is a primary source—how can an AI identify fake data saying that 90% of students are secretly furries or that vaccines cause autism</p>
</li>
</ul>
<p>Back to my point about the web: Google is diving head-first into the death of the web, because I can answer my queries with <a href="https://searchengineland.com/google-search-zero-click-study-2024-443869">zero-click searches</a>—that's even without using Gemini explicitly.
And of course, ChatGPT <em>was never</em> a search engine to begin with, so every answer it gave you was zero-click.</p>
<p>So why have the results at all?</p>
<h2 id="critical-thinking">Critical Thinking</h2>
<p>I haven't thought this out too much—I'm going to write another view on what AI means for education, especially for the sort of future I want my young kids to experience.</p>
<p>But right now, in 2025, some students live in the ludicrous world you already know about: they press a button to receive an essay, which is submitted to a teacher who might just pass it back to another AI to grade it.</p>
<p>One naïve interpretation here is that this may weed out performative assessments.
Is the kind of counter-intuitive 'benefit' here we can dismiss with topics that students don't actually want to learn?
Is this today's calculator?
(Of course, this assumes they want to learn <em>anything</em>.)</p>
<h2 id="primary-sources">Primary Sources</h2>
<p>I think we're moving towards a world where the importance of primary sources, content, whatever, is raised.</p>
<p>This blog post is itself a primary source—it's an opinion, which arguably still counts.
And LLMs will happily slurp it up probably minutes after I post it, just like every bit of content out there.</p>
<p>And cynically, you could twist my argument here to mean that everyone should be a &quot;content creator&quot;.</p>
<p>But I actually kind of mean that.
We should all be collecting (and publishing) <a href="https://www.youtube.com/watch?v=eDr6_cMtfdA">small data</a>.</p>
<p>If AIs are free, and they are effectively all-knowing (again, big ifs) then the benefit here is that they can identify new, novel data or connect the dots between data in interesting ways.
The average research paper is read by zero people (rounded down).</p>
<p>Is the next big thing going to be the idea of collecting new and novel data, or at least working out how to best and most effectively publish your own discoveries?</p>
<p>Will the new journalist class not publish 'articles' as we know now, but purely publish their primary discoveries in as plain-text as possible for a LLM to consume for it to surface to the world?</p>
<h2 id="discovery">Discovery</h2>
<p>Going back to Star Trek v. Wikipedia (2025), one contrasting issue with how we imagine people in the future interact with content is that of discovery.</p>
<p>A better question is where are the doom-scollers in the Star Trek universe?
Again, this doesn't make for great content, but is <a href="https://en.wikipedia.org/wiki/Brad_Boimler">Boimler</a> sitting in his bunk reading a book (AI-generated or not), or doom-scrolling his
TikTok feed?</p>
<p>And zero-click searches are great for <em>answers</em>, but are seemingly not great for discovering more.
(To be fair, maybe that will change.)
But for better or worse, Wikipedia is curated, and it has novel things like <em>headings</em> that I can scan to find out whether I really want to dig into the 'controvery' section for my favorite actor.</p>
<p>There's also the concern that an agent acting on my behalf will reinforce my biasies.
If I only want to know if my favorite actors own a dog, every question will start reporting that in favor of something that pushes me off that path.</p>
<h2 id="parting-thoughts">Parting Thoughts</h2>
<p>I don't know why I wanted to write this post.
I think I'm trying to imagine the world I want, given the constraints—given where we seem to be heading.</p>
<p>Do search engines survive this change?
Does journalism survive?
Does it need to?</p>
<p>These are all interesting questions I don't have the answer to.
Anyway, bye 👋</p>
<!-- 


## ...

- background: Wikipedia intends to be a summary of its primary sources
- thesis: Why does it exist when I can ask the AI to summarize those same sources?

- thought experiment: what does sci-fi tell us about content?
  - "reading blogs" is boring
  - but they all exist in a world of aggregation / computer aided summaries

Pros

- better discovery/inference of primary sources - not limited to the 'biased' selection on wikipedia page
  - e.g., celebrity "controversy" section - draw from quotes, police reports etc

Cons

- 'discover' is diminished
  - we don't read all of a Wikipedia page, but we scan for relevant parts, look at headings, other cues etc
- issues with implicit bias

Ramifications

- is the skill of "generating primary content" more important in the future?
   - we have a tool which aggregates / draws conclusions / creates interesting links
   - rounded down, no-one reads most research papers - this is a way to surface them

- we lose critical thinking skills
  - will this happen anyway (chatGPT writing my essay; it's a performative exercise)
  - can we focus on skills kids actually want to learn
    - (noble thought that they actually want to learn something)

Primary content

- how do we (or the AI) correctly identify "primary content"?
  - does every human get a private key that signs their work? (doesn't stop signing generated content) -->]]></description><link>https://samthor.au/2025/why-will-wikipedia-wither/</link><guid isPermaLink="true">https://samthor.au/2025/why-will-wikipedia-wither/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 25 May 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[All Browsers Get This Wrong]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2025%2Fall-browsers-get-this-wrong%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="all-browsers-get-this-wrong">All Browsers Get This Wrong</h1>
<p>We found a bug in Shadow DOM that's strangely consistent across Firefox, Safari and Chrome.
And this bug is strange: it's not actually a user-facing issue, but rather, something the developer tools across each browser just <em>gets wrong</em>. 🤯</p>
<h2 id="background">Background</h2>
<p>I'm a huge fan of Web Components, despite their various downsides.
Part of this was my time at Google: <a href="/2022/react-preact-wc/">I was paid to believe in them</a>, and it's hard to get out of that habit.</p>
<p>In building my startup <a href="https://gumnut.dev">Gumnut Dev</a> (we make your forms collaborative), we found something odd.</p>
<p>Our use-case for WCs is a bit different than normal.
We <em>provide</em> an element <code>&lt;gumnut-text&gt;</code> which drops-in to replace your <code>&lt;input&gt;</code> and <code>&lt;textarea&gt;</code> elements—but instead of being plain old editors, this editor now connects you to other users on the same page.
See a gif:</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/gumnut-demo.gif" alt="Shows a few users working on one collaborative editor" width="448" height="271" class="round" />
  <figcaption>Gumnut: the modern textbox.</figcaption>
</figure>
<p>Sounds great.
This element isn't styled by default, because we're a platform—we don't have an opinion on your styles.</p>
<p>But of course, it's been tricky to get it right: read on!</p>
<h2 id="issue">Issue</h2>
<p>WCs typically use Shadow DOM, which has a bit of an odd quirk.</p>
<p>The <code>display: ...</code> CSS value of your host element (i.e., <code>&lt;gumnut-text&gt;</code>) is used as the top-level <code>display: ...</code> property of the Shadow DOM.</p>
<p>Who cares, you ask?
Well, we care, because if you as a <em>user</em> of this element were to accidentally set the <code>&lt;gumnut-text&gt;</code> element to <code>display: inline</code>... this happens:</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/gumnut-issue.png" alt="The box model being funky on a textbox" width="1118" height="396" class="round" />
  <figcaption>Something is technically wrong</figcaption>
</figure>
<p>Now, who is going to do this, you ask?
Well, we're not sure, but one thing you learn shipping what is fundamentally an API to your users: you want to prevent them from &quot;holding it wrong&quot;, because if they can, they will. 🫠</p>
<h3 id="my-learning">My Learning</h3>
<p>I always thought this ability to override values was a limitation or a 'dumb choice' of WCs.
You have this element which doesn't really act independently because your end-users can muck with it.</p>
<p>However, I was wrong. 🥺</p>
<p>When you style your Shadow DOM, you create style which looks like this and can reference the special <code>:host</code> selector.
This selector refers to &quot;the container&quot;, so in our case, it styles <code>&lt;gumnut-text</code>.
Something like this:</p>
<pre><code class="language-css"><span class="hljs-selector-pseudo">:host</span> {
  <span class="hljs-attribute">display</span>: inline-block;
}
</code></pre>
<p>What I didn't know is that if you set <code>!important</code> on this rule, <strong>nothing can overwrite it</strong>.
This <code>:host</code> declaration always wins, even over 'external' style, the <code>style=&quot;...&quot;</code> attribute, even if that value is also set to <code>!important</code>.
And this is fundamentally counter-intuitive to my view of CSS priority.</p>
<p>So let's imagine I have an element like this (SD isn't defined like this, but let's pretend):</p>
<pre><code class="language-html"><span class="hljs-tag">&lt;<span class="hljs-name">gumnut-text</span> <span class="hljs-attr">style</span>=<span class="hljs-string">&quot;display: inline !important&quot;</span>&gt;</span>
  ::shadow-root
    <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="language-css">
      <span class="hljs-selector-pseudo">:host</span> {
        <span class="hljs-attribute">display</span>: inline-block <span class="hljs-meta">!important</span>;
      }
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>…your resolved <code>display</code> value will be <code>inline-block</code>.
Huzzah!</p>
<p>And of course, this applies to all properties; not just <code>display</code>: you could control your element's <code>border</code>, or <code>padding</code>, or <code>transform</code>.</p>
<h3 id="where-browsers-are-wrong">Where Browsers Are Wrong</h3>
<p>So the key of this post is that my uneducated view is actually not dissimilar to literally every browser's view, as they all have a bug in explaining what happens here.
And this is problematic, because it masks the behavior—it's not discoverable!</p>
<p>Let's look at what Chrome says:</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/chrome-display.jpeg" alt="Chrome DevTools with issues" width="1000" height="1770" class="round" />
  <figcaption>Chrome is a bit confused</figcaption>
</figure>
<p>Firefox and Safari are similar.
They all know the final resolved value, but can't explain how they got there.</p>
<h2 id="why-do-i-care%3F">Why Do I Care?</h2>
<p>Well, you care because this greatly expands your ability as an API author to ship components that <em>you control</em>.
WCs are a perfect fit for shippable components, because you don't have to rely on the user also using React, or Vue, or whatever framework of the week… they can <em>just work</em> regardless of your target.</p>
<p>And even though we at <a href="https://gumnut.dev">Gumnut Dev</a> also ship <a href="https://www.npmjs.com/package/@gumnutdev/react">React components</a> wrapping up our WCs, those components still expose styling/etc attributes that apply to the internal <code>&lt;gumnut-text&gt;</code>.
So without this control, it would be just as easy to shoot yourself in the foot and &quot;hold it wrong&quot;, even though users of React ostensibly have a layer of abstraction in the way.</p>
<p>Thanks for reading!
If you're interested in drop-in collaboration (no more overwriting each other's work), audit logs, history through character-level attribution—and having your forms work like they're from the future, getting out of the way of your <em>actual business</em>—do say <a href="mailto:hello@gumnut.dev">hello</a>. ♥️</p>
<h2 id="attributions">Attributions</h2>
<p>And finally, thanks to <a href="https://bsky.app/profile/bram.us/post/3lmvyckc4rc2n">bram.us</a> for clarifying the spec on how this should work.
Cheers!</p>
]]></description><link>https://samthor.au/2025/all-browsers-get-this-wrong/</link><guid isPermaLink="true">https://samthor.au/2025/all-browsers-get-this-wrong/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[Collaboration, Now!]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2025%2Fcollaboration-now%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="collaboration%2C-now!">Collaboration, Now!</h1>
<p><em>Update: If you're in Sydney in July 2025, join <a href="https://gumnut.dev/hack">our hackathon</a>!</em></p>
<p>I've a bold claim to make.
I think the web is stuck, stagnant, immovable, … more synonyms. 🛑</p>
<p>We're stuck in, fundamentally, a React-pinned era.
Around ~2018 or so, React had the complete suite: functional components and hooks.
And modern app development, whether you like it or not, is largely inspired by <em>that kind of idiom</em>—whether you're using actual React or one of the many libraries that <em>try to be like React</em>. 🤯</p>
<p>Instead of making the web better, we as developers are just trimming the hedge of its walled garden.
It's not growing, changing or evolving. 🧱</p>
<h2 id="multi-user-is-not-collaboration">Multi-User Is Not Collaboration</h2>
<p>Let's get right to it.
I've co-founded a startup called <a href="https://gumnut.dev">Gumnut Dev</a> because we want to bring the web <em>forward</em> into the future—and for us, that means collaboration.</p>
<p>Our problem statement—more specific than 'the web is stagnating'—goes something like this:</p>
<blockquote>
<p>Your product is designed for multiple users—but can they actually work <em>together</em>?
Or are they just digitally bumping shoulders and passing like ships in the night?</p>
</blockquote>
<p>Gumnut Dev enables inline collaboration within your existing forms, with no database changes.
Increase productivity between your real users <em>and</em> lets AI agents contribute natively.
And we store history over time, down to the keystroke, of who did what.</p>
<p>Here's a little preview:</p>
<figure>
  <video alt="demo of two users typing inside Gumnut" muted autoplay loop width="1516" height="856" class="round">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/gumnut-demo.mp4" type="video/mp4" />
  </video>
  <figcaption>demo of two users working together to edit a form</figcaption>
</figure>
<p>We're not solving every problem with the web.
Maybe later.
But for now, we're solving collaboration for your SaaS/product/web application 💖.</p>
<p>So why are we founding the startup?
It behooves us to start with…</p>
<h3 id="a-history-lesson-%F0%9F%93%9C">A History Lesson 📜</h3>
<p>Google Docs released in … let me check … two-thousand and six.
That's nearly twenty years ago.</p>
<p>And so as software engineers, as product managers, even as <em>users</em> of a product: we all <em>know</em> collaboration is possible.
But we look at Google Docs, or Notion, or Miro, or… any one of the number of collaboration <em>tools</em>, and somehow we believe they're special.</p>
<p>And so, collaboration outside these experiences is totally alien to the average user.
Why?</p>
<h3 id="cost%2Fbenefit-%F0%9F%92%B8">Cost/Benefit 💸</h3>
<p>Our big thesis is two-fold.
One is the broad React-stagnation problem.
The modern, app-like web, is stagnant.</p>
<p>The other is that the cost/benefit isn't there.
Why would I spend time adding something that isn't <em>really</em> core to my product?
I don't want to have to pivot to using a different database, or running a websocket for real-time editing, or—the list goes on.</p>
<p>And if you <em>do</em> want collaboration, you have to ask your CTO/management… can we get multiplayer content editing superpowers users expect in our own app without spending the next 12 months building something that doesn't meet expectations?</p>
<p>So of course, that's what got us thinking. 🤔</p>
<h3 id="the-right-dx-%F0%9F%A7%91%E2%80%8D%F0%9F%92%BB">The Right DX 🧑‍💻</h3>
<p>Collaboration is a relatively solved problem.
Technically, academically—there are solutions.
<a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDT</a>s, or <a href="https://en.wikipedia.org/wiki/Operational_transformation">Operational Transforms</a>, and products that implement them—they exist.
And they've come a <em>long way</em>.
CRDTs, for example, were classically overlooked because they were 'too heavy'.
Smarter people than me have worked out that they can be <a href="https://josephg.com/blog/crdts-go-brrr/">incredibly performant</a>.</p>
<p>This is all interesting.
But these are experiments at best: they're not a product you can <em>just use</em>.</p>
<p>We at Gumnut Dev are building a <em>product</em>: a new paradigm of how you can just add these things to your existing sites, web applications, and SaaS'es.</p>
<p>And ironically, despite my bellyaching, using Gumnut Dev is just a React library you drop in:</p>
<pre><code class="language-ts"><span class="hljs-keyword">function</span> <span class="hljs-title function_">YourForm</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// models other form libraries, but with collaboration!</span>
  <span class="hljs-keyword">const</span> s = <span class="hljs-title function_">useGumnutDoc</span>({ getToken, docId });

  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Edit AI Prompt<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">GumnutText</span> <span class="hljs-attr">control</span>=<span class="hljs-string">{s.control}</span> <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;prompt&quot;</span> <span class="hljs-attr">multiline</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{...}</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">GumnutData</span>
      <span class="hljs-attr">control</span>=<span class="hljs-string">{s.control}</span>
      <span class="hljs-attr">name</span>=<span class="hljs-string">&quot;enabled&quot;</span>
      <span class="hljs-attr">render</span>=<span class="hljs-string">{(arg)</span> =&gt;</span> (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>Enabled<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;checkbox&quot;</span> {<span class="hljs-attr">...arg.field</span>} /&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      )}
    /&gt;
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
}
</code></pre>
<p>Okay, there's a tiny bit more setup, but we're talking hours, not days or weeks, to get this sorcery working. 🎩</p>
<h2 id="some-internals">Some Internals</h2>
<p>This is all well and good—Sam, you're pitching me a startup, I get it.
But I assume my audience is technical enough to want to know a little bit more before diving in.</p>
<p>I think the best way to represent what do do under the hood is with a little video.</p>
<figure>
  <video alt="explainer showing steps of how Gumnut works" muted autoplay loop width="1512" height="1080" class="round">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/gumnut-explainer.mp4" type="video/mp4" />
  </video>
  <figcaption>let's show the steps for a Gumnut editing session</figcaption>
</figure>
<p>The way to think of what we do is: we get out of the way of your existing database, and host an editing session—think of it like a <code>git</code> branch—where people can work together.</p>
<p>Once your editing is complete, Gumnut orchestrates a simultaneous commit back to your database <em>and</em> a full-history snapshot witten back to Gumnut.
Your database doesn't know or care that the work was prepared in our &quot;temporary collaboration dimension&quot;; it just sees the new data, ready to be used.</p>
<h3 id="implications">Implications</h3>
<p>The core design is pretty—I don't want to say simple, because what Gumnut does <em>is</em> complex, and you <em>should</em> pay us to do it for you (no bias here).</p>
<p>But now that we help you <em>edit</em>, you can think of a user editing something in a boring old HTML form as not just this transient thing that happens in their browser, but something that <em>has a life of its own</em>.</p>
<p><strong>1. Collaboration</strong></p>
<p>The obvious implication, which I introduced at the top, is collaboration.
You're working on something <em>together</em>.
And who gets to work together, that's something we help you configure (via JWTs and fairly standard authorization practices).</p>
<p>Frankly, this stops your users <em>leaving</em> to go to a platform like Google Docs to get their work done.
You want users to be sticky, more engaged, and expecting <em>more</em> from your site—so you're easier to use than your competitors.
Ask yourself: how often do people get off your platform because the only way they <em>can</em> be productive is with an external tool?</p>
<p><strong>2. Review and approval</strong></p>
<p>Because an edit session is no longer just &quot;in someone's browser&quot;, why not ask/demand/expect that your content is reviewed before it's saved back to your database?
Gumnut could email or contact a list of reviewers—just like how we write code—and ask for their feedback before it allows you to save. 💾</p>
<p><strong>3. History</strong></p>
<p>Every time you commit, your product's database updates.
That's the way it should be—your database is for <em>running</em> your business.</p>
<p>But Gumnut can keep track of what you changed over time.
And this is an incredibly important feature that most SaaS'es overlook, at least initially: you want, nay, you <em>need</em>, a proper audit trail.</p>
<p>And if you've not thought about this, and you have compliance risk, whew!
This is the only part of this post which is trying to elicit 'fear', which is one way to promote a product.</p>
<p>But it's also important. 💭</p>
<p>Gumnut stores keystroke-level attribution every time you commit: you know who changed what, and when, and you can go back to that later.
You might load it via API, or inline in your own forms in a &quot;history&quot; mode—but the important part is that we store it for you for free, so when shit hits the fan, you can find out how you got there.</p>
<p>And again, this is all without modifying your existing database solution.</p>
<p><strong>4. AI Agents</strong></p>
<p>AI is a bit of an orthogonal dimension.
But right now, a regular SaaS with a multitude of forms doesn't have a great way to have AI help you.
Instead, AI agents can be collaborative users just like anyone else on your site—ask them to fix spelling mistakes, do rewrites, or just work with you to achieve a goal.</p>
<p>Here's a demo of something simple, just coming along and cleaning up spelling mistakes:</p>
<figure>
  <video alt="an AI agent fixing spelling mistakes" muted autoplay loop width="1248" height="260" class="round">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/spelly.mp4?v1" type="video/mp4" />
  </video>
  <figcaption>the AI can do <em>far</em> more than this, but it's a simple place to start</figcaption>
</figure>
<p>And because we're storing history—we know what the AI added versus anyone else.
So if you think the AI has hallucinated, well, you can find out exactly what parts you can cut out.  🔪</p>
<p><strong>5. More?</strong></p>
<p>We have a lot more ideas, but then this blog post would go on forever. 😂</p>
<p>But we fundamentally believe Gumnut is a new way of thinking about working together on the web—one that stays away from your existing choices <em>in a good way</em>—add us to your platform to make your product better.</p>
<h2 id="what-now%3F">What Now?</h2>
<p>Why are we building this startup?
We want to move the web forward.
And for me, that piece is collaboration—why shouldn't it come cheaply, with history and AI agent support built-in?
It's crazy to me that we are stuck in this rut, using amazing technologies all around the place but being totally unable to integrate it in 'regular' products.</p>
<p>To my original thesis: there's other parts of the web that need improvement, too—we'll come back to them once our first product is wildly successful. 😂</p>
<p>So we're eager for you to try it out, book a demo, all those things—the &quot;like and subscribe&quot; of the SaaS world.</p>
<p>👉 Hit us up at <a href="https://gumnut.dev">https://gumnut.dev</a>.
Book a demo, or join our update list.</p>
<p>Thanks for reading!
You're officially ahead of 99% of the internet. 💝</p>
]]></description><link>https://samthor.au/2025/collaboration-now/</link><guid isPermaLink="true">https://samthor.au/2025/collaboration-now/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate></item><item><title><![CDATA[Post-Request/Response Apps]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2024%2Fpost-request-response-apps%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="post-request%2Fresponse-apps">Post-Request/Response Apps</h1>
<p>Serverless or monolithic/'serverful' architectures are often presented as sides of the same coin: they're both just alternative ways to serve synchronous requests, for example, a HTTP server serving content, or handling API requests publicly or internally.</p>
<p>But this is a naïve viewpoint, and we as developers now almost see 'serverless-like' development as the silver bullet for potentially enormous scalability, throwing away the imperative nature of writing long-lived server code.
We've collectively forgotten how to write servers!</p>
<p>(Remember that serverless is broadly defined as computing which scales easily, but with no reliable state—every request <em>may</em> be handled by a new process, which is orthogonal to a long-lived server.)</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/dragons.png" alt="A reductive system design for a webapp" width="2814" height="1024" class="round" />
  <figcaption>Let's just focus on how to serve the green bit as fast as possible, yeah?</figcaption>
</figure>
<p>Of course, it's fairly easy to list the pros and cons of serverless vs monolith through the lens of synchronous requests <em>only</em>: many articles do.
But this doesn't consider an application, system, whatever you're doing, <em>holistically</em>—you're focusing only on the pointy end, how your components interact.</p>
<p>This article will talk through why serverless adds needless complexity, and provide actual concrete examples of how, where and why to adopt monolithic—importantly, <em>horizontally scalable monolithic</em>—approaches to backend design.
Read on. 🥳</p>
<h2 id="background-on-serverless-vs-other-approaches">Background on Serverless vs Other Approaches</h2>
<p>Again, serverless is computing that scales easily, with the tradeoff that it's stateless.
(And it can be great for work that's <em>actually</em> stateless!)</p>
<p>We see fairly broad implementations of serverless:</p>
<ul>
<li>you have <a href="https://workers.cloudflare.com/">Cloudflare Workers</a>, which pushes your code right to the edge, and has amazing performance properties</li>
<li>AWS Lambda is the original, but it really embodies the above definition: yes, it scales, but it's slow to start up and each instance only handles one request at once (which is painful when you're probably further I/O bound within the Lambda)</li>
<li>Google's <a href="https://cloud.google.com/functions">Cloud Functions</a> is a nice middle ground, as <a href="https://cloud.google.com/blog/products/serverless/cloud-functions-2nd-generation-now-generally-available">its v2</a> supports concurrency</li>
</ul>
<p>All of these can be wired up to serving user requests, webhooks, callbacks, whatever (the world is HTTP, after all). It's also worth honing in on what 'stateless' actually means—every request handled by your serverless code <em>may</em> be in a fresh VM.
(Although in reality, most providers reuse VMs for a short time, or this is configurable—restarting is costly for them too.)</p>
<p>On the other hand, we have non-serverless code, where a VM's lifetime is much longer (it won't live forever, but who does), and typically can serve many requests in parallel.
Environments that run this code often (but not always) allow you to scale <em>horizontally</em>, allowing you to run a configurable number of instances which are used based on the requestor's geolocation, server load, even randomly, or perhaps with a session affinity cookie/header.</p>
<ul>
<li>AWS has Fargate, which is their solution for &quot;just let me scale VMs please&quot;</li>
<li><a href="https://fly.io">fly.io</a> provides these options, as it provides a single anycast address plus HTTPS certs for any service you run—doing intelligent routing and running code worldwide</li>
<li>your suggestion here?</li>
</ul>
<h2 id="thesis">Thesis</h2>
<p>My thesis is that serverless' <em>biggest trade-off</em> is it being stateless.
Because running code this way is intentionally unreliable, serverless requires you to express your state externally: at best in a database, and at worse as the machniations of a complex system with queues, events and state offloaded to your cloud provider's… <em>&quot;primitives&quot;</em>.</p>
<p>We, as software engineers, already have a great way to hold state: <em>in memory</em>.</p>
<p>And the advent of serverless has encouraged us to think of all possible serving options <em>as analogies</em> to serverless, where their only job is to serve requests as fast as possible with the existential dread that the process may be evicted at any point.</p>
<p>We, as backend developers, have collectively forgotten how to write code that considers the current process' interaction with a client to be a long-term arrangement.
And, to be fair, this isn't guaranteed either, short of using a monolith (that is, O(1) server, rather than O(n)) server<em>s</em>): the internet is a hectic place, and two HTTP requests made by the same client might not be routed the same way.</p>
<h2 id="the-alternative">The Alternative</h2>
<p>So, Sam, you say that serverless is poor, but then disregard that point by telling me that we should either use an O(1) monolith <em>or</em> give up because HTTP requests will route oddly.
What's your point?</p>
<p>Well, the point is that there's a number of patterns that blow serverless's complexity right up, and we should actively be seeking out imperative solutions (even if a bit tricky) rather than just adding <em>more serverless</em>: those &quot;primitives&quot; I mentioned earlier.</p>
<p>To quote a developer I <a href="https://freakonomics.com/series-full/people-i-mostly-admire/">mostly</a> admire:</p>
<blockquote>
<p>The solution to serverless always looks like more serverless.</p>
</blockquote>
<p>…the meaning here being that serverless taints your thinking: it's always <a href="https://knowyourmeme.com/memes/one-more-lane-bro-one-more-lane-will-fix-it"><em>one more construct</em></a>, rather than just being able to write imperative code.</p>
<p>And while that above explanation sounds good, it's still abstract.
There's two main points you need to consider when architecting something that encapsulates long-term state.</p>
<ol>
<li>
<p>What types of tasks are good candidates.
This is the concrete 🏗️ example part—what are the goals with serverless that you keep thinking, if only I could bend over backwards <em>even more</em>…</p>
</li>
<li>
<p>How you can ensure that an end-user can access that state.
This is subtle, but important, because of the HTTP request routing above.
If a horizontal monolith is taking on some task, but a client (website, app, or even other service) no longer has affinity to that target (whatever reason), how do you 'join' again?</p>
</li>
</ol>
<h2 id="1.-serverless-sucks-for">1. Serverless Sucks For</h2>
<h3 id="medium-running-tasks">Medium-Running Tasks</h3>
<p>Let's say you have a user-initiated batch task: something that you'd love to do a in single HTTP request from a client, but which takes <em>just a bit too long</em>, say a data conversion or an export task.
And you're thinking—oh no, do I need to set up queues, event busses, more infrastructure just to do this thing?
Think a few minutes' long, where you'd like to give user feedback.</p>
<p>Serverless tends to make this hard because:</p>
<ul>
<li>serverless functions tend to have hard execution limits (although they are going <em>up</em> these days)</li>
<li>But they are modelled entirely as request/response beasts—yes, you could have the function emit status update logs you consume elsewhere, but fundamentally they're not designed to give intermediate feedback.</li>
</ul>
<p>With long-running instances, we can have the ability to simply <em>do work</em> for a long period of time, disconnected from the request/response cycle.
What a concept! 🤯</p>
<p>You might architect this through a task that subscribes to a database (used like a queue), enacts a lock on a task, and undertakes it.
If the job dies, the lock expires and you pick up the task again.</p>
<p>Or you could start these tasks in direct response to a user's network request, and your frontend simply undertakes some CPU-bound work for a while before its result chills in memory for the user, or is pushed back via a socket.</p>
<h3 id="single-homed-logic">Single-Homed Logic</h3>
<p>Think of Google Docs as a good example.
Each document has a unique ID and is fundamentally brought into memory and <em>run</em> somewhere when it is opened by any number of users.
It exists in a database, yes, but a client 'joins' a single process which manages its <a href="https://en.wikipedia.org/wiki/Operational_transformation">OT</a> logic.</p>
<p>In a serverless land… oh wow.
This is almost untenable; you <em>could</em> do it by having each operation (a keystroke, pasting text) talk to any serverless function, open a shared database, and perform an atomic operation on that database.
Simultaneously, the client is polling or fetching changes from that same database, so it can keep in sync with other clients.</p>
<p>This really rolls up two really tricky things for serverless:</p>
<ul>
<li>a place to run long-term imperative code over time without the risk of ☠️</li>
<li>global locks, or a way to say &quot;this instance is the host&quot; of the document 🔒🎯</li>
</ul>
<p>And the same idea could apply to more complex applications, for example, building a Slack or Discord-like service might have each 'server' run its logic imperatively on a unique machine.</p>
<blockquote>
<p>🌈 Shameless Plug 🌈 This is one of the use-cases of <a href="https://locksrv.dev">locksrv</a>, my offering that can provides fast yet global locking of unique IDs.</p>
</blockquote>
<p>But also, you can use <a href="https://developers.cloudflare.com/durable-objects/">Cloudflare's Durable Objects</a> for this purpose, too.</p>
<p>To be fair, this problem doesn't need <em>global</em> locks—each document ID could itself embed a home region (where most of its users are), and then a lookup to a fast key/value store like Dynamo could tell you who's currently the boss.</p>
<h3 id="agent-behavior">Agent Behavior</h3>
<p>Not <a href="https://en.wikipedia.org/wiki/Agent_Smith">that kind of agent</a> 🕴️.</p>
<p>I've danced around this point, but one of the approaches we've lost when building serverless is the idea that your interaction with a website, app, whatever, is actually both happening in the client <em>and on the server</em>.
And instead of just connecting to a grabbag of lambda helpers, we could connect to a task, the agent, which is dedicated to helping <em>us</em>.</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/agents.png" alt="Two alternative approaches to server design" width="2246" height="1018" class="round" />
  <figcaption>Your user is valuable, so why not act on their behalf?</figcaption>
</figure>
<p>Now, an agent here might not map 1:1 to each e.g., open browser tab—maybe it's an agent per sign-in, so the same auth details get help from the same VM.
Maybe it's an agent or pool of agents per company using your premium SaaS.</p>
<p>Why does an agent help you?
It acts as your generalized bridge into a hosted part of any product, doing things on behalf of the user.
Now, that's abstract, I confess, but…imagine a world where:</p>
<ul>
<li>you're using AWS, and you're recieving some update over SQS or an event bus</li>
<li>these services <em>don't</em> have a client-facing version, i.e., they can't be directly wired to an end user</li>
<li>it's totally <em>possible</em> to rig up serverless code to itself be called on an event, which puts something into a database, which a user polls, but…you see where I'm going here.</li>
</ul>
<p>Instead, have an agent working on behalf of the user which when instructed, listens to SQS and identifies events for its client.
It could even fall back to polling a database or checking logs if no answer arrives in time.
It then pushes the update over a user's open socket, or stores it in local memory until the user is able to read the answer—if the user 'disconnects' for a minute, the agent stays awake and ready for a short timeout!</p>
<p>What about a simpler example?
A user could make a complex SQL or SQL-like query, where the results are complex to compute.
While a backend can quickly return a small number of responses, an <em>agent</em> could continue preparing more—with the assumption that another request will arrive soon after for additional results.</p>
<p>The point being here is that dances like this should be compartmentalized inside <em>simple, imperative code</em> rather than a million lines of config orchestrating ostensibly cheap 'on-demand' infrastructure. 🔥</p>
<h2 id="2.-accessing-that-state%3B-or%2C-http-is-ephemeral">2. Accessing That State; or, HTTP is ephemeral</h2>
<p>However, back to my earlier issue: if HTTP requests route anywhere—isn't this just serverless all over again?—what's even the point of doing this work? 🤔🤔🤔</p>
<p>Again, this could be solved by an <em>actual monolith</em>, where you only have a single server.
But that isn't the point of this post.</p>
<h3 id="socket">Socket</h3>
<p>One way to guarantee that the user will get access to whatever imperative code is to <em>actively pin the connection to a remote server</em>, literally via a long-running connection: a <code>WebSocket</code>.
(In the future, this will include <code>WebTransport</code>, but it's not widely available yet.
You could also use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">Server-Sent Events</a>, but this is only ½ of the solution.)</p>
<p>This punches a hole through any kind of odd routing or concern about affinity.
Your client can directly connect to a server which might do work for you.</p>
<p>There's caveats: sockets are good for the 'agent' model, where you only have O(1) connection per-tab (or perhaps per <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">service worker</a>, but that's a whole other blog post).
And while the point of this post is that you can have long-lived code <em>with</em> horizontal scaling, if you don't have enough load to be distributed evenly, then one server VM may end up randomly being assigned too many socket connections.</p>
<p>Pedants might note that AWS, for example, has a <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-chat-app.html">serverless <code>WebSocket</code> solution</a>.
But it's fundamentally an antipattern: it <em>removes</em> all benefits of WebSocket, instead mapping it back to request/response.
It gives none of the benefits of a long-lived socket.</p>
<h3 id="http-or-request%2Fresponse">HTTP or request/response</h3>
<p>We could attempt to emulate a socket-style experience in HTTP with pure hope—modern HTTP/2+ is designed to use the same 'connection' for many requests.
But we can't guarantee that, and imagine a network drop: even our reliable <code>WebSocket</code> can disconnect and need to retry.</p>
<p>There's a couple of approaches here, some of which I've already alluded to:</p>
<ul>
<li>
<p>Most cloud providers allow for session affinity (via cookie, their front-ends will route you) or targeting specific VMs.</p>
<p>AWS has <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html">&quot;sticky sessions&quot;</a>, Google has <a href="https://cloud.google.com/load-balancing/docs/backend-service#session_affinity">affinity</a>, Fly.io allows <a href="https://fly.io/docs/networking/dynamic-request-routing/">targeting a specific instance</a>—any client in a relationship with a backend can retry its requests with this custom header.
If one of your servers sees a request for its peer—redirect it that way!</p>
</li>
<li>
<p>Using serverless-style front-ends which themselves perform affinity routing to a further layer based on your own code.
This can be <em>fairly</em> stateless, and less 'obvious' to evil actors—the affinity token might be encrypted or actually be a lookup to a global lock service (again, 🌈 <a href="https://locksrv.dev">let me know if I can help</a> 🌈).</p>
</li>
</ul>
<h3 id="best-effort">Best Effort</h3>
<p>Finally, the 'agent' example above doesn't actually care that much about session affinity—or, at least, you can leverage it when it works (which, again, is often in the world of HTTP/2+) and just accept when it doesn't as a &quot;cost of doing business&quot;.</p>
<p>Think of it like a boring old cache implemented in memory, but instead of purely caching some, e.g., database rows, you're actually 'caching' some running code on behalf of a user assuming that you'll see them again.
And if you <em>don't</em> see them again, there's no harm, the code stops after a timeout; the client will retry and hit another process which starts the agent anew.</p>
<h2 id="discussion">Discussion</h2>
<p>What is Serverless good for?
I want to be clear, this is not (entirely) a hit piece.
But it should be used sparingly, for reasons like:</p>
<ul>
<li>glue code between other services which need to trigger a callback—this is <em>literally</em> <a href="https://aws.amazon.com/blogs/aws/run-code-cloud/">the origin of it at AWS</a></li>
<li>short CPU-bound work that can be surprisingly unexpected for your service—if you say, batch convert images for the public, maybe you can do it here</li>
<li>as a stateless replacement for awkward configuration languages—e.g., Cloudflare/CloudFront Workers can set headers or wrap up tricky results</li>
<li>small/underutilized services where running 24/7 might be costly—although platforms like Fly.io and Cloud Run can actually mitigate that, by giving you far more control over your VM's start and stop times.</li>
</ul>
<p>I think the complexity of real servers (or VMs, whatever) can also be a lot for new developers.
I get that.
Firebase Functions (just wrapped Google Cloud Functions), for example, is pitched at a low/no-code world: you largely have a static site and just need to slowly add some dynamic logic.
As long as your model fits in that request/response model, sure—but don't let the tool limit your thinking that even toy apps can't or don't need a long-lived component.</p>
<h2 id="finishing-up">Finishing Up</h2>
<p>I'll say it again: I've seen serverless being used because it's viewed as this 'silver bullet' for one day landing on the front page of Hacker News.
And that it's &quot;all people know these days&quot;—just using React as a stack, which is almost just an attempt to make your job posting seem sexy at this point—so of course you'll mention the buzzword when hiring.
(I would!)</p>
<p>So while serverless <em>can</em> have its place, it represents the unfortunate baseline that other approaches tend to be measured against in common discussion.
And the reality is that it brings every other possible approach down to its level.</p>
<p>Cheers 👋</p>
<p>I run a course called <a href="https://galileo.ventures/polaris">Polaris</a> for upcoming leaders and CTOs of tech startups in Sydney, Australia, where I have <em>opinions</em> like this.
I'm also available <a href="/about/consulting/">for consulting</a> if you'd like to pay me to rephrase the content in this post especially for your company. 💸</p>
]]></description><link>https://samthor.au/2024/post-request-response-apps/</link><guid isPermaLink="true">https://samthor.au/2024/post-request-response-apps/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Thu, 10 Oct 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[Node.js can run TypeScript]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2024%2Fnode-run-typescript%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="node.js-can-run-typescript">Node.js can run TypeScript</h1>
<p>This is a fairly quick post, but it's impactful if you use Node.js.</p>
<p>Node.js can now <em>just run</em> TypeScript locally, without needing <code>tsx</code> or a build step.</p>
<h2 id="how">How</h2>
<p>Node v22 has added an experimental flag, <code>--experimental-transform-types</code>.
This supercedes a previous <code>strip</code> flag, <em>because</em> it supports code-based TypeScript features like <code>enum</code>.
(Maybe there's some features still missing, but it's &quot;good enough&quot; for now.)</p>
<p>To run a TypeScript file, you can now:</p>
<pre><code class="language-bash">$ node --experimental-transform-types file.ts
</code></pre>
<p>Just like that. 🤯</p>
<h3 id="configure-your-shell">Configure Your Shell</h3>
<p>Instead of passing the flag around everywhere, you can configure your environment to always do it.
Inside your &quot;.bashrc&quot;, &quot;.zshrc&quot;, or whatever, add:</p>
<pre><code class="language-bash"><span class="hljs-built_in">export</span> NODE_OPTIONS=<span class="hljs-string">&quot;--experimental-transform-types --disable-warning=ExperimentalWarning&quot;</span>
</code></pre>
<p>This means that every invocation of Node will transform TypeScript files, <em>and</em> it'll muffle the egregious warning that was being dumped in your terminal.</p>
<h3 id="configure-your-projects">Configure Your Projects</h3>
<p>This is tricky.
If you write a &quot;package.json&quot; file with scripts, you can't assume all users might have these flags configured.</p>
<p>You can add to <code>scripts</code>:</p>
<pre><code class="language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;yourproject&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;test&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;NODE_OPTIONS=&#x27;--experimental-transform-types --disable-warning=ExperimentalWarning&#x27; node --test **/*test.ts&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>This is a bit gross, but means that anyone with a sufficiently high version of Node will get the feature.</p>
<blockquote>
<p>Note that the above sample also uses Node.js' built-in test runner, which is also one of the greatest things since sliced bread. 🔪🍞</p>
</blockquote>
<h2 id="promotion">Promotion</h2>
<p>At some point, this flag will probably be enabled by default.
Will it be in Node 23, or 24?
Who knows.
<a href="/node-releases">Watch this space</a>.</p>
<h2 id="module-benefits">Module Benefits</h2>
<p>While subtle, the other huge benefit of built-in TypeScript support is that code like this now works:</p>
<pre><code class="language-ts"><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Worker</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node:worker_threads&#x27;</span>;

<span class="hljs-keyword">const</span> x = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Worker</span>(<span class="hljs-string">&#x27;./other-module.ts&#x27;</span>);
<span class="hljs-keyword">const</span> other = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">&#x27;./other.ts&#x27;</span>);
</code></pre>
<p>i.e., you can import <em>dependant</em> code even if it's written in TypeScript.</p>
<p>This is one of those things that <em>should never</em> have been hard, and now it's just solved—it was previously difficult to both transpile and for a tool like <code>tsx</code> to handle.</p>
<h2 id="fin">Fin</h2>
<p>Short post!
Enjoy. 💞</p>
]]></description><link>https://samthor.au/2024/node-run-typescript/</link><guid isPermaLink="true">https://samthor.au/2024/node-run-typescript/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[Kuto, a reverse JS bundler]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2024%2Fkuto%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="kuto%2C-a-reverse-js-bundler">Kuto, a reverse JS bundler</h1>
<p><a href="https://kuto.dev">Kuto</a> is a novel approach to shipping code on the web.
It lets you re-use code a client <em>already has</em> for shipping updates.</p>
<iframe width="736" height="414" src="https://www.youtube-nocookie.com/embed/OUi0Lh-gEy8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<p>For a 'real-world' site with ~3mb of JS, updating the React dependency resulted in:</p>
<ul>
<li>71% smaller download</li>
<li>28% faster start time (on a ~5yo old phone, a <a href="https://en.wikipedia.org/wiki/Pixel_3">Pixel 3</a>).</li>
</ul>
<p>…vs a single bundle, or any case where all the code is invalidated.</p>
<p>Note that Kuto works really well on the final ESM bundles of real sites or apps, but probably <em>not</em> libraries themselves, even though Kuto's output will be valid.
Kuto also works as a predictable 'chunk' generator for large bundles.</p>
<p>If this is interesting to you—do <em>you</em> have too much JavaScript?—then do the thing, and <a href="https://kuto.dev">do a Kuto on your code</a>.
(Is Kuto a verb?
Who knows.
I'm trying it out.) ✂️</p>
<h2 id="how-does-it-work%3F">How does it work?</h2>
<p>Instead of focusing on minifying output or anything idempotent, Kuto takes a different route.</p>
<ol>
<li>
<p>On the first build:</p>
<ul>
<li>Kuto splits source JS into a 'main' part, and a normally larger 'corpus' of code which has no side effects.</li>
<li>This corpus can be cached <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable">forever</a>, and a hashed timestamp is included in its output name.</li>
</ul>
</li>
</ol>
<ol start="2">
<li>
<p>On every further build:</p>
<ul>
<li>Kuto still splits out the source JS</li>
<li>It identifies code from any existing corpus that can be used to 'satisfy' the source JS
<ul>
<li>Each corpus will either stay the same or <em>shrink</em> as functions, statements etc change</li>
</ul>
</li>
<li>Any code that cannot be satisfied is put into a brand new corpus, which can also be cached forever.</li>
</ul>
</li>
</ol>
<p>This is a little complicated.
Here's a <s>gif</s> video:</p>
<figure>
  <video alt="demonstration of Kuto rebuilding stuff" muted autoplay loop width="1920" height="1080" class="round">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/kuto-demo.mp4" type="video/mp4" />
  </video>
  <figcaption>Kuto will build an additional corpus when something changes</figcaption>
</figure>
<p>After each build, you're likely to generate another corpus with changed code.
(This will eventually be an issue with fragmentation, more on this later.)</p>
<h2 id="why-does-it-work%3F">Why does it work?</h2>
<p>Each corpus, once fetched by a client, can be cached forever.
However, on every build, that corpus <em>may</em> shrink in size.
New clients will get the smaller version.
(Kuto's corpuses have a hashed timestamp, and you <em>do</em> have to set up your webserver to send the <a href="https://web.dev/articles/love-your-cache">right headers</a> here).</p>
<p>But…this breaks all we know about caching responses on the web!
I've changed an immutable file! 🤯</p>
<p>The result is that older clients will have bigger files, and newer clients will have smaller ones, even for the same filename.
But that bigger file simply has now-deprecated code.
Kuto's primary thesis is that disk I/O to load a slightly bigger file than you need is faster than compling anew.</p>
<p>(Note that at least <a href="https://v8.dev/blog/code-caching-for-devs">v8-based browsers</a> cache the bytecode of the source, which provides the speed benefit.
If you were compiling anew every time, Kuto wouldn't help.)</p>
<p>Aside the remarkably bonkers way this leverages your browser, Kuto also basically performs code-splitting in a completely predictable, useful and automated fashion.
Other bundlers either require you to:</p>
<ul>
<li>(not codesplit an output bundle at all)</li>
<li>explicitly mark dependencies to be put into their own bundle</li>
<li>put code in bundles on (effectively) random boundaries, just trying to restrict the size of each 'chunk'.</li>
</ul>
<h2 id="no-really%2C-how-does-it-work%3F">No really, how does it work?</h2>
<p>The above explanation was fairly high-level.
At a lower level, Kuto looks for code with no side effects to include in its corpuses, and it uses circular dependencies to ensure they're safe to call.</p>
<h3 id="no-side-effects">No Side Effects</h3>
<p>Turns out, defining a function has no side effects.
No, really!
The <em>definition</em> of a function does nothing except creates a variable:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params"></span>) {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">`Side effect`</span>);
}
</code></pre>
<p>Since <code>foo</code> isn't <em>actually called</em>, and we declare it in a module scope, nothing happens.
Until we run <code>foo</code>, it might as well be:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">foo</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// Blah blah blah blah, I&#x27;m Schrödinger&#x27;s function</span>
}
</code></pre>
<p>Kuto takes this theory to the extreme, putting classes into the same form, and even arbitrary statements.
We *hand wave* can hoist statements to be within a function.</p>
<h3 id="circular-dependencies">Circular Dependencies</h3>
<p>Each corpus contains silo'ed^ functions which reference back to the main file in order to work out their dependencies.
A good way to see this is to check out Kuto itself and run its &quot;./release.sh&quot; script, which builds Kuto itself this way.</p>
<p>For a trivial site that appends a custom element to your page, where the custom element has changed and been rebuilt, this might look like:</p>
<pre><code class="language-js"><span class="hljs-comment">// main.js</span>
<span class="hljs-keyword">import</span> { _1 } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./main.kt-abc.js&#x27;</span>;
<span class="hljs-keyword">import</span> { _2 } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./main.kt-def.js&#x27;</span>;
<span class="hljs-title function_">_1</span>();
<span class="hljs-keyword">export</span> { _2 };

<span class="hljs-comment">// main.kt-abc.js</span>
<span class="hljs-keyword">import</span> { _2 } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./main.js&#x27;</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">var</span> _1 = <span class="hljs-keyword">function</span> <span class="hljs-title function_">setupSite</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> element = <span class="hljs-keyword">new</span> <span class="hljs-title function_">_2</span>();
  <span class="hljs-variable language_">document</span>.<span class="hljs-property">body</span>.<span class="hljs-title function_">append</span>(element);
}

<span class="hljs-comment">// main.kt-def.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">var</span> _2 = <span class="hljs-keyword">class</span> <span class="hljs-title class_">MyElement</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">HTMLElement</span> { <span class="hljs-comment">/* ... */</span> };
</code></pre>
<p>This all looks awkward…and it is, but the output isn't really meant for human consumption, but we benefit from ESM's &quot;seen as a bug&quot; feature of circular dependencies.</p>
<p><small>^Kuto will in future reference <em>between</em> corpuses</small></p>
<h2 id="should-i-use-this%3F">Should I use this?</h2>
<p>Maybe!</p>
<p>Kuto is new, and while the science says it works, it's a bit weird.
And as I've mentioned above, it works really well on:</p>
<ul>
<li>single bundle output sites</li>
<li>that have a large JS bundle (maybe &gt;1mb?)</li>
<li>…which are made up of lots of top-level ESM code, such as functions and classes</li>
</ul>
<p>You will need to:</p>
<ul>
<li>use a regular bundler first</li>
<li>keep or have access to your old build artifacts—Kuto doesn't know what you <em>already shipped</em> by magic 🪄</li>
<li>trade off a slightly larger <em>first load</em> for a better update experience</li>
</ul>
<p>The other issue with this is that more and more browsers are moving to a world where cache is regularly evicted.
If your visitors don't have your site cached, then Kuto <em>is</em> pointless—every load is slightly more expensive for an update that never happens.
YMMV.</p>
<p>Regardless, the tool emits a few statistics, including how much overhead the initial load costing you, and what % of code is able to be identifed as having &quot;no side effects&quot; and put into a corpus.
And to be clear, running Kuto on tiny codebases has no benefit—the 'cost' of parsing a few kb of JS is trivial, even for potato phones. 🥔</p>
<p>So you can experiment and see if it works for you.
My view is that Kuto will help for enterprise apps (because the JS is bloated, and no-one <em>really</em> cares) or social media (because power users come <em>so often</em>).</p>
<h3 id="fragmentation">Fragmentation</h3>
<p>Kuto generates multiple files over time.
Right now, it actually only uses the top 4 (by size) previous bundles, although this is configurable by flag.
This is…not very good, and I'm yet to come up with a good automatic metric as to when to 'clean up' vs 're-use'.</p>
<p>Consider this though: if you just don't provide Kuto with historic bundles, it… obviously can't use them.
So you can decide whether that typo fix generating a 100-byte file is worth it for the <em>next build</em>.</p>
<h2 id="why-only-28%25-faster%3F">Why Only 28% Faster?</h2>
<p>Ignoring some of the possible downsides, there's a big question for me at play.
My test case above showed 71% reduction in size, but only 28% increase in speed. 🤔</p>
<p>I have a suspicion that v8's assembly of a bunch of module code <em>is still quite costly</em>.
Yes, it's great that huge chunks of static code can be outsourced and cached forever, but in the end, your website still needs to assemble it all together.</p>
<h2 id="an-ask">An Ask</h2>
<p>I would love to hear from you if Kuto makes a material difference to the way you bundle and update your code, even in just a theoretical test environment.
Please feel free to contact me, including filing <a href="https://github.com/samthor/kuto/issues/new">a GitHub issue</a>, if you can give me some sweet, sweet numbers.</p>
<h2 id="thanks">Thanks</h2>
<p>Thanks for reading!
Kuto has been incredibly interesting to build, yet honestly it's only taken me a few days of full-time engineering to make it work.
It's been an idea I've been noodling on for a few years, and I'm really proud to have it come to fruition. 🍇</p>
]]></description><link>https://samthor.au/2024/kuto/</link><guid isPermaLink="true">https://samthor.au/2024/kuto/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 24 Mar 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[CJS Equivalency]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2024%2Fcjs-equivalency%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="cjs-equivalency">CJS Equivalency</h1>
<p>I've been working on bundler-like things and I've thought again about the classic CJS vs ESM interop issue.
It occurs to me that one way to transform CJS to ESM is to treat each CJS file as a 'builder'.
This isn't <em>bundling</em>, but perhaps a step on the way, or for a serving library which purely provides a transform.</p>
<h2 id="example">Example</h2>
<p>Here's a reductive example.
We have a CJS file which has both a top-level and lazy import:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> other = <span class="hljs-built_in">require</span>(<span class="hljs-string">&quot;./other.js&quot;</span>);
<span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span>.<span class="hljs-property">performAction</span> = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> lazy = <span class="hljs-built_in">require</span>(<span class="hljs-string">&quot;lazy&quot;</span>);
  <span class="hljs-keyword">return</span> other.<span class="hljs-title function_">helper</span>(lazy);
};
</code></pre>
<p>Can be thought of as, with the original code largely unchanged in <code>internalBuild</code>:</p>
<pre><code class="language-js"><span class="hljs-keyword">import</span> otherBuild <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;./other.js&quot;</span>;
<span class="hljs-keyword">import</span> lazyBuild <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;./node_modules/lazy/index.js&quot;</span>;

<span class="hljs-keyword">const</span> builders = {
  <span class="hljs-attr">other</span>: otherBuild,
  <span class="hljs-attr">lazy</span>: lazyBuild,
};
<span class="hljs-keyword">const</span> <span class="hljs-title function_">require</span> = (<span class="hljs-params">name</span>) =&gt; builders[name]();

<span class="hljs-keyword">function</span> <span class="hljs-title function_">internalBuild</span>(<span class="hljs-params"><span class="hljs-variable language_">module</span></span>) {
  <span class="hljs-keyword">const</span> other = <span class="hljs-built_in">require</span>(<span class="hljs-string">&quot;other&quot;</span>);
  <span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span>.<span class="hljs-property">performAction</span> = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> lazy = <span class="hljs-built_in">require</span>(<span class="hljs-string">&quot;lazy&quot;</span>);
    <span class="hljs-keyword">return</span> other.<span class="hljs-title function_">helper</span>(lazy);
  };
}

<span class="hljs-keyword">let</span> cache;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">build</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">if</span> (!cache) {
    cache = { <span class="hljs-built_in">exports</span> };
    <span class="hljs-title function_">internalBuild</span>(cache);
  }
  <span class="hljs-keyword">return</span> cache.<span class="hljs-property">exports</span>;
}
</code></pre>
<p>In practice, you'd autogenerate this code.
Watch this space.</p>
<p>Note that <code>cache</code> is important as otherwise we won't get singleton behavior.
Each import or require of the same file should return the same object—it's only evaluated once.</p>
<h2 id="what's-missing">What's missing</h2>
<ol>
<li>
<p>The final user of the CJS file (whether browser, or a real ESM) needs to invoke its default export.
So you can't just rewrite the CJS—the caller needs to know, too.</p>
</li>
<li>
<p>The builder is still exposed on <code>default</code>, and you can't treat individual export properties as special—CJS is always just exporting 'a singleton'.</p>
</li>
<li>
<p>Dynamic <code>require()</code> statements don't have a good mapping here.
The equivalent in ESM works <em>because</em> it has to be <code>async</code>, and <code>require</code> isn't going to suddenly start supporting that now.
Anyway, no serious bundler supports this either.</p>
</li>
<li>
<p>Going back from CJS <em>to</em> ESM (i.e., a <code>require()</code> that points to a ES Module) could work the same way (moving it inside a builder), but now some of the seams are breaking a bit, because the point of ESM is that we can be purists about it and not rewrite most of that code.</p>
</li>
</ol>
<h2 id="why-this-works">Why this works</h2>
<p>I think my initial instinct was to try to convert each <code>require</code> call to be dynamic, somehow.
Instead, we can basically precalculate the graph in an ESM-style without actually executing <em>any</em> CJS code.
Importing a file which has been transformed will evaluate none of the original code, but set it up in a way <em>that it's ready to be</em>.</p>
<h2 id="why-this-post-exists">Why this post exists</h2>
<p>Why not.
You've read it.
Enjoy!</p>
]]></description><link>https://samthor.au/2024/cjs-equivalency/</link><guid isPermaLink="true">https://samthor.au/2024/cjs-equivalency/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Tue, 06 Feb 2024 00:00:00 GMT</pubDate></item><item><title><![CDATA[December Tech Vibe Check]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2023%2Fdecember-vibe-check%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="december-tech-vibe-check">December Tech Vibe Check</h1>
<p>It's the end of 2023.
This isn't a &quot;year in review&quot;, just some musings about what's been going on recently.</p>
<p>Read on to find out more about:</p>
<ul>
<li>What I'm doing in the Cloud</li>
<li>What open-source code I'm publishing</li>
<li>Some business plans and musings.</li>
</ul>
<h2 id="cloud-tech-stack">Cloud Tech Stack</h2>
<p>There's been some cool updates in the way I build projects.</p>
<h3 id="cloud-run">Cloud Run</h3>
<p>I bit the bullet and moved this blog over to Cloud Run with all the bells and whistles.
This is <em>fine</em>, but I end up paying for a myriad of various services just for what is a relatively simple site.</p>
<p>I pay for:</p>
<ul>
<li>the Cloud Run itself</li>
<li>a LB in front of it</li>
<li>the V4 address</li>
<li>the V6 address</li>
<li>the http -&gt; https redirector for <em>both v4 and v6</em></li>
<li>the CDN</li>
</ul>
<p>…yesh.
It actually honestly works really well, but it just feels like a lot for a tiny blog. 🤯</p>
<p>Also, it connects directly to my GitHub repository and builds whenever I push.
This is 👌: I previously had the incredibly awkward credential delegation stuff set up so GitHub could deploy on my behalf.
(I do need to also trigger regular builds somehow in order to update 'the most popular' posts, but hopefully I make enough changes that I'll regularly see this update <em>anyway</em>.)</p>
<p>By the way, the swap to Cloud Run was largely spurred by App Engine's <a href="https://www.google.com/search?q=app+engine+custom+domains+latency">broken Custom Domains feature</a> which added stupid amounts of latency.
Honestly, this was embarassing: I used to work at Google <em>literally on</em> how to make your sites faster, and my own blog was incurring ~100-200ms+ wasted latency for some users.</p>
<p>Anyway, as far as I can tell, this is a failure of Google's internal teams to talk to each other (it's not run by Cloud, oh no) <em>and</em> a bit of legacy around what IPs it uses.
So as usual, just use the new shiny because no-one at Google wants to fix, document or care about the broken feature.</p>
<h3 id="fly.io">Fly.io</h3>
<p>I've been hacking with <a href="https://fly.io">Fly.io</a> lately.
It's a really interesting inversion of the traditional big cloud model—you can set up Docker-like VMs which spin up very fast and can be <em>trivially</em> replicated worldwide.
(This feels 'hard' to do on traditional cloud providers—Google only vaguely supports this IIRC on Firebase/Cloud Functions.)</p>
<p>Fly's model is that there is no &quot;batteries included&quot;.</p>
<ul>
<li>If you want a database on Fly, you… just run a database.</li>
<li>If you want an event queue, you… just run an event queue.</li>
<li>If you want a CDN, you… get the idea.</li>
</ul>
<p>This is not the fever dream of people who long for fully integrated <a href="https://en.wikipedia.org/wiki/Platform_as_a_service">PaaS</a>, because you have to do a lot yourself.
Ironically, one of the clearest use-cases for Fly is something like a fast static blog with low/minimal server part, because you can just blat it worldwide trivially, but your synchronization story becomes complex (or at least still singly-homed).</p>
<p>I even wrote a small library, <a href="https://pkg.go.dev/github.com/samthor/hangar">Hangar</a>, to help develop Fly apps locally, basically helping you emulate the distributed production environment.</p>
<p>Also, I have a longer post about building webapps using long-lived services like this coming soon. 👀</p>
<h3 id="github-pages-deploy">GitHub Pages Deploy</h3>
<p>I'm now using GitHub to deploy content to GitHub pages for quick/lazy hosting for demos.</p>
<p>I think what's notable here is that I can now build a GitHub Action that deploys directly to GitHub Pages <em>without</em> having to awkwardly push to another branch and/or folder.
At least previously, you had to choose a path to serve from, and it was always super weird to have your action commit its artifacts.</p>
<p>For example, I've deployed a hacky project <a href="https://samthor.github.io/svg-earth/">svg-earth</a>.
Its build file <a href="https://github.com/samthor/svg-earth/blob/main/.github/workflows/pages.yml">is here</a>, but you can basically follow the obvious path.</p>
<p>Amusingly, <a href="https://github.com/actions/starter-workflows/tree/main/pages">all the examples</a> use frameworks but none just call <code>npm build</code>.
Weird.</p>
<h3 id="vite">Vite</h3>
<p>I've normalized a bit on using Vite for everything web.</p>
<p>In old times, I wrote <a href="https://www.npmjs.com/package/dhost">dhost</a>, which included cache-busting and module import rewrite support (just for .js).
It still gets used a bit when I want to serve build artifacts, but its time is otherwise done.</p>
<p>Sort of related, Santa Tracker, even though I'm not involved anymore, went off without a hitch this year.
The way I designed its build system is very Vite-like, and I think even possibly predates it: a local dev serving path that rewrites every file, but a more complex build step.
I never productionized it for anyone else, though.</p>
<h2 id="cloud-libraries">Cloud Libraries</h2>
<p>I regularly find myself reusing code from prior projects.
And honestly, managing individual packages is a pain.
So I've started publishing 'super-packages' that contain things I need, but in a way that makes them reasonably tree-shakable.</p>
<h3 id="thorish-(js%2Fnpm)">thorish (JS/NPM)</h3>
<p>I publish <a href="https://npmjs.com/package/thorish">thorish</a> for JS.
This mostly contains primitives around concurrency (well, JS' concurrency…)—things like work queues that I find myself often recreating.</p>
<p>By the way, I like (although it seems broken/slow at times) the autogenerated docs <a href="https://tsdocs.dev/search/docs/thorish">tsdocs.dev</a> as it's very Go-like and it also means I don't have to bother generating docs myself.</p>
<h3 id="thorgo-(go)">thorgo (Go)</h3>
<p>And speaking of Go, I publish <a href="https://pkg.go.dev/github.com/samthor/thorgo">thorgo</a>.
I've started to work more with <code>context.Context</code> in Go, writing some helpers for that.
I've realized that it's really paramount to JS' <code>AbortController/AbortSignal</code>, and it should be used far more often for close/shutdown mechanics for any type of helper—i.e., you only ever have a <code>Join</code> method, and shutdown is entirely based on the passed context.</p>
<p>My <a href="https://pkg.go.dev/github.com/samthor/thorgo@v0.0.0-20231226220557-fbc0f8e39c16/queue">Queue</a> type, for instance, allows multiple users to subscribe to a queue of events.
There's no channels used here, avoiding oddities with consuming/draining.</p>
<h2 id="business-stack">Business Stack</h2>
<p>After leaving the startup earlier this year, I set up a Pty Ltd in Australia for consulting and services work.
I'm building some basic SaaS for developers that may or may not land well, but at least it's something that I want myself.</p>
<h3 id="wise">Wise</h3>
<p>I am overwhelmingly impressed with <a href="https://wise.com">Wise</a>, which let me set up a business banking account with almost zero fuss (and only a one-time $22 payment), and immediately provided me with both a digital and physical (in the mail) debit card.</p>
<p>This is in contrast to traditional banks, which needed umpteen forms of ID <em>brought in-person</em> to an office.
And then they still have the gall to charge me $10/mo for a debit card.</p>
<p>To be fair, I think Wise won't and doesn't pay any interest on cash held by the account, but I have reasonably small amounts.</p>
<h3 id="invoicing">Invoicing</h3>
<p>I haven't really found a &quot;simple invoice generator&quot; yet.
Yes, I'm aware there's a million out there behind a Google search.
No, this isn't the SaaS I'm building—don't worry.</p>
<p>All I've done so far is created a basic template in Google Docs and have edited it every time I've sent a new invoice, before storing the final generated PDF in a Drive folder for posterity (and you know, tax).</p>
<h2 id="fin">Fin</h2>
<p>That's all for now.
Happy new year, watch this space, etc. 🎉</p>
]]></description><link>https://samthor.au/2023/december-vibe-check/</link><guid isPermaLink="true">https://samthor.au/2023/december-vibe-check/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sat, 30 Dec 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Baldur's Gate 3 Character Choice]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2023%2Fbaldurs-gate-play%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="baldur's-gate-3-character-choice">Baldur's Gate 3 Character Choice</h1>
<p>My partner and I have been playing co-op <a href="https://en.wikipedia.org/wiki/Baldur%27s_Gate_3">Baldur's Gate 3</a> on PS5.</p>
<p>It's good, although a bit clunky on controller, and while the split-screen mode is <em>amazing</em> that it works at all, it's also the source of pretty hefty FPS drops.
When cross-play is added, one of us will probably swap to a Mac to keep playing.</p>
<h2 id="how-your-character-choice-works">How Your Character Choice Works</h2>
<p>We found character creation confusing, especially as you're sort of dumped into it… what does it mean to pick an &quot;origin character&quot;—a premade character with backstory—versus a character you build yourself.</p>
<p><strong>If you choose an origin character, you play &quot;as&quot; them— they are your 'avatar'.</strong>
This sounds reductive, but the differences are simple:</p>
<ul>
<li>There are absolutely no voiced lines from your character: you imagine yourself saying things as you choose dialogue options</li>
<li>You get additional backstory in the form of extra choices, cutscenes during rest, etc.</li>
</ul>
<p>(This choice is especially confusing because the character picker has a small cutscene where the characters 'pitch' you on choosing them in a fun little voiced story.
These are enjoyable, and you'll pick a character you like, but then you'll be incredibly disappointed when you finally realize <em>you won't get to hear them for the rest of the game</em>). 🔇</p>
<p><strong>If you collect origin characters during play, you see their stories from outside.</strong>
It doesn't matter if you're another origin character or a self-made character.</p>
<ul>
<li>You'll get to interact with them, possibly with special lines as an origin character yourself</li>
<li>They'll be fully voiced</li>
<li>You won't get their extra cutscnes, but instead only hear about their experiences as they discuss with you.</li>
</ul>
<p>(There's also a large number of PCs who you collect—like <a href="https://baldursgate3.wiki.fextralife.com/Halsin">Halsin</a>, the Druid—who come later, so you could never play &quot;as&quot; them.
These aren't really origin characters, but act like ones that you could have never taken as your avatar.)</p>
<p><strong>Finally, if you build your own character, you'll need to invent your own backstory.</strong>
This is almost the most 'observational' way to play.
You'll have some particular origin as defined by D&amp;D—in my case, I think I'm a sage or something—and that will effect your rolls and experience.
But there's no hidden backstory to your character that you'll uncover or learn more about during the game, you primarily experience the story as an outsider.</p>
<h3 id="in-game">In-Game</h3>
<p>Confusingly, even though you have a clear avatar, you can choose any character to 'lead' your party.
This is incredibly versatile!
You can split your party into multiple groups, have them in different parts of the map, etc.
You can even initiate discussions with NPCs not as your avatar.
Any dialogue lines at this point continue to be unvoiced, and the relationship implications of your dialogue still apply to your avatar.</p>
<p>For example, &quot;Shadowheart approves&quot; can occur even if Shadowheart (as a 'collected' party member) is the only one in dialogue.</p>
<p>Additionally, there are a number of global states that are applied party-wide.
At one point we (in co-op) interacted with NPCs far apart on the map rapidly in sequence, and had differing conversations that included lines like… &quot;we hear you've discussed this problem with «the other party»&quot;, which we'd done literally seconds ago.</p>
<h3 id="a-note-on-replayability">A Note On Replayability</h3>
<p>The origin characters make BG3 especially replayable because every one has hidden development that will only be exposed if you play as them.
This is honestly very D&amp;D-like, again, Baldur's Gate is literally D&amp;D, but more in the sense of… here is some backstory that the DM only shares with you, even though clearly playing the game is <em>more or less</em> a shared co-op experience where you want to win together.</p>
<p>If you want my opinion on who to choose in single-player… I think it depends if you're the sort of person who might replay the game.</p>
<ul>
<li>
<p>If you're only going to play through once, I'd suggest choosing an origin character.
The experience of extra hidden backstory is interesting enough.</p>
</li>
<li>
<p>Otherwise, I'd suggest playing as a BYO character, and then you can find the most interesting origin characters to play as <em>next time</em>.</p>
</li>
</ul>
<p>Of course, if you're playing co-op…</p>
<h2 id="how-does-this-interact-with-co-op">How Does This Interact With Co-Op</h2>
<p>For Nicky and I, we're playing as Lae'zel and a BYO character.
I think that's probably a good combination because we get to experience unique plot as one origin character while still having the full gamet of other collected characters to interact with.</p>
<p>We tend to then have one additional party member under our control, as you have to explicitly assign them in multiplayer—this effects how you move them on the map and who is playing as them during combat.
I find the explicit control actually a bit odd, because there's already a clear &quot;grouping&quot; mechanic—but I digress.</p>
<p>If we'd chosen two origin characters (which is possible, if everyone playing joins at the start of the game) you end up in the slightly odd state where your two origin character avatars don't really interact—if <em>everyone</em> playing multiplayer has chosen an origin character, you basically just don't have dialogue together, romance options, etc.</p>
<p>For us, we can't interact between our BYO character and Lae'zel, but that's less impactful as the BYO character is already in that &quot;outsider&quot; role.</p>
<p>Notably the way this works has really hit people who want to play <em>four-player</em> on PC, because then your party (maximum size 4) is literally full of people who… just don't have anything to do with each other.
Honestly, I wouldn't recommend BG3 multiplayer for more than two, because it's really fun to have voiced characters hanging around giving their opinions.
At this point it already feels restrictive only having two slots for them—we're having to swap them in and out far more than I'd like.</p>
<p>Yes, all the origin characters hang around at camp and have opinions <em>there</em>, and I think it's even possible to finish parts of their side quests without them being <em>actively in your party</em>.
But this is just a bit odd.</p>
<h2 id="fin">Fin</h2>
<p>We're enjoying BG3 as co-op (on easy, ha) but I'd say we're still naïve at some of the mechanics.
I certainly haven't really played D&amp;D or anything similar since University times.
Our actions tend to be using fire cantrips (always available spells) or just regular hand attacks.
The game itself is interesting enough without really getting super complex with our characters, though, as most of our abilities are only impacting our effectiveness in combat.</p>
<p>(We would totally recommend having a character who can Speak with Animals and jump really far, though).</p>
]]></description><link>https://samthor.au/2023/baldurs-gate-play/</link><guid isPermaLink="true">https://samthor.au/2023/baldurs-gate-play/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 17 Sep 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Opinions On Sydney Rail Transport]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2023%2Fopinions-on-sydney-transport%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="opinions-on-sydney-rail-transport">Opinions On Sydney Rail Transport</h1>
<p>This is a post containing my niche opinions on Sydney's public transport system.
If you're from Sydney and a transport nerd, this is probably interesting.
Otherwise, maybe read something else.</p>
<p>I'm not going to give background, suffice to say that Sydney is Australia's biggest city and has the highest number of commuters who use public transport.</p>
<p>Without further ado, some spicy takes. 🌶️</p>
<!-- 
I recently resigned and am self-employed via consulting.
We're also expecting a new baby in the next little while, so I've had _some_ downtime: in only that by contrast, once the little person arrives, my life will get a lot more chaotic.
Again.

So, what a natural time to get into trains.

This post is a short summary of what's happening right now, and a moderate vision of what could happen in the future.

Sydney is Australia's biggest city, and various pundits (TODO: links) believe it has the best transport network in Australia.
Modulo some pandemic noise, it also has the highest ratio of commuters who use public transport (TODO: cite).
This is despite Melbourne's huge inner-city tram network, which while making Melbourne's CBD easy to get around, doesn't seem to have a significant dent on the city's sprawling nature. -->
<h2 id="takes">Takes</h2>
<h3 id="isolate-the-eastern-suburbs-line">Isolate the Eastern Suburbs line</h3>
<p>Sydney has an interesting history in that all its rails are standard gauge, making the entire train network actually useful for interstate travel and freight. (This is unlike every other city with trains in Australia, which all have a largely isolated narrow or wide gauge network.)
However, parts of the network are useless for this because they're <em>so isolated</em>.</p>
<p><strong>Proposal: The Eastern Suburbs line from Redfern to Central should be &quot;cut off&quot; from the rest of the network.</strong></p>
<p>It makes no sense that I can get on a train at 9:13 from Bondi Junction and end up in Kiama at 11:39.
Kiama is no different than Gosford or Newcastle in this regard: it's an intercity location, so you can encourage tranfers.</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/T4-west.png" alt="A line that goes from Central to the west" width="1558" height="670" class="round" />
  <figcaption>You get metro-like inner-city service with only adding a couple of new stops</figcaption>
</figure>
<p>Now, you could <strong>extend the line westward</strong> from west of either the existing Central or Redfern stop, with stops at places like <a href="https://goo.gl/maps/wBkRsfuELq4cnQAy6">RPA</a> (woefully underserved right now for a major health district), Leichardt, and perhaps Ashfield for another interchange.
This extension works because you can <em>keep running</em> the T4 line between Central and Bondi Junction while you do the construction; just splice in new tunnels to extend the already underground route west, and keep the portal to the rest of the system.</p>
<p>This doesn't need to be a metro in the &quot;driverless&quot; sense, but it's an acceptance that you don't need to run trains so far.</p>
<h3 id="don't-metro-everything">Don't Metro Everything</h3>
<p>The last few months have seen a few takes that the Sydenham to Bankstown part of the metro conversion might happen later, or not at all.
<strong>I tend to be fine with this</strong>, even though a lot of the work has gone into upgrading stations already.</p>
<p>Sydenham is just a great place to do an interchange^.
You don't need to run trains so far if you can literally swap onto another train with a couple of minutes <em>anyway</em>, and it gives you more backup in the network.
To me, while the T3 line is kind of this weird bastard child of Sydney's ancient rail network, it seems to serve as a relief to the main western corridor, and converting it to a metro prevents interesting use like this.</p>
<p>So yes, remove the T3 lines from the City Circle, of course.
But just stop it at the three-pronged boundaries of Sydenham, Lidcombe, and Liverpool.</p>
<p><small>^except the track arrangement is a bit weird, because you only have the Metro lines then four other stations which need to be shared.
Could you stop <em>both</em> the T4 line and the T3 lines here, forcing people onto Metro/T8 or something like shuttles around the City Circle?</small></p>
<h3 id="badgery's-creek-airport">Badgery's Creek Airport</h3>
<p>Metros are sexy and the Liberals hated the unions, so build a driverless metro in the middle of no-where.
Uhhh.... sure.</p>
<p>Yes, this line <strong>should just be built into the Sydney Trains network</strong>, and eventually &quot;circle around&quot; to Leppington.</p>
<p>This is probably the least likely thing to happen on this list though, because it's not even technically compatible with the rest of the network (25kW AC).</p>
<h3 id="picton">Picton</h3>
<p>This technically isn't Sydney trains, and not even a spicy take, but <strong>just straighten the line south</strong>.
It's a no-brainer and you have to start somewhere to reduce time to regions like Bowral, or even Goulburn or Canberra.
The problem with all governments around these infrastructure projects is that they all think it should be huge, monumental, or have a giant business case.
No!
Just straighten some curves, and do a little bit every year.</p>
<p>Honestly, the same advice goes for the train north to Gosford and Newcastle, but the terrain makes it so much harder to do: we're relying on hand-cut tunnels from ~100 years ago, which are miraculous but probably hard to reproduce today (i.e., safety costs money).</p>
<h3 id="ring-light-rail">Ring Light Rail</h3>
<p>I watched a video a while ago on &quot;ring rail&quot;, specifically &quot;ring light rail&quot;.
Sydney is okay at not just having a hub-and-spoke model (*cough* Melbourne *cough*) but not perfect.
If you want to have a few CBDs and not just route everyone into or around the CBD, you need alternative transport routes.</p>
<p>For me, the most obvious place to put something like this is to connect the numerous western lines in Sydney, allowing for fast interchange between these 'corridors'.
Honestly, it could be a metro instead, but light rail has the side effect of actively <em>removing</em> options to drive. (Yes, sorry.)</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/campsie-to-kogarah.png" alt="Ring rail from Burwood to the south" width="1316" height="1222" class="round" />
  <figcaption>Leverage the new Metro West stop on Paramatta Road and take it south as far as you can</figcaption>
</figure>
<p>You'd connect these major locations:</p>
<ul>
<li>the new Metro West stop on Paramatta Road in Burwood</li>
<li>down Burwood Road, directly connecting to Burwood Station</li>
<li>to Campsie</li>
<li>via Bexley or Kingsgrove</li>
<li>to Rockdale or Kogarah</li>
<li>optionally ending at Brighton-Le-Sands.</li>
</ul>
<p>The biggest win here is honestly anything that connects to the Metro West stop, which is kind of weirdly placed on Paramatta Rd—folks aren't interchanging from their cars, so you need to make the station work with feeders from around the area.
It just makes sense to extend south to Burwood, and then the other centers are a long way away, but again, the goal here is to reduce friction going into the city and back.</p>
<h3 id="ignore-the-northern-beaches">Ignore the Northern Beaches</h3>
<p>Honestly, the Northern Beaches (like Bondi Beach) have shot themselves in the foot for decades.
There's occasional thoughts of running train lines up to high NIMBY areas, but it's just not worth it: it's time to give up.</p>
<h2 id="less-spicy-takes">Less Spicy Takes</h2>
<ul>
<li>High-density around train and light rail stations (of course)</li>
<li>Fix the Dulwich Hill end of the L1 line so trams can run more than every ~10 minutes in peak</li>
<li>Turnback for Airport Line trains after Wolli Creek, so you can run shuttle services</li>
<li>Extend the L3 light rail to Maroubra and beyond (maybe even turning down to Maroubra Beach 🏖️): Anzac Parade was literally designed for it</li>
<li>Extend the metro to Schofields, but no further: make the Richmond line more useful to more people (yes, even though it's suburban hell)</li>
</ul>
<h2 id="that's-all">That's all</h2>
<p>I'm funemployed and have time to think about these while juggling a new baby.
So this has been an incredibly quick writeup.</p>
<p>I also admit that I am hippy who lives in Glebe in a detached house and owns a Tesla, so even though I'm pro-development, maybe that voids my opinion.
So tell me how wrong I am on <a href="https://twitter.com/samthor">the birdsite</a>.</p>
<p>Or if you're from the NSW Government, call me 🤙</p>
]]></description><link>https://samthor.au/2023/opinions-on-sydney-transport/</link><guid isPermaLink="true">https://samthor.au/2023/opinions-on-sydney-transport/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Mon, 22 May 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[AWS Amplify Is A Grift]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2023%2Faws-amplify-is-a-grift%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="aws-amplify-is-a-grift">AWS Amplify Is A Grift</h1>
<p>Yes, this is a punchy headline, but if you'll join me on this journey, you'll see how. 👀</p>
<p>So, here's the context.
For the past year or so, I was at a renewable energy startup.
It was a great experience, but I recently resigned: I'm having another child, and I just don't need to work full-time—so I'm not going to.
To be clear, this post is entirely my opinion: as of writing, I'm only employed <a href="/about/consulting/">by myself</a>.</p>
<p>The startup heavily used Amplify—I inherited that decision—but one of my major initatives was removing as much of it as possible.
This took hundreds of engineering hours away from <em>actual work</em>.</p>
<p>So, let me be as clear as possible, and you can quote me personally on this: &quot;AWS Amplify is <em>actively harmful</em>&quot;, primarily because of its database choice.</p>
<p>Consider using just a normal database—personally for me that would be Firestore (Google-hosted) or MongoDB (basically self-hotsed), because I strongly prefer scalable NoSQL databases.
But for most people, it's probably going to literally just be Postgres.
You don't need more than Postgres.</p>
<h2 id="how-can-a-framework-be-actively-harmful%3F">How can a framework be actively harmful?</h2>
<p>A core idea of cloud providers is that you can build your infra and not have to worry about scale.
AWS Amplify makes promises that it will be:</p>
<blockquote>
<p>Easy to start, easy to scale</p>
</blockquote>
<p>But herein lies the grift: it's useless for anything except toy applications with trivial amounts of <em>data</em>.
And these toy apps are conveniently easy to demo.</p>
<p>And data is the key here; there's a lot of AWS Amplify which is just fine and <em>boring</em>, like its user authentication libraries.
The issue lies with GraphQL and the way it stores data.</p>
<p>The challenge, of course, is that you don't know that it's terrible going in: that's why I'm writing this post. The issue is that it it's built by the world's biggest cloud provider, who is completely happy to pay US$200k/yrs to DevRels to push this unusable bit of software—so you might think, sure, it's fine.</p>
<p>But it's not. 💥</p>
<h2 id="the-boring-stuff">The Boring Stuff</h2>
<p>Let's get the boring stuff out of the way.
By that, I mean the parts of AWS Amplify that are innocuous, indifferent, and are plainly things that are hard to mess up <em>or</em> be particularly amazing at.</p>
<ul>
<li>
<p><strong>Cognito</strong>: Amplify sets up an auth stack.
It's got its own problems (including poor SSO integration that actively leaks all configured integrations), but if all you want is basic email/password login, it's fine.
I've heard on the grapevine that Amplify's client-side JS is actually the best way to interact with Cognito.</p>
</li>
<li>
<p><strong>Hosting</strong>: There's a YAML-based config language which sets up some basic CI/CD to deploy your website.
It's fine.</p>
</li>
<li>
<p><strong>Analytics</strong>: It's broken.
If the same user signs in on a large number of different browsers (you'd never do that while debugging, in multiple Incognito windows—never!) then you generate too many IDs for that user and events can no longer be logged.</p>
</li>
<li>
<p><strong>Some AI stuff</strong>: I haven't used this.
It seems like it's just AWS wrapping up its more complex services with… exactly the same level of complexity, but now it has a 🌈 brand 🌈.</p>
</li>
</ul>
<p>Again, this is fine.
Any provider pitched at the same level—Firebase, Supabase, Azure probably has one—has this kind of &quot;app-builder&quot; stack.</p>
<h2 id="data%2C-it's-what-apps-crave">Data, It's What Apps Crave</h2>
<p>AWS Amplify allows you to specify GraphQL models that turn into database rows.
To be fair, this is an annoying problem: let's say you're DIYing it, and writing a GraphQL server and database integration yourself.</p>
<ul>
<li>If you're using a traditional SQL database you'll have to write both the GraphQL schema and the table schema.</li>
<li>If you're using NoSQL, you may have to massage your GraphQL types before storing them—since GraphQL's type system is needlessly restrictive.</li>
</ul>
<p>However, the fundamental mistake that's made here is that <em>AWS Amplify puts your data into DynamoDB</em>, which is not a general-purpose database.</p>
<p>Amplify's approach in using DynamoDB is literally called out <a href="https://aws.amazon.com/blogs/database/single-table-vs-multi-table-design-in-amazon-dynamodb/">as something you should avoid by AWS</a>, because it uses a single table per resource.
Additionally, you cannot filter or sort by arbitrary columns—DynamoDB is a high-performance, low-level database, that doesn't let you do arbitrary queries.
That's the point.</p>
<p>Yes, Amplify awkwardly describes the way you <a href="https://docs.amplify.aws/cli/graphql/data-modeling/">can create secondary indexes</a>, but it's often not what you need (everything needs primary AND secondary keys, etc), and you can't index on any sort of ACLs.</p>
<p>So using it is literally an anti-pattern.
And part of the problem is, once you're in production using a database—whatever database, not just DynamoDB—it's hard to get out.</p>
<h3 id="but-dynamodb-scales%3F">But DynamoDB Scales?</h3>
<p>But Sam, you say—DynamoDB is a fast, scalable database.
Surely these tradeoffs are worth it?</p>
<p>Yes, in some cases.
But you should only use it when a traditional database won't cut it (and if you think you know what that threshold is, go and double the number and come back to me), and you can confidently design your queries up-front.</p>
<p>Amplify, which is literally pitched at startups, isn't suitable because you're going to pivot.
Your CEO is going to ask you for some weird data 'shape' that just isn't possible because Dynamo plainly cannot answer your arbitrary queries.
Yes, no data is ever in a perfect table, but this makes it worse.</p>
<p>There is a flipside, which is that Dynamo is fairly fast at a &quot;scan&quot;, which is a fancy term for &quot;load every row and filter it at runtime&quot;.
And yes, if you don't have much data—again, think of a number and add an order of magnitude to it—you can just do that, but then why are you using DynamoDB?
Dynamo scales by sharding—your primary key space is divided ito partitions.
But a partition <a href="https://www.google.com/search?q=dynamo+partition+size">is literally 10gb</a> of data.</p>
<ul>
<li>If you're storing that much plain text data, good for you—maybe DynamoDB can help you!</li>
<li>If you've only hit 10gb since you're putting large binary data into your database, you should rethink your choices.</li>
</ul>
<p>So if your company doesn't have that much data (even 100gb might be a good target), but chooses to use Dynamo, your effective option to do arbitrary queries is just to… scan all the data.</p>
<p>So why are you using Dynamo, a hyper-performant and extremely limited database, again? 🤔</p>
<h3 id="but-dynamodb-is-fast%3F">But DynamoDB Is Fast?</h3>
<p>Ah, well, here's the real killer.</p>
<p>If you put access controls on your data, and say, a user wants to retrieve their &quot;Foos&quot;… AWS Amplify literally has to fetch matching data and then filter it down.</p>
<p><strong>Let me say that again.</strong>
If you have 1,000 users, and each privately owns one row of data, Amplify's default pagination of 100 items per fetch will take—in the worst case—<code>O(users/100)</code> pages to retrieve the user's item.
For 1,000, that's 10 pages.
For 100,000, that's 100 pages—100 round trips from your user's web browser back to the database.</p>
<p>The VTL, a language used by AppSync, which is Amplify's underlying provider, looks like the following (a tiny bit snipped for brevity).
And note that the 'data source' items are in <code>$ctx.result.items</code>—that's the page we have to scan to see if a user can see their own data.</p>
<pre><code class="language-text">#set( $items = [] )
#foreach( $item in $ctx.result.items )
  ## [Start] Dynamic Group Authorization Checks **
  #set( $isLocalDynamicGroupAuthorized = false )
  ## Authorization rule: { allow: groups, groupsField: &quot;groupsWithRead&quot;, groupClaim: &quot;cognito:groups&quot; } **
  #set( $allowedGroups = $util.defaultIfNull($item.groupsWithRead, []) )
  #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get(&quot;cognito:groups&quot;), []) )
  #foreach( $userGroup in $userGroups )
    #if( $allowedGroups.contains($userGroup) )
      #set( $isLocalDynamicGroupAuthorized = true )
    #end
  #end
  ## [End] Dynamic Group Authorization Checks **

  #if( ($isLocalDynamicGroupAuthorized == true) )
    $util.qr($items.add($item))
  #end
#end
#set( $ctx.result.items = $items )
</code></pre>
<p>Yes.
It's really that bad.
Of course, Amazon's <a href="https://docs.aws.amazon.com/appsync/latest/devguide/configuring-resolvers.html">sample resolvers</a> (not that you write VTL directly with AWS Amplify) don't include this—they have a single line saying, &quot;oh, you'll just pass all the data back to the client&quot;.</p>
<h2 id="what-else-is-wrong%3F">What Else Is Wrong?</h2>
<p>The above criticism, the run-time filtering of user data, is by far the most egregious failure of AWS Amplify.
There's a number of other poor areas, too.</p>
<p>Firstly, GraphQL subscriptions are basically useless.
There's a <a href="https://blog.purple-technology.com/lessons-learned-aws-appsync-subscriptions/">great blog post here</a> which covers this far better than I can.</p>
<p>And Amplify's <a href="https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js/">DataStore</a> fails fundamentally <em>because</em> it relies on GraphQL subscriptions and Amplify's view on that.
It doesn't support dynamic ACLs (which is <em>technically listed in the docs</em> but not at all clear), so you can only listen to public items, which is bizzare.
And it falls into the traps above.
There are <a href="https://github.com/aws-amplify/amplify-cli/issues/4794">so</a> <a href="https://github.com/aws-amplify/amplify-js/issues/7069">many</a> <a href="https://github.com/aws-amplify/amplify-js/issues/7989">issues</a> about how it's unusable and how the people filing didn't realize until they were already knee-deep in Amplify's grift.</p>
<p>Additionally, the JS used by AWS Amplify allows for race conditions in listening to changes.
What do you do when you want a list of &quot;live&quot; objects?
You'd imagine something like:</p>
<ul>
<li>You request the complete list of objects</li>
<li>You listen for changes to those objects</li>
</ul>
<p>And that's what Amplify does, … but here's the kicker, in a diagram:</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/subscription.png" alt="Diagram of traditional subscription model" width="950" height="384" class="round" />
  <figcaption>The red line is 'danger'—what happened there?</figcaption>
</figure>
<p>So you're going to risk losing changes.
And for most users—sure, that's fine.
What are the odds of something happening in that red area?
But it's poor design.</p>
<p>I know there's a slightly tweaked approach that has revision numbers and so on, but it's not commonly understood.</p>
<p>I cannot stress how much Google's Firestore just solves the 'real-time' problem.
Instead, for days and days, I've had to work around Amplify's limitations.</p>
<h2 id="what-should-aws-do%3F">What Should AWS Do?</h2>
<p>So, you say: Sam, you've complained a lot.
But Amplify has some redeeming characteristics.
How could you make it better?</p>
<p>If I was AWS, I would…</p>
<ul>
<li><strong>provide Postgres or similar as a database choice</strong>: This allows a developer's database schemas to include fairly arbitrary indexes (because you're not limited by DynamoDB)</li>
<li><strong>incorporate ACLs into that database's indexes</strong>: ACL-ed queries are 'slow' because they take the dumbest approach</li>
<li><strong>rebuild subscriptions without GraphQL</strong>: Implement a log-based solution which lets users catch up on a single table they're interested in, but trade off &quot;nested&quot; queries (it's not GraphQL anymore)</li>
<li><strong>rethink whether GraphQL is actually fit for purpose</strong>: It's just not very good, <em>aside</em> from nested queries. That's a whole different post, though.</li>
</ul>
<h2 id="what-should-you-do%3F">What Should You Do?</h2>
<p>If you're already commited to AWS Amplify, then I'm sorry.
The auth and non-database world—that's fine.
I even enjoy or think that the JWT-based approach to auth is actually fairly good, and you can write alternative backends which use a user's claims to enforce ACLs.</p>
<p>I think the thing I'd do is… if you're somewhat &quot;successfully&quot; using Amplify right now, I believe that your model and data won't actually be that large—because it actually breaks down at big numbers.
Therefore it is the right time to rethink.</p>
<p>Look at your queries which are particularly awkward—what does your app or company have the most of?
Can that be refactored into its own system, potentially wrapping up a database that's <em>more</em> fit for purpose?</p>
<p>Remember too that most applications will have a notion of login, user, settings—fairly boring stuff.
If that's in Amplify, fine.
A lot of it is probably ID-based, which Dynamo actually <em>does excel at</em>—you're not listing every user to get to your own.
So that can remain.</p>
<h2 id="thanks-for-reading">Thanks For Reading</h2>
<p>This space isn't perfect, but I honestly believe Amplify has too many potholes to justify its use, and those are wrapped up in poor documentation that hides a lot of nuances that <em>aren't obvious</em> when you start and only hit you when you're actually trying to scale.</p>
<p>Best of luck. 😬</p>
<p>Find me on <a href="https://bsky.app/profile/samthor.au">Bluesky</a>.
I'm also available <a href="/about/consulting/">for consulting</a>.</p>
<h2 id="an-addendum">An Addendum</h2>
<p><em>Update, April 2023</em>: This post ended up on the front page of HN.
I'd like to call out some themes in the comments:</p>
<ul>
<li>
<p><a href="https://news.ycombinator.com/item?id=35514877">AWS uses Dynamo as it has no other Serverless database offering</a>: This is actually a fairly astute observation.
Amplify is pitched as a serverless product, and I'm not sure what AWS has in the database space that doesn't &quot;run all the time&quot;—running Postgres or Mongo would cost you ¢/hour.</p>
</li>
<li>
<p><a href="https://news.ycombinator.com/item?id=35514562">You shouldn't be using Dynamo for your data model</a>: Sure, that's why this post exists.
As a developer using a new app builder platform, it's not clear where the minefields are, and AWS Amplify doesn't make it clear from the outset that it has these really hard limitations around ACLs, speed etc.
AWS isn't going to come out and say <em>&quot;Our database is a bit shit, but the rest of Amplify is perfectly cromulent.&quot;</em>.</p>
</li>
<li>
<p><a href="https://news.ycombinator.com/item?id=35515147">Postgres vs NoSQL</a>: I don't see Firestore (NoSQL) or Postgres being <em>that different</em>.
I like Firestore.
I think it's the world's best general-purpose database.
But Postgres is similar enough, I think it just comes down to personal preference.
DynamoDB, on the other hand, … is not really either of those things to me.
It's a lower-level construct that people <em>often build real general-purpose databases with</em>, but which is highly limited on its own.
This is my thesis: Amplify is doing the tech space a misservice by using DynamoDB.</p>
</li>
</ul>
]]></description><link>https://samthor.au/2023/aws-amplify-is-a-grift/</link><guid isPermaLink="true">https://samthor.au/2023/aws-amplify-is-a-grift/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Fri, 31 Mar 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[AbortController for expiry]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2023%2Fabortcontroller-for-expiry%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="abortcontroller-for-expiry">AbortController for expiry</h1>
<p>My <a href="/2022/abortcontroller-is-your-friend/">last post</a> on <code>AbortController</code> continues to be my most read blog post, ever.
This one probably won't do quite as well, but that's fine—it's a bit more niche, but it's definitely also an <em>extension</em> of that one.</p>
<p>Today, the problem I'll solve is:</p>
<ul>
<li>you have a token, key, or some object (e.g., some key to access a system)</li>
<li>it's valid only for <em>some time</em> (e.g., an hour) - it has a <em>lifetime</em></li>
<li>you want to ensure users of that object don't overstay their welcome. 💥</li>
</ul>
<h2 id="background">Background</h2>
<p>The idea of <code>AbortController</code> is to generate an <code>AbortSignal</code> you can <em>pass in</em> to some object or system to later shut it down.
For example, you can have one signal for an entire request, but shut down all its dependent work with one single call to <code>.abort()</code>.</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/abort-lifetime.png" alt="Diagram showing creating and aborting an AbortController" width="1151" height="658" class="round" />
  <figcaption>Create an <code>AbortController</code>, do some stuff, get out</figcaption>
</figure>
<!-- Something like:

```js
function handleRequest(message) {
  const c = new AbortController();
  try {
    await processThing(message, { signal: c.signal });
  } finally {
    c.abort();
  }
}
``` -->
<p>This background might feel a bit needless, but it actually reminds you that the <code>AbortController</code> has a very close relationship to the idea of &quot;context&quot; as it pertains to RPCs, network requests and so on: whenever your service handles an external request, create a single <code>AbortController</code>, and pass its signal (aka &quot;context&quot;) <em>everywhere</em>.</p>
<p>If you're familiar with Go, then it has a <a href="https://pkg.go.dev/context">standard library version</a> of this.</p>
<p>Anyway, read on. 👇</p>
<h2 id="approach">Approach</h2>
<p>To remind you, we're here to manage the expiry of some complex object: something that itself has a lifetime.</p>
<p>So instead of starting with a context-like object, you can create a helper function that builds your <em>thing</em> and its lifetime—where its lifetime is described via an <code>AbortSignal</code>.
Something like:</p>
<pre><code class="language-js"><span class="hljs-comment">/**
 * Builds an expirable role. Creates a new lifetime in its AbortSignal.
 */</span>
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">buildExpirableRole</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> { expiresAt, token } = <span class="hljs-keyword">await</span> <span class="hljs-title function_">getRole</span>();  <span class="hljs-comment">// the hard work!</span>

  <span class="hljs-keyword">const</span> c = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
  <span class="hljs-keyword">const</span> expiresAfter = (+expiresAt - +<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>);  <span class="hljs-comment">// gives back an expiry Date</span>
  <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> c.<span class="hljs-title function_">abort</span>(), expiresAfter);

  <span class="hljs-keyword">return</span> { <span class="hljs-attr">result</span>: token, <span class="hljs-attr">signal</span>: c.<span class="hljs-property">signal</span> };
}
</code></pre>
<p>When I call this method, I now get a result <em>and</em> a signal pair.
I can use this to determine how long the result is valid for: has the signal been aborted?
If so, it's no longer valid. 🙅</p>
<h3 id="re-using-existing-lifetimes">Re-using existing lifetimes</h3>
<p>I've just created an expirable object that returns a new <code>AbortSignal</code>.
What if the object has a <em>lesser</em> lifetime than some outer context?
Then, you can just pass in an existing <code>AbortSignal</code> and create effectively a derived one:</p>
<pre><code class="language-js"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">buildDerivedExpirableRole</span>(<span class="hljs-params">signal</span>) {
  <span class="hljs-keyword">const</span> { expiresAt, token } = <span class="hljs-keyword">await</span> <span class="hljs-title function_">getRole</span>();
  <span class="hljs-keyword">const</span> c = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
  <span class="hljs-keyword">const</span> expiresAfter = (+expiresAt - +<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>);
  <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> c.<span class="hljs-title function_">abort</span>(), expiresAfter);

  <span class="hljs-comment">// NEW CODE HERE</span>
  <span class="hljs-keyword">if</span> (signal.<span class="hljs-property">aborted</span>) {
    c.<span class="hljs-title function_">abort</span>();
  }
  signal.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;abort&#x27;</span>, <span class="hljs-function">() =&gt;</span> c.<span class="hljs-title function_">abort</span>());
  <span class="hljs-comment">// END NEW CODE</span>

  <span class="hljs-keyword">return</span> { <span class="hljs-attr">result</span>: token, <span class="hljs-attr">signal</span>: c.<span class="hljs-property">signal</span> };
}
</code></pre>
<p>We literally make the inner signal's lifetime shorter by piggybacking on the outer one's aborted state.
In doing so, you'll have to deal with the awkward nature of <code>AbortSignal</code>: if it's <em>already</em> aborted, you can't just add an event listener, because it's <em>already</em> fired.
(I have a helper for this, since I do this so often.)</p>
<h2 id="memoizing-this-approach">Memoizing This Approach</h2>
<p>Okay, you've got a thing and its lifetime—a pair of state.
You can call this, but now you've got to <em>hold onto</em> this pair forever.</p>
<p>Instead, you'll want to memoize this: create a function which will automatically fetch a new value whenever the previous' signal has aborted/expired.</p>
<p>This is <em>pretty</em> simple.
If you have some type <code>ExpirableResult&lt;R&gt;</code>, that contains the result and a <code>.signal</code> property (in TypeScript), then you can create a helper which holds onto the <code>Promise</code> while it is valid.</p>
<p>I've swapped into TypeScript for this one, since the types help a lot:</p>
<pre><code class="language-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> memoizeExpirable&lt;R&gt;(
  <span class="hljs-attr">fn</span>: <span class="hljs-function">() =&gt;</span> <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-title class_">ExpirableResult</span>&lt;R&gt;&gt;,
): <span class="hljs-function">() =&gt;</span> <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-title class_">ExpirableResult</span>&lt;R&gt;&gt; {
  <span class="hljs-keyword">let</span> <span class="hljs-attr">activePromise</span>: <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-title class_">ExpirableResult</span>&lt;R&gt;&gt; | <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (activePromise !== <span class="hljs-literal">undefined</span>) {
      <span class="hljs-keyword">return</span> activePromise;  <span class="hljs-comment">// return already active promiswe</span>
    }

    <span class="hljs-comment">// there&#x27;s no valid result, fetch a new one</span>
    activePromise = <span class="hljs-title function_">fn</span>().<span class="hljs-title function_">then</span>(<span class="hljs-function">(<span class="hljs-params">ret</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (ret.<span class="hljs-property">signal</span>.<span class="hljs-property">aborted</span>) {
        <span class="hljs-comment">// already aborted! clear result immediately</span>
        activePromise = <span class="hljs-literal">undefined</span>;
      }
      ret.<span class="hljs-property">signal</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;abort&#x27;</span>, <span class="hljs-function">() =&gt;</span> activePromise = <span class="hljs-literal">undefined</span>);
      <span class="hljs-keyword">return</span> ret;
    });

    <span class="hljs-keyword">return</span> activePromise;
  };
}
</code></pre>
<p>Okay, that's a bit of code, but it's all fairly required.
Again, this has the awkward part of managing an already aborted vs. newly aborted <code>AbortSignal</code>.</p>
<p>To use, you might do something like:</p>
<pre><code class="language-ts"><span class="hljs-keyword">const</span> getExpirableRole = <span class="hljs-title function_">memoizeExpirable</span>(buildExpirableRole);
</code></pre>
<p>Now, you can call <code>getExpirableRole</code>—it'll return a <code>Promise</code>—to always get a valid role.
Amaze! 🎉</p>
<h2 id="an-interlude-on-lifetimes-and-context">An interlude on lifetimes and context</h2>
<p>The previous example just accepts a function which computes an outcome with an expiry, but it doesn't accept a previous signal that <em>has its own lifetime</em>.</p>
<p>This is interesting to think about because you basically have two lifetimes when writing any sort of application.</p>
<ol>
<li>The lifetime of a user's request (e.g., a network handler on a backend, a RPC), or interaction (perhaps as the user undertakes a complex interaction)</li>
<li>The lifetime of the app as a whole.</li>
</ol>
<p>You might also have a concept of a &quot;session&quot;, although this is hazy on the web.</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/server-lifetime.png" alt="Shows a diagram of requests living within the total lifetime of a server" width="1516" height="1504" class="round" />
  <figcaption>The two lifetimes to deal with in server-land</figcaption>
</figure>
<p>Often these interactions are inexorably linked.
Cloud Functions, or Lambdas, tend to be created <em>on-demand</em> on a user's initial request—they don't run all the time, otherwise you're paying the cost for that even when your server isn't being used.
Lambdas will then have a keep-alive time when they can handle further requests (although this may only be a single request, if your activity is low).</p>
<p>So if you're getting a credential or role… what is the right &quot;context&quot;, ala, the global <code>AbortSignal</code> to use?
If it's the lifetime—well, you might not have a signal at all, because modern backends are overwhelmingly designed &quot;to die&quot;.
So &quot;to die&quot; is basically when the server just shuts down in an uncontrolled manner.</p>
<p>(Something I learnt early-on in my technical career is that no-one is ever &quot;neatly&quot; shutting down a backend in a controlled way.
You build a thing, it's designed to run for as long as it can or to handle as many requests as possible, and there's no such thing as 'normal' shutdown. 🧨)</p>
<h2 id="thanks">Thanks</h2>
<p>Thanks!
That's all for today.</p>
]]></description><link>https://samthor.au/2023/abortcontroller-for-expiry/</link><guid isPermaLink="true">https://samthor.au/2023/abortcontroller-for-expiry/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sat, 21 Jan 2023 00:00:00 GMT</pubDate></item><item><title><![CDATA[Practical Python Modules]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Fpractical-python-modules%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="practical-python-modules">Practical Python Modules</h1>
<p>I grew up writing a lot of Python, which I guess speaks to how old and unorganized it is.
And since then, I've been primarily a JS developer—that ecosystem has its failings, but the rules around how things are imported inside &quot;node_modules&quot; are mostly well established, even despite its quirks.</p>
<p>For modules, though—Python is a mess.
But I think there's some simple knowledge that you might be missing. 🤔</p>
<h2 id="so%2C-you're-building-a-package">So, You're Building A Package</h2>
<p>Just some package that has some behavior or functionality.
It does whatever.
It might have some dependencies.
But you want to structure it in a sensible way. 💡</p>
<h3 id="you-want-module-mode!">You Want Module Mode!</h3>
<p>Unless you're writing a <em>single script file</em>, I assert that you almost always want to run your code <em>as a module</em>.</p>
<pre><code class="language-bash"><span class="hljs-comment"># where &quot;path/to/module.py&quot; exists</span>
python -m path.to.module
</code></pre>
<p>If &quot;path/to/module&quot; is a folder, Python will look for &quot;__main__.py&quot; within that folder.
If it's a file, it'll run it directly.
In both cases, <code>__name__ == &quot;__main__&quot;</code>: you can still use that old trick to determine whether you're the startup script.</p>
<h3 id="benefits">Benefits</h3>
<p>The benefit of this is you get an actually sane relative import system.
You can, and should <em>only</em>, write imports like this:</p>
<pre><code class="language-python"><span class="hljs-keyword">from</span> .foo <span class="hljs-keyword">import</span> whatever
</code></pre>
<p>…to import <code>whatever</code> from the peer file &quot;foo.py&quot;.</p>
<p>You can also write things like:</p>
<pre><code class="language-python"><span class="hljs-comment"># import &quot;./foo_folder/something.py&quot;</span>
<span class="hljs-keyword">from</span> .foo_folder <span class="hljs-keyword">import</span> something

<span class="hljs-comment"># import &quot;./foo_folder/another_folder/blah.py&quot;</span>
<span class="hljs-keyword">from</span> .foo_folder.another_folder <span class="hljs-keyword">import</span> blah

<span class="hljs-comment"># import &quot;./hello.py&quot;</span>
<span class="hljs-keyword">from</span> . <span class="hljs-keyword">import</span> hello

<span class="hljs-comment"># import &quot;../up.py&quot; (caveat: more on this later)</span>
<span class="hljs-keyword">from</span> .. <span class="hljs-keyword">import</span> up
</code></pre>
<p>And best of all, if you have a file called &quot;types.py&quot;, you don't have to <em>literally worry about importing Python's built-in &quot;types&quot; module</em>—you import it as <code>.types</code>.</p>
<h3 id="gotcha%3A-directory-layout-%2F-working-directory">Gotcha: Directory Layout / Working Directory</h3>
<p>To run module code, it must be within a subdirectory of your <em>current working directory</em>.</p>
<p>Here's an example.
This, unfortunately, works:</p>
<pre><code class="language-bash"><span class="hljs-built_in">touch</span> test1.py
python -m test1
</code></pre>
<p>But it quickly fails if we try to import something:</p>
<pre><code class="language-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;from . import test1&quot;</span> &gt; test2.py
python -m test2
<span class="hljs-comment"># ImportError: attempted relative import with no known parent package</span>
</code></pre>
<p>If you were to go up literally one directory, e.g.:</p>
<pre><code class="language-bash"><span class="hljs-built_in">cd</span> ../
python -m pythonsucks.test2
</code></pre>
<p>…this will import and run fine.</p>
<p>In a similar way, you can't <em>escape</em> the top-level package.
I can't <code>import ..foo</code> if I'm within one of either &quot;test1.py&quot; or &quot;test2.py&quot;, <em>because</em> there's only one directory above them in our module path.</p>
<p>This means that <strong>Python packages are gated to the current working directory of your Python interpreter</strong>.
This restriction is absolutely bonkers (especially coming from a JS background), but once you know it, you'll appreciate what you can and cannot do.</p>
<blockquote>
<p>Fun fact: You can run any Python file on your computer in a sane way by going to &quot;/&quot;.</p>
<pre><code class="language-bash"><span class="hljs-built_in">cd</span> /
python -m Users.Sam.Desktop.pythonsucks.test2
</code></pre>
<p>This doesn't work if your folders have &quot;-&quot; or other weird characters in them, but it works.</p>
</blockquote>
<h3 id="gotcha%3A-import-syntax-restrictions">Gotcha: Import Syntax Restrictions</h3>
<p>You can't import a relative file like this—Python's syntax doesn't allow it:</p>
<pre><code class="language-python"><span class="hljs-keyword">import</span> .hello  <span class="hljs-comment"># &lt;-- this does NOT work, so...</span>
</code></pre>
<p>You instead have to do one of two options:</p>
<pre><code class="language-python"><span class="hljs-comment"># For side-effects only - imports just a single symbol</span>
<span class="hljs-keyword">from</span> .hello <span class="hljs-keyword">import</span> __name__ <span class="hljs-keyword">as</span> _ 

<span class="hljs-comment"># Import from self directory</span>
<span class="hljs-keyword">from</span> . <span class="hljs-keyword">import</span> hello
</code></pre>
<p>The latter option works fairly well, and can be extended to e.g., the parent folder or subfolders:</p>
<pre><code class="language-python"><span class="hljs-keyword">from</span> .. <span class="hljs-keyword">import</span> peer_of_my_folder
<span class="hljs-keyword">from</span> .subfolder <span class="hljs-keyword">import</span> something_in_subfolder
</code></pre>
<h3 id="caveat%3A-can't-run-file">Caveat: Can't Run File</h3>
<p>In the examples above, even though the folder name is &quot;pythonsucks&quot;, I'm <em>not</em> running Python by calling &quot;python -m pythonsucks/test2.py&quot;.
In fact, Python gives us an extremely unhelpful error message:</p>
<pre><code class="language-bash">python -m pythonsucks/test2.py
<span class="hljs-comment"># ImportError: attempted relative import with no known parent package</span>
</code></pre>
<p>So whenever you run a file, you need to (a) replace &quot;/&quot; with &quot;.&quot; and remove the &quot;.py&quot; suffix, and (b) make sure you're in a parent directory (as I said above, parent relative imports may fail unless you're above <em>all</em> of your Python code).</p>
<p>Notably this is an absolute pain in the ass when you're doing development work:</p>
<ul>
<li>Your shell probably isn't going to autocomplete module names</li>
<li>You have to &quot;cd&quot; up to a parent folder whenever you want to test a file</li>
</ul>
<p>It also means that you should effectively <em>never</em> put a &quot;.py&quot; file in the top-level of e.g., your git repository—it has no way of being executed in any sane way.</p>
<p>This is all a pain in the ass…but <em>at least</em>, now you know.
Sigh. 😩</p>
<h2 id="what-about-requirements%3F">What About Requirements?</h2>
<p>You might have some dependencies you can install.
Python's dependency management systems are bonkers, too—that's a different post.
(I have recently learned about <a href="https://pipenv.pypa.io/en/latest/">pipenv</a>, which is inspired by sane package managers—it may work for you.)</p>
<p>No matter how you install them, though, you should always import them with a name that <em>does not</em> include any leading dots.</p>
<p>Conversely, you should <strong>always</strong> import your <strong>own local code</strong> <em>with</em> a leading dot.
(Sorry, I put a lot of emphasis in that sentence, but it's the rule.)</p>
<h2 id="what-about-virtualenv%3F">What about virtualenv?</h2>
<p>I'm sorry.
This post doesn't yet cover how this all interacts with that, but I suspect the answer is not much—the <em>same</em> module problems exist there, it's just that it's a way to include specific requirements/dependencies.</p>
<p>So you might be able to use the learnings here.</p>
<h2 id="that's-it.">That's it.</h2>
<p>Modern Python is nuts.
I've spent hours pulling my hair out on this. 🫠</p>
<p>What I honestly want is a helper that lets me say:</p>
<pre><code class="language-bash">python-module file.py
</code></pre>
<p>This helper would:</p>
<ol>
<li>Walk my directory tree and find some &quot;module root&quot; marker (maybe this is the &quot;.git&quot; folder, or even &quot;/&quot;)</li>
<li>Run &quot;file.py&quot; in the &quot;path.to.file&quot; syntax, treating it as a module</li>
</ol>
<p>Phew. 😡</p>
<p>Hit me up <a href="https://twitter.com/samthor">on Twitter</a> for any comments.</p>
]]></description><link>https://samthor.au/2022/practical-python-modules/</link><guid isPermaLink="true">https://samthor.au/2022/practical-python-modules/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Fri, 05 Aug 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Focus Management in 2022 📺]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Ffocus-management-in-2022%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="focus-management-in-2022-%F0%9F%93%BA">Focus Management in 2022 📺</h1>
<p>I recently gave a talk!
It's on <code>&lt;dialog&gt;</code> and the <code>inert</code> attribute, with a mention of <code>&lt;fieldset disabled&gt;</code>, too.</p>
<p>You can find <a href="https://portal.gitnation.org/contents/ensuring-your-users-are-on-the-right-path-the-future-of-modals-and-focus-management">the talk on JSNation</a>, who graciously hosted me and motivated me to get this done. 🎉</p>
<p>But also, it's just on YouTube here (albeit without my face and a snazzy intro/outro):</p>
<iframe width="736" height="414" src="https://www.youtube-nocookie.com/embed/jlS9PLqN1UY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<p>This talk combines one of my quite popular blogposts—<a href="https://samthor.au/2021/in-defence-of-dialog/">In Defence Of Dialog</a>, including some great demos—with a bunch more new content about the other focus primitives you have available to you.</p>
<h2 id="inert-release-%2F-fieldset-disabled">Inert Release / Fieldset Disabled</h2>
<p>Everything I mentioned in my talk can work today, except…</p>
<p><strong>As of July 2022, Firefox still hasn't shipped the <code>inert</code> attribute.</strong>
This is something that I called out in my talk.
It's completely <em>implemented</em>, but for some reason, Firefox won't throw it over the wall.
This is amazing considering even Safari has come to the table.</p>
<p>I'll update this section when or if they do.
But if you want some basic workarounds, consider <code>&lt;fieldset disabled&gt;</code> for forms.
If you have a HTML form, you can make every <code>&lt;input&gt;</code>, <code>&lt;button&gt;</code> etc contained within disabled all-at-once:</p>
<pre><code class="language-html"><span class="hljs-tag">&lt;<span class="hljs-name">fieldset</span> <span class="hljs-attr">disabled</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span>&gt;</span>I&#x27;m disabled because the fieldset is disabled!<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">fieldset</span>&gt;</span>

<span class="hljs-comment">&lt;!-- This &quot;wins&quot; over the lower fieldset,
     so you can wrap complex forms in it --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">fieldset</span> <span class="hljs-attr">disabled</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
          <span class="hljs-comment">&lt;!-- If the parent is disabled,
               this one is disabled too --&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">fieldset</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;another_one&quot;</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">&quot;text&quot;</span> <span class="hljs-attr">value</span>=<span class="hljs-string">&quot;I&#x27;m disabled too, because fieldset[disabled] propogates down&quot;</span> /&gt;</span>

          <span class="hljs-tag">&lt;/<span class="hljs-name">fieldset</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">fieldset</span>&gt;</span>
</code></pre>
<p>Try a demo, too:</p>
<demo-fieldset no-demo style="color: red;">
  Sorry, this demo isn't available here.
</demo-fieldset>
<p>Note that <code>&lt;fieldset&gt;</code> has some default CSS (you can see it in the demo) that gives it some spacing and a default border.
You can reset it by doing this:</p>
<pre><code class="language-css"><span class="hljs-selector-tag">fieldset</span> {
  <span class="hljs-attribute">border</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
}
</code></pre>
<p>Or even better, you can do this so it has no representation whatsoever:</p>
<pre><code class="language-css"><span class="hljs-selector-tag">fieldset</span> {
  <span class="hljs-attribute">display</span>: contents;
}
</code></pre>
<p>Again, this won't disable things like <code>&lt;a href=&quot;...&quot;&gt;</code>, but <em>will</em> disable any HTML form element.
Which may be good enough. 🤷</p>
<h2 id="thanks">Thanks</h2>
<p>Check out the video!
Even on 2x.
I promise it's interesting, and can give you far more power when writing complex web experiences—especially useful for folks interested in low-level primitives.</p>
<p>👋</p>
]]></description><link>https://samthor.au/2022/focus-management-in-2022/</link><guid isPermaLink="true">https://samthor.au/2022/focus-management-in-2022/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sat, 23 Jul 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Event-Driven JavaScript Development]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Fevent-driven-js%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="event-driven-javascript-development">Event-Driven JavaScript Development</h1>
<p>Every JS developer has written code to handle &quot;click&quot; listeners—press a button, tap a link—and you'll do something interesting.
Events are core to the way we write for the web and use the DOM. 👆</p>
<!-- And yet, most JS projects don't generate events themselves—e.g., an "accept" event, if a user accepted your ToS; or a "ready" event for when something has been set up and ready to use.
This is _especially_ true in [VDOM](https://en.wikipedia.org/wiki/Virtual_DOM)-based frameworks like {P}react, because components might not map directly to real DOM nodes (and `ref` is a huge mess).
 -->
<p>But what if I were to tell you that you should and could be using event-driven development for <em>yourself</em>—not even in the DOM? 🤔</p>
<p>To do that, we can now subclass <code>EventTarget</code>, giving your code access to fire its own events, and listen to those events using <code>.addEventListener</code>.
This lets you properly encapsulate behavior—your abstraction can do some work, and other parts of your code can simply listen to when it's done.</p>
<demo-events no-demo style="color: red;">
  Sorry, your browser doesn't support an inline demo.
</demo-events>
<p>Here's a simplified version of the demo's code:</p>
<pre><code class="language-js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SomethingController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">EventTarget</span> {
  <span class="hljs-keyword">async</span> <span class="hljs-title function_">doSomething</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-title function_">someLongTask</span>();
    <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;amazing&#x27;</span>));
  }
}
<span class="hljs-comment">// now:</span>
<span class="hljs-keyword">const</span> c = <span class="hljs-keyword">new</span> <span class="hljs-title class_">SomethingController</span>();
c.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;amazing&#x27;</span>, <span class="hljs-function">() =&gt;</span> ...);
</code></pre>
<p>Great! 🎉</p>
<p>Of course, I could have created the cute effect any way I liked—I didn't need events.
But technically, the progress bar / button <em>is</em> totally separate from the effect—it's only connected with an event.</p>
<p>Hopefully the tl;dr code snippet is useful enough.
But read on to find out:</p>
<ol>
<li>Why this is better than <code>.on</code> or your own DIY event system</li>
<li>When to dispatch an <code>Event</code>, a <code>CustomEvent</code> or a subclass of either</li>
<li>How to make tell TypeScript about your new events, e.g., allow you to autocomplete in VSCode and have type-safety in the compiler</li>
<li>How to use a general-purpose object, which emits events, with {P}react</li>
</ol>
<p>First, though!
A bit of background. 🏔️</p>
<!-- 
Read on for more, including 

Think about classes like `WebSocket`.
If fires events like "open" and "message" when certain events occur, and you can use your classic `.addEventListener()` chops to listen for, and handle, these occurances.

Read on to find out three different ways you could be wrapping up behavior. -->
<h2 id="background-on-events">Background On Events</h2>
<p>Elements on the page can emit events, such as a &quot;click&quot; event I mentioned above.</p>
<p>Depending on how the event is dispatched by the browser, it might be <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable">cancelable</a>, and it <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture">may bubble/capture</a> in interesting ways.
This is just an accepted standard.
For example:</p>
<ul>
<li>&quot;click&quot; events will typically bubble and be cancelable (because they're often on <code>&lt;a href&gt;</code> or some other interactive element, and you might want to disable that behavior)</li>
<li>whereas &quot;change&quot; isn't cancelable, becuase it reflects a change on an <code>&lt;input&gt;</code> that's <em>already happened</em></li>
</ul>
<p>I think that the average web developer has a vague sense of these rules: e.g., when you click something, it'll generate an event which bubbles and fires at all places &quot;up&quot; the chain (finishing on <code>document.body</code>). ⛓️</p>
<p>However!
This post is about subclassing <code>EventTarget</code> <em>directly</em>, and wrapping up behavior outside of the DOM.
So things like bubbling have no analogy here: a pure <code>EventTarget</code> isn't in the DOM, so that property is irrelevant.</p>
<p>And subclassing already happens in some web APIs you might be used to!
These include, but are not limited to, <code>WebSocket</code>, the legacy <code>XMLHttpRequest</code>, and the objects in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a>—these all have events, but aren't physically on the page itself. ❌📃</p>
<h2 id="in-detail">In Detail</h2>
<h3 id="why-this-way%3F">Why this way?</h3>
<p>An obvious question is—why do events this way?
If you've been around the block, including in Node.js, you'll probably be well-versed in APIs that provide methods like <code>.on('foo', () =&gt; ...)</code>, or <code>.addListener(() =&gt; ...</code>—really, whatever custom way to add events.</p>
<p>Yes, it's a little verbose, but compatibility / standardization is really the crux of it.
The <code>EventTarget</code> and <code>addEventListener</code> combination is standard and built-in, so your users know exactly how they'll work and what they'll get—it's like any other object which emits events.</p>
<pre><code class="language-js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">MyObject</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">EventTarget</span> {
  <span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-title function_">doSetupWork</span>().<span class="hljs-title function_">then</span>(<span class="hljs-function">() =&gt;</span> <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;ready&#x27;</span>)));
  }
}
<span class="hljs-keyword">const</span> myObject = <span class="hljs-keyword">new</span> <span class="hljs-title class_">MyObject</span>();

myObject.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;ready&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  ...
});
</code></pre>
<blockquote>
<p>Also, you can use <code>AbortSignal</code> to make your use of <code>.addEventListener</code> more streamlined!
Check out <a href="https://samthor.au/2022/abortcontroller-is-your-friend/">AbortController is your friend</a>.</p>
</blockquote>
<h3 id="what-type-of-event-to-dispatch">What type of Event to dispatch</h3>
<p>To fire an event, you have to create an <em>instance</em> of <code>Event</code>.
In the example at the top of the page, I've written a line like:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> e = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;something_done&#x27;</span>);
<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(e);
</code></pre>
<p>And this is absolutely fine if your event is just a name with no attached data.
Great! 👍</p>
<p>But if you want to attach some data to the event, you'll need to take a different approach.
We have two ways to add something here.
The first is to use <code>CustomEvent</code>, which allows you to add anything on its <code>.detail</code> property:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> detail = <span class="hljs-number">123</span>;  <span class="hljs-comment">// literally anything can go here</span>
<span class="hljs-keyword">const</span> e = <span class="hljs-keyword">new</span> <span class="hljs-title class_">CustomEvent</span>(<span class="hljs-string">&#x27;something_done&#x27;</span>, { detail });
<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(e);

<span class="hljs-comment">// later</span>
foo.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;something_done&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(e.<span class="hljs-property">detail</span>);  <span class="hljs-comment">// &quot;123&quot;</span>
});
</code></pre>
<p>The second is to subclass <code>Event</code> and define your own properties:</p>
<pre><code class="language-js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SomethingDoneEvent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">Event</span> {
  <span class="hljs-title function_">constructor</span>(<span class="hljs-params">value</span>) {
    <span class="hljs-variable language_">super</span>(<span class="hljs-string">&#x27;something_done&#x27;</span>);
    <span class="hljs-variable language_">this</span>.<span class="hljs-property">value</span> = value;
  }
}

<span class="hljs-comment">// use like this</span>
<span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">SomethingDoneEvent</span>(<span class="hljs-number">123</span>));

<span class="hljs-comment">// later</span>
foo.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;something_done&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(e.<span class="hljs-property">value</span>);  <span class="hljs-comment">// &quot;123&quot;</span>
});
</code></pre>
<p>So, what should you do?</p>
<ul>
<li>If an event doesn't need extra data, keep using <code>Event</code> with a custom name—this is totally fine</li>
<li>But if you need to pass data around, I'd suggest subclassing: it's a bit more up-front work, but it's easier to understand the <em>shape</em> of the event: you're not awkwardly trying to remember what <code>detail</code> is supposed to be (although read on—types can help too) 😕</li>
</ul>
<p>And, while it's definitely advanced, imagine a subclass of <code>Event</code> that has some additional functionality baked-in: maybe it acts like a stream between dispatcher and listener, or lets you call some function on it at a later point in time.
While we're sending it around <em>as</em> an <code>Event</code>, it's still just a regular class.</p>
<p>And again, this is all <em>possible</em> with an arbitrary <code>.details</code> object, but gets awkward real fast. 🙅</p>
<h3 id="type-safety">Type Safety</h3>
<p>When we use VSCode to interact with subclasses of <code>EventTarget</code>, we want to be able to type and see our options <em>just appear</em>.
You probably also want typesafety if you compile with <code>tsc</code>, but honestly, the biggest win for me is VSCode not giving me a red squiggly line or having to cast to <code>any</code>. 😊</p>
<figure>
  <video alt="demo of VSCode autocompleting 'WebSocket' event types" muted autoplay loop width="2050" height="1532" class="round">
    <source src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/websocket-infer.mp4" type="video/mp4" />
  </video>
  <figcaption>VSCode will autocomplete events and their event types</figcaption>
</figure>
<p>This doesn't happen for free just because <code>WebSocket</code> dispatches events.
If you create a new object, like <code>SomethingController</code>, we have to actually tell TypeScript about the events it might dispatch.</p>
<p>But… this space is a mess.
If we look at <code>WebSocket</code> in the TypeScript source code, it's <em>ugly</em>: 🤢</p>
<pre><code class="language-ts"><span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> You don&#x27;t have to do this! Keep reading!</span>

<span class="hljs-keyword">interface</span> <span class="hljs-title class_">WebSocket</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">EventTarget</span> {
  addEventListener&lt;K <span class="hljs-keyword">extends</span> keyof <span class="hljs-title class_">WebSocketEventMap</span>&gt;(<span class="hljs-attr">type</span>: K, <span class="hljs-attr">listener</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-variable language_">this</span>: WebSocket, ev: WebSocketEventMap[K]</span>) =&gt;</span> <span class="hljs-built_in">any</span>, options?: <span class="hljs-built_in">boolean</span> | <span class="hljs-title class_">AddEventListenerOptions</span>): <span class="hljs-built_in">void</span>;
  <span class="hljs-title function_">addEventListener</span>(<span class="hljs-attr">type</span>: <span class="hljs-built_in">string</span>, <span class="hljs-attr">listener</span>: <span class="hljs-title class_">EventListenerOrEventListenerObject</span>, options?: <span class="hljs-built_in">boolean</span> | <span class="hljs-title class_">AddEventListenerOptions</span>): <span class="hljs-built_in">void</span>;
  removeEventListener&lt;K <span class="hljs-keyword">extends</span> keyof <span class="hljs-title class_">WebSocketEventMap</span>&gt;(<span class="hljs-attr">type</span>: K, <span class="hljs-attr">listener</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-variable language_">this</span>: WebSocket, ev: WebSocketEventMap[K]</span>) =&gt;</span> <span class="hljs-built_in">any</span>, options?: <span class="hljs-built_in">boolean</span> | <span class="hljs-title class_">EventListenerOptions</span>): <span class="hljs-built_in">void</span>;
  <span class="hljs-title function_">removeEventListener</span>(<span class="hljs-attr">type</span>: <span class="hljs-built_in">string</span>, <span class="hljs-attr">listener</span>: <span class="hljs-title class_">EventListenerOrEventListenerObject</span>, options?: <span class="hljs-built_in">boolean</span> | <span class="hljs-title class_">EventListenerOptions</span>): <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">interface</span> <span class="hljs-title class_">WebSocketEventMap</span> {
  <span class="hljs-string">&quot;close&quot;</span>: <span class="hljs-title class_">CloseEvent</span>;
  <span class="hljs-string">&quot;error&quot;</span>: <span class="hljs-title class_">Event</span>;
  <span class="hljs-string">&quot;message&quot;</span>: <span class="hljs-title class_">MessageEvent</span>;
  <span class="hljs-string">&quot;open&quot;</span>: <span class="hljs-title class_">Event</span>;
}
</code></pre>
<p>TypeScript struggles with this kind of inheritance.
So, the solution used in standard library is to include this <em>incredibly verbose</em> block every time, which has to itself know about events that could have been fired on the &quot;parent&quot;, too.
Not fun.</p>
<!-- The top part—the `addEventListener` and `removeEventListener` declarations—need to exist, and they reference `WebSocketEventMap` and `WebSocket` itself.
The lower part, the event map, is where the actual configuration lives: the names of supported events and their type.
(Some of them aren't custom, so they're just `Event`.
That's fine!
You should still declare them, even though it's possible to send arbitrary events.)

Note that this none of the above code adds any actual functionality.
That already works because `WebSocket` itself, as a `class`, extends `EventTarget`.
That's the same as our DIY subclasses above.

But we can add a _parallel_ interface—with the same name as your class—that also extends `EventTarget`, to specify the supported event names.

Again, this is just a lot.
And TypeScript struggles to make this [more generic](https://github.com/microsoft/TypeScript/issues/11700).
So naïvely, you need to copy the above boilerplate everywhere, because TypeScript sometimes to "narrow" the supported event types from `string` (i.e., any event) down to your specific one. -->
<p>However!
I'm here to make it easy. 🧑🏻‍🎤</p>
<h3 id="solution">Solution</h3>
<p>I've built a type that provides a way to extend <code>EventTarget</code> (or things that already extend it, like <code>HTMLElement</code> or <code>WebSocket</code>, but that's not really the point of this post) with arbitrary new events, making them all typesafe.
You can use it like this:</p>
<pre><code class="language-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">AddEvents</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;typed-event-types&#x27;</span>;

<span class="hljs-keyword">interface</span> <span class="hljs-title class_">ObjectEventMap</span> {
  <span class="hljs-string">&quot;whatever&quot;</span>: <span class="hljs-title class_">WhateverEvent</span>;
  <span class="hljs-string">&quot;ready&quot;</span>: <span class="hljs-title class_">Event</span>;
}

<span class="hljs-keyword">class</span> <span class="hljs-title class_">ObjectThatHasEvents</span> <span class="hljs-keyword">extends</span> (
  <span class="hljs-title class_">EventTarget</span> <span class="hljs-keyword">as</span> <span class="hljs-title class_">AddEvents</span>&lt;<span class="hljs-keyword">typeof</span> <span class="hljs-title class_">EventTarget</span>, <span class="hljs-title class_">ObjectEventMap</span>&gt;
) {
  <span class="hljs-comment">// your class code goes here</span>
}

<span class="hljs-keyword">const</span> o = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ObjectThatHasEvents</span>();
o.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;whatever&#x27;</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-comment">// hooray! This will autocomplete and `e` will be of type `WhateverEvent`.</span>
});
</code></pre>
<p>It's a little verbose (and I'd happily accept PRs to make it better).
But it's definitely nicer than having to manually plumb in new <code>addEventListener</code> calls.</p>
<p>Go use it in <a href="https://npmjs.com/package/typed-event-types">your projects</a>.
And note!
This is <em>just</em> adding types—there's no runtime cost to this—we're just hinting to TypeScript that these events exist.</p>
<h2 id="use-eventtarget-with-%7Bp%7Dreact-hooks">Use EventTarget with {P}react Hooks</h2>
<p>If you're a developer who only operates in {P}react-land, you might be saying: this is great, but how do I cause a hook or render to 'trigger'?
Can I still use {P}react idioms here?</p>
<p>Well, the answer is—you can, and you can't.
Once you're writing classes to wrap up functionality, you are well out of {P}react-land.
But that can be a useful construct as now you're not trying to &quot;fit&quot; into a framework's box.</p>
<p>But effecting state?
That's definitely possible, although has a bit of boilerplate.
Let's say our <code>SomethingController</code> changes some value on itself, and fires the &quot;whatever&quot; event when that happens.
You can provide a hook like this:</p>
<pre><code class="language-ts"><span class="hljs-keyword">const</span> <span class="hljs-title function_">useSomethingValue</span> = (<span class="hljs-params">c: SomethingController</span>) =&gt; {
  <span class="hljs-keyword">const</span> [value, setValue] = <span class="hljs-title function_">useState</span>(<span class="hljs-function">() =&gt;</span> c.<span class="hljs-property">value</span>);

  <span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> <span class="hljs-title function_">handler</span> = (<span class="hljs-params"></span>) =&gt; <span class="hljs-title function_">setValue</span>(c.<span class="hljs-property">value</span>);
    c.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;whatever&#x27;</span>, handler);
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> c.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">&#x27;whatever&#x27;</span>, handler);
  }, [c]);

  <span class="hljs-keyword">return</span> value;
};
</code></pre>
<p>And now your users can interface with <code>SomethingController</code>, and have a hook that provides a value that's always up-to-date. 🗓️</p>
<h2 id="an-extension-on-events">An Extension On Events</h2>
<p>There's some other interesting facts about events that didn't fit elsewhere.</p>
<ul>
<li><strong>When an event is dispatched by a browser, it's a <em>single object</em>.</strong>
Naïvely, you might assume the object is cloned and sent anew—sadly, no.</li>
</ul>
<p>So, if you have several &quot;foo&quot; listeners triggered by a user action, they'll each recieve the <em>same</em> instance of the event you passed to <code>.dispatchEvent(...)</code>.
This is not overly important <em>except</em> if you start passing data around, either with the <code>CustomEvent</code> instance, or your custom subclass—if your handler mucks with it, another listener will see the change.
(But who knows—maybe this is a feature to you, not a bug. 🪲)</p>
<ul>
<li><strong>Events are delivered synchronously.</strong>
This is actually largely unlike event loops in many native platforms.</li>
</ul>
<p>When you call <code>.dispatchEvent(...)</code>, all registered listeners are invoked <em>before</em> the next line of your code runs.
In fact, if any event listener throws an exception, that'll stop execution of all others.</p>
<p>I have two thoughts on this.
I actually think that this behavior is fine, but I think that events that occur after long-running tasks can be nicely wrapped up in a microtask.
This looks something like:</p>
<pre><code class="language-js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Foo</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">EventTarget</span> {
  <span class="hljs-keyword">async</span> <span class="hljs-title function_">longTask</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-title function_">longThing</span>();
    <span class="hljs-title function_">queueMicrotask</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;long_task_done&#x27;</span>));
    });
    <span class="hljs-comment">// maybe something else happens here</span>
  }
}
</code></pre>
<p>In this way, your <code>Foo</code> won't experience a crash itself—it'll just show up as a global error.</p>
<p>This is somewhat controversial—lots of documentation says that <a href="https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror"><code>window.onerror</code></a> (in your browser) and <a href="https://nodejs.org/api/process.html#event-uncaughtexception">&quot;uncaughtException&quot;</a> (in Node) are definitely only meant as last-resorts, and you shouldn't be actively trying to trigger them.</p>
<p>But your event listeners that crash on when triggered from native code, e.g., event handlers for <code>WebSocket</code> or any DOM element- well, they <em>already cause this kind of error</em>.</p>
<p>So of course, you should attempt to write event handlers in a way that avoids crashes.
(If you're really worried, try wrapping the body of your handler in a <code>try/catch</code> block.)</p>
<ul>
<li><strong>Events handlers can be async.</strong>
They technically shouldn't be—they're called synchronously, so… hear me out.</li>
</ul>
<p>Perhaps counter-intuitively, my point above absolves your sins in writing <code>async</code> event handlers.
I say this a bit <a href="https://www.google.com/search?q=define%3Afacetious">facetiously</a>.
I've previously criticized event handlers that are <code>async</code>: &quot;it's not async, events are fired synchronously, please fix that code&quot;. 😤</p>
<p>But!
Event handlers can crash in uncontrollable ways <em>anyway</em>.
You can't control this.
So making them <code>async</code> is not any more dangerous.
They might still crash!
Just… later. 🤣</p>
<p>The most important take-away from async event handlers to remember that your async event handler <em>will not block other handlers</em>.
For example, this code:</p>
<pre><code class="language-js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Foo</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">EventTarget</span> {
  <span class="hljs-title function_">testEvent</span>(<span class="hljs-params"></span>) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;before&#x27;</span>);
    <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">dispatchEvent</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;testEvent&#x27;</span>));
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;after&#x27;</span>);
  }
}

<span class="hljs-keyword">const</span> f = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Foo</span>();

f.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;testEvent&#x27;</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;async handler immediate&#x27;</span>);
  <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">0</span>));
  <span class="hljs-comment">// this will happen after everything else is done:</span>
  <span class="hljs-comment">// it doesn&#x27;t block &quot;testEvent&quot; from completing</span>
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;async handler after&#x27;</span>);
});

f.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;testEvent&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;non-async handler&#x27;</span>);
});

f.<span class="hljs-title function_">testEvent</span>();
</code></pre>
<p>…will output:</p>
<pre><code class="language-text">before
async handler immediate
non-async handler
after
async handler after
</code></pre>
<p>Phew.
Rant over!</p>
<h2 id="why-event-driven-js%3F">Why Event-Driven JS?</h2>
<p>Well… these objects look fine, but maybe you're still asking why writing a controller, or object, helps you at all. 🤔</p>
<p>Using events falls into a category of—there are lots of traditional software engineering patterns that I think on the web tend to be forgotten <em>in favor</em> of trying to fit awkwardly into a framework's box.
By looking at how the platform itself provides abstractions, like <code>WebSocket</code>, and building them ourselves, we can build better software—you <em>should</em> be hiding complex behavior inside primitive classes.</p>
<p>Personally though?</p>
<p>I am a somewhat senior <em>generalist</em> software engineer.
Yes, I write about the web, my role is the CTO of an <a href="https://gridcog.com">energy SaaS</a> that has a single, large React app.
But software engineering, more than a specific technology, is about building complex systems and making them work together.</p>
<p>And how do you do that?
<em>With good abstractions.</em> 💡</p>
<p>What that means is—if you find yourself trying to mix two different types of state, or hitting the limit in your current development &quot;context&quot;, whether that's a… {P}react component, callback, whatever, maybe you should consider pulling out the thing you're trying to do into its <em>own object</em>.</p>
<p>And then, what this blog post is all about, use events <em>as one of the many ways</em> you can interact with that object.
As well as, potentially, writing a TypeScript interface for that object, so that the <em>how</em> is separated out from the <em>what</em>.</p>
<p>As an extension—this post isn't about interfaces, after all—the <a href="https://www.google.com/search?q=why+interfaces">standard answer</a> to why you need an interface is that you have <em>many classes doing the same thing</em>.
That's not actually the best reason for them, in my opinion.</p>
<p>For me, an interface or any abstraction <em>forces you</em> to separate out concerns—this lets you think of put some functionality away in a nice box 📦, and compartmentalize it.
Your team, or even your future self, will thank you!
That complex functionality is now hidden behind just a few, clear methods, rather than leaking all over the rest of your code. 🫠</p>
<h2 id="done">Done</h2>
<p>This has been a long post.
Like everything I write, I hope it's been useful! 😬</p>
<p>For me, researching the TypeScript definitions and finding a solution to the 'generic events' problem has been invaluable, and I hope you'll consider depending on the <a href="https://npmjs.com/package/typed-event-types">&quot;typed-event-types&quot; package</a>—again, it's <em>purely types</em>, so has no impact on your build size—to embrace event-driven JS.</p>
<p>See you next time!
Follow me <a href="https://twitter.com/samthor">on Twitter</a>. 🐦</p>
<!-- ## How WebSocket Works

Just how does `WebSocket` work—it's not an `Element`, so how can it fire events?
Well, it inherits from something called `EventTarget`, which is the base class that lets you listen to events, via `.addEventListener`, and _dispatch_ events, with `.dispatchEvent`.

Notably, this means that on a `WebSocket`—like literally any `Element` on your page—you as a developer can actually fire arbitrary events.
It may not be useful, but you can do something like this:

```js
const websocket = new WebSocket(...);

// NOTE: Don't do this without a good reason!
// pretend to be closed
websocket.dispatchEvent(new Event('close'));
```

This won't _do anything_ in the browser—this won't magically close the socket.
This is similar to firing a "click" event on a link—it might act like you've clicked the link, but you can't trigger the native behavior this way.

This is a bit contrived, but leads us into&hellip;

## You Can Do It, Too

If you want to wrap up some functionality, you too can inherit from `EventTarget`.
This looks something like:

```js
class FormController extends EventTarget {

  async doSomething() {
    const result = await fetch('/something');
    const json = await result.json();

    const e = new Event('something_happened');
    this.dispatchEvent(e);
  }

}

// later
const c = new FormController();
c.addEventListener('something_happened', (e) => {
  // respond to "something_happened"
});
```

Hooray!
You just did some event-driven JS development.

 -->
]]></description><link>https://samthor.au/2022/event-driven-js/</link><guid isPermaLink="true">https://samthor.au/2022/event-driven-js/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 17 Jul 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Cross-Tab Title Hints]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Fcross-tab-hints%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="cross-tab-title-hints">Cross-Tab Title Hints</h1>
<p>Let's say you've built a website that shows items—maybe things you can buy online, or a catalogue of TV episodes.
Your users are opening lots of tabs at once 📈 to compare and contrast, or decide what to buy, or whatever.</p>
<p>Turns out, we can use just the <em>hidden</em> tabs as a surface to help the user out.
But note that this is really a desktop-only feature, because tabs aren't really visible on mobile. 📱</p>
<p>So first!
Play with the demo:</p>
<demo-tab-hints no-demo style="color: red; min-height: 250px; display: block;">
  Sorry, you can't see the demo here.
</demo-tab-hints>
<p>And check out the video if you'd like to hear about it in &lt;2 minutes:</p>
<iframe width="736" height="414" src="https://www.youtube-nocookie.com/embed/VVggD_S5DrY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<p>The very short version of this is: if your users are trying to compare and contrast items, you can give your users visual cues by taking over hidden tabs' titles (and favicons, but that's an exercise for the reader) and showing them relevant information–without having to switch tabs.</p>
<p>To put it differently:</p>
<figure>
  <img loading="lazy" src="https://storage.googleapis.com/hwhistlr.appspot.com/assets/free-real-estate.jpeg" alt="Free Real Estate meme" width="250" height="170" class="round" />
  <figcaption>I know how to be cool with the kids and meme it up</figcaption>
</figure>
<h2 id="build-it">Build It</h2>
<p>Let's talk about what makes this work and how to build it!</p>
<h3 id="broadcast-channel">Broadcast Channel</h3>
<p>First, let's learn about the humble <code>BroadcastChannel</code>.
This is a tool which lets all pages <em>and</em> workers on the same domain broadcast messages to another.
You can actually test it entirely on one page, although that's not really powerful on its own:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> b1 = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BroadcastChannel</span>(<span class="hljs-string">&#x27;some-topic-name&#x27;</span>);
<span class="hljs-keyword">const</span> b2 = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BroadcastChannel</span>(<span class="hljs-string">&#x27;some-topic-name&#x27;</span>);

b2.<span class="hljs-property">onmessage</span> = <span class="hljs-function">(<span class="hljs-params">{ data }</span>) =&gt;</span> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">&#x27;got data&#x27;</span>, data);

<span class="hljs-comment">// will go to b2 and other matching channels</span>
b1.<span class="hljs-title function_">postMessage</span>(<span class="hljs-string">&#x27;hi&#x27;</span>);
</code></pre>
<p>The power 🔌 comes in that any other page that creates the same topic'ed <code>BroadcastChannel</code> will recieve the message too.
Neat!</p>
<p>This is pretty much as basic as it gets.
It's not enormously different than posting directly to a <code>Worker</code> or another window, except that it <em>doesn't</em> support <a href="https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects">transferable objects</a>—there's not a 1:1 mapping to the target, so it's not clear who would own it. 📝</p>
<h3 id="focus-management">Focus Management</h3>
<p>This blog post is a bit of a 2-for-1: focus management is wholly unrelated to 🔊 broadcast, but here we are.</p>
<p>The problem: you want to <em>do something</em> while an element is focused, and stop when it's not.
I think naïvely people think, okay, let's create one <code>&quot;focus&quot;</code> and one <code>&quot;blur&quot;</code> handler and try to maintain state.
But you can honestly get yourself into knots. 🪢</p>
<p>It's simpler to instead, set up one handler which itself adds a cleanup task:</p>
<pre><code class="language-js">thing.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;focus&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">doStart</span>();

  thing.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;blur&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-title function_">doStop</span>();
  }, { <span class="hljs-attr">once</span>: <span class="hljs-literal">true</span> });  <span class="hljs-comment">// important!</span>
});
</code></pre>
<p>This works because the browser basically guarantees event order—we'll always see an element focus and blur before any others do.
This also works when a tab is wholly unfocused—your browser only has one tab and one element active at once 1️⃣, not one tab per page. ❌📃📃📃</p>
<small>
<p>(In the video I talk about <code>&quot;focusout&quot;</code>, which just bubbles up—I feel like there's some reason it's safer to use, but <code>&quot;blur&quot;</code> is probably fine here too.)</p>
</small>
<h2 id="putting-it-together">Putting It Together</h2>
<p>Now that we can track focus, and broadcast, we can request that other tabs make noise.
Start with HTML like:</p>
<pre><code class="language-html"><span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">tr</span> <span class="hljs-attr">tabindex</span>=<span class="hljs-string">&quot;0&quot;</span> <span class="hljs-attr">id</span>=<span class="hljs-string">&quot;tableRow&quot;</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Rating<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>⭐⭐⭐<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
</code></pre>
<p>And JS to match:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> channel = <span class="hljs-keyword">new</span> <span class="hljs-title class_">BroadcastChannel</span>(<span class="hljs-string">&#x27;displayInfo&#x27;</span>);

tableRow.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;focus&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  channel.<span class="hljs-title function_">postMessage</span>({ <span class="hljs-attr">action</span>: <span class="hljs-string">&#x27;start&#x27;</span>, <span class="hljs-attr">prop</span>: <span class="hljs-string">&#x27;rating&#x27;</span> });

  tableRow.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;blur&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
    channel.<span class="hljs-title function_">postMessage</span>({ <span class="hljs-attr">action</span>: <span class="hljs-string">&#x27;stop&#x27;</span> });
  }, { <span class="hljs-attr">once</span>: <span class="hljs-literal">true</span> });
});
</code></pre>
<p>Great!
…except, we now need to listen to requests and update our title accordingly:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> originalTitle = <span class="hljs-variable language_">document</span>.<span class="hljs-property">title</span>;

channel.<span class="hljs-property">onmessage</span> = <span class="hljs-function">(<span class="hljs-params">{ data }</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (data.<span class="hljs-property">action</span> === <span class="hljs-string">&#x27;start&#x27;</span>) {
    <span class="hljs-variable language_">document</span>.<span class="hljs-property">title</span> = <span class="hljs-title function_">getMyRating</span>() + <span class="hljs-string">&#x27;-&#x27;</span> + originalTitle;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-variable language_">document</span>.<span class="hljs-property">title</span> = originalTitle;
  }
};

<span class="hljs-keyword">function</span> <span class="hljs-title function_">getMyRating</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> something like:</span>
  <span class="hljs-keyword">return</span> tableRow.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;td&#x27;</span>).<span class="hljs-property">textContent</span>;
}
</code></pre>
<p>And that's it, albeit a fairly simplified version.
Easy visibility for your users to compare and contrast information. 📊</p>
<h2 id="done">Done</h2>
<p>Thanks for reading!</p>
<p>There are some caveats to this demo.
First and foremost, as I mentioned above, this doesn't really help you on mobile, because tab titles are rarely (if at all) visible.</p>
<p>Secondly, and this is a big one, browsers may totally reserve the right to stop you being annoying.</p>
<p>And this feature does drift dangerously close to that: imagine a tab that, when opened, constantly changed its title to say &quot;HEY LOOK AT ME&quot;.
My hunch is that if you're above a certain engagement threshold—and this is a concept inside Chrome and others, which you can find at <code>chrome://site-engagement/</code>—it'll be allowed, and not otherwise.
That's probably fine.</p>
<p>This was largely inspired by a tweet I can't find any more, where someone claimed that IKEA was doing this for furniture sizes.
Actually, this was a cute feature of Safari—it was getting rid of a common title prefix.
But I think it's cute to do yourself, and the interactivity lets us extend on it just a little bit. 🆒</p>
<p>Follow me <a href="https://twitter.com/samthor">on Twitter</a> for more like this. 🐦</p>
]]></description><link>https://samthor.au/2022/cross-tab-hints/</link><guid isPermaLink="true">https://samthor.au/2022/cross-tab-hints/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 19 Jun 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[AbortController is your friend]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Fabortcontroller-is-your-friend%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="abortcontroller-is-your-friend">AbortController is your friend</h1>
<aside>
<p><em>Also available in: [<a href="https://velog.io/@sehyunny/abort-controller-is-your-friend">한국어 번역</a>]</em></p>
</aside>
<iframe width="736" height="414" src="https://www.youtube-nocookie.com/embed/-ZWrN-TGr2Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<p>One of my favorite new features of JS is the humble <code>AbortController</code>, and its <code>AbortSignal</code>.
It enables some new development patterns, which I'll cover below, but first: the canonical demo.</p>
<p>It's to use <code>AbortController</code> to provide a <code>fetch()</code> you can abort early:</p>
<demo-abort no-demo style="color: red;">
  Sorry, your browser doesn't support an inline demo.
</demo-abort>
<p>And here's a simplified version of the demo's code:</p>
<pre><code class="language-js">fetchButton.<span class="hljs-property">onclick</span> = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();

  <span class="hljs-comment">// wire up abort button</span>
  abortButton.<span class="hljs-property">onclick</span> = <span class="hljs-function">() =&gt;</span> controller.<span class="hljs-title function_">abort</span>();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">&#x27;/json&#x27;</span>, { <span class="hljs-attr">signal</span>: controller.<span class="hljs-property">signal</span> });
    <span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> r.<span class="hljs-title function_">json</span>();
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> do something 🤷</span>
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-keyword">const</span> isUserAbort = (e.<span class="hljs-property">name</span> === <span class="hljs-string">&#x27;AbortError&#x27;</span>);
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> show err 🧨</span>
    <span class="hljs-comment">// this will be a DOMException named AbortError if aborted 🎉</span>
  }
};
</code></pre>
<p>This example is important because it shows something that <em>wasn't possible</em> before <code>AbortController</code> came along: that is, aggressively cancelling a network request.
The browser will stop fetching early, potentially saving the user's network bandwidth.
It doesn't have to be <em>user-initiated</em>, either: imagine you might <code>Promise.race()</code> two different network requests, and cancel the one that lost. 🚙🚗💨</p>
<p>Great!
And while this is cool, <code>AbortController</code> and the signal it generates actually enable a number of new <em>patterns</em>, which I'll cover here.
Read on. 👀</p>
<h2 id="prelude%3A-controller-vs-signal">Prelude: Controller vs Signal</h2>
<p>I've demonstrated constructing an <code>AbortController</code>.
It provides an attached subordinate <em>signal</em>, known as <code>AbortSignal</code>:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
<span class="hljs-keyword">const</span> { signal } = controller;
</code></pre>
<p>Why are there two different classes here?
Well, they serve different purposes.</p>
<ul>
<li>
<p>The controller lets the holder abort its attached signal via <code>controller.abort()</code>.</p>
</li>
<li>
<p>The signal can't be aborted directly, but you can pass it to calls like <code>fetch()</code>, or listen to its aborted state directly.</p>
<p>You can check its state with <code>signal.aborted</code>, or add an event listener for the <code>&quot;abort&quot;</code> event.
(<code>fetch()</code> is doing this internally—this is just if your code needs to listen to it.)</p>
</li>
</ul>
<p>Put differently, the thing being aborted shouldn't be able to abort itself, hence why it only gets the <code>AbortSignal</code>.</p>
<h2 id="use-cases">Use-Cases</h2>
<h3 id="abort-legacy-objects">Abort legacy objects</h3>
<p>Some older parts of our DOM APIs don't actually support <code>AbortSignal</code>.
One example is the humble <code>WebSocket</code>, which only has a <code>.close()</code> method you can call when done.
You might allow it to be aborted like this:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">abortableSocket</span>(<span class="hljs-params">url, signal</span>) {
  <span class="hljs-keyword">const</span> w = <span class="hljs-keyword">new</span> <span class="hljs-title class_">WebSocket</span>(url);

  <span class="hljs-keyword">if</span> (signal.<span class="hljs-property">aborted</span>) {
    w.<span class="hljs-title function_">close</span>();  <span class="hljs-comment">// already aborted, fail immediately</span>
  }
  signal.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;abort&#x27;</span>, <span class="hljs-function">() =&gt;</span> w.<span class="hljs-title function_">close</span>());

  <span class="hljs-keyword">return</span> w;
}
</code></pre>
<p>This is pretty simple, but has a big caveat: note that <code>AbortSignal</code> doesn't fire its <code>&quot;abort&quot;</code> even if it's <em>already</em> aborted, so we actually have to check whether it's already finished, and <code>.close()</code> immediately in that case.</p>
<p>As an aside, it is a bit bizzare to create a working <code>WebSocket</code> here and immediately cancel it, but to do otherwise might break the contract with our caller, which expects to be returned a <code>WebSocket</code>, <em>just with</em> the knowledge that it might be aborted at some point.
Immediately is a valid &quot;some point&quot;, so that seems fine to me! 🤣</p>
<h3 id="removing-event-handlers">Removing Event Handlers</h3>
<p>One particularly annoying part of learning JS and the DOM is the realization that event handlers and function references don't work this way:</p>
<pre><code class="language-js"><span class="hljs-variable language_">window</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;resize&#x27;</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-title function_">doSomething</span>());

<span class="hljs-comment">// later (DON&#x27;T DO THIS)</span>
<span class="hljs-variable language_">window</span>.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">&#x27;resize&#x27;</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-title function_">doSomething</span>());
</code></pre>
<p>…the two callbacks are <em>different objects</em>, so the DOM, in its infinite wisdom—just fails to remove the callback silently, without an error. 🤦
<small>
(I actually think this is totally reasonable, but it <em>is</em> something that can trip up absolute novice developers.)
</small></p>
<p>The net effect of this is that a lot of code that deals with event handlers just has to <em>keep hold</em> of the original reference, which can be a pain.</p>
<p>You can see where I'm going with this.</p>
<p>With <code>AbortSignal</code>, you can simply get the signal to remove it for you:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
<span class="hljs-keyword">const</span> { signal } = controller;

<span class="hljs-variable language_">window</span>.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;resize&#x27;</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-title function_">doSomething</span>(), { signal });

<span class="hljs-comment">// later</span>
controller.<span class="hljs-title function_">abort</span>();
</code></pre>
<p>Just like that, you've simplified event handler management!</p>
<p>⚠️ This doesn't work (and will fail silently) on some older Chrome versions, and Safari before 15, but this will go away as time moves on.
You can check (and add a polyfill) using <a href="https://gist.github.com/samthor/2e11de5976fe673557b0ee14a3cb621a">my code here</a>.</p>
<h3 id="constructor-pattern">Constructor pattern</h3>
<p>If you're encapsulating some complex behavior in JavaScript, it can be non-obvious how to manage its lifecycle.
This matters for code which has a clear start and end point—let's say your code does a regular network fetch, or renders something to the screen, or even uses something like <code>WebSocket</code>—all things that you want to start, do for a while, then stop. ✅➡️🛑</p>
<p>Traditionally you might write code like this:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> someObject = <span class="hljs-keyword">new</span> <span class="hljs-title class_">SomeObject</span>();
someObject.<span class="hljs-title function_">start</span>();

<span class="hljs-comment">// later</span>
someObject.<span class="hljs-title function_">stop</span>();
</code></pre>
<p>This is fine, but we can make it more <a href="https://www.google.com/search?q=define%3Aergonomic">ergonomic</a> by accepting an <code>AbortSignal</code>:</p>
<pre><code class="language-js"><span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
<span class="hljs-keyword">const</span> { signal } = controller;

<span class="hljs-keyword">const</span> someObject = <span class="hljs-keyword">new</span> <span class="hljs-title class_">SomeObject</span>(signal);

<span class="hljs-comment">// later</span>
controller.<span class="hljs-title function_">abort</span>();
</code></pre>
<p>Why do you want to do this?</p>
<ol>
<li>
<p>This <em>limits</em> the <code>SomeObject</code> to only transition from start → stopped—never back to started again.
This is opinionated, but I actually believe it simplifies building these kinds of objects—it's clear they're single-use, and are just done when the signal is aborted.
If you want <em>another</em> <code>SomeObject</code>, construct it again.</p>
</li>
<li>
<p>You can pass a shared <code>AbortSignal</code> from somewhere else, and aborting <code>SomeObject</code> doesn't require you to <em>hold</em> <code>SomeObject</code>—a good example here is, let's say several bits of functionality are tied to a start/stop cycle, that stop button can just call the effectively global <code>controller.abort()</code> when it's done.</p>
</li>
<li>
<p>If <code>SomeObject</code> does built-in operations like <code>fetch()</code>, you can simply pass down the <code>AbortSignal</code> even further!
Everything it does can be externally stopped, and this is a way to ensure its world is being torn down properly.</p>
</li>
</ol>
<p>Here's how you might use it:</p>
<pre><code class="language-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SomeObject</span> {
  <span class="hljs-title function_">constructor</span>(<span class="hljs-params">signal</span>) {
    <span class="hljs-variable language_">this</span>.<span class="hljs-property">signal</span> = signal;

    <span class="hljs-comment">// do e.g., an initial fetch</span>
    <span class="hljs-keyword">const</span> p = <span class="hljs-title function_">fetch</span>(<span class="hljs-string">&#x27;/json&#x27;</span>, { signal });
  }

  <span class="hljs-title function_">doComplexOperation</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">signal</span>.<span class="hljs-property">aborted</span>) {
      <span class="hljs-comment">// prevent misuse - don&#x27;t do something complex after abort</span>
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">`thing stopped`</span>);
    }
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1_000_000</span>; ++i) {
      <span class="hljs-comment">// do complex thing a lot 🧠</span>
    }
  }
}
</code></pre>
<p>This is showing off two ways to use the signal: one is to pass it to built-in methods which <em>further</em> accept it (case 3. above), and just checking whether calls are allowed (case 1. above) before doing something expensive.</p>
<h3 id="async-work-in-(p)react-hooks">Async work in (P)react hooks</h3>
<p>Despite some recent controversy about <em>what exactly you should do inside <code>useEffect</code></em>, it's pretty clear that a lot of people do use it for fetching from the network.
And that's fine, but the typical pattern seems to be to make the callback do <code>async</code> work.</p>
<p><em>And this is basically gambling. 🎲</em></p>
<p>Why?
Well, because if your effect doesn't finish before it's fired again, you don't find that out—the effect just runs in parallel.
Something like this:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">function</span> <span class="hljs-title function_">FooComponent</span>(<span class="hljs-params">{ something }</span>) {
  <span class="hljs-title function_">useEffect</span>(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> j = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url + something);
    <span class="hljs-comment">// do something with J</span>
  }, [something]);

<span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>...<span class="hljs-tag">&lt;&gt;</span>;
}
</span></code></pre>
<p>What you should do instead, is create a controller that you abort whenever the next <code>useEffect</code> call runs:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">function</span> <span class="hljs-title function_">FooComponent</span>(<span class="hljs-params">{ something }</span>) {
  <span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
    <span class="hljs-keyword">const</span> { signal } = controller;

    <span class="hljs-keyword">const</span> p = (<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-comment">// !!! actual work goes here</span>
      <span class="hljs-keyword">const</span> j = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url + something, { signal });
      <span class="hljs-comment">// do something with J</span>
    })();

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> controller.<span class="hljs-title function_">abort</span>();
  }, [something]);

  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>...<span class="hljs-tag">&lt;&gt;</span>;
}
</span></code></pre>
<p>Now that's a very simplified version that requires you to wrap your calls.
You might want to consider writing your own hook (e.g., <code>useEffectAsync</code>), or using a library to help you.</p>
<p>However, remember that in hook-land, the lifecycle of what you have access to <em>after</em> your first <code>await</code> call is really unclear—your code technically references the <em>previous</em> run.
Things like setting state tend to be fine, but <em>getting</em> state is not going to work:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">AsyncDemoComponent</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> [value, setValue] = <span class="hljs-title function_">useState</span>(<span class="hljs-number">0</span>);

  <span class="hljs-title function_">useEffectAsync</span>(<span class="hljs-keyword">async</span> (signal) =&gt; {
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">r</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(r, <span class="hljs-number">1000</span>));

    <span class="hljs-comment">// What is &quot;value&quot; here?</span>
    <span class="hljs-comment">// It&#x27;s always going to be 0, the initial value, even if the button</span>
    <span class="hljs-comment">// below was pressed.</span>
  }, []);

  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setValue((v) =&gt; v + 1)}&gt;Increment<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
}
</code></pre>
<p>Anyway, that's a whole other post on React lifecycle gotchas.</p>
<h2 id="helpers-that-may-or-may-not-exist">Helpers that may or may not exist</h2>
<p>There's a few helpers which may or may not be available by the time you read this post.
I've mostly demoed very simple use of <code>AbortController</code>, including aborting it yourself.</p>
<ul>
<li><strong><code>AbortSignal.timeout(ms)</code></strong>: This creates a solo <code>AbortSignal</code> which will be automatically aborted after a given timeout.
This is easy to create yourself if you need to:</li>
</ul>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">abortTimeout</span>(<span class="hljs-params">ms</span>) {
  <span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
  <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> controller.<span class="hljs-title function_">abort</span>(), ms);
  <span class="hljs-keyword">return</span> controller.<span class="hljs-property">signal</span>;
}
</code></pre>
<ul>
<li><strong><code>AbortSignal.any(signals)</code></strong>: This creates a signal which is aborted if any of the passed signals are aborted.
Again, you can construct this youself—but watch out, if you pass no signals, the derived signal will <em>never</em> abort:</li>
</ul>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">abortAny</span>(<span class="hljs-params">signals</span>) {
  <span class="hljs-keyword">const</span> controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();
  signals.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">signal</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (signal.<span class="hljs-property">aborted</span>) {
      controller.<span class="hljs-title function_">abort</span>();
    } <span class="hljs-keyword">else</span> {
      signal.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;abort&#x27;</span>, <span class="hljs-function">() =&gt;</span> controller.<span class="hljs-title function_">abort</span>());
    }
  });
  <span class="hljs-keyword">return</span> controller.<span class="hljs-property">signal</span>;
}
</code></pre>
<ul>
<li><strong><code>AbortSignal.throwIfAborted()</code></strong>: This is a helper on <code>AbortSignal</code> which throws an error if aborted—preventing you from constantly checking it.
You use it like:</li>
</ul>
<pre><code class="language-js">  <span class="hljs-keyword">if</span> (signal.<span class="hljs-property">aborted</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(...);
  }
  <span class="hljs-comment">// becomes</span>
  signal.<span class="hljs-title function_">throwIfAborted</span>();
</code></pre>
<p>This is harder to polyfill, but you could write a helper like:</p>
<pre><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">throwIfSignalAborted</span>(<span class="hljs-params">signal</span>) {
  <span class="hljs-keyword">if</span> (signal.<span class="hljs-property">aborted</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(...);
  }
}
</code></pre>
<h2 id="all-done">All done</h2>
<p>That's it!
I hope that has been an interesting summary on <code>AbortController</code> and <code>AbortSignal</code>.</p>
<p>Follow me <a href="https://twitter.com/samthor">on Twitter</a> for more sass. 🐦</p>
]]></description><link>https://samthor.au/2022/abortcontroller-is-your-friend/</link><guid isPermaLink="true">https://samthor.au/2022/abortcontroller-is-your-friend/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sun, 12 Jun 2022 00:00:00 GMT</pubDate></item><item><title><![CDATA[Unit Testing React without Jest]]></title><description><![CDATA[<img src="https://samthor.au/hello.gif?env=rss&p=%2F2022%2Ftest-react-builtin%2F" width="1" height="1" alt="" aria-hidden="true" />
<h1 id="unit-testing-react-without-jest">Unit Testing React without Jest</h1>
<p>Jest is clearly a polished testing tool, but with <a href="https://samthor.au/2022/builltin-nodejs-test/">the advent of Node.js 18</a>, we really don't need it any more.
Jest also has a huge surface area, is perhaps inexorably linked to Webpack, and needs its own binary—you can't just &quot;run the tests&quot; as a script.
So it has some modern challenges.</p>
<p>This blog post will teach you how to set up your code, rather than provide an all-in-one solution, and is not about React testing <em>itself</em>—just the environment where you can do it.
We'll still be using the <code>@testing-library/react</code> dependency, and <code>esbuild</code> to do our build.</p>
<p>As it's just a set up guide, you might still want to write a small wrapper or use a library.
But by the time you've finished reading, you should have a better sense of what's going on—unit testing isn't magic ✨, but it is important.</p>
<h2 id="the-steps">The Steps</h2>
<p>Let's get right into it. 🤠</p>
<h3 id="0.-build-with-esbuild">0. Build with esbuild</h3>
<p>This guide assumes you're building with <code>esbuild</code>, but you could swap this out for another build tool.
You should be able to build your React code already to a regular JS file, with a command something like this:</p>
<pre><code class="language-bash">$ esbuild index.tsx --bundle --format=esm --outfile=dist.js
</code></pre>
<h3 id="1.-write-a-test">1. Write a test</h3>
<p>Well, you'll need a test.
Here's a simple one and the component that's under test:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">const</span> <span class="hljs-title function_">FooComponent</span> = (<span class="hljs-params">{ text }: { text: <span class="hljs-built_in">string</span> }</span>) =&gt; {
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Hello <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">data-testid</span>=<span class="hljs-string">&quot;hold&quot;</span>&gt;</span>{text}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>;
};

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;test component&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">render</span>(
    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">FooComponent</span> <span class="hljs-attr">name</span>=<span class="hljs-string">{Sam}</span> /&gt;</span></span>,
  );
  <span class="hljs-comment">// Check that FooComponent renders my name properly.</span>
  <span class="hljs-keyword">const</span> span = result.<span class="hljs-title function_">getByTestId</span>(<span class="hljs-string">&#x27;hold&#x27;</span>);
  assert.<span class="hljs-title function_">strictEqual</span>(span.<span class="hljs-property">textContent</span>, <span class="hljs-string">&#x27;Sam&#x27;</span>);
});
</code></pre>
<p>It says hello to someone, and we'll just check that the contents of the element are correct.
Sure, it's just for a demo.</p>
<p>So if we add the right imports, and build/run our test, …the test won't run, since we're using a lot of globals that don't exist—<code>render</code>, <code>test</code>, and <code>assert</code>.
Let's keep going 👇</p>
<h3 id="2.-add-the-right-imports">2. Add the right imports</h3>
<p>Since we're no longer using Jest, we need a couple of important imports—there aren't just magic globals available anymore.
Let's add these:</p>
<pre><code class="language-tsx"><span class="hljs-comment">// You may want to / need to import React, depending on build setup</span>
<span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;react&#x27;</span>;

<span class="hljs-comment">// Node&#x27;s built-in testing libraries</span>
<span class="hljs-keyword">import</span> test <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node:test&#x27;</span>;
<span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node:assert&#x27;</span>;

<span class="hljs-comment">// We still want a helper, and this is a great one</span>
<span class="hljs-keyword">import</span> { render } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@testing-library/react&#x27;</span>;
</code></pre>
<p>Great! Try running your script again, …and it still won't run, because <code>document</code> isn't defined.
We're not really on the web in a browser, but we can fix that with JSDOM.</p>
<h3 id="3.-add-jsdom">3. Add JSDOM</h3>
<p>You should add <code>jsdom</code> to your project.
You can't just import it and have it be global—it's designed to provide an instance of <code>Window</code> and such that you can pass around.</p>
<p>So, you should add a new file like &quot;global-jsdom.ts&quot; to make it global:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> jsdom <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;jsdom&#x27;</span>;

<span class="hljs-keyword">const</span> j = <span class="hljs-keyword">new</span> jsdom.<span class="hljs-title function_">JSDOM</span>(<span class="hljs-literal">undefined</span>, {
  <span class="hljs-comment">// Many APIs are confused without being &quot;on a real URL&quot;</span>
  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;http://localhost&#x27;</span>,
  <span class="hljs-comment">// This adds dummy requestAnimationFrame and friends</span>
  <span class="hljs-attr">pretendToBeVisual</span>: <span class="hljs-literal">true</span>,
});

<span class="hljs-comment">// We need to add everything on JSDOM&#x27;s window object to global scope.</span>
<span class="hljs-comment">// We don&#x27;t add anything starting with _, or anything that&#x27;s already there.</span>
<span class="hljs-title class_">Object</span>.<span class="hljs-title function_">getOwnPropertyNames</span>(j.<span class="hljs-property">window</span>)
    .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">k</span>) =&gt;</span> !k.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">&#x27;_&#x27;</span>) &amp;&amp; !(k <span class="hljs-keyword">in</span> <span class="hljs-variable language_">global</span>))
    .<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">k</span>) =&gt;</span> <span class="hljs-variable language_">global</span>[k] = j.<span class="hljs-property">window</span>[k])

<span class="hljs-comment">// Finally, tell React 18+ that we are not really a browser.</span>
<span class="hljs-variable language_">global</span>.<span class="hljs-property">IS_REACT_ACT_ENVIRONMENT</span> = <span class="hljs-literal">true</span>;
</code></pre>
<p>There's a few lines there but basically it creates a JSDOM and attaches it to the page.
You <em>should not</em> be importing this anywhere but in tests.</p>
<p>Add the import to your test file:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">import</span> <span class="hljs-string">&#x27;./global-jsdom&#x27;</span>;
</code></pre>
<p>If you build the test now, you might be in business… but it's likely <code>esbuild</code> or Node will complain about dependencies.
Let's take a look.</p>
<h3 id="4.-check-your-build">4. Check your build</h3>
<p>We need to update our build command a bit before running the test:</p>
<pre><code class="language-bash">$ esbuild test.tsx --bundle --format=esm --outfile=_test.js \
      --platform=node \
      --external:jsdom
$ node _test.js
</code></pre>
<p>The big change here is that as we're using Node's built-in testing library, and JSDOM uses parts of Node, we need to set <code>--platform:node</code>.
We also need to mark <code>jsdom</code> as external: it uses some tricky loading code to avoid loading e.g., its HTML <code>&lt;canvas&gt;</code> polyfill if it's not available, and <code>esbuild</code> (maybe others) will try to bundle that rather than letting that tricky code live.
<small>
(You can add the <code>canvas</code> package to fix this, but it needs a boatload of native dependencies to work. Avoid if you can.)
</small></p>
<h3 id="5.-success!">5. Success!</h3>
<p>Hopefully you'll see the testing library do its thing.
My output looks something like:</p>
<pre><code class="language-text">$ esbuild --bundle code-test.tsx --platform=node --format=esm --external:jsdom --outfile=_test.js &amp;&amp; node _test.js

  _test.js  1.6mb ⚠️

⚡ Done in 54ms
(node:3855) ExperimentalWarning: The test runner is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
TAP version 13
ok 1 - test component
  ---
  duration_ms: 0.010834917
  ...
1..1
# tests 1
# pass 1
# fail 0
# skipped 0
# todo 0
# duration_ms 0.22451975
</code></pre>
<p>…the important part being <code># pass 1</code>, which means the test worked.</p>
<h3 id="6.-cleanup-and-helpers">6. Cleanup and helpers</h3>
<p>Our library works fine, but there's a big omission from Node's built-in testing library: it won't clean up for us.
What this means if that if we run multiple tests in a row, then our JSDOM-created-DOM will get polluted with the output from each individual test.</p>
<p>We can fix this pretty quickly by doing this:</p>
<pre><code class="language-tsx"><span class="hljs-comment">// update our import to include cleanup</span>
<span class="hljs-keyword">import</span> { render, cleanup } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@testing-library/react&#x27;</span>;

<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;test component&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-title function_">cleanup</span>();  <span class="hljs-comment">// always cleanup first</span>

  <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">render</span>(...);
  assert.<span class="hljs-title function_">strictEqual</span>(result.<span class="hljs-title function_">getByTestId</span>(<span class="hljs-string">&#x27;hold&#x27;</span>).<span class="hljs-property">textContent</span>, <span class="hljs-string">&#x27;Sam&#x27;</span>);
});
</code></pre>
<p>But this is a bit annoying to do every time.
We might be better off defining a helper in a new file—perhaps throwing in the &quot;global-jsdom.ts&quot; file for good measure—like this:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">import</span> <span class="hljs-string">&#x27;./global-jsdom&#x27;</span>;  <span class="hljs-comment">// just have to import this _somewhere_</span>
<span class="hljs-keyword">import</span> test <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node:test&#x27;</span>;
<span class="hljs-keyword">import</span> { cleanup } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@testing-library/react&#x27;</span>;
<span class="hljs-keyword">export</span> { render } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@testing-library/react&#x27;</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-title function_">reactTest</span> = (<span class="hljs-params">name, fn</span>) =&gt; {
  <span class="hljs-keyword">return</span> <span class="hljs-title function_">test</span>(name, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-title function_">cleanup</span>();
    <span class="hljs-keyword">return</span> <span class="hljs-title function_">fn</span>();
  });
};
</code></pre>
<p>And then you can use that helper, all at once, like this:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;react&#x27;</span>;
<span class="hljs-keyword">import</span> { reactTest, render } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./react-test&#x27;</span>;
<span class="hljs-keyword">import</span> assert <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node:assert&#x27;</span>;

<span class="hljs-title function_">reactTest</span>(<span class="hljs-string">&#x27;test component&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">render</span>(...);
  assert.<span class="hljs-title function_">strictEqual</span>(result.<span class="hljs-title function_">getByTestId</span>(<span class="hljs-string">&#x27;hold&#x27;</span>).<span class="hljs-property">textContent</span>, <span class="hljs-string">&#x27;Sam&#x27;</span>);
});

<span class="hljs-title function_">reactTest</span>(<span class="hljs-string">&#x27;test something else&#x27;</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">render</span>(...);
  assert.<span class="hljs-title function_">strictEqual</span>(...);
});

</code></pre>
<p>Hooray! 🥳</p>
<h2 id="bonus-tips">Bonus Tips</h2>
<p><strong>Context</strong>: In my (P)react projects, I tend to have a helper like <code>renderForTest</code> which often sets up a number of context providers that components under test might expect.
A good example might be providing flags, or a faked out data provider.
So rather than calling <code>render</code> directly, you'd do something like:</p>
<pre><code class="language-tsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-title function_">renderForTest</span> = (<span class="hljs-params">children</span>) =&gt; {
  <span class="hljs-comment">// you might need to wrap in the render() method from</span>
  <span class="hljs-comment">// &#x27;@testing-library/react&#x27; depending on your use case!</span>
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">FakeFooProvider</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">FlagsProvider</span> <span class="hljs-attr">flags</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">environment:</span> &#x27;<span class="hljs-attr">test</span>&#x27; }}&gt;</span>
        {children}
      <span class="hljs-tag">&lt;/<span class="hljs-name">FlagsProvider</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">FakeFooProvider</span>&gt;</span>
  <span class="hljs-tag">&lt;/&gt;</span></span>;
};
</code></pre>
<p>You're basically just making sure that your code, which might look for context to source things like flags or other types, always has access to that—perhaps through a provider of a 'fake'.</p>
<p><strong>Mocking</strong>: Mocking is hard!
I haven't got a complete answer here.
One strong advantage of using <code>jest</code> is that it uses CJS by default, and can more easily mock things out.
If you're using ESM to build, it's hard or possible impossible to swap out your dependencies.
Go on, have a try:</p>
<pre><code class="language-js"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> allMethods <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;network-tool&#x27;</span>;
allMethods.<span class="hljs-property">doNetworkThing</span> = <span class="hljs-function">() =&gt;</span> { ... };
<span class="hljs-comment">// TypeError: Cannot assign to read only property &#x27;doNetworkThing&#x27; of object &#x27;[object Module]&#x27;</span>
</code></pre>
<p>In a 🌈 perfect world 🌈 of abstractions, mocks are unnessecary—you should be replacing e.g., a context object or helper class at once to catch all external API calls.
But the reality is any code is going to do various calls to <code>fetch()</code> or whatever.</p>
<p>So, yeah.</p>
<p>If this was important to my projects, I'd probably:</p>
<ul>
<li>find all methods which get external data</li>
<li>make my code import them from an internal location</li>
<li>allow that internal location to swap them for mocks</li>
</ul>
<p>The naïve way to do this looks like this helper:</p>
<pre><code class="language-ts"><span class="hljs-keyword">const</span> isUnderTest = ...;  <span class="hljs-comment">// maybe check process.env.SOMETHING ?</span>
<span class="hljs-keyword">const</span> fetch = (isUnderTest ? mockedFetch : globalThis.<span class="hljs-property">fetch</span>);
<span class="hljs-keyword">export</span> { fetch };
</code></pre>
<p>…this could be simplified with <a href="https://nodejs.org/api/packages.html#conditional-exports">conditional exports</a>, but that's a different post. 📬</p>
<h2 id="done">Done</h2>
<p>That's it.
I hope you're writing unit tests as we speak.</p>
<p>If you want to learn more about writing React tests, then I suggest:</p>
<ul>
<li>Looking at the <a href="https://testing-library.com/docs/react-testing-library/api#render-result">result type</a> returned by the above <code>render()</code> method—you'll want to use it to find elements, and check their state</li>
<li>Reading about <a href="https://testing-library.com/docs/user-event/intro">user-event</a>, which helps you click on things or type data</li>
</ul>
<p>You can get a huge amount of benefit out of writing unit tests for components <em>without a real browser</em>, and I hope you'll give it a go. 🌇</p>
]]></description><link>https://samthor.au/2022/test-react-builtin/</link><guid isPermaLink="true">https://samthor.au/2022/test-react-builtin/</guid><dc:creator><![CDATA[Sam Thorogood]]></dc:creator><pubDate>Sat, 04 Jun 2022 00:00:00 GMT</pubDate></item></channel></rss>