<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tim Etler</title>
    <description>Personal Blog</description>
    <link>https://www.timetler.com/</link>
    <atom:link href="https://www.timetler.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 27 Mar 2026 21:57:17 +0000</pubDate>
    <lastBuildDate>Fri, 27 Mar 2026 21:57:17 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>DelegateStream: a Higher-Order Transform for Sequentially Injecting Streams Within Streams</title>
        <description>&lt;h2 id=&quot;streaming-parallel-recursive-ai-swarms&quot;&gt;Streaming Parallel Recursive AI Swarms&lt;/h2&gt;

&lt;p&gt;Here’s a fun demo: 50 LLM agents streaming recursively and in parallel to summarize the entire Star Wars franchise in order, in 30 seconds. Normally this would be a 5½ minutes blocking operation, but with stream injection, you can stream different parts of the response simultaneously.&lt;/p&gt;

&lt;p&gt;In this contrived example, I built a franchise series summary agent swarm with three levels of agents: a franchise agent that lists the installments, an installment agent that lists out the acts, and an act synopsis agent that provides a detailed, multi-paragraph synopsis of each act (watch until the end):&lt;/p&gt;

&lt;video controls=&quot;&quot;&gt;&lt;source src=&quot;/static/videos/swarm-weaver.mp4&quot; type=&quot;video/mp4&quot; /&gt;&lt;img src=&quot;/static/images/swarm-weaver.jpg&quot; /&gt;&lt;/video&gt;

&lt;p&gt;This produces &lt;em&gt;a lot&lt;/em&gt; of tokens very fast. So many, in fact, that this run on Claude 4 Sonnet cost me 30¢ (a single run on 4 Opus costs $1.50)! To compare the speed, I ran the same task through a monolithic prompt in a single agent call.&lt;/p&gt;

&lt;p&gt;The traditional monolithic prompt produced about 10k tokens in 5½ minutes, producing 30 tokens/sec. However, with the parallel streaming approach, it produced about 8.7k tokens in 30 seconds, spawning 50 agents with a blended rate of 281 tokens/sec!&lt;/p&gt;

&lt;p&gt;This is a parallel recursive streaming AI swarm in action. By injecting granular child agents, it enables recursive parallel streams. In this example, it’s summarizing Star Wars. The output is sequential, but the child agents are not blocked. After the main franchise agent spawns a child agent, it keeps generating tokens. While that first child agent is still running, it can spawn another child agent. These child agents can then spawn another child agent, and another, all running simultaneously. The client gets tokens the instant they’re ready, while downstream tokens that were already produced get unleashed immediately. This enables streams to produce other streams running in parallel while maintaining sequential order.&lt;/p&gt;

&lt;p&gt;Watch the demo closely and notice it begins by printing movie titles. To start, there’s a network delay while it swaps to the act-writing stream (this can be reduced with HTTP request pre-warming or with cluster-local LLMs to decrease round-trip time). Then the act agent starts streaming the first act of “The Phantom Menace”. By the time Act 1 completes, the remaining acts are almost done, causing it to rapidly jump between acts. Blink and you’ll miss it: once the first movie completes, we start jumping entire movies at a time, first to the end of “Attack of the Clones” then to the end of “Revenge of the Sith”. By the time the prequels are complete, all 9 movies are already done and we zoom ahead to the end. The tokens come so fast, the output needed to be throttled just to show what’s going on.&lt;/p&gt;

&lt;p&gt;The speed is insane, but it unlocks more power by splitting up agents to simplify prompts. If you want to play around with it, check out my &lt;a href=&quot;https://github.com/etler/swarm-weaver&quot;&gt;proof of concept repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;stream-injection&quot;&gt;Stream Injection&lt;/h2&gt;

&lt;p&gt;The ability to sequentially inject a stream within another stream enables remarkably powerful behavior. Each injected stream can perform its own work in parallel, while consumers of the output stream still get content in order without worrying about what’s happening inside the stream. Streams composed within streams are higher-order streams.&lt;/p&gt;

&lt;p&gt;When combined with streaming LLM calls, stream injection lets us split complex monolithic prompts into child agents, each with their own simpler, more focused prompts, solving vast swaths of prompt complexity issues. Additionally, stream injection allows each of the child streams to run in parallel, simultaneously constructing different parts of the response ahead of what’s currently streaming. Not only do we get better answers, they finish at a ludicrous speed.&lt;/p&gt;

&lt;p&gt;Spawning parallel recursive AI agents was already possible on our backend, but existing methods break streaming output and often block on parent agent output before spawning children.&lt;/p&gt;

&lt;h2 id=&quot;prompt-complexity-and-attention-dilution&quot;&gt;Prompt Complexity and Attention Dilution&lt;/h2&gt;

&lt;p&gt;I discovered this need while exploring a way to embed &lt;a href=&quot;/2025-08-19-unlocking-rich-ui-components-in-ai.md&quot;&gt;JSX component rendering in markdown responses&lt;/a&gt; to power AI agent responses at my company &lt;a href=&quot;https://vetted.ai&quot;&gt;Vetted&lt;/a&gt;. I’m very excited about the new UI experiences it will power within our AI answers, but getting agents to output JSX component tags in the correct format has been pushing the latest LLM models to their limit. We hit a complexity wall where current model limits prevent us from adding more advanced component composition to our answers.&lt;/p&gt;

&lt;p&gt;When a prompt gets too large and complicated on the backend, our standard approach has been to break it down into smaller tasks and synthesize those outputs into an answer. We’ve already implemented agent swarms in our backend to distribute research and keep agents accurate with tightly scoped tasks, but the problem with that approach on the frontend is that the client needs to support streaming.&lt;/p&gt;

&lt;p&gt;On the backend, this isn’t an issue. We can run agents in parallel, have agents spawn subagents recursively, and wait for each block of work to complete before processing it. However, on the client, blocking is not an option. Our answers take a while to produce, and while users are willing to wait for high quality research, they want content to start streaming as early as possible. It’s the same as the time-to-first-paint problem: you don’t need the entire page ready and interactive immediately, but you need to show something is happening as soon as you can.&lt;/p&gt;

&lt;p&gt;Breaking down a monolithic agent into simpler tasks would enable several very important capabilities. It would solve “attention dilution” problems where an overloaded prompt becomes unable to follow complex instructions accurately because instruction attention gets spread too thin. It would also enable optimizing our model choices. With a monolithic prompt, only one model can be used at a time, meaning tradeoffs must be made between speed, cost, and specialization. By breaking up a prompt, we could choose the optimal model for each subtask.&lt;/p&gt;

&lt;p&gt;Stream injection also makes it possible to interpolate actively streaming agents with data from any external data source such as databases, caches, and APIs. An injected stream doesn’t need to be an AI stream; it could be any data stream. Injecting newly discovered work as data streams in can enable significant streaming orchestrations and optimization improvements.&lt;/p&gt;

&lt;p&gt;To do all this, we need a way to provide ordered consumption of parallel production over streams.&lt;/p&gt;

&lt;h2 id=&quot;injecting-and-hot-swapping-streams&quot;&gt;Injecting and Hot Swapping Streams&lt;/h2&gt;

&lt;p&gt;So what’s the solution? How do you execute multiple prompts in parallel without breaking streaming?&lt;/p&gt;

&lt;p&gt;First, you need a way to swap between streams. When a parent agent spawns a child agent, the child agent needs to take over streaming duties. Second, you need a way to split a stream in the middle of its response; injecting the child stream into the parent stream, then swapping to it and back. Third, you need to do this on the fly so a parent stream can spawn a child stream in the middle of stream execution.&lt;/p&gt;

&lt;p&gt;Conceptually, you could do this with a transform stream that parses its input stream for directives to spawn a child stream. You could then orchestrate those streams to swap from the main stream to the child stream and back again. I later found out that these are called higher-order streams. They’re a common pattern in functional streaming, but don’t seem to have an equivalence in procedural streaming.&lt;/p&gt;

&lt;h2 id=&quot;a-simple-solution-to-a-hard-problem&quot;&gt;A Simple Solution to a Hard Problem&lt;/h2&gt;

&lt;p&gt;When I first started working on the solution, it seemed like I had my work cut out for me. At first, I thought I would need some complex central orchestrator to keep track of all the child agents and nested child agents and so forth, along with tremendous memory buffering overhead.&lt;/p&gt;

&lt;p&gt;Then I started breaking down the problem more. I wanted to use every tool available, so I started reading the web streams spec front to back. While doing that, I was reminded of something: streams in JavaScript are also async iterables. That opened up a new angle I wasn’t considering. I didn’t need to orchestrate streams, I needed to orchestrate async iterables. What if, instead of a complex central orchestration layer, I could combine async iterables together?&lt;/p&gt;

&lt;p&gt;Refreshing my memory on async iterables, I found the next key component. Generators allow you to delegate to other iterables using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt;, enabling you to swap from one stream to another. Still, we needed the ability to spawn new work dynamically as we discovered it on the stream. The generator would start with a finite chain of iterators and end without adding any new child agents. I needed the generator to stop and wait for a new iterator when it ran out of iterators to delegate to. Naturally, this called for a promise.&lt;/p&gt;

&lt;p&gt;At this point, I had all the components I needed. The solution was to combine async iterables, generator delegation, and an unresolved promise that would eventually resolve with the next iterable or a termination signal.&lt;/p&gt;

&lt;h2 id=&quot;the-relay-pattern&quot;&gt;The Relay Pattern&lt;/h2&gt;

&lt;p&gt;I’ve looked extensively, and haven’t this pattern mentioned anywhere, so I called it the “Relay Pattern”. Like a relay race, each iterable runs through a stint. When it completes its stint, it awaits an iterable promise (waiting for the next racer) to be ready and then delegates the main iterator to the next iterator (passing the baton). At its core, it uses promises to coordinate delegation handoffs to the next iterator. This lets you chain new iterables on the fly without knowing about those iterables ahead of time. When a consumer consumes the main async iterable sequence, they can do so transparently without needing to be aware that a swap even happened.&lt;/p&gt;

&lt;p&gt;While checking prior art, &lt;a href=&quot;https://samthor.au/2020/async-generators-input/&quot;&gt;I found a similar approach done at the item level&lt;/a&gt; using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt;, but couldn’t find the pattern using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt;. When paired with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt;, the pattern essentially turns a pull-only generator into a push/pull stream, but when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt; is used instead, it turns it into a higher-order push/pull stream akin to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flatten&lt;/code&gt; operation.&lt;/p&gt;

&lt;!-- Raw is needed for the code portions to not break where template syntax is used --&gt;

&lt;h2 id=&quot;asynciterablesequencer&quot;&gt;AsyncIterableSequencer&lt;/h2&gt;

&lt;p&gt;To implement this pattern, I’ve built a library: &lt;a href=&quot;https://www.npmjs.com/package/async-iterable-sequencer&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async-iterable-sequencer&lt;/code&gt;&lt;/a&gt;. As the name suggests, it creates an async iterable that sequences other async iterables. The code is so simple I can paste it here. Excluding types, it’s only 25 lines of code:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AnyIterable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncIterable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Iterable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chainable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AnyIterable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chainable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncIterableSequencerReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncGenerator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;asyncIterableSequencer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AsyncIterableSequencerReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Chainable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;withResolvers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Chainable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;resolver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nextIterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nextIterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chainable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;iterator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})(),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;resolver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The factory function returns an async iterator called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sequence&lt;/code&gt; and a bound function called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chain&lt;/code&gt; for adding new iterators. Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chain&lt;/code&gt; fulfills a promise, it can be called both synchronously and asynchronously at any point in time.&lt;/p&gt;

&lt;p&gt;This pattern allows us to create a chain of async iterables, and because web streams are a superset of async iterables, it enables us to enqueue entire streams; the main requirement for stream injection. As async iterables do not need to be complete at the time they’re chained, we can connect multiple batches of async work to be completed in any order and in parallel, while still maintaining their sequential ordering. Because this uses iterator delegation, there’s no additional buffering overhead for orchestrating the streams; we can use language level imperative constructs to do this in a cleaner way.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsyncIterableSequencers&lt;/code&gt; enable the parallel streaming part of parallel recursive streaming AI swarms, but we still need to solve the recursive part.&lt;/p&gt;

&lt;h2 id=&quot;delegatestream&quot;&gt;DelegateStream&lt;/h2&gt;

&lt;p&gt;That brings us to the next library I created: &lt;a href=&quot;https://www.npmjs.com/package/delegate-stream&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delegate-stream&lt;/code&gt;&lt;/a&gt;. This library takes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsyncIterableSequencer&lt;/code&gt; and combines it with a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/TransformStream/TransformStream&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TransformStream&lt;/code&gt;&lt;/a&gt; interface. A transform stream allows you to intercept a stream and process chunks as they pass through, optionally transforming chunks from an input stream, and enqueuing them onto an output stream. The difference is that a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegateStream&lt;/code&gt; doesn’t enqueue chunks like a transform stream; it chains entire streams as well as any other async iterables.&lt;/p&gt;

&lt;p&gt;It is a higher-order version of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TransformStream&lt;/code&gt; that streams other streams. It’s akin to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flatMap&lt;/code&gt; in functional streaming libraries, but unlike &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flatMap&lt;/code&gt;, it adheres to imperative interface-based streaming patterns allowing it to be used with common standard library stream interfaces like &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ReadableStream&lt;/code&gt;&lt;/a&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pipeThrough&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means that an in-progress stream can be parsed for a directive to generate a new stream, and then that stream can be injected into the middle of the in-progress output stream by chaining it before continuing. Since those injected streams can be another delegate stream, delegate streams can inject delegate streams recursively, fulfilling the recursive streaming part of parallel recursive streaming AI swarms.&lt;/p&gt;

&lt;p&gt;While the chaining is recursive, the output is flattened. When consuming the stream, the internal implementation details are completely transparent, adding no additional requirements for consumption. While the concept of flattening streams exists in functional stream programming, I have yet to see higher-order streams implemented around a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TransformStream&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;It’s a “Delegate” because it is powered by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield*&lt;/code&gt; used under the hood of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsyncIterableSequencer&lt;/code&gt;. Yielding to another generator is called delegation so when a stream is added to the chain, it will eventually delegate iteration to the chained stream.&lt;/p&gt;

&lt;p&gt;As async iterable sequencers are so simple, so are delegate streams:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;asyncIterableSequencer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;async-iterable-sequencer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DelegateStreamOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;finish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Chain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DelegateStream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;readable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReadableStream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;writable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WritableStream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;finish&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;DelegateStreamOptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;asyncIterableSequencer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;readable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReadableStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sequence&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;writable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WritableStream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;I&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;finish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s little more than a class wrapper that implements the transform stream interface and exposes some lifecycle callbacks. All the magic happens within the async iterable sequencer. The delegate stream simply allows you to interface with it in the middle of another stream.&lt;/p&gt;

&lt;p&gt;With all the building blocks in place, we can now build the engine to power parallel recursive streaming AI swarms.&lt;/p&gt;

&lt;h3 id=&quot;a-missing-abstraction&quot;&gt;A Missing Abstraction?&lt;/h3&gt;

&lt;p&gt;Surprisingly, I’ve failed to find any reference or implementation of higher-order transform streams. For first-order streams there’s an abstraction spectrum with async generators serving imperative pull streams, interface-based streams, and functional streams. These abstractions have their uses with a tradeoff between control and composability.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Async generators&lt;/strong&gt; are the least abstract, providing low-level imperative control, allowing you to suspend values with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt;. They provide the maximum amount of control, exposing control flow operations; however, their semantics make them harder to compose.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interface-based streams&lt;/strong&gt; are in between, wrapping streams in a context that can encapsulate a procedure while exposing a controller for output emission. They find a balance, giving you procedural lifecycle control, with a middle ground for composability.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Functional streams&lt;/strong&gt; are the most abstract, coordinating pipelines with declarative transformations. While they have the least granular control based on function types, they provide the most safety and composability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looking at the transform use case, there’s a clear progression between the abstractions:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;yield → TransformStream → stream.map
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But what about higher-order streams of streams? There seems to be a gap:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;yield* → ???Stream → stream.flatMap
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What’s the interface-based stream equivalence in this progression? I couldn’t find it, which is why I implemented my own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DelegateStream&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;ai-swarm-orchestration&quot;&gt;AI Swarm Orchestration&lt;/h2&gt;

&lt;p&gt;For the AI swarm proof of concept demo, I’ve created a CLI tool called &lt;a href=&quot;https://github.com/etler/swarm-weaver&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;swarm-weaver&lt;/code&gt;&lt;/a&gt;. It has two kinds of delegators: one for prompts and one for agent execution. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PromptDelegator&lt;/code&gt; takes a stream of content that it interpolates into a prompt, and then chains an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AgentDelegator&lt;/code&gt; onto it once the prompt input stream is ready to be sent to an LLM provider for content generation. The output of that agent delegator is then parsed for child agent directives. For the proof of concept, I used XML tags.&lt;/p&gt;

&lt;h3 id=&quot;franchise-example-in-depth&quot;&gt;Franchise Example In Depth&lt;/h3&gt;

&lt;p&gt;In the example video, it generated a synopsis of a media franchise using multiple layers of child agents. The entrypoint &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; prompt is used to generate text containing XML directives which are used to spawn child agents. Here is an example of what those prompts look like (simplified for brevity):&lt;/p&gt;

&lt;h4 id=&quot;main-prompt&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; prompt&lt;/h4&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You are an agent that outlines the installments of book series, movie franchises, and similar large-scale series. Your task is to take a user&apos;s request about a series and output a specific template format listing all installments.

&lt;span class=&quot;gs&quot;&gt;**Input:**&lt;/span&gt;
{{_content_}}

&lt;span class=&quot;gs&quot;&gt;**Output Format:**&lt;/span&gt;
&lt;span class=&quot;gh&quot;&gt;# [series title]&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## [installment title]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;installment&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[series name]&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[installment title]&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## [installment title]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;installment&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[series name]&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[installment title]&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{_content_}}&lt;/code&gt; is an interpolation placeholder that gets replaced with the prompt’s input stream. For the root prompt, this stream comes from stdin.&lt;/p&gt;

&lt;p&gt;The tool also substitutes, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{_context_}}&lt;/code&gt; with the tokens generated so far by the parent stream to make it easier to pass in the full context generated so far, however it’s not used in these examples.&lt;/p&gt;

&lt;p&gt;When the prompt generates XML tags, those tags trigger a child prompt. In this example, the root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; agent spawns child agents for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; prompt:&lt;/p&gt;

&lt;h4 id=&quot;installment-prompt&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; prompt&lt;/h4&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Generate chronological act summaries for the specified media content. Each act should be a single, concise sentence that captures the essential narrative progression and provides sufficient context for further elaboration.

&lt;span class=&quot;gs&quot;&gt;**Input:**&lt;/span&gt;
Series: {{series}}
Title: {{title}}

&lt;span class=&quot;gs&quot;&gt;**Output Format:**&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;### Act [number]&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;act&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[series name]&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[title]&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
[Single sentence act summary]
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/act&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, instead of receiving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{_content_}}&lt;/code&gt;, the prompt receives attribute interpolation values for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{series}}&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{title}}&lt;/code&gt;. These come from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; agent’s output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;installment series=&quot;[series name]&quot; title=&quot;[installment title]&quot;/&amp;gt;&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; agent then spawns another child agent, feeding its output into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;act&lt;/code&gt; prompt:&lt;/p&gt;

&lt;h4 id=&quot;act-prompt&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;act&lt;/code&gt; prompt&lt;/h4&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You are tasked with creating an engaging 1-3 paragraph synopsis for the following media based on the provided act outline.

&lt;span class=&quot;gs&quot;&gt;**Input:**&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Series: {{series}}
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Title: {{title}}

&lt;span class=&quot;gs&quot;&gt;**Output Format:**&lt;/span&gt;
[1 - 3 paragraphs]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;act&lt;/code&gt; prompt receives the series and title values again, as well as the special &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{{_content_}}&lt;/code&gt; value, which is populated with the act summary that was generated inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;act&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;h4 id=&quot;executing-the-swarm&quot;&gt;Executing the Swarm&lt;/h4&gt;

&lt;p&gt;Once the application starts, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; agent executes first. It outputs something like this:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;# Star Wars&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## Episode I: The Phantom Menace&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;installment&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Star Wars&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Episode I: The Phantom Menace&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
...
&lt;span class=&quot;gu&quot;&gt;## Episode IX: The Rise of Skywalker&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;installment&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Star Wars&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Episode IX: The Rise of Skywalker&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Non-XML content is chained onto the output stream as a string, while XML tags trigger the chaining of a new prompt delegator. The chain created by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; agent then looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;stdout &amp;lt; string &amp;lt; installment_prompt &amp;lt; string &amp;lt; installment_prompt &amp;lt; ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once content sent to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; prompts is complete those prompt delegators chain the corresponding agent delegators in their place. At this point the chain looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;stdout &amp;lt; string &amp;lt; installment_agent &amp;lt; string &amp;lt; installment_agent &amp;lt; ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; agents now generate their own content. The resulting output looks like this:&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;# Star Wars&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## Episode I: The Phantom Menace&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;### Act 1&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;act&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;series=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Star Wars&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Episode I: The Phantom Menace&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
Jedi Master Qui-Gon Jinn and his apprentice Obi-Wan Kenobi are dispatched to negotiate a trade dispute with the Trade Federation, but discover a sinister plot involving an invasion of the peaceful planet Naboo and encounter a young slave named Anakin Skywalker on Tatooine.
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/act&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The chain has now been expanded with newly injected delegate streams:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;stdout &amp;lt; string &amp;lt; act_agent &amp;lt; string &amp;lt; act_agent &amp;lt; ... &amp;lt; string &amp;lt; installment_agent &amp;lt; ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;act&lt;/code&gt; agents receive the summary sentences generated by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;installment&lt;/code&gt; agents and uses that context to generate a synopsis with more detail. As the summary sentence contains the starting and ending context of the act, the generated text flows seamlessly from one act to the next.&lt;/p&gt;

&lt;p&gt;Each delegator expands its work directly in place within the chain, maintaining sequential order while creating new subtasks at its position.&lt;/p&gt;

&lt;p&gt;This approach solves the problem of overly complicated prompts. By breaking problems down without interrupting streams, we can create focused prompts that do fewer things more effectively. Even better, breaking out the prompts unlocks ludicrous speed. You not only get the time-to-first-token benefits of streaming, but also a swarm of agents generating and expanding content in place and in parallel with no downside.&lt;/p&gt;

&lt;p&gt;The biggest issue I’ve encountered is that the output is &lt;em&gt;way too fast&lt;/em&gt;. Presenting it reasonably to the user, without huge chunks of content suddenly appearing, will require smoothing and throttling.&lt;/p&gt;

&lt;h3 id=&quot;meta-prompting&quot;&gt;Meta-Prompting&lt;/h3&gt;

&lt;p&gt;Because I decided to represent child agent directives as XML tags, it got me thinking. What would happen if I put an agent inside another agent? As text inside a child agent tag is interpolated into its prompt, nesting one agent tag inside another should logically send the inner agent’s output to the outer agent’s prompt.&lt;/p&gt;

&lt;p&gt;By sending the output of one agent to another, we enable meta-prompting: agents prompting agents. To accomplish this, I implemented a tag stack that allows inner agent tags to stream their content to the prompt delegator of the outer agent tag. This content can include yet another nested agent tag, allowing meta-prompts to stream recursively up the chain of nested agent tags.&lt;/p&gt;

&lt;p&gt;With this approach, you can use sub-agents as idea generators or context generators for the containing agent. The inner tags don’t even have to spawn agents. They could be standard subroutines that stream in crawl data, a database call, an API call, or any other source of information you can think of.&lt;/p&gt;

&lt;p&gt;I believe meta-prompting can enable incredibly powerful patterns, and with the relay pattern powering it all, content is unblocked, processed, and streamed as soon as it is resolved and available.&lt;/p&gt;

&lt;h3 id=&quot;beyond-ai&quot;&gt;Beyond AI&lt;/h3&gt;

&lt;p&gt;While functional streaming libraries excel at pipeline processing with declarative transformations, the lifecycle control provided by interface-based streams simplifies certain architectural patterns, particularly those involving complex control flow with shared context. In the meta-prompting example, lifecycle hooks remain accessible while parsing nested tags and managing their nested streams.&lt;/p&gt;

&lt;p&gt;By making higher-order streams accessible to other paradigms, I hope these powerful patterns can be integrated seamlessly into a wider range of environments. I’m currently exploring their use in streaming web rendering frameworks, where I’ve found lifecycle control helps modularize the complex data flow of modern frameworks.&lt;/p&gt;

&lt;p&gt;This all started when I set out to solve a specific problem: parallel AI prompts couldn’t be streamed without breaking output order. Higher-order streams offered an elegant approach, but weren’t available in the interface-based streaming environment I needed. The relay pattern provided a solution in only 25 lines of code. It brings this powerful pattern from functional streaming paradigms to interface-based streams.&lt;/p&gt;

&lt;p&gt;Any async operation can be injected as a stream. From database queries to API calls to file processing, anything can be composed and processed with higher-order streams. Finding the right abstraction cuts through complexity, and often the simplest solutions are the most powerful.&lt;/p&gt;
</description>
        <pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/08/23/parallel-recursive-streaming-ai-swarms/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/08/23/parallel-recursive-streaming-ai-swarms/</guid>
        
        
      </item>
    
      <item>
        <title>Unlocking Rich UI Component Rendering in AI Responses</title>
        <description>&lt;p&gt;LLMs have enabled us to solve a new class of problems with more flexibility than ever. But language models are inherently text-based, which has led to AI interfaces being heavily text dominated.&lt;/p&gt;

&lt;p&gt;As someone who has been creating web technology experiences my entire life, I’m not satisfied with so much UI being replaced with text. At &lt;a href=&quot;https://vetted.ai&quot;&gt;Vetted&lt;/a&gt;, we have been building a shopping research assistant, and shopping is inherently visual and interface-heavy. Products need to display images, and the UI needs to present structured data that lets users navigate between products and compare them.&lt;/p&gt;

&lt;p&gt;Over the years, we have been experimenting with new ways to incorporate rich UI components into our LLM responses. We’ve done this by supplementing our text payloads with structured product data that renders as product cards and research components, but we weren’t happy with these elements being separated from the main text response. So, I set out to find a way for LLM output to embed UI components directly in our markdown output.&lt;/p&gt;

&lt;h2 id=&quot;mdx-enhanced-dynamic-markdown&quot;&gt;MDX Enhanced Dynamic Markdown&lt;/h2&gt;

&lt;p&gt;I built &lt;a href=&quot;https://www.npmjs.com/package/react-markdown-with-mdx&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown-with-mdx&lt;/code&gt;&lt;/a&gt;: an HOC (Higher-Order Component) wrapper around &lt;a href=&quot;https://www.npmjs.com/package/react-markdown&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt;&lt;/a&gt; that enhances its markdown processing to support JSX component tags. You can register approved React components with it, ensuring only a safe subset of JSX component tags can be rendered. The library includes an optional validation helper function called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mdxComponent&lt;/code&gt;, which pairs React components with &lt;a href=&quot;https://zod.dev/&quot;&gt;zod&lt;/a&gt; validators to validate JSX attributes.&lt;/p&gt;

&lt;p&gt;This lets you prompt LLM calls to generate JSX tags that can be rendered safely with a clean and easy integration. Here’s what it looks like in action in our prototype UI at Vetted:&lt;/p&gt;

&lt;video controls=&quot;&quot;&gt;&lt;source src=&quot;/static/videos/rich-ui-components.mp4&quot; type=&quot;video/mp4&quot; /&gt;&lt;img src=&quot;/static/images/rich-ui-components.png&quot; /&gt;&lt;/video&gt;

&lt;p&gt;The code looks something like this:&lt;/p&gt;

&lt;div class=&quot;language-tsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReactMarkdown&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;react-markdown&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;withMdx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mdxComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MdxComponents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;react-markdown-with-mdx&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MdxReactMarkdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;withMdx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ReactMarkdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MdxMarkdownRendererProps&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MdxMarkdownRenderer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;MdxMarkdownRendererProps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MdxReactMarkdown&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;components&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;components&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;markdown&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MdxReactMarkdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;components&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MdxComponents&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;card-carousel&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mdxComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;MdxCardCarousel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;editorial-card&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mdxComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;MdxEditorialCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;award&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;rating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;ranking&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;optional&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;product-card&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mdxComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;MdxProductCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unlike projects like &lt;a href=&quot;https://mcpui.dev/&quot;&gt;MCP-UI&lt;/a&gt;, these components aren’t loaded in externally via an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iframe&lt;/code&gt; that needs window message passing for integration, and they aren’t relegated to a separate message outside the main text. Instead, they’re processed into framework-native React components that are embedded directly in the main LLM-generated text. This essentially enables HTML component-like behavior in React and other JSX frameworks, letting you extend markdown with any UI component you desire!&lt;/p&gt;

&lt;p&gt;To power the response streaming shown in the video, I needed to balance the HTML tag tree and truncate incomplete tags. This ensures the MDX parser receives valid HTML and blocks partial tag tokens until they’re complete. To enable this, I also created &lt;a href=&quot;https://www.npmjs.com/package/html-balancer-stream&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;html-balancer-stream&lt;/code&gt;&lt;/a&gt;. It allows you to auto-close and balance unclosed tags or strip them out until they’re ready. It provides both streaming and non-streaming APIs.&lt;/p&gt;

&lt;h2 id=&quot;how-it-works-powering-up-ai-markdown-responses-with-mdx&quot;&gt;How it Works: Powering up AI Markdown responses with MDX&lt;/h2&gt;

&lt;p&gt;I originally experimented with this by creating a prototype using HTML components. This approach worked well because HTML components are registered directly with the browser DOM. This allowed custom HTML components to be injected as HTML tag strings directly via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;innerHTML&lt;/code&gt; with no additional render hooks. This was fine for a prototype, but it was definitely not ready for production. Doing that would introduce major security risks and create side effects within our rendering framework. I wanted to find a way to do this safely, and tightly integrate it with React, which we use at Vetted.&lt;/p&gt;

&lt;p&gt;For Markdown rendering, we already used the library &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; to convert LLM-generated markdown text into React components. Markdown supports adding raw HTML but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; rightly does not support this. It converts HTML tags into sanitized strings because allowing arbitrary HTML in dynamic markdown content would be incredibly dangerous. My goal was to render the tags as framework-native React components to ensure that the components could only run trusted encapsulated code.&lt;/p&gt;

&lt;p&gt;To determine the best way to do that, I started looking under the hood. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; is powered by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unified&lt;/code&gt; project; a framework and community ecosystem for parsing and transforming syntax trees that can combine and convert various languages. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; is actually a thin wrapper around &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unified&lt;/code&gt; that parses the markdown AST, converts it to an HTML AST, and then into React components.&lt;/p&gt;

&lt;p&gt;It does this by parsing markdown with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remark&lt;/code&gt;: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unified&lt;/code&gt; markdown processor that converts markdown into an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mdast&lt;/code&gt; markdown AST. It then converts that to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hast&lt;/code&gt; HTML AST from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rehype&lt;/code&gt; using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remark-rehype&lt;/code&gt;. Finally, it uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hast-util-to-jsx-runtime&lt;/code&gt; to safely convert the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hast&lt;/code&gt; AST into JSX components that can be rendered by React and other JSX-based rendering frameworks.&lt;/p&gt;

&lt;p&gt;After understanding how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; works, I felt the best approach for adding React component support to markdown was extending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remark&lt;/code&gt; to enable JSX parsing. Fortunately, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown&lt;/code&gt; project mentions this in its readme and refers developers to the &lt;a href=&quot;https://github.com/mdx-js/mdx/&quot;&gt;MDX&lt;/a&gt; project, which does exactly that. It lets you put JSX syntax directly in Markdown. Problem solved!&lt;/p&gt;

&lt;p&gt;Of course, it’s never that simple. MDX is a very impressive project. It’s so impressive that it doesn’t just let you put &lt;em&gt;static&lt;/em&gt; JSX components in your Markdown, it supports the &lt;em&gt;entire JSX syntax&lt;/em&gt;, including imports, exports, and dynamic Javascript expressions.&lt;/p&gt;

&lt;p&gt;Unfortunately, that takes us back to square one. Using it with dynamic LLM text output would be incredibly insecure and open up major security vulnerabilities. The MDX author is fully aware of this. MDX is designed for compile-time rendering of trusted static content, not runtime rendering.&lt;/p&gt;

&lt;p&gt;Thankfully, MDX is built on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unified&lt;/code&gt; project which has a modular parsing and transformation pipeline. While I could not use MDX out of the box, I could leverage its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remark-mdx&lt;/code&gt; parser to build an MDX AST in a custom pipeline. This let me add the exact subset of functionality I needed for static JSX components in markdown. Now, our LLM output could include JSX tags with safe conversion to framework-native runtime components!&lt;/p&gt;

&lt;p&gt;To complete the pipeline, I needed to create a few new libraries. The MDX parser detects JSX tags in the markdown AST and adds them to the syntax tree as MDX nodes. With pipeline control, I could strip the MDX nodes with dynamic syntax while preserving the static ones.&lt;/p&gt;

&lt;p&gt;I created &lt;a href=&quot;https://www.npmjs.com/package/rehype-mdx-elements&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rehype-mdx-elements&lt;/code&gt;&lt;/a&gt; to do this. It filters for static JSX tags and converts them into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hast&lt;/code&gt; HTML AST elements so they can be processed by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hast-util-to-jsx-runtime&lt;/code&gt; library. It converts the HTML AST nodes into runtime components that must be registered with the utility, ensuring only trusted code gets executed.&lt;/p&gt;

&lt;p&gt;I also needed to make &lt;a href=&quot;https://www.npmjs.com/package/remark-unravel-mdx&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remark-unravel-mdx&lt;/code&gt;&lt;/a&gt;, which removes unnecessary paragraph wrappers around MDX nodes, producing a cleaner component tree.&lt;/p&gt;

&lt;p&gt;These libraries power the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown-with-mdx&lt;/code&gt; processing pipeline. They enable the safe MDX-to-React component transformation shown in the demo.&lt;/p&gt;

&lt;h2 id=&quot;generating-llm-jsx-output&quot;&gt;Generating LLM JSX Output&lt;/h2&gt;

&lt;p&gt;To use this with AI-generated output, you can prompt an LLM with your JSX component tag and attribute schemas, their acceptable children, and examples of their usage. LLMs excel at generating static JSX tags because they share the same syntax as XML, a language they’re heavily trained on.&lt;/p&gt;

&lt;p&gt;As JSX tags with many attributes can confuse LLMs, it’s best to keep attributes minimal. When you ask an LLM to generate a tag with coupled attributes, they can fall out of sync and lead to hallucinations. We avoid this by having the LLM fill in simple ID strings that components can look up in shared state to retrieve detailed data. The only data we ask the LLM to generate directly is context-sensitive content like text snippets from research sources.&lt;/p&gt;

&lt;p&gt;Since the outputs are JSX tags, they can conform 1:1 with your existing components, so existing documentation and schemas can be shared between your prompts and codebase. This lets you keep their definitions and versions in sync.&lt;/p&gt;

&lt;p&gt;I’ve long felt the need for stronger connections between LLM outputs and rendering frameworks. I’m hopeful &lt;a href=&quot;https://www.npmjs.com/package/react-markdown-with-mdx&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;react-markdown-with-mdx&lt;/code&gt;&lt;/a&gt; will help fill that gap!&lt;/p&gt;
</description>
        <pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/08/19/unlocking-rich-ui-components-in-ai/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/08/19/unlocking-rich-ui-components-in-ai/</guid>
        
        
      </item>
    
      <item>
        <title>Do What AI Can&apos;t Do: How to Protect Your Role as a Developer</title>
        <description>&lt;p&gt;&lt;img src=&quot;/static/images/do-what-ai-cant-do-danger.jpg&quot; alt=&quot;I&apos;m in Danger&quot; title=&quot;Ralph Wiggum&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Using AI in your development workflow can be a double edged sword. If you’re using AI to do your thinking for you, you’re self sabotaging your skills. Over reliance on AI will lead to your skills atrophying if you stop learning.&lt;/p&gt;

&lt;p&gt;If you’re just passing tasks along to an AI, you’re adding very little value, and it’s very likely it will replace you.&lt;/p&gt;

&lt;p&gt;If you don’t want to be replaced by AI, you’ll need to be good at the things that AI is bad at and focus on improving your ability to do the tasks it can’t do well, while offloading menial tasks that it can do well.&lt;/p&gt;

&lt;p&gt;There are some use cases I’ve found that AI is particularly good at:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One shotting boilerplate or very pattern matchy features&lt;/li&gt;
  &lt;li&gt;Producing prototypes to demonstrate that a concept works with throwaway code&lt;/li&gt;
  &lt;li&gt;Using it as a learning tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think the people who are in the most danger are those that fall under the umbrella of “technicians”. I classify them as people who implement patterns, but don’t create new ones. In other fields, that would be like an electrician as opposed to an electrical engineer.&lt;/p&gt;

&lt;p&gt;In software development, this looks like developers who primarily implement business logic within well-defined patterns and frameworks. These kinds of tasks are particularly pattern matchy, such as writing CRUD operations or translating designs to components. They’re skilled at following established patterns but rarely need to think at a system design level or solve novel technical problems.&lt;/p&gt;

&lt;p&gt;Those very pattern matchy roles can be replaced by AI, and it’s one of the areas where AI has shown significant improvement. With better emerging context management strategies, AI will be able to do those tasks with very high proficiency.&lt;/p&gt;

&lt;p&gt;However, in my experience it has barely gotten better at producing high-quality code in a vacuum unless you have very rigid examples or very common boilerplate it can follow.&lt;/p&gt;

&lt;p&gt;I’ve also seen a dramatic slowdown in improvements to its ability to reason about foundational and complex problems. I have not been happy with its output for architectural-level tasks, both in and out of codebases.&lt;/p&gt;

&lt;p&gt;These architectural tasks tend to look like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Choosing ideal abstractions to cleanly solve a problem&lt;/li&gt;
  &lt;li&gt;Determining ideal project structures for the problem&lt;/li&gt;
  &lt;li&gt;Deciding on and enforcing up-to-date best practices&lt;/li&gt;
  &lt;li&gt;Setting up novel optimized tooling flows&lt;/li&gt;
  &lt;li&gt;Iterating on a complex problem and finding more elegant alternative solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s also bad at certain classes of problems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Knowing when it doesn’t know something&lt;/li&gt;
  &lt;li&gt;Proactively searching for better solutions&lt;/li&gt;
  &lt;li&gt;Identifying the root of novel problems&lt;/li&gt;
  &lt;li&gt;Debugging obscure bugs and bugs in underlying code layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To differentiate yourself from AI and provide value that AI cannot, you will need to level up at those kinds of tasks. Luckily, we have a great tool available to us to help us uplevel: AI.&lt;/p&gt;

&lt;p&gt;While AI is not good at applying these concepts, it’s still an amazing research and learning tool. Using it as a learning assistant can rapidly help you progress your personal development. I have found that &lt;a href=&quot;./2025-04-08-prototyping-the-right-way-with-ai.md&quot;&gt;using it as a learning and prototyping tool&lt;/a&gt; in a loop helps me quickly grasp new concepts deeply.&lt;/p&gt;

&lt;p&gt;If you are offloading your thinking and work to it and being lazy, you are not learning, and you will not build the skills you will need to keep your job. AI will move every job function up a level of abstraction, and that’s the area where improvement will provide the greatest rewards.&lt;/p&gt;
</description>
        <pubDate>Sat, 19 Jul 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/07/19/do-what-ai-cant-do/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/07/19/do-what-ai-cant-do/</guid>
        
        
      </item>
    
      <item>
        <title>The future of AI will be dominated by the companies that pick the right abstractions today.</title>
        <description>&lt;p&gt;The Internet today isn’t fundamentally that different from the Internet of the 90s. The hardware is faster and more reliable, but the core principals have not changed on a foundational level. TCP/IP is more or less the same. HTTP is basically the same. HTML has hardly changed at all. JavaScript has evolved greatly, but we still target old engines with cross-compilation and polyfills.&lt;/p&gt;

&lt;p&gt;There was no groundbreaking tech that enabled the Internet to become so much more capable and pervasive today than it was in the past. The underlying hardware gradually improved, but many of the capabilities we have today could have been achieved much earlier with the right programming techniques. The biggest things that changed were the ideas powering it. It took us decades to refine and build out the abstractions, patterns, and frameworks behind the internet we use today.&lt;/p&gt;

&lt;p&gt;Internet companies in the 90s and early 00s failed because they over-promised and under-delivered on a vision they presented but they could not deliver on. It was not because the underlying technology wasn’t capable enough. They chose the wrong abstractions, couldn’t deliver, got buried under tech debt, burned out, withered, and died. The same story is happening again today with AI, and just like with the Internet boom and bubble and bust, the winners and losers of the future will be decided by the abstractions we choose today.&lt;/p&gt;

&lt;p&gt;We were told in that era to move fast and break things, and we still have to, but those that plan and think out their implementation strategies  before even making their first moves will be far more successful. An upfront investment in planning out your system design is a small fraction of the investment of building things. If you build on the wrong abstraction you’ll build the wrong foundation and everything you’ve built on top of that will come crashing down.&lt;/p&gt;

&lt;p&gt;Nobody knows what the right abstractions for AI are yet and most companies will be more focused on building things fast than building things right. We’re in uncharted territory where the most intuitive patterns for building reliable, scalable AI systems are still being discovered. Building fast without a plan is a risky gamble hoping you’ll stumble on the right patterns by accident. The companies that think through their abstractions early and choose them thoughtfully will be able to lead at the forefront of technology and refine their direction deliberately instead of scrambling to keep up.&lt;/p&gt;

&lt;p&gt;If the progress of AI were frozen today and the foundational tech stopped getting better, there would still be so much untapped potential waiting to be unlocked by those who figure out the right abstractions, patterns and frameworks. How you manage data flow and context and sub-agents and tool calls are all pivotal to the capabilities, quality, reliability, and success of your AI agents. The abstractions needed to build with AI the right way are still being figured out on the fly.&lt;/p&gt;

&lt;p&gt;Developers have started using AI to build things for them, but they’re just rehashing the same old patterns to do things we have already done. Over reliance on AI will lead you to recycling the existing abstractions that the AI was trained on. The breakthrough patterns and tools we need to build don’t exist yet but AI can help us get there faster if we use it to empower ourselves and learn and grow so we can make the ideas we need a reality. Those that use AI to be lazy and offload their work and thinking are only detaching themselves from developing the underlying abstractions needed to better utilize AI and they will fall behind.&lt;/p&gt;

&lt;p&gt;The Internet bubble was disastrous for many, but for those that got it right, it was the greatest opportunity that could have been afforded to them. That same opportunity has emerged again with AI, and this time we can learn from the past to get it right. Those that can lead through the chaos will be the ones that succeed.&lt;/p&gt;
</description>
        <pubDate>Tue, 15 Jul 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/07/15/pick-the-right-ai-abstractions/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/07/15/pick-the-right-ai-abstractions/</guid>
        
        
      </item>
    
      <item>
        <title>The Philosophical Programmer</title>
        <description>&lt;p&gt;&lt;strong&gt;The Pragmatic Programmer&lt;/strong&gt; (Andrew Hunt/David Thomas, 1999) has withstood the test of time as essential reading for developers early in their careers. Its practical advice and actionable pragmas work well when your primary job is shipping features and business logic. The patterns and advice it provides offer immediately applicable actions that developers can integrate into their development process.&lt;/p&gt;

&lt;p&gt;Where I find it lacking is in providing a core mental framework for the art of software design. This becomes much more important as you grow beyond writing features for end users to architecting platforms for other engineers. That’s where &lt;strong&gt;A Philosophy of Software Design&lt;/strong&gt; (John Ousterhout, 2018) steps up and provides the philosophy you need to tackle tough architectural problems. The book breaks down software design into fundamental first principles to construct mental frameworks, then re-derives best practices from that foundation.&lt;/p&gt;

&lt;p&gt;The core abstraction the book identifies as the philosophical basis for writing good software is complexity. Software design is all about managing complexity, and good design minimizes complexity while maintaining expressiveness. Complexity directly leads to increased cognitive load, which impacts our ability to build on top of a system and reduces efficiency. The framework through which complexity is managed is interfaces. The ways that information flows through a software system or any other information system is an interface.&lt;/p&gt;

&lt;h2 id=&quot;learning-from-music-theory&quot;&gt;Learning from Music Theory&lt;/h2&gt;

&lt;p&gt;I like to compare it to music theory. A musician who writes a song may have intuited patterns and practices that correspond to music theory without actually understanding the study of the theory describing the emergent behaviors behind their music. This can be completely fine for a musician who occasionally writes their own music that they perform themselves, but what about a composer, who’s primary job is to do nothing but write music for other musicians? Not only do composers write music for other musicians, they must also structure their work in a way that other musicians can more easily understand. Composers can greatly improve their output and job performance by understanding music theory, and applying that study to their work. In the same way that feature developers benefit from practical advice, developers working at the architecture level can benefit greatly from studying theory and philosophy around software design.&lt;/p&gt;

&lt;p&gt;Both music theory and software design recognize patterns in the emergent properties of their foundational medium. In music, the foundational medium is sound waves, the physical basis that all music is built on. In software, the foundational medium is information and how it flows from one part of a system to another.&lt;/p&gt;

&lt;p&gt;The behavioral characteristics of sound waves exhibit emergent mathematical patterns in how they interact with each other. The abstraction on top of music’s physical medium is patterns, and the patterns that sound harmonious to humans are dictated by the physical characteristics of sound waves. In software, that abstraction is complexity, and there are intrinsic properties of complexity that emerge from the flow of information.&lt;/p&gt;

&lt;p&gt;That abstraction has a human expression in how we experience it. In music theory, that’s the tension and release you feel from those patterns, and composers play with these dynamics. In software, the human expression of complexity is cognitive load. As complexity increases, so does our cognitive load and our frustration in understanding the information system.&lt;/p&gt;

&lt;p&gt;These abstractions and expressions can be conceptualized into mental frameworks that help us organize our thinking. In music, this framework is musical form, the structural principles that guide how we arrange and organize musical elements. In software, our framework is interfaces, the systematic ways we design how information flows between different parts of a system.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Music&lt;/th&gt;
      &lt;th&gt;Software&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;Sound Waves&lt;/td&gt;
      &lt;td&gt;Information&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Abstraction&lt;/td&gt;
      &lt;td&gt;Patterns&lt;/td&gt;
      &lt;td&gt;Complexity&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Expression&lt;/td&gt;
      &lt;td&gt;Tension &amp;amp; Release&lt;/td&gt;
      &lt;td&gt;Cognitive Load&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Framework&lt;/td&gt;
      &lt;td&gt;Musical Form&lt;/td&gt;
      &lt;td&gt;Interfaces&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;deriving-best-practices-from-first-principles&quot;&gt;Deriving Best Practices from First Principles&lt;/h2&gt;

&lt;p&gt;When thinking philosophically and theoretically, you can take that kind of meta study and apply it to your practice. You can then derive the pragmatic advice you’ve been following from first principles. If everything is an interface, then that includes all forms of data flow. Function I/O is of course an interface, but so are module imports and exports, error throwing, global variables, and configuration. Even side effects can be viewed as a different form of interface providing a different way data can flow.&lt;/p&gt;

&lt;p&gt;The advice of keeping your function interfaces and modules clean, not throwing exceptions for expected errors in program behavior, and avoiding global variables and side effects all become united under a single mental framework. That unified worldview can inform every aspect of how you design and architect software systems. Instead of memorizing pragmas, you can derive best practices for conceptual building blocks that can be applied more broadly.&lt;/p&gt;

&lt;p&gt;While I have intuited most of the lessons that can be derived from Ousterhout’s mental framework over years of trial and error, the structured thought process and meticulous study into the behavior of complexity in software design has helped me organize and unite my thinking. The framework, nomenclature, and behavioral patterns observed by Ousterhout are invaluable, and make his book something I believe should be required reading for all software developers.&lt;/p&gt;

&lt;h2 id=&quot;becoming-a-philosophical-programmer&quot;&gt;Becoming a Philosophical Programmer&lt;/h2&gt;

&lt;p&gt;Being pragmatic is a great way to get started and navigate the complex world of computer programming. I believe the best way to move beyond that to designing and architecting software and information systems is to become a philosophical programmer. When reading &lt;strong&gt;A Philosophy of Software Design&lt;/strong&gt;, don’t go in looking for specific pragmas to follow. Instead, try to find the conceptual patterns behind software patterns broadly so you can apply the lessons and philosophy flexibly. Shift from focusing on simply getting the next task done to thinking abstractly about the underlying problem to find better, simpler solutions.&lt;/p&gt;
</description>
        <pubDate>Wed, 04 Jun 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/06/04/the-philisophical-programmer/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/06/04/the-philisophical-programmer/</guid>
        
        
      </item>
    
      <item>
        <title>Conducting Better AI-proof Programming Interviews</title>
        <description>&lt;p&gt;&lt;img src=&quot;/static/images/better-programming-interviews-skinner.jpg&quot; alt=&quot;Are my interview questions out of touch? No. It&apos;s the candidates that are wrong!&quot; title=&quot;Principal Skinner&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-broken-state-of-programming-interviews&quot;&gt;The Broken State of Programming Interviews&lt;/h2&gt;

&lt;p&gt;Programming interviews have long focused on trivia or common tutorial style questions that reward candidates who are good at memorization and interviewing practice, but not necessarily job performance. These types of questions are inefficient at assessing a candidate’s actual job abilities because they’re not evaluating realistic work scenarios.&lt;/p&gt;

&lt;p&gt;Rote memorization is not the most important skill you need on the job. Most development revolves around learning and problem solving. There’s too much to know and too many options to choose from to know it all off the top of your head. Interviews should reflect that and focus on how candidates learn and adapt as opposed to implementing algorithms that require memorization and practice.&lt;/p&gt;

&lt;h3 id=&quot;ai-further-breaks-interviews&quot;&gt;AI Further Breaks Interviews&lt;/h3&gt;

&lt;p&gt;AI excels at memorization problems, which will make the situation worse. &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/tech-hiring-inflection-point&quot;&gt;Cheating is becoming a bigger problem&lt;/a&gt; and it can be hard to detect with standard questions. There are now companies that offer real-time AI interview assistants that can produce live instructions for the candidate to follow to pass an interview, telling them what to type and even what to say.&lt;/p&gt;

&lt;p&gt;To combat cheating, companies are trending toward draconian and dehumanizing measures like eye tracking, spyware, invasive room tours, and even disallowing bathroom breaks. Anti-plagiarism tech is also very inconsistent and can lead to false accusations against legitimate candidates. These invasive acts immediately put the interview on the wrong foot and starts the company relationship in a negative light.&lt;/p&gt;

&lt;p&gt;A return to in-person interviewing is one solution, but it still doesn’t help with the screening phase, and relying on resume content rewards those who use AI to tailor their resumes to each position’s requirements. Flying out a candidate who faked their way through the screening process is an incredibly expensive mistake to make.&lt;/p&gt;

&lt;p&gt;The solution isn’t an arms race between cheat and anti-cheat tech. That will further dehumanizes candidates and create a more negative interview experience. We need interviews that can break AI, and the way you break AI is the same way you create better interview questions that are more reflective of actual work skills.&lt;/p&gt;

&lt;p&gt;The interview process was already broken and AI is just exacerbating the problems. We shouldn’t be putting so much effort into protecting something that’s already broken.&lt;/p&gt;

&lt;h2 id=&quot;test-learning-not-memorization&quot;&gt;Test Learning, Not Memorization&lt;/h2&gt;

&lt;p&gt;The most effective technique I’ve found for finding high quality candidates is to focus on learning style questions. If a question requires the candidate to learn new information before they can solve the problem, then they won’t be able to prompt an AI properly. AI can solve problems for you, but they can’t learn for you.&lt;/p&gt;

&lt;p&gt;I’ve found the best way to test for this is to give the candidate a somewhat obscure and interesting API that requires them to read documentation and understand a more unique programming construct. Unlike recalling the answer to a puzzle question, it’s hard to fake that you’re learning. It’s also harder to provide an AI with API documentation context than to copy paste a puzzle description, which is basically already a perfectly structured AI prompt.&lt;/p&gt;

&lt;p&gt;I’ll point candidates to specific methods they’ll need to use. Even if the AI is already up to date with a lesser known API, it will still have a hard time looking like it’s learning. If the solution includes methods you didn’t provide links to and the candidate didn’t look up then that’s an obvious red flag.&lt;/p&gt;

&lt;p&gt;I’ve found the Canvas API to work particularly well for interviewing. It uses a less intuitive stack pattern for paths that must be cleared to avoid overdrawing the same line again. So far I have yet to encounter a candidate who could remember it off the top of their head. For these questions I make it totally open book and encourage them to ask me any reference questions they have. The more they are encouraged to ask questions the better.&lt;/p&gt;

&lt;p&gt;To help compare candidate skill levels I’ll progressively add requirements after prior requirements are completed. This establishes a measurable metric of how many requirements could be completed in the given time frame, and also tests a candidate’s ability to refactor their code and adapt to changing specifications and further test their comprehension of the APIs they’ve been introduced to.&lt;/p&gt;

&lt;h3 id=&quot;proprietary-interview-apis-for-large-companies&quot;&gt;Proprietary Interview APIs for Large Companies&lt;/h3&gt;

&lt;p&gt;If you’re a big company that needs to deal with question leaks you can take this another step further and create your own unique internal API to use just for interviewing. This means the API won’t be in an AI’s training data, making it much harder to get the context needed to feed into the AI. Even if they managed to get your internal API documentation, having specific knowledge of an internal proprietary API is a huge red flag.&lt;/p&gt;

&lt;p&gt;By using your own API you gain control over the process that can help you guard against leaks. You can dedicate some resources to gradually changing that API so even if it does leak, it will end up getting out of date. In a large organization, the relative investment for doing this would be small and provide a large impact. You could even set up the API to make it obvious if it’s being analyzed by an assistant by setting up fake methods you don’t point out to candidates in the interview as honey pots.&lt;/p&gt;

&lt;h2 id=&quot;focus-on-problem-solving&quot;&gt;Focus on Problem Solving&lt;/h2&gt;

&lt;p&gt;Learning quickly is incredibly important, but you also need to be able to apply what you’ve learned and find elegant ways to integrate your research into solutions. This requires strong problem solving skills to deal with unique problems and technology combinations.&lt;/p&gt;

&lt;p&gt;AI knows nearly everything there is to know, but it’s still poor at architecting solutions and designing elegant systems on its own without proper guidance and prompting. Tightly encapsulated algorithm questions don’t expose these skills.&lt;/p&gt;

&lt;h3 id=&quot;real-world-problems-over-puzzles&quot;&gt;Real World Problems Over Puzzles&lt;/h3&gt;

&lt;p&gt;Leetcode style problems will certainly be documented already in AI training data. Instead of re-using common problems, start keeping tabs on the real world problems you encounter during work and try to find ones that are encapsulated enough to put in an interview. Solutions that were hard to find will require stronger problem solving skills from candidates, and AI will be less likely to solve it easily.&lt;/p&gt;

&lt;p&gt;Your utility functions are probably a good place to look for examples of real programming problems your team had to solve without a readily available library because the problem was either too obscure or too use case specific.&lt;/p&gt;

&lt;h3 id=&quot;debugging-style-questions&quot;&gt;Debugging Style Questions&lt;/h3&gt;

&lt;p&gt;Debugging style questions are also effective at testing real problem-solving skills. Take a working program and introduce unique bugs then ask the candidate to fix it to comply with provided behavior requirements. By default, AI is not hooked up to the output of your programs, making it near impossible for it to solve this kind of question if you form the bugs correctly to require more complex observability. Without the AI iterating on the output of the program or using a debugger, it will have a hard time narrowing down bugs. Even if AI gets advanced enough to do live screen grab input to be able to debug programs, the way it would do so would be extremely unnatural. If a candidate seems to magically solve multiple bugs without iteration, that’s a huge red flag.&lt;/p&gt;

&lt;p&gt;As an example, I’ve used a simple whack-a-mole react app with common react mistakes introduced into the components, hooks, effects, and game loop. While an AI might be able to solve those issues, it would not be able to do so in a natural way by iteratively testing and interacting with the game without proper setup. A candidate solving these problems will do so by identifying specific issues, and then analyzing the program state by debugging.&lt;/p&gt;

&lt;h3 id=&quot;go-deep-on-past-work&quot;&gt;Go Deep on Past Work&lt;/h3&gt;

&lt;p&gt;Learning about a candidate’s prior accomplishments is also a great way to find out what they’re capable of solving, but I find that unless you conduct this kind of conversation properly, it ends up turning into non-informative buzzword soup. The central failing here is focusing on quantity over quality. When learning about someone’s prior work it’s tempting to try and learn about a bunch of projects they’ve worked on in the past and their results, but going broad will make it harder to tell whether they have legitimate skills.&lt;/p&gt;

&lt;p&gt;The best information I’ve been able to extract from these questions has been from doing a deep dive into a specific difficult problem they’ve solved. I’ll ask them to explain a specific project that they are particularly proud of or found especially challenging. It takes a lot more focus to dive into a tough technical challenge and properly understand their solution, especially when it’s something done in a different domain than you’re used to. It’s tempting to accept the high level explanation and move on, but it’s important to dig deep and properly understand their thought process and solution.&lt;/p&gt;

&lt;p&gt;Understanding what they did will expose a lot of very important aspects about the candidate. Can they take on complex problems? How deep can they dig into hard technical spaces? Are they capable of applying those solutions in practice? Do they do a good job at explaining their work to other people?&lt;/p&gt;

&lt;p&gt;It’s important to follow your genuine curiosity instead of using scripted questions. If you hear buzzwords or concepts you’re not familiar with, ask what they mean and keep asking until it’s clear. If a candidate can teach you something complex, that’s revealing of their expertise. When someone really understands their work, they can explain it clearly and go deep on the projects they’ve worked on. At high levels, it’s hard to tell what they actually did themselves versus what was part of a larger project or handled by the rest of the team. Asking for lower level details will expose how much they truly know and understand.&lt;/p&gt;

&lt;p&gt;Conversational, context heavy questions with follow-up questions guided by your natural curiosity will expose the true knowledge of the candidates you’re interviewing, and is something that can’t be effectively replicated by AI.&lt;/p&gt;

&lt;h3 id=&quot;focus-on-process-not-results&quot;&gt;Focus on Process, Not Results&lt;/h3&gt;

&lt;p&gt;With all these approaches it’s important to make the focus on the process not the result. Encourage discussion, ask about their thought process and for them to narrate what they’re thinking. Don’t be a blank slate and just observe. Get involved, encourage them to ask questions, and make the format more collaborative with open-book question and answer sessions. In addition to being harder to game, these approaches also put developers on a more level playing field because they aren’t standard puzzle questions that reward a candidate’s random knowledge.&lt;/p&gt;

&lt;h2 id=&quot;developer-strengths-are-ai-weaknesses&quot;&gt;Developer Strengths are AI Weaknesses&lt;/h2&gt;

&lt;p&gt;AI is getting more and more capable, but it is still not a replacement for highly skilled developers with strong problem solving, software design, and debugging skills. If it gets there, all office jobs will be gone, not just programming. Testing against AI weaknesses not only prevents cheating, it also finds you a better team. You’re not looking for someone who copy pastes requirements into an LLM, you want someone who can do things the LLM cannot.&lt;/p&gt;

&lt;p&gt;We should also consider embracing the role of AI and try adapting the interview process to suit it. AI is getting used more often on the job, so having AI coding skills isn’t a bad thing. Interviews could focus more on explaining code and discussing what it does and how it could be improved. Talking about code naturally is very hard to do if you’re being fed information instead of speaking from your own understanding. If you’re inviting candidates to work with AI in the interview process, it becomes harder to abuse it to gain an advantage over other candidates.&lt;/p&gt;

&lt;p&gt;Learning to solve unique problems and thoughtfully iterating on them to find more elegant implementations is a vital skill on the job, not easy to fake, and something AI is not good at. Dump the leetcode questions, encourage discussion, and focus on real world skills.&lt;/p&gt;
</description>
        <pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/06/01/better-programming-interviews/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/06/01/better-programming-interviews/</guid>
        
        
      </item>
    
      <item>
        <title>The Role of UI in the AI Age</title>
        <description>&lt;p&gt;As AI has progressed I’ve been thinking about the role of UI in a landscape increasingly dominated by chat apps. I never liked the idea that plain chat would be the future of interfaces. Typing is slow and I don’t like talking to my phone. Rich UI still communicates information more efficiently than language in most cases and I shouldn’t need to solve a mini prompt engineering problem for every AI interaction I have. In most domain specific use cases I don’t necessarily know the best way to ask for a solution in the first place.&lt;/p&gt;

&lt;p&gt;I’ve firmly believed since the advent of LLMs that text only chat isn’t the future of interfaces and that we still need rich UI. At Vetted we started working on a product recommendation chat agent shortly after GPT-3.5 was announced, and at the time I knew I didn’t want to do anything text only and I wanted it to produce rich shopping UI components. Incorporating rich UI into LLM answers back then was harder than it is today, but it was absolutely necessary to provide a better product shopping experience and differentiate ourselves. When reviewing results with traditional product components it’s much easier to take in a large list of products because of the visuals, consistency of the design, and emphasis on the most important data points. The same information in text formats is much harder to keep track of. I think chat heavy LLM experiences became commonplace because it’s the simplest to implement, not because it’s the best interface.&lt;/p&gt;

&lt;p&gt;Rich UI components are getting integrated into text chat more often now and new ones are getting added to major LLM providers’ chat interfaces, with products, images, and generated artifacts to name a few. I can envision a future where major LLM chat UIs produce flexible content from diverse data sources. Would that mean they’ll capture every use case with a single super app? I still don’t believe so, a product is more than its UI components.&lt;/p&gt;

&lt;h2 id=&quot;products-as-mental-models&quot;&gt;Products as Mental Models&lt;/h2&gt;

&lt;p&gt;I’ve been trying to figure out what these do-everything AI products have been lacking compared to traditional software. My view is that a product is fundamentally about selling a mental model and workflow. A mental model provides an information model to better understand a problem, while a workflow offers a process to solve it based on that framework through UI. Take email: it has a mental model built around sending and receiving messages to addresses, with interfaces providing workflows for finding, reading, and responding to those messages. Almost any application can be broken down into its proposed mental model paired with a workflow implementation. While a super chat app might handle most use cases, it cannot perfect a specific mental model and workflow for every problem. Domain-specific products can continue to differentiate themselves, innovate, and persist.&lt;/p&gt;

&lt;p&gt;Developing each mental model and corresponding workflow requires significant effort. Adding a new workflow to a do-everything agent is still challenging, and designing an intuitive workflow with a repeatable user friendly process takes substantial work. It’s also subjective. There are no universally ideal workflows, so no single app can optimally solve every problem for all users. Well designed software solutions are needed because few people possess the domain expertise to envision and implement polished workflows for their specific challenges.&lt;/p&gt;

&lt;h2 id=&quot;enabling-new-workflows-raise-new-challenges&quot;&gt;Enabling New Workflows Raise New Challenges&lt;/h2&gt;

&lt;p&gt;Many of the attempts I’ve seen at incorporating AI into existing products have revolve around taking their data and creating summaries from it. RAG to text summarization is going to be a solved problem, doesn’t differentiate you, and doesn’t help users do anything new. With AI, we can now create more powerful and flexible workflows that were previously impossible, but the focus needs to be on designing the best solution for the problem, not building what’s easiest with the technology. The real question hasn’t changed. What’s the ideal mental model and workflow to solve this problem? How can you perfect that workflow and make it reproducible and intuitive? That’s the core of all products, and with AI’s flexibility, we can better realize that ideal than we can with traditional CRUD apps. Whether chat is involved, or you need a hybrid approach, or managed artifacts, or a traditional UI, LLMs can help you further achieve that optimal workflow.&lt;/p&gt;

&lt;p&gt;However, flexibility comes at a cost. We’re experiencing a paradigm shift, and harnessing LLMs to enable more powerful workflows remains a poorly defined process. What AI enables doesn’t make designing UI easier, it makes it harder. It allows us to solve more complex use cases with greater detail and flexibility, but that complexity requires harder to build workflows. We gain power but face more edge cases and complications. AI might make us more productive, but we’ll need that productivity boost to manage that coming complexity. After years of building stability through deterministic software design, we are now adding a massive non-deterministic component. Incorporating LLMs into our software in sophisticated ways will present significant new challenges.&lt;/p&gt;

&lt;h2 id=&quot;designing-for-problems-not-technology&quot;&gt;Designing for Problems, Not Technology&lt;/h2&gt;

&lt;p&gt;I find the current state of AI harkens back to the early days of software and the internet. Each paradigm shift introduced new ways to model problems and create workflows to solve them and getting it right required extensive iteration. We initially created UI based on technological limitations. CLI design reflected early computing constraints, and document based websites were limited by the static nature of the early web. As we extracted more from those platforms, we mimicked existing solutions through skeuomorphic design, copying workflows from around the office. Over time, we gradually refined our software solutions to offer features only possible in the digital realm, with design patterns like infinite scrolling and command pallets.&lt;/p&gt;

&lt;p&gt;On the path of doing what’s easy, to doing what’s familiar, to doing something entirely new, we remain in the “doing what’s easy” phase with AI and we’ve barely begun to harness what’s possible. I’ve observed a lack of vision for using AI to enable fully realized mental models and workflows, and there’s a tendency to design around the technology rather than the problem being solved. We need to put the focus on the workflow first, then work out how to apply the power and flexibility of LLMs to achieve that vision. Doing that will require refining our design and development processes, and inventing the necessary techniques will take time.&lt;/p&gt;

&lt;p&gt;I’ve seen plenty of uncertainty from companies about finding their place in the AI era, or whether they even have one at all. Ultimately the fundamental challenge remains unchanged: you’re selling a solution to a problem through a mental model and workflow. AI offers a powerful new way to build those solutions, but it is not the solution in and of itself.&lt;/p&gt;

&lt;!-- ### Continue Reading
Viewing your product as mental models and workflows can help you align your focus on what you&apos;re building, but when it comes to actually building it you need to structure your solution in more detail. Viewing your product as its interfaces can help you focus on the right level of abstraction for figuring out how to build your product. [Read &quot;Your Product is Your Interfaces&quot;](/2025/06/##/your-product-is-your-interfaces) --&gt;
</description>
        <pubDate>Wed, 14 May 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/05/14/ui-in-the-ai-age/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/05/14/ui-in-the-ai-age/</guid>
        
        
      </item>
    
      <item>
        <title>Prototyping the Right Way with AI</title>
        <description>&lt;h2 id=&quot;starting-from-a-higher-abstraction&quot;&gt;Starting from a higher abstraction&lt;/h2&gt;
&lt;p&gt;Prototyping is often misunderstood, commonly executed poorly, and in many cases approached backwards. When asked to build a prototype, most engineers instinctively dive in at the implementation level and try to make a fully functional test of the solution they want to try. They end up making something akin to a mechanical prototype where you make an unrefined physical prototype to test out a product. However, when it comes to software design, this skips over many layers of abstraction that need to be considered first and jumps directly into creating a high-fidelity prototype that often overlooks other potential solutions and overinvests in one direction.&lt;/p&gt;

&lt;p&gt;Creating a high-fidelity prototype on your first attempt invests significant time into a single approach. This first attempt often consumes the entire time budget for prototyping, which is the main reason we often see prototypes making it into production.&lt;/p&gt;

&lt;h2 id=&quot;anything-can-be-a-prototype&quot;&gt;Anything can be a prototype&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;We tend to think of prototypes as code-based, but they don’t always have to be.&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;“The Pragmatic Programmer”&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;A prototype doesn’t need to be at the implementation level. It’s important to begin prototyping at higher levels of abstraction first to better explore the problem space and produce lower fidelity and lower investment prototypes that can be easily thrown away. Anything that can be experimented on or tested can be a prototype at every level of abstraction within an information system.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A system design diagram can be a prototype to see how the high-level components of different system architectures connect to each other and how they integrate with components of the existing architecture.&lt;/li&gt;
  &lt;li&gt;An RFC (Request For Comments) spec can serve as a prototype for exploring and testing ideas with team members. The name itself highlights its purpose as a feedback-driven, iterative artifact.&lt;/li&gt;
  &lt;li&gt;Interface definitions can prototype different abstractions to test out usability, complexity, and expose limitations. Simple mock implementations can be tested with developers in your actual code base to evaluate real-world usage patterns before full development.&lt;/li&gt;
  &lt;li&gt;A semi-functional interactive design can also be a prototype to test out different UX and UI approaches and be tested with real users.&lt;/li&gt;
  &lt;li&gt;An outline can serve as a prototype for an article, like &lt;a href=&quot;javascript:open(URL.createObjectURL(new Blob([&apos;* Prototyping is misunderstood, it\&apos;s not always at the fully functional implementation level.\n* Prototypes can be done at any level of abstraction.\n* Prototyping is often done poorly because engineers have a penchant for prototyping at the most expensive implementation level and over engineering their prototypes.\n* This leads to prototypes being so expensive that there\&apos;s no time to build the real thing so prototypes end up becoming the actual production system.\n* Prototypes should be done from high levels of abstraction first.\n* Prototypes should be easy to throw away.\n* You do not want to skip to lower levels of abstraction before exploring\n* A system diagram can be a prototype.\n* Interface definitions can be a prototype.\n* An interactive design can be a prototype.\n* Anything that can be presented and tested out to get feedback and check functionality can be a prototype.\n* LLMs are great at prototyping because they give you a research assistant with unlimited time and provide output that is easy to throw away.\n* LLMs have a penchant for choosing easy naive solutions. Terrible for production code, but great for prototyping.\n* LLMs can help you exhaustively research solutions to a problem space and then help you interactively pare it down. This helps you consider options you may have overlooked. Even bad options can help you better identify the strengths and weaknesses of preferred options.\n* Even bad feedback can be informative if it triggers you to think more deeply about a problem or explore a problem space you didn\&apos;t consider.\n* LLMs are amazing rubber duckies and a conversation log is a great way to do a brain dump and get a mind map.\n* LLMs are also good at summarizing and organizing that brain dump.\n* Let\&apos;s you get a first pass of feedback before you use up someone else\&apos;s finite time.\n* Think of LLMs as being a scratch pad and research assistant, not as being a tool to do your work for you.\n* Explore the information from LLMs deeply. This is not vibe coding, the goal is to improve your understanding of the problem and solution spaces, not to create a deliverable for production.\n* When evaluating prototypes, consider the complexity of the primary interfaces and the trade off between simplicity, expressivity, and performance.\n* You want interfaces to be as simple as possible without losing the abilities to implement solutions cleanly at the necessary level of detail for your requirements and also meeting your performance goals.\n&apos;], {type: &apos;text/plain&apos;})))&quot;&gt;the notes I took when writing this one&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fortunate aspect of higher abstraction levels is that they can be explored with broad, quick prototypes that are cheaper to build, discard, and iterate on. Higher-level prototypes also allow you to more broadly explore the problem space you’re working in and avoid closing off potential solutions prematurely, and once you do eliminate a path, it eliminates a ton of lower-level solution spaces along with it.&lt;/p&gt;

&lt;h2 id=&quot;prototyping-faster-with-ai&quot;&gt;Prototyping faster with AI&lt;/h2&gt;
&lt;p&gt;When working with AI to help you prototype, I don’t mean telling the AI to go off and come up with prototypes for you, but rather using them as a sounding board to organize your thoughts, a research assistant, and a drafter to help you create prototypes faster and speed up your pace of iteration. LLMs have unlimited time to do work in parallel in the background and they provide output that’s easy to throw away.&lt;/p&gt;

&lt;h3 id=&quot;organizing-your-thoughts-more-efficiently&quot;&gt;Organizing your thoughts more efficiently&lt;/h3&gt;
&lt;p&gt;LLMs make for particularly effective rubber duckies that can actually respond to, and help you organize your ramblings instead of sitting there with a blank plastic stare. As opposed to inanimate objects, they can help you expand your problem solving abilities. Even when they’re woefully wrong, when correcting their mistakes, I’ve come up with solutions I wouldn’t have explored otherwise. Even bad feedback can be helpful at times. The context of a chat is also a great place to dump unorganized thoughts that you can then reference to create mind maps with the help of an LLM re-organizing your inefficient ramblings, while still maintaining a context history to pull back from. I’ve always found doing this manually to be very inefficient as I have to re-read and re-write and re-organize my notes to clarify my thinking.&lt;/p&gt;

&lt;h3 id=&quot;researching-faster-and-more-comprehensively&quot;&gt;Researching faster and more comprehensively&lt;/h3&gt;
&lt;p&gt;As research assistants, I find they do a good job at surfacing potential solutions for a problem more exhaustively than doing research manually. While they don’t always pick out the best or most relevant solutions, you can get them to spit out a wide breadth of solutions and also use them to help filter them based on specific criteria you give them. With the sheer scope of technologies to choose from for every possible problem, I’ve found it hard to compile a comprehensive list when researching myself and have found LLMs good at looking up that knowledge from their internal stores and from large external searches that would be very time-consuming to sort through myself.&lt;/p&gt;

&lt;p&gt;The quality of research you get from an LLM is directly proportional to how much context you can provide and how specifically you can frame your questions. The more domain knowledge you can bring to the conversation, the better the LLM performs as a research assistant. LLMs contain vast amounts of specialized knowledge, but it often stays buried unless you know how to ask the right questions at the right level of detail. LLMs build off your input text, so you need to establish the proper domain specific context to get the LLM to reference the relevant parts of its training data.&lt;/p&gt;

&lt;p&gt;LLMs can help you explore the space more efficiently and I’ve had success finding new solutions to consider that I had a hard time surfacing myself. Even when LLMs surface inadequate solution options, analyzing the weaknesses of that option can help you refine your thinking on what strengths you actually do need.&lt;/p&gt;

&lt;h3 id=&quot;drafting-prototypes-quickly&quot;&gt;Drafting prototypes quickly&lt;/h3&gt;
&lt;p&gt;They also have a particular weakness which I actually view as a bonus for creating prototypes. LLMs tend to pick overly simplistic naive solutions to problems. While that’s a terrible trait for creating production code, this is a great thing for prototyping, and it can help engineers that are used to working with best practices in mind let go a little and create a lower quality prototype that’s easier to throw away. I find that since most of the time we spend coding we’re trying to write high quality code that follows best practices, we’re so practiced at it that we tend to overengineer prototypes and have a hard time taking ourselves out of that mindset. There are AI tools for turning your notes and thoughts into nearly every kind of prototype at any level of abstraction, and unlike you, it will do a worse job at it, meaning you’ve successfully created a prototype you want to throw away, and saved yourself time.&lt;/p&gt;

&lt;p&gt;There’s also the nice aspect that when using an LLM, you’re not wasting anyone’s time. I try to be respectful of the time of other engineers on my team because I know how busy they are and I want to present my questions and requests for feedback to them efficiently. Having a sounding board for your thoughts can help you clean up your requests and sometimes even help you figure things out yourself.&lt;/p&gt;

&lt;p&gt;These are all workflows I find especially helpful for creating prototypes and creating them faster. Remember, the goal here isn’t to obsess over quality, it’s to explore the problem and solution spaces faster and then throw it all away and just taking with you the knowledge you’ve acquired.&lt;/p&gt;

&lt;h2 id=&quot;be-careful-when-using-ai&quot;&gt;Be careful when using AI&lt;/h2&gt;
&lt;p&gt;As helpful as they can be, LLMs have some major pitfalls you need to account for. They can’t do everything, and you should be using them for exploration, not for leading the process.&lt;/p&gt;

&lt;h3 id=&quot;accuracy&quot;&gt;Accuracy&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;I have approximate knowledge of many things&lt;/p&gt;

  &lt;p&gt;-Demon Cat&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, they lie, they lie frequently, very confidently, and they have no shame. Don’t just take everything it says as fact, make sure you cross reference what they tell you. The more specific the information, the more likely it is to be incorrect, so be more careful as you explore lower-level abstractions.&lt;/p&gt;

&lt;h3 id=&quot;recency&quot;&gt;Recency&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;I’m with it. I’m hip.&lt;/p&gt;

  &lt;p&gt;-Dr. Evil&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They also fall out of date. Keep in mind the nature of the information they’re providing and how often that information changes. Also, as with accuracy, the more specific the information is, the less likely it is to be up to date.&lt;/p&gt;

&lt;h3 id=&quot;confirmation-biases&quot;&gt;Confirmation Biases&lt;/h3&gt;
&lt;blockquote&gt;
  &lt;p&gt;You’re goddamn right&lt;/p&gt;

  &lt;p&gt;-Walter White&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LLMs are also very agreeable. Don’t let them make you think you’re a genius and that your first idea is amazing. Focus on your own learning and gaining knowledge in the problem space, don’t ask them to do subjective evaluations or make decisions for you.&lt;/p&gt;

&lt;p&gt;It’s good to introduce external information into their context windows to help with alignment to improve their accuracy and update their knowledge. You should treat LLMs like you have an infinite number of interns who have an unlimited amount of time to do research. They can be useful when you ask it to help you in specific ways and for compiling information or creating rough drafts, but you still need to manage them and review their output. Don’t try to get LLMs to do your job for you, use them as tools to help you do your job better.&lt;/p&gt;

&lt;p&gt;This is not vibe coding. The goal is to improve your understanding of the problem and solution spaces, and that requires understanding all of their outputs deeply.&lt;/p&gt;

&lt;h2 id=&quot;how-to-evaluate-prototypes&quot;&gt;How to evaluate prototypes&lt;/h2&gt;
&lt;p&gt;The purpose of a prototype is to give you some kind of deliverable that you can use to test and explore potential solutions. Think about what you are trying to learn, and what tests you can perform on your prototype at the level of abstraction it’s exploring.&lt;/p&gt;

&lt;h3 id=&quot;testing&quot;&gt;Testing&lt;/h3&gt;
&lt;p&gt;Testing a prototype is not always going to necessarily be a functional test that you would get from an implementation level prototype, but can also be more conceptual tests that make sense for that level of abstraction.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A system diagram test could be trying to integrate it with a system diagram of your existing architecture.&lt;/li&gt;
  &lt;li&gt;An RFC test could be checking it with other engineers for understandability, comprehension, and exposing unanswered questions.&lt;/li&gt;
  &lt;li&gt;An interface definition test could use dummy data and dummy implementations to see how easy it is to work with in your actual source code.&lt;/li&gt;
  &lt;li&gt;A UI prototype test could be running user testing to see how well users understand the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;evaluation&quot;&gt;Evaluation&lt;/h3&gt;
&lt;p&gt;Think of what kinds of interfaces you are testing with your prototype. Good interfaces should be as simple to use and comprehend as possible without losing expressivity to allow you to implement solutions cleanly at the level of detail the problem requires. You need to evaluate how easy it is to use, how well it fulfills your requirements, and whether that abstraction adds any restrictions that will prevent you from fulfilling your performance goals. How you balance that will depend on the problem you are trying to solve.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;With a system diagram, how well does it integrate with the existing system? How many interdependencies does it add? Can that be minimized without losing flexibility?&lt;/li&gt;
  &lt;li&gt;An RFC can help expose issues with scope and help catch scope creep early. Is the solution abstraction trying to do too much? Can it be simplified by being broken up or reducing its responsibilities?&lt;/li&gt;
  &lt;li&gt;An interface definition integration test can help you identify how easy the interface is to work with. How much boilerplate does it introduce? Does it maintain a strong source of truth? How easy is it to change? Is the abstraction encapsulating the problem at the right level? How many interdependencies does it introduce?&lt;/li&gt;
  &lt;li&gt;A UI prototype can expose areas of the UI that are confusing, overloaded, or hard to interact with. Does the user notice the details? Can they figure out how to finish their task? Are key flows hidden? Are the interface patterns easy to use?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As prototypes begin to explore lower-level higher-fidelity abstractions, they become more time-consuming to produce and more complicated to test. Evaluating and choosing solutions based on prototypes should happen from the highest level of abstraction first where prototyping is both cheaper, and selecting solutions narrows down the largest parts of the problem space.&lt;/p&gt;

&lt;h2 id=&quot;go-build-better-prototypes&quot;&gt;Go build better prototypes&lt;/h2&gt;
&lt;p&gt;We all recognize the value of prototypes for helping us engineer better solutions, but historically it has been hard to build them fast enough for us to fully evaluate all the potential solutions in the relatively short amount of time we allocate to them. To me, the best use of AI in coding is for accelerating prototype iteration speed, and that increase in efficiency for generating prototypes has helped me find and try out solutions I wouldn’t have had time to explore otherwise and create better, more elegant architectures with better fitting technology choices for the problems I’m solving.&lt;/p&gt;

&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading:&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;“A Philosophy of Software Design” Chapter 11: Design It Twice&lt;/li&gt;
  &lt;li&gt;“The Pragmatic Programmer” Topic 13: Prototypes and Post-it Notes&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 08 Apr 2025 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2025/04/08/prototyping-the-right-way-with-ai/</link>
        <guid isPermaLink="true">https://www.timetler.com/2025/04/08/prototyping-the-right-way-with-ai/</guid>
        
        
      </item>
    
      <item>
        <title>Building Favitimer</title>
        <description>&lt;p&gt;Somewhat recently Google came out with a cool new search feature that lets you &lt;a href=&quot;https://www.google.com/search?q=10%20minute%20timer&quot;&gt;start a timer via a search&lt;/a&gt;. I really liked this workflow since it let me really quickly set up a timer from the browser by using the chrome “tab to search” functionality. I’d just open a new tab, type in “google.com”, hit tab, type “x minute timer”, hit enter, and boom, instant timer. I could get a timer set up in seconds. But there was an annoyance. I had to switch to the tab to see how much time was left! How could I live like this? So I came up with a solution; what if the favicon updated to show how much time was left? I mostly set timers while I’m cooking, so this would allow me to pull up a recipe on one tab, and be able to instantly glance at how much time is left in the other tab. By using the favicon I could also pin the tab to use less space.&lt;/p&gt;

&lt;p&gt;I did a thorough search and couldn’t find anything that did this, and &lt;a href=&quot;http://www.favitimer.com&quot;&gt;favitimer.com&lt;/a&gt; was available, so I bought the domain and took a weekend to go make it. While building it, I ended up using a few interesting techniques, so I thought it’d be nice to do a write up on them. If you’d like to see the code, everything is up on &lt;a href=&quot;https://github.com/etler/favitimer&quot;&gt;GitHub&lt;/a&gt;;&lt;/p&gt;

&lt;h2 id=&quot;updating-the-favicon&quot;&gt;Updating the Favicon&lt;/h2&gt;

&lt;p&gt;The crux of this mini app is setting a dynamic image as the tab favicon. Not all browsers support this, so straight off, IE and Safari are out. IE doesn’t support updating the favicon at all before IE11, and Safari doesn’t show favicons at all. Favicons can only be updating via the meta tag’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href&lt;/code&gt; attribute, which only leaves us with one option for updating it dynamically, which is with a data URI. Data URIs are a great new browser feature that enable a whole host of new tricks, and I end up using it in a variety of different ways later on. It’s a way of encoding any kind of data, including binary data as an inline string. For binary data, you can encode it in base 64. You also include a MIME type to let the browser know what kind of data you’re encoding.&lt;/p&gt;

&lt;p&gt;Now that we have a way of updating the favicon, we need a way to generate the image data. Currently the best way to generate bitmap images is through the canvas API, and even better, the canvas API already provides a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toDataURL&lt;/code&gt;&lt;/a&gt; method for generating a data URI so updating the favicon is very easy. All you have to do is render something on your canvas and set the favicon link &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;href&lt;/code&gt; attribute to the data URI result.&lt;/p&gt;

&lt;p&gt;To show how much time is left on the timer, I opted for a circle outline that slowly fills up radially until it forms a complete circle. For example, a timer that’s 25% done will show a quarter circle. The canvas API also provides an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arc&lt;/code&gt;&lt;/a&gt; drawing method that takes a start and end radian that makes it easy to create a semi-circle.&lt;/p&gt;

&lt;p&gt;Here’s the full drawing code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;draw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;percent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;2d&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lineWidth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;percent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strokeStyle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#00cc00&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strokeStyle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#dd0000&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strokeStyle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;strokeStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;font&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;bold 7.5px monospace&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textAlign&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;center&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fillStyle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#333333&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clearRect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;beginPath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;diameter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.375&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;percent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stroke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fillText&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;drawFavicon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;percent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;draw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;percent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;canvas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toDataURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;image/png&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I reuse the image in the body of the page, so I made the method generic. Color is used to indicate the state of the timer, and the time left is represented in milliseconds, showing hours, minutes, or seconds left depending on how much time is left (the unit is left out because the number needs to fit into the small space of a favicon).&lt;/p&gt;

&lt;h2 id=&quot;smoother-animation&quot;&gt;Smoother Animation&lt;/h2&gt;

&lt;p&gt;Because the timer is displayed in the favicon, it’s intended to reside in an inactive tab so you can just glance at it while doing something else. But while building the app, I discovered a limitation to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestAnimationFrame&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setInterval&lt;/code&gt;, which is that they gets throttled to a few fps when the tab is inactive. The browser doesn’t want inactive tabs sucking up resources when nobody is looking at them. While that’s fine most of the time, it wasn’t OK for this app since you want to see the timer updating accurately when off the page!&lt;/p&gt;

&lt;p&gt;After some research I found that you can workaround this issue by using a web worker. Web workers are designed to run in parallel, so they’re not throttled when a tab is inactive. Since web workers don’t have access to the original calling context, I wasn’t able to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestAnimationFrame&lt;/code&gt; as that relies on the original window context, so that left my best option as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setInterval&lt;/code&gt;. I created an interval worker that simply ran its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setInterval&lt;/code&gt; and passed a message back whenever it fired, and that fixed the animation smoothness problem.&lt;/p&gt;

&lt;p&gt;The worker script is super simple:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;To start or stop a worker you just send a message with either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{start: true}&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{stop: true}&lt;/code&gt;. I didn’t want to break support with browsers that don’t have web workers enabled, so I also wrapped the interval methods with a fallback to the regular &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setInterval&lt;/code&gt; method along with its caveat:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;startInterval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;workerURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;callback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;stopInterval&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;terminate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As a side note, never assume that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setInterval&lt;/code&gt; will actually execute in the time you tell it to, &lt;a href=&quot;http://ejohn.org/blog/accuracy-of-javascript-time/&quot;&gt;there are tons of factors that prevent them from being accurate&lt;/a&gt;, so if you need to base something on time elapsed, create a start timestamp and calculate the difference.&lt;/p&gt;

&lt;h2 id=&quot;adding-sound&quot;&gt;Adding Sound&lt;/h2&gt;

&lt;p&gt;I played around with a couple ways of adding sound for the beep noise that plays when the timer finishes. Originally I used an inline base 64 encoded audio file and played it using the Audio API, but I wasn’t a fan of having a big binary file in the middle of the code. Originally I used this bit of code:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;beep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sound&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;data:audio/wav;base64,...&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;sound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Looking further I found the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AudioContext&quot;&gt;AudioContext API&lt;/a&gt; for creating sounds programmatically. The new beep function required more code, but was much more flexible, and cut down on the file size a lot:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;beep&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;frequency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;volume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;oscillator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;oscillator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;audioContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createOscillator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;audioContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createGain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;oscillator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;audioContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;oscillator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;frequency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;frequency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;oscillator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setTargetAtTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;volume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;audioContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;currentTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;gainNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setTargetAtTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;audioContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;currentTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AudioContext&lt;/code&gt; API works by creating and hooking together various AudioNodes to create different tones. An &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OscillatorNode&lt;/code&gt; creates a wave function that sounds like a solid tone. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GainNode&lt;/code&gt; adjusts the volume of the oscillator node. The oscillator node is then connected to the gain node which is hooked up to the audio context output. The parameters for the sound are set, with gain setting the volume, and frequency setting the pitch of the tone. When I first tried starting and stopping the oscillator, it had the unfortunate side effect of creating a popping noise when it turned on and off, due to the sudden change of volume. To solve that, I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setTargetAtTime&lt;/code&gt;, which gradually changes the volume over time. It takes a target volume, an absolute time to change the volume at, and a value to set how quickly the sound should transition on an exponential basis. With that quick transition I was able to get rid of that popping noise, and the resulting beep was (while still purposefully annoying), less jarring.&lt;/p&gt;

&lt;h2 id=&quot;making-it-fast&quot;&gt;Making it Fast&lt;/h2&gt;

&lt;p&gt;Because the app is a simple single page Javascript utility, I wanted to make the page load as lightweight as possible. To do this I wanted to get the page down to a single request*, and that means inlining everything. For the most part it was pretty simple.&lt;/p&gt;

&lt;p&gt;To handle the inlining and minification of the CSS and Javascript I just used the &lt;a href=&quot;https://www.npmjs.com/package/gulp-inline&quot;&gt;gulp-inline npm package&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I decided against using any frameworks of utility libraries so I wouldn’t have to manage a big inline library, and also to cut back on the file size of the page. That means using nothing but the pure native DOM API. The compatibility benefits of libraries like underscore or jQuery weren’t an issue as only modern browsers support canvas and updating the favicon dynamically anyways. It’s also nice to do a project pure every now and then to reconnect with the browser :).&lt;/p&gt;

&lt;p&gt;Not all the inlining was trivial though. The most interesting part was inlining the web worker script. The web worker API takes a URL string to the worker script you want to use for the worker, so we can use the same data URI trick, but I didn’t want to have to re-encode the script every time I made an edit, so after doing some research I found that you can provide a local script by passing the string version of a function using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toString&lt;/code&gt;&lt;/a&gt; to the web worker API by using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Blob&quot;&gt;Blob&lt;/a&gt; data type. A Blob is a like a lightweight temporary file, and you can put any kind of data in it, including Javascript. After instantiating a Blob you can get a blob URI for it in the format of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blob:(domain)/(hash)&lt;/code&gt; by passing it to the URL API using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL&quot;&gt;createObjectURL&lt;/a&gt;. After the temporary URL is created it acts like a normal URL. You can visit it, and include it, allowing you to use it as a worker script:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nx&quot;&gt;workerURL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;intervalWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;workerString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;intervalWorker&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;workerString&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intervalWorker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;)();&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;workerString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;application/javascript&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;BlobBuilder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WebKitBlobBuilder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MozBlobBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;Builder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;BlobBuilder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;WebKitBlobBuilder&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;MozBlobBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;workerString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getBlob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createObjectURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;interval.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})();&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workerString&lt;/code&gt; needs to be wrapped with the parenthesis to execute the function, otherwise it will just define it in the worker and do nothing.&lt;/p&gt;

&lt;p&gt;In this project, I use data URIs in image, and code format. I think it really shows off the power and versatility of this cool feature. Since you can use them pretty much anywhere you’re pretty much just limited to your imagination!&lt;/p&gt;

&lt;p&gt;The result of all this? The final payload is a single request that gzips down to 4.2kb. The page will load pretty much as quickly as the host will respond. On my internet the page loads consistently in under 50 ms minus the DNS lookup, and sometimes under 20ms.&lt;/p&gt;

&lt;p&gt;* Ok, I have a Google Analytics script to spy on you guys, but that’s not a blocking resource for running the app, so it doesn’t slow anything down.&lt;/p&gt;

&lt;h2 id=&quot;parsing-input&quot;&gt;Parsing Input&lt;/h2&gt;

&lt;p&gt;Another feature I wanted was semantic parsing of a query string so I could start a timer quickly from the URL bar. This was one of the main features I liked from setting a timer on Google, so I wanted to make sure I replicated this for my own convenience.&lt;/p&gt;

&lt;p&gt;To start out, I wanted to allow setting the timer via a query parameter. I jumped around a bunch of possible parsing solutions with the main consideration being how flexible I wanted to make it. I wanted it to be able to accept multiple ways of writing time units, such as “seconds”, “second”, “secs”, “sec”, “s”, etc. Since I wanted pretty much every permutation, I settled on a regular expression that checked the first two starting characters of the unit, and then optionally consumed the rest of the letters for that unit, ignoring omitted letters.&lt;/p&gt;

&lt;p&gt;I also wanted to be able to put in fractions of units, such as “0.5”, or “1/2”. To do that, I used a very carefully placed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eval&lt;/code&gt;. Since the only input it can take is the result of a regex match, it’s only as safe as the regex is, which only looks for integers, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; for decimals, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; for fractions.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;executeSearch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;searchRegex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;((?:\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\.&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\/\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)?\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)[\s&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;se&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;o&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;|mi&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;|ho&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;|$&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)(?![&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;a-z&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/gi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;hours&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;searchRegex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;unit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;minutes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;hours&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;nl&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Each time the regex executes, its keeps track of what position it has consumed up to thus far, so calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; multiple times ends up finding all matching fragments in the location query string. Since it ignores any part of the string that doesn’t match, it also works for semantically written sentences such as “set a 10 minute and 30 second timer”.&lt;/p&gt;

&lt;p&gt;I also wanted to enable &lt;a href=&quot;https://www.chromium.org/tab-to-search&quot;&gt;tab to search&lt;/a&gt; to let you fill out the time in the address bar, which is enabled in chrome using an &lt;a href=&quot;http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_description_document&quot;&gt;OpenSearch description document&lt;/a&gt;. To tell the browser where to find it, you include a special link tag in the head portion of the document:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/opensearchdescription+xml&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;search&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/search.xml&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search.xml&lt;/code&gt; file looks like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-xml&quot; data-lang=&quot;xml&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;OpenSearchDescription&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://a9.com/-/spec/opensearch/1.1/&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
   &lt;span class=&quot;nt&quot;&gt;&amp;lt;ShortName&amp;gt;&lt;/span&gt;Favitimer&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ShortName&amp;gt;&lt;/span&gt;
   &lt;span class=&quot;nt&quot;&gt;&amp;lt;Description&amp;gt;&lt;/span&gt;Enter a period of time to set a timer.&lt;span class=&quot;nt&quot;&gt;&amp;lt;/Description&amp;gt;&lt;/span&gt;
   &lt;span class=&quot;nt&quot;&gt;&amp;lt;Url&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text/html&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;get&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;template=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://www.favitimer.com/?q={searchTerms}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/OpenSearchDescription&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This describes the pattern and functionality of the search to the browser.&lt;/p&gt;

&lt;h2 id=&quot;that-was-fun&quot;&gt;That Was Fun&lt;/h2&gt;

&lt;p&gt;So even though this was a small and simple project, there’s still a good deal of novel tech going on in it. It was fun to work with some of the new web technologies and really focus on small details in a very scoped side project. I’m pretty proud of how the code came out, so &lt;a href=&quot;https://github.com/etler/favitimer/blob/master/source/script.js&quot;&gt;give it a look&lt;/a&gt;. I hope you learned something in this write up, and happy coding!&lt;/p&gt;
</description>
        <pubDate>Sat, 21 Nov 2015 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2015/11/21/favitimer/</link>
        <guid isPermaLink="true">https://www.timetler.com/2015/11/21/favitimer/</guid>
        
        
      </item>
    
      <item>
        <title>The space character you didn&apos;t know about</title>
        <description>&lt;p&gt;You probably know about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;nbsp;&lt;/code&gt;, which stands for non-breaking space. The problem with the non-breaking space character is that the browser will avoid wrapping the line on those characters. If you have a list such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;foo,&amp;amp;nbsp;bar,&amp;amp;nbsp;buzz&lt;/code&gt;, that entire sequence will be treated as one word, which means it might display like this:&lt;/p&gt;

&lt;section style=&quot;width: 100px; border: 1px solid #CCC; padding: 10px; margin: 10px;&quot;&gt;Hello foo,&amp;#160;bar,&amp;#160;buzz&lt;/section&gt;

&lt;p&gt;when you want to display it like this:&lt;/p&gt;

&lt;section style=&quot;width: 100px; border: 1px solid #CCC; padding: 10px; margin: 10px;&quot;&gt;Hello foo, bar, buzz&lt;/section&gt;

&lt;p&gt;There’s no named html entity for a breaking space character, so we have to use a unicode character code instead. We can’t use the ASCII space character, hex code &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;#x20;&lt;/code&gt; either, as that will be treated like a normal space and be stripped out using the same rules.&lt;/p&gt;

&lt;p&gt;Instead we need to use a punctuation space, to indicate that the space is not part of the code, and should only be interpreted as part of the text. The unicode hex range 2000–206F is reserved for general punctuation.&lt;/p&gt;

&lt;p&gt;The space we want is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;#x2008;&lt;/code&gt;, a normal punctuation space. There are many other spaces of &lt;a href=&quot;http://www.unicode.org/charts/PDF/U2000.pdf&quot;&gt;varying width, or even no width&lt;/a&gt;. However, if you don’t want to explicitly specify the width, and just want the normal space used elsewhere, you should use the punctuation space. A punctuation space is specifically supposed to represent the width of a period. In my tests over multiple fonts, the punctuation space is the same width as the ASCII space.&lt;/p&gt;

&lt;p&gt;Of course, you should still only use this space when you need an explicit space that is part of a sentence’s puctuation, and not for layout purposes, in which case you should use CSS. If you find yourself using multiple spaces, that’s a good indication you should either modify the style, or use a space of a greater width instead. I found explicit spaces useful in Jade, where each element has its own line, and I want a space at the end of a line to be visible.&lt;/p&gt;

&lt;p&gt;tl;dr, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;#x2008;&lt;/code&gt;&lt;/p&gt;
</description>
        <pubDate>Sat, 08 Mar 2014 00:00:00 +0000</pubDate>
        <link>https://www.timetler.com/2014/03/08/explicit-space/</link>
        <guid isPermaLink="true">https://www.timetler.com/2014/03/08/explicit-space/</guid>
        
        
      </item>
    
  </channel>
</rss>
