<?xml version="1.0" encoding="utf-8" standalone="no"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0"><channel><title>tetsuo.github.io</title><managingEditor>noemail@noemail.org (Onur Gündüz)</managingEditor><pubDate>Wed, 20 Aug 2025 13:37:00 GMT</pubDate><link>https://tetsuo.github.io</link><language>en-us</language><itunes:explicit>no</itunes:explicit><itunes:owner><itunes:email>noemail@noemail.org</itunes:email></itunes:owner><item><title>Old-school LLM chatroom</title><link>https://tetsuo.github.io/old-school-llm-chatroom.html</link><category>go</category><category>js</category><category>llm</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Tue, 19 Aug 2025 13:37:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2025-08-19:/old-school-llm-chatroom</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/tetsuo/burp"&gt;&lt;strong&gt;burp&lt;/strong&gt;&lt;/a&gt; is a chat server that connects clients to &lt;a href="https://openai.com/api/"&gt;OpenAI&lt;/a&gt; and &lt;a href="https://www.anthropic.com/api"&gt;Anthropic&lt;/a&gt; APIs. It provides HTTP endpoints and a browser-based chat frontend.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="burp" src="https://tetsuo.github.io/images/burp-screenshot.png"/&gt;&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;go install github.com/tetsuo/burp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs a &lt;code&gt;burp&lt;/code&gt; executable to your &lt;code&gt;$GOPATH/bin&lt;/code&gt;.&lt;/p&gt;
&lt;h5&gt;Start the server&lt;/h5&gt;
&lt;pre&gt;&lt;code class="bash language-bash"&gt;burp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default it binds to &lt;code&gt;localhost:9042&lt;/code&gt;. You can change this with the &lt;code&gt;-addr&lt;/code&gt; argument.&lt;/p&gt;
&lt;p&gt;Set API keys via environment variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt; for OpenAI models&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; for Claude models&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Send messages&lt;/h5&gt;
&lt;p&gt;POST to &lt;code&gt;/ask?id=&amp;lt;channel&amp;gt;&amp;amp;model=&amp;lt;model&amp;gt;&lt;/code&gt; with body text&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;curl --header "Content-Type: text/plain" \
  --request POST \
  --data "Tell a joke" \
  "http://localhost:9042/chat?id=emu&amp;amp;model=claude-3-haiku-20240307&amp;amp;temp=0.75"
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Receive messages&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/wait?id=&amp;lt;channel&amp;gt;&lt;/code&gt; - long-poll up to 30s&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/recent?id=&amp;lt;channel&amp;gt;&lt;/code&gt; - fetch message history&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Use the web UI&lt;/h5&gt;
&lt;p&gt;Open &lt;code&gt;/chat?id=&amp;lt;channel&amp;gt;&amp;amp;model=&amp;lt;model&amp;gt;&lt;/code&gt; in a browser&lt;/p&gt;
&lt;hr/&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;p&gt;&lt;img alt="burp" src="https://tetsuo.github.io/images/burp_chat_flow.svg"/&gt;&lt;/p&gt;
&lt;h2&gt;Sending messages&lt;/h2&gt;
&lt;h3&gt;Message history &amp;amp; Context&lt;/h3&gt;
&lt;p&gt;burp keeps a rolling, in-memory buffer of recent messages per channel (default: last 50 entries). Old messages are dropped after ~1 hour or when the buffer grows beyond the minimum keep size.&lt;/p&gt;
&lt;p&gt;You can access the channel history by calling &lt;code&gt;/recent?id=&amp;lt;channel&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This history is also passed back into the provider on each &lt;code&gt;/ask&lt;/code&gt; request, together with the system prompt, so replies remain contextual.&lt;/p&gt;
&lt;h3&gt;Message parameters&lt;/h3&gt;
&lt;p&gt;When you send a message, burp forwards generation parameters to the chosen provider.
Supported fields include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;temp&lt;/code&gt; – temperature, always set (0.0–2.0 for OpenAI, 0.0–1.0 for Anthropic)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_tokens&lt;/code&gt; – capped per-model, defaults to the model’s maximum&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top_p&lt;/code&gt; – optional nucleus sampling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top_k&lt;/code&gt; – optional, Anthropic only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These values are stored alongside the channel state and displayed in the chat UI, so you always know what settings were used.&lt;/p&gt;
&lt;h2&gt;Receiving messages&lt;/h2&gt;
&lt;p&gt;Responses from OpenAI or Anthropic are streamed into an in-memory queue powered by &lt;a href="https://github.com/tetsuo/bbq"&gt;github.com/tetsuo/bbq&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each request gets its own small queue: the provider SDK writes token deltas into it, and a batching loop reads from it to publish messages back into the channel.&lt;/p&gt;
&lt;h3&gt;Long polling&lt;/h3&gt;
&lt;p&gt;When a client calls &lt;code&gt;/wait?id=&amp;lt;channel&amp;gt;&amp;amp;after=&amp;lt;time&amp;gt;&lt;/code&gt;, the server will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hold the request open for up to 30 seconds.&lt;/li&gt;
&lt;li&gt;If a new message arrives in that channel after the given &lt;code&gt;after&lt;/code&gt; timestamp, it is immediately returned.&lt;/li&gt;
&lt;li&gt;If no message arrives in that window, the server responds with a timeout marker message and the client retries with the latest cursor.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What's next?&lt;/h2&gt;
&lt;p&gt;I built this mainly as a console for my LLM dev environment. The chat UI reuses bits from a WebRTC-powered &lt;a href="https://github.com/tetsuo/r2"&gt;P2P chat app&lt;/a&gt; I hacked together years ago at a Mendix hackathon.&lt;/p&gt;
&lt;p&gt;In the future I might add slash commands (IRC-style) or even &lt;a href="https://matrix.org/"&gt;Matrix&lt;/a&gt; integration, but for now the chatroom alone is already a solid base, especially for wiring into tools like Google Sheets or Excel as an extension backend/UI.&lt;/p&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">burp is a chat server that connects clients to OpenAI and Anthropic APIs. It provides HTTP endpoints and a browser-based chat frontend.</media:description><media:content xmlns:media="http://search.yahoo.com/mrss/" fileSize="465782" height="1056" medium="image" type="image/png" url="https://tetsuo.github.io/images/burp-screenshot.png" width="1796"/><media:content xmlns:media="http://search.yahoo.com/mrss/" fileSize="9108" height="475" medium="image" type="image/svg+xml" url="https://tetsuo.github.io/images/burp_chat_flow.svg" width="519"/></item><item><title>Old-school JSX syntax</title><link>https://tetsuo.github.io/old-school-jsx-syntax.html</link><category>go</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Thu, 5 Jun 2025 00:00:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2025-06-05:/old-school-jsx-syntax</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/tetsuo/restache"&gt;&lt;strong&gt;restache&lt;/strong&gt;&lt;/a&gt; extends plain HTML with curly braces and compiles to modern JSX, so you can write React components like it's 2013 again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Example: Dashboard&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/tetsuo/dashboard"&gt;&lt;code&gt;tetsuo/dashboard&lt;/code&gt;&lt;/a&gt; repository provides an example setup with &lt;a href="https://esbuild.github.io/"&gt;ESBuild&lt;/a&gt; and a set of basic UI components demonstrating the current capabilities &#128073; &lt;strong&gt;&lt;a href="https://tetsuo.github.io/dashboard/"&gt;view it online&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Try it yourself&lt;/h2&gt;
&lt;p&gt;Here's an example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
  {#fruits}
    &amp;lt;li&amp;gt;{name}&amp;lt;/li&amp;gt;
  {/fruits}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;restache v0 draft&lt;/h2&gt;
&lt;p&gt;restache is an HTML syntax extension that lets you write JSX-style templating directly inside HTML.&lt;/p&gt;
&lt;p&gt;It extends HTML5 with &lt;a href="https://mustache.github.io/"&gt;Mustache&lt;/a&gt;-like primitives to support variables, conditionals, and loops.&lt;/p&gt;
&lt;h3&gt;Variables&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Variables&lt;/strong&gt; provide access to data in the current scope using dot notation.&lt;/p&gt;
&lt;p&gt;Accessing component props&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;article class="fruit-card" data-fruit-id={id}&amp;gt;
  &amp;lt;h3&amp;gt;{name}, eaten at {ateAt}&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;{color} on the outside, {flavor.profile} on the inside&amp;lt;/p&amp;gt;
  &amp;lt;img src={image.src} alt={image.altText} /&amp;gt;
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They can only appear within text nodes or as full attribute values inside a tag.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;✅ can insert variable {here}, or &amp;lt;img href={here}&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;When&lt;/h3&gt;
&lt;p&gt;Renders block when expression is truthy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{?loggedIn}
  &amp;lt;welcome-banner user={user} /&amp;gt;
{/loggedIn}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Unless&lt;/h3&gt;
&lt;p&gt;Renders block when expression is falsy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{^hasPermission}
  &amp;lt;p&amp;gt;You do not have access to this section.&amp;lt;/p&amp;gt;
{/hasPermission}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Range&lt;/h3&gt;
&lt;p&gt;Iterates over a list value&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
  {#fruits}
    &amp;lt;li&amp;gt;
      {name}
      &amp;lt;ul&amp;gt;
        {#vitamins}
          &amp;lt;li&amp;gt;{name}&amp;lt;/li&amp;gt;
        {/vitamins}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/li&amp;gt;
  {/fruits}
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Range blocks&lt;/strong&gt; create a new lexical scope. Inside the block, &lt;code&gt;{name}&lt;/code&gt; refers to the local object in context; outer scope variables are not accessible.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;⚠️ Control structures must wrap well-formed elements (or other well-formed control constructs), and cannot appear inside tags.&lt;/p&gt;
&lt;hr/&gt;
&lt;h3&gt;Comments&lt;/h3&gt;
&lt;p&gt;There are two types of comments&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Comment example --&amp;gt;

&amp;lt;span&amp;gt;{! TODO: fix bugs } hi&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Standard HTML comments are removed from the generated output.&lt;/p&gt;
&lt;p&gt;restache comments compile into JSX comments.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Generating JSX&lt;/h2&gt;
&lt;p&gt;restache transpiler generates a React JSX component from each input and handles JSX-specific quirks where necessary.&lt;/p&gt;
&lt;h3&gt;Fragment wrapping&lt;/h3&gt;
&lt;p&gt;Multiple root elements are wrapped in a Fragment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;{description}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;This also applies within control blocks.&lt;/li&gt;
&lt;li&gt;If you only have one root element, then a Fragment is omitted.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Case conversion&lt;/h3&gt;
&lt;p&gt;React requires component names to start with a capital letter and prop names to use camelCase. In contrast, HTML tags and attribute names are not case-sensitive.&lt;/p&gt;
&lt;p&gt;To ensure compatibility, restache applies the following transformations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Elements written in kebab-case (e.g. &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt;) are automatically converted to PascalCase (&lt;code&gt;MyButton&lt;/code&gt;) in the output.&lt;/li&gt;
&lt;li&gt;Similarly, kebab-case attributes (like &lt;code&gt;disable-padding&lt;/code&gt;) are converted to camelCase (&lt;code&gt;disablePadding&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;kebab-case &#128284; React case&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;search-bar
  hint-text="Type to search..."
  data-max-items="10"
  aria-label="Site search"
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ℹ️ &lt;em&gt;Attributes starting with &lt;code&gt;data-&lt;/code&gt; or &lt;code&gt;aria-&lt;/code&gt; are &lt;strong&gt;preserved as-is&lt;/strong&gt;, in line with React's conventions.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Attribute name normalization&lt;/h3&gt;
&lt;p&gt;Certain attributes are automatically renamed for React compatibility&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form enctype="multipart/form-data" accept-charset="UTF-8"&amp;gt;
  &amp;lt;input name="username" popovertarget="hint"&amp;gt;
  &amp;lt;textarea maxlength="200" autocapitalize="sentences"&amp;gt;&amp;lt;/textarea&amp;gt;
  &amp;lt;button formaction="/submit" formtarget="_blank"&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;video controlslist="nodownload"&amp;gt;
  &amp;lt;source src="video.mp4" srcset="video-480.mp4 480w, video-720.mp4 720w"&amp;gt;
&amp;lt;/video&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attribute renaming only occurs when the attribute is valid for the tag. For instance, &lt;code&gt;formaction&lt;/code&gt; isn't renamed on &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; since it isn't valid there.&lt;/p&gt;
&lt;p&gt;However, some attributes are renamed globally, regardless of which element they're used on. These include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All standard event handler attributes (&lt;code&gt;onclick&lt;/code&gt;, &lt;code&gt;onchange&lt;/code&gt;, etc.), which are converted to their camelCased React equivalents (e.g. &lt;code&gt;onClick&lt;/code&gt;, &lt;code&gt;onChange&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Common HTML aliases and reserved keywords like &lt;code&gt;class&lt;/code&gt; and &lt;code&gt;for&lt;/code&gt;, which are renamed to &lt;code&gt;className&lt;/code&gt; and &lt;code&gt;htmlFor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Certain accessibility- and editing-related attributes, such as &lt;code&gt;spellcheck&lt;/code&gt; and &lt;code&gt;tabindex&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;See &lt;a href="https://github.com/tetsuo/restache/blob/v0.x/table.go"&gt;&lt;code&gt;table.go&lt;/code&gt;&lt;/a&gt; for the full list.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Implicit key insertion in loops&lt;/h3&gt;
&lt;p&gt;When rendering lists, restache inserts a &lt;code&gt;key&lt;/code&gt; prop automatically, assigning it to the top-level element or to a wrapping Fragment if there are multiple root elements.&lt;/p&gt;
&lt;p&gt;Key is passed to the root element inside a loop&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{#images}
  &amp;lt;img src={src}&amp;gt;
{/images}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there are multiple roots, it goes on the Fragment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{#items}
  &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;
  &amp;lt;h3&amp;gt;{description}&amp;lt;/h3&amp;gt;
{/items}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Manually set key when there's single root&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{#images}
  &amp;lt;img key={id} src={src}&amp;gt;
{/images}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Importing other components&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;restache supports an implicit module system where custom elements (i.e., tags that are not part of the HTML spec) are automatically resolved to file-based components.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Component imports are inferred from the tag names. The following examples show how different components are resolved:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HTML&lt;/th&gt;
&lt;th&gt;JSX&lt;/th&gt;
&lt;th&gt;Import path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;MyButton&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./MyButton&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;ui:card-header&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;UiCardHeader&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./ui/CardHeader&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not resolved, standard tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;ui:div&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;UiDiv&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./ui/div&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Component resolution&lt;/h3&gt;
&lt;p&gt;Any tag that isn't a known HTML element is treated as a component.&lt;/p&gt;
&lt;p&gt;When the parser encounters such a tag, it follows these steps:&lt;/p&gt;
&lt;h4&gt;1. Check for namespace&lt;/h4&gt;
&lt;p&gt;restache first determines whether the tag uses a namespace. Namespaced tags contain a prefix and a component name (e.g., &lt;code&gt;&amp;lt;ui:button&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;h4&gt;2. Standard custom tags&lt;/h4&gt;
&lt;p&gt;If the tag &lt;strong&gt;does not&lt;/strong&gt; contain a namespace (e.g., &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;restache first looks for an exact match in the build configuration's &lt;code&gt;tagMappings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If no mapping is found, it falls back to searching in the current directory.
For example, &lt;code&gt;&amp;lt;my-button&amp;gt;&lt;/code&gt; could resolve to either &lt;code&gt;./my-button&lt;/code&gt; or &lt;code&gt;./MyButton&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Namespaced tags&lt;/h4&gt;
&lt;p&gt;If the tag &lt;strong&gt;does&lt;/strong&gt; contain a namespace (e.g., &lt;code&gt;&amp;lt;ui:button&amp;gt;&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;restache first checks the &lt;code&gt;tagPrefixes&lt;/code&gt; configuration. If a prefix (e.g., &lt;code&gt;ui&lt;/code&gt;) is defined, it uses the mapped path.
For example, if &lt;code&gt;mui&lt;/code&gt; is mapped to &lt;code&gt;@mui/material&lt;/code&gt;, then &lt;code&gt;&amp;lt;mui:app-bar&amp;gt;&lt;/code&gt; resolves to &lt;code&gt;@mui/material/AppBar&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If no mapping is found, it attempts to resolve the component from a subdirectory:
e.g., &lt;code&gt;&amp;lt;ui:button&amp;gt;&lt;/code&gt; → &lt;code&gt;ui/Button.js&lt;/code&gt;, &lt;code&gt;ui/button.jsx&lt;/code&gt;, &lt;code&gt;ui/button.tsx&lt;/code&gt;, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Note:&lt;/strong&gt; Standard HTML tags are not resolved as components, even if identically named files exist in the current directory.
However, &lt;strong&gt;namespacing can override this behavior&lt;/strong&gt;. For example, &lt;code&gt;&amp;lt;ui:div&amp;gt;&lt;/code&gt; will resolve to &lt;code&gt;./ui/div&lt;/code&gt; (or &lt;code&gt;./ui/Div&lt;/code&gt;), even though &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is a native HTML element.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;ESBuild integration&lt;/h2&gt;
&lt;p&gt;restache includes an ESBuild plugin that makes integration simple and easy in Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Register &lt;code&gt;.html&lt;/code&gt; loader and pass it to the plugin&lt;/li&gt;
&lt;li&gt;Plugin uses restache compiler to convert to &lt;code&gt;.jsx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;No runtime library needed; everything is transpiled ahead of time&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;a href="https://github.com/tetsuo/dashboard"&gt;&lt;code&gt;dashboard&lt;/code&gt;&lt;/a&gt; project includes a working build script (&lt;a href="https://github.com/tetsuo/dashboard/blob/master/build.go"&gt;&lt;code&gt;build.go&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's currently no support for Node.JS environment, but planned.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Roadmap&lt;/h2&gt;
&lt;h3&gt;Integration with React hooks&lt;/h3&gt;
&lt;p&gt;Currently, most logic must live in a &lt;code&gt;.jsx&lt;/code&gt; file next to the corresponding &lt;code&gt;.html&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;The long-term plan is to introduce a minimal set of expressions into the language itself so that common hooks like &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useContext&lt;/code&gt;, and &lt;code&gt;useSelector&lt;/code&gt; from Redux can be inferred from markup and compiled automatically.&lt;/p&gt;
&lt;hr/&gt;
&lt;h3&gt;Relational and logical expressions&lt;/h3&gt;
&lt;p&gt;Support for predicates inside range blocks is planned for v1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{#products: price &amp;lt; 100 &amp;amp;&amp;amp; inStock}
  &amp;lt;product-card /&amp;gt;
{/products}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This could be compiled as a filter, and potentially mapped to things like backend queries (e.g. MongoDB, CouchDB, Algolia, ...) as well.&lt;/p&gt;
&lt;hr/&gt;
&lt;h3&gt;Smarter code generation&lt;/h3&gt;
&lt;p&gt;Before adding expressions, there's still a lot that can be optimized with the syntax that's already in place.&lt;/p&gt;
&lt;p&gt;Consider pattern matching. Since restache doesn't support expressions beyond dot notation, patterns have to be represented structurally. For example, using an object with a mutually exclusive key set:&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;{
  home: {...},
  settings: undefined,
  products: undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in the template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{?home}    &amp;lt;home /&amp;gt;    {/home}
{?settings}&amp;lt;settings /&amp;gt;{/settings}
{?products}&amp;lt;products /&amp;gt;{/products}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compiles to:&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;if (props.home) &amp;lt;Home /&amp;gt;
if (props.settings) &amp;lt;Settings /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in an &lt;code&gt;O(n)&lt;/code&gt; operation instead of an &lt;code&gt;O(1)&lt;/code&gt; equality check like &lt;code&gt;switch(route)&lt;/code&gt;, but the difference is negligible unless you're dealing with many conditions.&lt;/p&gt;
&lt;p&gt;Future versions of restache will generate &lt;code&gt;if/else&lt;/code&gt; or &lt;code&gt;switch&lt;/code&gt; statements when keyed unions are used, along with other optimizations such as merging adjacent &lt;code&gt;{?x}&lt;/code&gt; and &lt;code&gt;{^x}&lt;/code&gt; blocks into a single conditional.&lt;/p&gt;
&lt;hr/&gt;
&lt;h3&gt;More codegen targets&lt;/h3&gt;
&lt;p&gt;Plans include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Emitting a real JavaScript AST instead of raw JSX strings&lt;/li&gt;
&lt;li&gt;Supporting things other than React&lt;/li&gt;
&lt;li&gt;Option to emit TSX&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When TypeScript is used and type information is available at build time, more advanced optimizations may be possible. But even without that, structural inference allows detecting optionals and iterables, which can be used to emit a generic type for the component.&lt;/p&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">restache extends plain HTML with curly braces and compiles to modern JSX, so you can write React components like it's 2013 again.</media:description></item><item><title>Fortune as a Service</title><link>https://tetsuo.github.io/fortune-as-a-service.html</link><category>go</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Mon, 31 Mar 2025 00:00:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2025-03-31:/fortune-as-a-service</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/tetsuo/fortune"&gt;&lt;strong&gt;fortune&lt;/strong&gt;&lt;/a&gt; is a simple HTTP API that delivers random fortune cookies, built as a ready-to-deploy reference for setting up observability and telemetry on GCP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;API&lt;/h4&gt;
&lt;p&gt;The fortune API has two endpoints. &lt;code&gt;GET /&lt;/code&gt; returns a random fortune from the database. &lt;code&gt;POST /&lt;/code&gt; accepts a plain text body with fortunes separated by &lt;code&gt;%&lt;/code&gt;, as per the original format of the Unix &lt;code&gt;fortune&lt;/code&gt; command.&lt;/p&gt;
&lt;h4&gt;Third-party code&lt;/h4&gt;
&lt;p&gt;I reused some internals from &lt;a href="https://go.googlesource.com/pkgsite/"&gt;&lt;strong&gt;pkgsite&lt;/strong&gt;&lt;/a&gt; (the Go package index) and adapted them to work with MySQL.&lt;/p&gt;
&lt;p&gt;Specifically, I modified the &lt;code&gt;internal/database&lt;/code&gt; package to support MySQL, along with the CLI tool in &lt;code&gt;devtools/cmd/db&lt;/code&gt;. The other borrowed packages from pkgsite (&lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;middleware&lt;/code&gt;, &lt;code&gt;wraperr&lt;/code&gt;) remain largely unchanged, aside from minor cleanup.&lt;/p&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;The repo is here: &lt;a href="https://github.com/tetsuo/fortune"&gt;&lt;strong&gt;github.com/tetsuo/fortune&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Docs cover local setup, development, and deployment.&lt;/p&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">fortune is a simple HTTP API that delivers random fortune cookies, built as a ready-to-deploy reference for setting up observability and telemetry on GCP.</media:description></item><item><title>Cubic Limit in Effect</title><link>https://tetsuo.github.io/cubic-limit-in-effect.html</link><category>typescript</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Sat, 8 Feb 2025 14:55:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2025-02-08:/cubic-limit-in-effect</guid><description>&lt;blockquote&gt;
&lt;p&gt;Recreating the iconic artwork of &lt;a href="https://www.emohr.com/"&gt;Manfred Mohr&lt;/a&gt; the hard way, by rolling my own 3D graphics DSL in TypeScript with &lt;a href="https://effect.website/"&gt;Effect&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Shown below is &lt;em&gt;Cubic Limit, P-161&lt;/em&gt; rendered on &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, inspired by Mohr's &lt;a href="https://www.emohr.com/mohr_cube1_161.html"&gt;1975 plotter drawings&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://tetsuo.github.io/f/cubiclimit.html"&gt;&lt;img alt="mohr-p161" src="https://tetsuo.github.io/images/cubiclimit.jpg"/&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;h5&gt;Source code&lt;/h5&gt;
&lt;p&gt;The full source code that produces this image is available in the repository &lt;a href="https://github.com/tetsuo/cubic-limit"&gt;&lt;code&gt;tetsuo/cubic-limit&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Under the hood it uses an &lt;a href="https://effect.website/"&gt;Effect&lt;/a&gt;-based adaptation of &lt;a href="https://github.com/gcanti/graphics-ts"&gt;graphics-ts&lt;/a&gt; (originally &lt;a href="https://github.com/gcanti/fp-ts"&gt;fp-ts&lt;/a&gt;) with added support for 3D rendering. You can clone it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;git clone git@github.com:tetsuo/cubic-limit.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The repository also includes a version of &lt;em&gt;P-197&lt;/em&gt;, but this post will focus solely on &lt;em&gt;P-161&lt;/em&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;In this post, we'll use this artwork as a starting point and build a functional interface around it for drawing 3D shapes and applying transformations to them.&lt;/p&gt;
&lt;h2&gt;Cube vertices &amp;amp; edges&lt;/h2&gt;
&lt;p&gt;We'll work with a type &lt;code&gt;Vec&lt;/code&gt; to hold &lt;code&gt;[x, y, z]&lt;/code&gt; coordinates.&lt;/p&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;type Vec = NonEmptyReadonlyArray&amp;lt;number&amp;gt;
// e.g. [x, y, z]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A unit cube (scaled from -1 to +1) has 8 vertices:&lt;/p&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;const cubePoints: NonEmptyReadonlyArray&amp;lt;Vec&amp;gt; = [
  [-1, -1, -1],
  [ 1, -1, -1],
  [ 1,  1, -1],
  [-1,  1, -1],
  [-1, -1,  1],
  [ 1, -1,  1],
  [ 1,  1,  1],
  [-1,  1,  1],
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It has 12 edges. We can index them by grouping bottom edges, top edges, and vertical edges. For each &lt;code&gt;i&lt;/code&gt; in &lt;code&gt;0..3&lt;/code&gt;, we build three edges:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const getEdges = (i: number): NonEmptyReadonlyArray&amp;lt;Vec&amp;gt; =&amp;gt; [
  [i, (i + 1) % 4],
  [i + 4, ((i + 1) % 4) + 4],
  [i, i + 4],
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combining all &lt;code&gt;i&lt;/code&gt; values yields 12 edges. Each edge is a pair of indices into &lt;code&gt;cubePoints&lt;/code&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Cube configurations&lt;/h2&gt;
&lt;p&gt;Mohr's piece shows a 31×31 grid of partially drawn cubes. Each cube is unique and displayed with exactly six edges (&lt;code&gt;n = 6&lt;/code&gt;). To represent this mathematically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Label each of the cube's 12 edges as a &lt;strong&gt;bit&lt;/strong&gt; in a &lt;strong&gt;12-bit integer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Turning on an edge = setting the corresponding bit to &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means each valid cube configuration is a 12-bit number with exactly six bits set to &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;How many such configurations exist? Exactly &lt;strong&gt;924&lt;/strong&gt;, as determined by the formula &lt;code&gt;C(12, 6) = 924&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These numbers correspond to &lt;a href="https://oeis.org/A023688"&gt;OEIS sequence A023688&lt;/a&gt;, which lists integers with six &lt;code&gt;1&lt;/code&gt;s in their binary representation. Within the 12-bit range, the sequence starts at 63 (binary &lt;code&gt;000000111111&lt;/code&gt;) and ends just under 4095 (binary &lt;code&gt;111111111111&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;63    -- 000000111111
95    -- 000001011111
111   -- 000001101111
.
.
4032  -- 111111000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generating all 12-bit numbers with six 1s&lt;/h3&gt;
&lt;p&gt;To iterate through all 12-bit integers that have exactly six bits set, I turned to a classic resource: &lt;a href="https://graphics.stanford.edu/~seander/bithacks.html"&gt;Bit Twiddling Hacks&lt;/a&gt;. At the bottom of that page is the code to compute the &lt;a href="https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation"&gt;lexicographically next bit permutation&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { pipe } from 'effect/Function'

const nextNumber = (v: number) =&amp;gt;
  pipe(
    (v | (v - 1)) + 1,
    t =&amp;gt; t | ((((t &amp;amp; -t) / (v &amp;amp; -v)) &amp;gt;&amp;gt; 1) - 1)
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function finds the next integer with the same number of bits set. For example, if &lt;code&gt;v&lt;/code&gt; has exactly 6 bits set, it will produce the next integer that also has 6 bits set.&lt;/p&gt;
&lt;p&gt;With it, we can now accumulate all valid edge combinations:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { unfold } from 'effect/Array'
import { none, some } from 'effect/Option'

const seq: number[] = unfold(63, a =&amp;gt; a &amp;lt; 4095 ? some([a, nextNumber(a)]) : none()
)
// =&amp;gt; [63, 95, 111, 119, ... 4032, etc.]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each integer in &lt;code&gt;seq&lt;/code&gt; encodes a unique cube with exactly 6 edges, totaling 924 configurations.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Representing geometry&lt;/h2&gt;
&lt;h3&gt;Shape&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Shape&lt;/code&gt; type is originally defined in the &lt;a href="https://github.com/paf31/purescript-drawing/blob/master/src/Graphics/Drawing.purs#L34"&gt;purescript-drawing&lt;/a&gt; library:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;type Point = { x :: Number, y :: Number }

data Shape
  = Path Boolean (List Point)
  | Composite (List Shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For our implementation, we borrow the &lt;a href="https://github.com/gcanti/src/graphics-ts"&gt;graphics-ts&lt;/a&gt; port of &lt;code&gt;Shape&lt;/code&gt; with one critical change: our points will be multi-dimensional. Therefore, the &lt;code&gt;Point&lt;/code&gt; type will alias to &lt;code&gt;Vec&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;type Point = Vec

const point = (x: number, y: number, z: number): Point =&amp;gt; [x, y, z]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a &lt;code&gt;Shape&lt;/code&gt; is a &lt;strong&gt;union&lt;/strong&gt; of &lt;code&gt;Path&lt;/code&gt; and &lt;code&gt;Composite&lt;/code&gt;, meaning it can represent one of two things. In TypeScript, this translates to:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;type Shape = Path | Composite
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Path&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;Path&lt;/code&gt; is a &lt;a href="https://effect.website/docs/data-types/chunk/"&gt;&lt;code&gt;Chunk&lt;/code&gt;&lt;/a&gt; of points connected by lines, plus a &lt;code&gt;boolean&lt;/code&gt; to indicate whether the path is closed (i.e., the last point connects back to the first):&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { Chunk } from '@effect/data/Chunk'

interface Path {
  readonly _tag: 'Path'
  readonly closed: boolean
  readonly points: Chunk&amp;lt;Point&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We define two constructors, &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;closed&lt;/code&gt;, which take a &lt;code&gt;Foldable&lt;/code&gt;, an abstraction for collections that can be reduced to a single value (e.g., an &lt;code&gt;Array&lt;/code&gt;). We also define a &lt;code&gt;Monoid&lt;/code&gt; instance for &lt;code&gt;Path&lt;/code&gt;, which combines two paths by concatenating their points and checking if either path is closed.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { MonoidSome } from '@effect/typeclass/data/Boolean'
import { constant } from 'effect/Function'
import { fromSemigroup, Monoid, struct } from '@effect/typeclass/Monoid'
import { make } from '@effect/typeclass/Semigroup'
import { Foldable } from '@effect/typeclass/Foldable'
import { Kind, TypeLambda } from 'effect/HKT'
import { Chunk, append, appendAll, empty } from '@effect/data/Chunk'

// Constructors

const closed =
  &amp;lt;F extends TypeLambda&amp;gt;(F: Foldable&amp;lt;F&amp;gt;): ((fa: Kind&amp;lt;F, unknown, unknown, unknown, Point&amp;gt;) =&amp;gt; Path) =&amp;gt;
  fa =&amp;gt;
    F.reduce(fa, monoidPath.empty, (b, a) =&amp;gt; ({
      _tag: 'Path',
      closed: true,
      points: append(b.points, a),
    }))

const path =
  &amp;lt;F extends TypeLambda&amp;gt;(F: Foldable&amp;lt;F&amp;gt;): ((fa: Kind&amp;lt;F, unknown, unknown, unknown, Point&amp;gt;) =&amp;gt; Path) =&amp;gt;
  fa =&amp;gt;
    F.reduce(fa, monoidPath.empty, (b, a) =&amp;gt; ({
      _tag: 'Path',
      closed: false,
      points: append(b.points, a),
    }))

// Monoid instance

const monoidPath: Monoid&amp;lt;Path&amp;gt; = struct({
  _tag: fromSemigroup&amp;lt;'Path'&amp;gt;(make(constant('Path')), 'Path'),
  closed: MonoidSome,
  points: fromSemigroup(make&amp;lt;Chunk&amp;lt;Point&amp;gt;&amp;gt;(appendAll), empty()),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;monoidPath&lt;/code&gt; combines two &lt;code&gt;Path&lt;/code&gt; objects by merging their point arrays. The &lt;code&gt;MonoidSome&lt;/code&gt; ensures that if any of the paths are closed, the result is also considered closed.&lt;/p&gt;
&lt;h3&gt;Composite&lt;/h3&gt;
&lt;p&gt;The other variant of &lt;code&gt;Shape&lt;/code&gt; is &lt;code&gt;Composite&lt;/code&gt;, which basically serves as a container for multiple &lt;code&gt;Shape&lt;/code&gt;s:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;interface Composite {
  readonly _tag: 'Composite'
  readonly shapes: ReadonlyArray&amp;lt;Shape&amp;gt;
}

const composite = (shapes: ReadonlyArray&amp;lt;Shape&amp;gt;): Composite =&amp;gt; ({
  _tag: 'Composite',
  shapes,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Constructing a full cube&lt;/h2&gt;
&lt;p&gt;Bringing it all together, here's how we can define a full cube (all 12 edges visible) as a &lt;code&gt;Composite&lt;/code&gt; of 12 paths:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { pipe } from 'effect/Function'
import { flatMap, map, range } from 'effect/Array'
import { Foldable } from '@effect/typeclass/data/Array'
import { tuple } from 'effect/Data'
import * as S from './Shape'

const path = S.path(Foldable)

const cubeShape = S.composite(
  pipe(
    range(0, 3),
    flatMap(i =&amp;gt;
      pipe(
        getEdges(i),
        map(ix =&amp;gt;
          path(
            pipe(
              tuple(points[ix[0]], points[ix[1]]),
              map(vec =&amp;gt; S.point(vec[0], vec[1], vec[2]))
            )
          )
        )
      )
    )
  )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Cube as a composite of paths&lt;/h5&gt;
&lt;p&gt;The &lt;code&gt;cubeShape&lt;/code&gt; structure represents the cube's 12 edges as pairs of points in 3D space. Each point is a 3D vector&lt;code&gt;[x, y, z]&lt;/code&gt;, and each edge is defined by two such points:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  [
    [-1, -1, -1], // Start point
    [1, -1, -1],  // End point
  ],
  [
    [-1, -1, 1],
    [1, -1, 1],
  ],
  ... (12 pairs in total)
]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Each innermost array defines a &lt;code&gt;Point&lt;/code&gt;, represented by its x, y, and z coordinates.&lt;/li&gt;
&lt;li&gt;Each array containing point tuples describes a &lt;code&gt;Path&lt;/code&gt;, which corresponds to a single edge of the cube.&lt;/li&gt;
&lt;li&gt;12 paths are grouped together into a &lt;code&gt;Composite&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2&gt;Toggling edges to form partial cubes&lt;/h2&gt;
&lt;p&gt;Since Mohr's P-161 selectively displays only 6 edges of the cube, we need a way to toggle edges (paths) on/off. We can do that by passing in a &lt;em&gt;predicate&lt;/em&gt; that checks if an edge is active.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { Predicate } from 'effect/Predicate'

const cubeShape = (shouldDrawEdge: Predicate&amp;lt;number&amp;gt;): S.Composite =&amp;gt;
  S.composite(
    pipe(
      range(0, 3),
      flatMapArray(i =&amp;gt;
        pipe(
          getEdges(i),
          mapArray((ix, j) =&amp;gt;
            path(
              shouldDrawEdge(i + j * 4)
                ? pipe(
                    tuple(points[ix[0]], points[ix[1]]),
                    mapArray(vec =&amp;gt; S.point(vec[0], vec[1], vec[2]))
                  )
                : [] // ← empty when shouldDrawEdge(edge) returns false
            )
          )
        )
      )
    )
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We define a utility &lt;code&gt;isBitSet&lt;/code&gt;, returning &lt;code&gt;true&lt;/code&gt; if a given bit is set in &lt;code&gt;n&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const isBitSet = (n: number) =&amp;gt; (index: number) =&amp;gt;
  Boolean(n &amp;amp; (1 &amp;lt;&amp;lt; index))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we build a function &lt;code&gt;cubeFromNumber&lt;/code&gt; that uses &lt;code&gt;isBitSet&lt;/code&gt; to determine which edges should be drawn:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { flow } from 'effect/Function'

const cubeFromNumber = flow(isBitSet, cubeShape)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;n&lt;/code&gt; has bits 3, 5, 8, 10, etc., turned on, only those edges of the cube are visible.&lt;/p&gt;
&lt;p&gt;For example, calling &lt;code&gt;cubeFromNumber(63)&lt;/code&gt; (binary &lt;code&gt;000000111111&lt;/code&gt;) will omit edges 0 through 5 and include edges 6 through 11, producing a half-formed cube.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Generating all cube shapes&lt;/h2&gt;
&lt;p&gt;Finally, we can list all 924 cube shapes corresponding to &lt;code&gt;n = 6&lt;/code&gt; by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generating all 12-bit numbers with 6 bits set (using &lt;code&gt;unfold&lt;/code&gt; and &lt;code&gt;nextNumber&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Mapping each integer to a &lt;code&gt;Shape&lt;/code&gt; with &lt;code&gt;cubeFromNumber&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { none, some } from 'effect/Option'
import { map, unfold } from 'effect/Array'

const cubes: Composite[] = pipe(
  unfold(63, a =&amp;gt; (a &amp;lt; 4095 ? some([a, nextNumber(a)]) : none())),
  map(cubeFromNumber)
)

// cubes is now an array of 924 partial cube shapes
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Representing styles &amp;amp; transformations&lt;/h2&gt;
&lt;h3&gt;Drawing&lt;/h3&gt;
&lt;p&gt;Next, we define a new type that captures &lt;em&gt;what&lt;/em&gt; we want to do with shapes, whether to fill them, outline them, apply transformations, or clip:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;type Drawing =
  | { _tag: 'Translate'; translateX: number; translateY: number; translateZ: number; drawing: Drawing }
  | { _tag: 'Rotate'; rotateX: Angle; rotateY: Angle; rotateZ: Angle; drawing: Drawing }
  | { _tag: 'Scale'; scaleX: number; scaleY: number; scaleZ: number; drawing: Drawing }
  | { _tag: 'Many'; drawings: ReadonlyArray&amp;lt;Drawing&amp;gt; }
  | { _tag: 'Clipped'; shape: Shape; drawing: Drawing }
  | { _tag: 'Fill'; shape: Shape; style: FillStyle }
  | { _tag: 'Outline'; shape: Shape; style: OutlineStyle }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;code&gt;Drawing&lt;/code&gt; acts like a &lt;em&gt;scene graph&lt;/em&gt;, where transformations (&lt;code&gt;Scale&lt;/code&gt;, &lt;code&gt;Rotate&lt;/code&gt;, &lt;code&gt;Translate&lt;/code&gt;) can be nested around shapes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To convert a &lt;code&gt;Shape&lt;/code&gt; into a &lt;code&gt;Drawing&lt;/code&gt;, we use one of three options: &lt;code&gt;Clipped&lt;/code&gt;, &lt;code&gt;Fill&lt;/code&gt;, or &lt;code&gt;Outline&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Outline&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Outline&lt;/code&gt; variant in &lt;code&gt;Drawing&lt;/code&gt; represents shapes rendered with an outline. The function &lt;code&gt;outline&lt;/code&gt; constructs an outlined shape with a given &lt;code&gt;OutlineStyle&lt;/code&gt; (color, lineWidth, etc.):&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const outline: (shape: Shape, style: OutlineStyle) =&amp;gt; Drawing = (shape, style) =&amp;gt; ({
  _tag: 'Outline',
  shape,
  style,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Applying outlines to cubes:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import * as D from './Drawing'

const lineColor = D.outlineColor(white)

const cubeLineStyle = D.monoidOutlineStyle.combine(lineColor, D.lineCap('round'))

const cubes = pipe(
  unfold(63, a =&amp;gt; (a &amp;lt; 4095 ? some([a, nextNumber(a)]) : none())),
  map(flow(cubeFromNumber, cube =&amp;gt; D.outline(cube, cubeLineStyle)))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fill&lt;/h3&gt;
&lt;p&gt;Just like &lt;code&gt;Outline&lt;/code&gt; defines shapes with an outline, the &lt;code&gt;Fill&lt;/code&gt; variant in &lt;code&gt;Drawing&lt;/code&gt; represents shapes rendered with a fill. The function &lt;code&gt;fill&lt;/code&gt; creates a filled shape using a specified &lt;code&gt;FillStyle&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const fill: (shape: Shape, style: FillStyle) =&amp;gt; Drawing = (shape, style) =&amp;gt; ({
  _tag: 'Fill',
  shape,
  style,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Drawing background:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { Foldable } from '@effect/typeclass/data/Array'
import { hsl } from './Color'
import * as S from './Shape'
import * as D from './Drawing'

const closed = S.closed(Foldable)

const drawBackground = ({ width, height }: Size, bgColor: Color): D.Drawing =&amp;gt;
  D.fill(
    closed([
      [0, 0, 0],
      [width, 0, 0],
      [width, height, 0],
      [0, height, 0],
    ]),
    D.fillStyle(bgColor)
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Many&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Many&lt;/code&gt; variant allows combining multiple &lt;code&gt;Drawing&lt;/code&gt; objects into a single composition, conceptually just a list.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const many: (drawings: ReadonlyArray&amp;lt;Drawing&amp;gt;) =&amp;gt; Drawing = drawings =&amp;gt; ({
  _tag: 'Many',
  drawings,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Drawing a grid of lines:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { map, range } from 'effect/Array'
import { white } from './Color'
import * as D from './Drawing'

const lineColor = D.outlineColor(white)

// drawLines creates evenly spaced vertical or horizontal lines by dividing
// the given height into n segments and mapping them into outlined paths.
const drawLines = (n: number, height: number, vertical: boolean): D.Drawing =&amp;gt; {
  const size = height / n
  return D.many(
    pipe(
      range(1, n - 1),
      map(i =&amp;gt;
        D.outline(
          path(
            vertical
              ? [
                  [i * size, 0, 0],
                  [i * size, height, 0],
                ]
              : [
                  [0, i * size, 0],
                  [height, i * size, 0],
                ]
          ),
          lineColor
        )
      )
    )
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Translate/Scale/Rotate&lt;/h3&gt;
&lt;p&gt;The each of the following constructors wraps a &lt;code&gt;Drawing&lt;/code&gt; and includes X, Y, Z parameters to translate, rotate, or scale whatever is inside.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const translate = (
  translateX: number,
  translateY: number,
  translateZ: number,
  drawing: Drawing
): Drawing =&amp;gt; ({ _tag: 'Translate', translateX, translateY, translateZ, drawing })

const scale = (
  scaleX: number,
  scaleY: number,
  scaleZ: number,
  drawing: Drawing
): Drawing =&amp;gt; ({ _tag: 'Scale', scaleX, scaleY, scaleZ, drawing })

const rotate = (
  rotateX: Angle,
  rotateY: Angle,
  rotateZ: Angle,
  drawing: Drawing
): Drawing =&amp;gt; ({ _tag: 'Rotate', rotateX, rotateY, rotateZ, drawing })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rotating, scaling, and positioning cubes:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { map, unfold } from 'effect/Array'
import { none, some } from 'effect/Option'
import * as D from './Drawing'
import * as S from './Shape'

// drawCubes generates a grid of outlined cubes.
const drawCubes = (numCells: number, cellSize: number): D.Drawing =&amp;gt; {

  // translateCube positions each cube based on its row/column index.
  const translateCube = (drawing: D.Drawing, i: number): D.Drawing =&amp;gt; {
    const translateX = cellSize * (numCells - Math.floor(i / numCells) - 0.5)
    const translateY = cellSize * (0.5 + (i % numCells))
    return D.translate(translateX, translateY, 0, drawing)
  }

  // Scale factor compensates for the original cube points being doubled.
  const scaleFactor = cellSize / 5

  return D.rotate(
    S.degrees(30),
    S.degrees(-60),
    S.degrees(0),
    D.scale(
      scaleFactor,
      scaleFactor,
      1, // Z remains unchanged.
      D.many(
        pipe(
          unfold(63, a =&amp;gt; (a &amp;lt; 4095 ? some([a, nextNumber(a)]) : none())),
          map(flow(cubeFromNumber, cube =&amp;gt; D.outline(cube, cubeLineStyle))),
          map(translateCube)
        )
      )
    )
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Drawing P-161&lt;/h2&gt;
&lt;p&gt;Finally, we end up with a &lt;code&gt;Drawing&lt;/code&gt; that visually represents Cubic Limit, P-161 in its entirety.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const drawP161 = (size: Size, bgColor: Color): D.Drawing =&amp;gt; {
  const numCells = 31
  const cellSize = size.width / numCells

  const background = drawBackground(size, bgColor)

  const lines = D.many([
    drawLines(numCells, size.width, false),
    drawLines(numCells, size.height, true)
  ])

  const cubes = drawCubes(numCells, cellSize)

  return D.many([background, lines, cubes])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;img alt="Manfred Mohr and Estarose Wolfson" src="https://tetsuo.github.io/images/zkm-01-0134-02-0427.jpg"/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Manfred Mohr and Estarose Wolfson look at the Benson plotter in the Centre de Calcul de la Météorologie Nationale, 1971 / © Manfred Mohr (&lt;a href="https://zkm.de/en/manfred-mohr"&gt;Source&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Cubic Limit series was originally drawn using a &lt;strong&gt;Benson 1284 flatbed plotter&lt;/strong&gt;. Mohr developed his algorithms on a &lt;strong&gt;CDC 6400&lt;/strong&gt; mainframe with &lt;strong&gt;Fortran IV&lt;/strong&gt;, storing his code on punch cards.&lt;/p&gt;
&lt;p&gt;While details about this plotter's proprietary language are scarce, it probably shared similarities with Hewlett Packard's &lt;strong&gt;HP-GL&lt;/strong&gt;. For example, drawing a rectangle in HP-GL might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SP1          // Select pen 1
PU 0,0       // Pen up (move without drawing)
PD 100,0     // Pen down (start drawing)
PD 100,100
PD 0,100
PD 0,0
PU           // Pen up (stop drawing)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that these plotter languages somewhat resemble the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API"&gt;&lt;strong&gt;Canvas API&lt;/strong&gt;&lt;/a&gt;. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PU&lt;/code&gt; (Pen Up) corresponds to &lt;code&gt;moveTo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PD&lt;/code&gt; (Pen Down) corresponds to &lt;code&gt;lineTo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is a basic rectangle-drawing example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(100, 0);
ctx.lineTo(100, 100);
ctx.lineTo(0, 100);
ctx.closePath();
ctx.stroke();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;p&gt;The &lt;code&gt;Drawing&lt;/code&gt; type we defined is essentially an algebraic data type (specifically, a sum type) that describes the semantics of a domain-specific language for vector graphics. It doesn't have its own syntax, nor does it need one.&lt;/p&gt;
&lt;p&gt;All it requires is an &lt;strong&gt;interpreter&lt;/strong&gt; to integrate with the Canvas API and, ultimately, generate canvas commands.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Interpreting geometry&lt;/h2&gt;
&lt;p&gt;That said, before we proceed, we must address a key limitation: the Canvas API only supports 2D coordinates, not 3D. To enable 3D functionality on &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, we'll define &lt;code&gt;Path3D&lt;/code&gt; (as an implementation of the &lt;a href="https://html.spec.whatwg.org/multipage/canvas.html#building-paths"&gt;&lt;code&gt;CanvasPath&lt;/code&gt;&lt;/a&gt; interface), much like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Path2D"&gt;&lt;code&gt;Path2D&lt;/code&gt;&lt;/a&gt; but with support for 3D coordinates.&lt;/p&gt;
&lt;h3&gt;Path3D&lt;/h3&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;type Path3D = ReadonlyArray&amp;lt;ReadonlyArray&amp;lt;Point&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Path3D&lt;/code&gt; is an intermediate representation that transforms high-level &lt;code&gt;Shape&lt;/code&gt; definitions into a format more suitable for applying transformations and rendering. Essentially, it &lt;em&gt;flattens&lt;/em&gt; a shape into an array of &lt;strong&gt;subpaths&lt;/strong&gt;, where each subpath is a sequence of 3D points (&lt;code&gt;[x, y, z]&lt;/code&gt;).&lt;/p&gt;
&lt;h4&gt;Converting a Shape to Path3D&lt;/h4&gt;
&lt;p&gt;The helper function &lt;code&gt;fromShape&lt;/code&gt; performs this conversion:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const fromShape: (shape: Shape) =&amp;gt; Path3D = shape =&amp;gt; {
  switch (shape._tag) {
    case 'Composite':
      // For composites, recursively process each child shape and concatenate the results.
      return shape.shapes.flatMap(fromShape)
    case 'Path':
      // For a simple path, convert its points into a subpath.
      return pipe(
        toReadonlyArray(shape.points),
        matchLeft({
          onEmpty: empty,
          onNonEmpty: (head, tail) =&amp;gt;
            pipe(
              tail,
              map(lineTo),
              reduce(moveTo(head)([]), (acc, f) =&amp;gt; f(acc)),
              path =&amp;gt; (shape.closed ? closePath(path).slice(0, -1) : path)
            ),
        })
      )
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;For a &lt;code&gt;Composite&lt;/code&gt;, &lt;code&gt;fromShape&lt;/code&gt; simply calls &lt;code&gt;fromShape&lt;/code&gt; on each sub-shape and concatenates the resulting &lt;code&gt;Path3D&lt;/code&gt; arrays.&lt;/li&gt;
&lt;li&gt;For a &lt;code&gt;Path&lt;/code&gt;, &lt;code&gt;fromShape&lt;/code&gt; breaks the &lt;code&gt;points&lt;/code&gt; into line segments (via &lt;code&gt;moveTo&lt;/code&gt; and &lt;code&gt;lineTo&lt;/code&gt;). If &lt;code&gt;closed&lt;/code&gt; is true, it calls &lt;code&gt;closePath&lt;/code&gt;, making the last point connect to the first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Path3D combinators&lt;/h3&gt;
&lt;p&gt;These functions implement the path-drawing algorithm:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;moveTo&lt;/code&gt; begins a new subpath at the specified &lt;code&gt;point&lt;/code&gt; (if the point is finite).&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const moveTo = (point: Point) =&amp;gt; (path: Path3D): Path3D =&amp;gt;
  isPointFinite(point)
    ? // Create a new subpath with the specified point.
      append(path, [point])
    : path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lineTo&lt;/code&gt; extends the current subpath by connecting the last point to the new &lt;code&gt;point&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const lineTo = (point: Point) =&amp;gt; (path: Path3D): Path3D =&amp;gt;
  isPointFinite(point)
    ? isNonEmptyReadonlyArray(path)
      ? // Connect the last point in the subpath to the given point.
        pipe(path, modifyNonEmptyLast(append(point)))
      : // If path has no subpaths, ensure there is a subpath.
        moveTo(point)(path)
    : path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;closePath&lt;/code&gt; closes the current subpath by connecting its last point back to the first.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const closePath = (path: Path3D): Path3D =&amp;gt; {
  // Do nothing if path has no subpaths.
  if (!isNonEmptyReadonlyArray(path)) {
    return path
  }

  const cur = lastNonEmpty(path)

  // Do nothing if the last path contains a single point.
  if (!(isNonEmptyReadonlyArray(cur) &amp;amp;&amp;amp; cur.length &amp;gt; 1)) {
    return path
  }

  const end = lastNonEmpty(cur)
  const start = cur[0]

  // Do nothing if both ends are the same point.
  if (end[0] === start[0] &amp;amp;&amp;amp; end[1] === start[1] &amp;amp;&amp;amp; end[2] === start[2]) {
    return path
  }

  // Mark the last path as closed adding a new subpath whose first point
  // is the same as the previous subpath's first point.
  return append(setNonEmptyLast(append(cur, start) as ReadonlyArray&amp;lt;Point&amp;gt;)(path), [start])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Transformation matrices&lt;/h2&gt;
&lt;p&gt;Transformations are typically encoded in &lt;strong&gt;4×4 matrices&lt;/strong&gt; when dealing with 3D points. We can define a matrix type &lt;code&gt;Mat&lt;/code&gt; as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;type Mat = NonEmptyReadonlyArray&amp;lt;Vec&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common 3D transforms&lt;/h3&gt;
&lt;p&gt;A standard 4×4 &lt;strong&gt;transform&lt;/strong&gt; matrix looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;[sx   0    0    tx]
[0    sy   0    ty]
[0    0    sz   tz]
[0    0    0    1 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sx&lt;/code&gt;, &lt;code&gt;sy&lt;/code&gt; and &lt;code&gt;sz&lt;/code&gt; control &lt;strong&gt;scaling&lt;/strong&gt; on each axis.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tx&lt;/code&gt;, &lt;code&gt;ty&lt;/code&gt; and &lt;code&gt;tz&lt;/code&gt; control &lt;strong&gt;translation&lt;/strong&gt; along each axis.&lt;/li&gt;
&lt;li&gt;Off-diagonal terms can encode &lt;strong&gt;rotation&lt;/strong&gt; and other transformations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, a pure &lt;strong&gt;scale&lt;/strong&gt; transformation might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const scale = (v: Vec): Mat =&amp;gt; [
  [v[0], 0,    0,    0],
  [0,    v[1], 0,    0],
  [0,    0,    v[2], 0],
  [0,    0,    0,    1],
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Likewise, a &lt;strong&gt;rotation&lt;/strong&gt; about the X-axis by some &lt;code&gt;angle&lt;/code&gt; can be represented as:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const sin = (deg: number) =&amp;gt; Math.sin((deg * Math.PI) / 180)
const cos = (deg: number) =&amp;gt; Math.cos((deg * Math.PI) / 180)

const rotateX = (angle: number): Mat =&amp;gt; [
  [1, 0,           0,          0],
  [0, cos(angle),  sin(angle), 0],
  [0, -sin(angle), cos(angle), 0],
  [0, 0,           0,          1],
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Combining multiple transforms&lt;/h3&gt;
&lt;p&gt;Often we need to combine several transformations (e.g. translate first, then rotate, then scale). In matrix math, combining transformations is done by &lt;em&gt;matrix multiplication&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;declare function mul(y: Mat): (x: Mat) =&amp;gt; Mat

pipe(
  identity, // The 4×4 identity matrix (no transformation)
  mul(translate([10, 0, 0])),
  mul(rotateX(45)),
  mul(rotateY(30)),
  mul(scale([2, 2, 2]))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final result is a single 4×4 matrix encoding all these operations in the correct order. When you apply that matrix to a point &lt;code&gt;[x, y, z, 1]&lt;/code&gt;, it performs the entire sequence of transformations-translation, then rotation on X, then rotation on Y, then scaling.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128196; &lt;strong&gt;See the full implementation in &lt;a href="https://github.com/tetsuo/cubic-limit/blob/master/src/Mat.ts#L17"&gt;&lt;code&gt;Mat.ts&lt;/code&gt;&lt;/a&gt; file.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reminder&lt;/strong&gt;: Matrix multiplication is &lt;strong&gt;not&lt;/strong&gt; &lt;em&gt;commutative&lt;/em&gt;. The order you multiply matters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Semigroup &amp;amp; Monoid for matrices&lt;/h3&gt;
&lt;p&gt;Because matrix multiplication is &lt;em&gt;associative&lt;/em&gt;, we can define a &lt;strong&gt;Semigroup&lt;/strong&gt; for &lt;code&gt;Mat&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { Semigroup, make } from '@effect/typeclass/Semigroup'

// semigroupMat: given two matrices x and y, how do we combine them?
const semigroupMat: Semigroup&amp;lt;Mat&amp;gt; = make((x, y) =&amp;gt; {
  // matrix multiplication logic
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And from that Semigroup, we get a &lt;strong&gt;Monoid&lt;/strong&gt; by adding the identity matrix (which leaves any vector unchanged):&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { Monoid, fromSemigroup } from '@effect/typeclass/Monoid'

const monoidMat: Monoid&amp;lt;Mat&amp;gt; = fromSemigroup(
  semigroupMat,
  identity // 4×4 identity matrix
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way, we can compose an array of transformations neatly:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;import { monoidMat } from './Mat'

const combined = monoidMat.combineAll([
  translate([5, 0, 0]),
  scale([2, 2, 2]),
  rotateZ(90),
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Applying 3D transformations&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;toCoords&lt;/code&gt; helper function takes a &lt;code&gt;Shape&lt;/code&gt;, extracts its constituent subpaths, applies a transformation matrix, and outputs the final coordinates to be sent to the Canvas API.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const toCoords = (shape: Shape, transform: Mat): ReadonlyArray&amp;lt;ReadonlyArray&amp;lt;Point&amp;gt;&amp;gt; =&amp;gt;
  pipe(
    // 1. Convert the shape into an array of subpaths.
    fromShape(shape),
    // 2. Attempt to convert each subpath into a NonEmpty array.
    map(validateNonEmpty),
    // 3. Filter out any empty subpaths.
    compactArray,
    // 4. Append a 1 to each coordinate for homogeneous transformation.
    map(map(append(1))),
    // 5. Multiply each coordinate by the transformation matrix.
    map(mul(transform))
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Render service&lt;/h2&gt;
&lt;p&gt;Now that everything is in place, let's define the &lt;code&gt;Render&lt;/code&gt; &lt;a href="https://effect.website/docs/requirements-management/services/"&gt;service&lt;/a&gt; as a tagged interface that encapsulates methods mirroring those of the native &lt;code&gt;CanvasRenderingContext2D&lt;/code&gt; (e.g. &lt;code&gt;lineTo&lt;/code&gt;, &lt;code&gt;moveTo&lt;/code&gt;, &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;stroke&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;p&gt;Here's the full definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;class Render extends Tag('Render')&amp;lt;
  Render,
  {
    readonly lineTo: (point: Vec) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly moveTo: (point: Vec) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly fill: (fillRule?: CanvasFillRule) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly clip: (fillRule?: CanvasFillRule) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly stroke: () =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly beginPath: () =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly closePath: () =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly save: () =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly restore: () =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly setFillStyle: (style: string) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly setStrokeStyle: (style: string) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly setLineWidth: (width: number) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly setLineCap: (cap: D.LineCap) =&amp;gt; Micro&amp;lt;void&amp;gt;
    readonly setLineJoin: (join: D.LineJoin) =&amp;gt; Micro&amp;lt;void&amp;gt;
  }
&amp;gt;() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Producing render effect&lt;/h2&gt;
&lt;p&gt;The function &lt;code&gt;renderDrawing&lt;/code&gt; is our core interpreter for the Drawing DSL. It takes a &lt;code&gt;Drawing&lt;/code&gt; and recursively transforms it into a series of canvas commands, while internally maintaining a transformation matrix that accumulates and applies all transformations.&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const renderDrawing = (d: D.Drawing): Micro&amp;lt;void, never, Render&amp;gt; =&amp;gt;
  service(Render).pipe(
    andThen(c =&amp;gt; {
      // Wrap an operation with save/restore for context isolation.
      const withContext = (fa: Micro&amp;lt;void&amp;gt;) =&amp;gt;
        pipe(
          c.save(),
          andThen(() =&amp;gt; fa),
          andThen(c.restore)
        )

      const applyStyle: &amp;lt;A&amp;gt;(o: Option&amp;lt;A&amp;gt;, f: (a: A) =&amp;gt; Micro&amp;lt;void&amp;gt;) =&amp;gt; Micro&amp;lt;void&amp;gt; = (fa, f) =&amp;gt;
        isSome(fa) ? f(fa.value) : success

      // Render a single sub-path using moveTo and lineTo.
      const renderSubPath: (subPath: ReadonlyArray&amp;lt;Point&amp;gt;) =&amp;gt; Micro&amp;lt;void&amp;gt; = matchLeft({
        onEmpty: () =&amp;gt; success,
        onNonEmpty: (head, tail) =&amp;gt;
          pipe(
            c.moveTo(head),
            andThen(() =&amp;gt; forEach(tail, c.lineTo, { discard: true }))
          ),
      })

      // Convert a Shape to a Path3D using fromShape, transform it, then render each sub-path.
      const renderShape = (shape: Shape, transform: Mat) =&amp;gt;
        forEach(toCoords(shape, transform), renderSubPath, { discard: true })

      // The recursive interpreter that handles all variants of Drawing.
      const go: (drawing: D.Drawing, transform: Mat) =&amp;gt; Micro&amp;lt;void&amp;gt; = (d, t) =&amp;gt; {
        switch (d._tag) {
          case 'Many':
            return forEach(d.drawings, d =&amp;gt; go(d, t), { discard: true })
          case 'Scale':
            return go(d.drawing, semigroupMat.combine(t, scale([d.scaleX, d.scaleY, d.scaleZ])))
          case 'Rotate':
            return go(
              d.drawing,
              semigroupMat.combineMany(t, [
                rotateZ(angle(d.rotateZ)),
                rotateY(angle(d.rotateY)),
                rotateX(angle(d.rotateX)),
              ])
            )
          case 'Translate':
            return go(
              d.drawing,
              semigroupMat.combine(t, translate([d.translateX, d.translateY, d.translateZ]))
            )
          case 'Outline':
            return withContext(
              pipe(
                applyStyle(d.style.color, flow(Color.toCss, c.setStrokeStyle)),
                andThen(() =&amp;gt; applyStyle(d.style.lineWidth, c.setLineWidth)),
                andThen(() =&amp;gt; applyStyle(d.style.lineCap, c.setLineCap)),
                andThen(() =&amp;gt; applyStyle(d.style.lineJoin, c.setLineJoin)),
                andThen(c.beginPath),
                andThen(() =&amp;gt; renderShape(d.shape, t)),
                andThen(c.stroke)
              )
            )
          case 'Fill':
            return withContext(
              pipe(
                applyStyle(d.style.color, flow(Color.toCss, c.setFillStyle)),
                andThen(c.beginPath),
                andThen(() =&amp;gt; renderShape(d.shape, t)),
                andThen(() =&amp;gt; c.fill())
              )
            )
          case 'Clipped':
            return withContext(
              pipe(
                c.beginPath(),
                andThen(() =&amp;gt; renderShape(d.shape, t)),
                andThen(() =&amp;gt; c.clip()),
                andThen(() =&amp;gt; go(d.drawing, t))
              )
            )
        }
      }
      return go(d, identity)
    })
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Transformation composition&lt;/h3&gt;
&lt;p&gt;The recursive function &lt;code&gt;go&lt;/code&gt; walks through a &lt;code&gt;Drawing&lt;/code&gt;, updating the transform matrix (with operations like &lt;code&gt;Scale&lt;/code&gt;, &lt;code&gt;Rotate&lt;/code&gt;, and &lt;code&gt;Translate&lt;/code&gt;) as it recurses through nested drawings.&lt;/p&gt;
&lt;h3&gt;Style and context management&lt;/h3&gt;
&lt;p&gt;For instructions like &lt;code&gt;Outline&lt;/code&gt; and &lt;code&gt;Fill&lt;/code&gt;, the function saves the canvas state, applies style settings, begins a new path, renders the shape, executes the appropriate drawing command (stroke or fill), and finally restores the canvas state.&lt;/p&gt;
&lt;h3&gt;Sub-path rendering&lt;/h3&gt;
&lt;p&gt;The helper &lt;code&gt;renderSubPath&lt;/code&gt; converts a sub-path (a list of 3D points) into the corresponding canvas calls (&lt;code&gt;moveTo&lt;/code&gt; for the first point, followed by &lt;code&gt;lineTo&lt;/code&gt; for subsequent points).&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Wiring up the real canvas&lt;/h2&gt;
&lt;p&gt;The final step is the &lt;code&gt;render&lt;/code&gt; function, which "plugs in" a real &lt;code&gt;CanvasRenderingContext2D&lt;/code&gt; implementation by providing a concrete instance of the &lt;strong&gt;Render&lt;/strong&gt; service. This function builds the effect (from &lt;code&gt;renderDrawing&lt;/code&gt;) and then supplies a real implementation that calls the actual Canvas API:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const render = (d: D.Drawing, ctx: CanvasRenderingContext2D): Micro&amp;lt;void, never, never&amp;gt; =&amp;gt;
  provideService(renderDrawing(d), Render, {
    fill(fillRule) {
      ctx.fill(fillRule)
      return succeed(undefined)
    },
    clip(fillRule) {
      ctx.clip(fillRule)
      return succeed(undefined)
    },
    setFillStyle(style: string) {
      ctx.fillStyle = style
      return succeed(undefined)
    },
    setStrokeStyle(style: string) {
      ctx.strokeStyle = style
      return succeed(undefined)
    },
    setLineWidth(width: number) {
      ctx.lineWidth = width
      return succeed(undefined)
    },
    setLineJoin(join: D.LineJoin) {
      ctx.lineJoin = join
      return succeed(undefined)
    },
    setLineCap(cap: D.LineCap) {
      ctx.lineCap = cap
      return succeed(undefined)
    },
    stroke() {
      ctx.stroke()
      return succeed(undefined)
    },
    save() {
      ctx.save()
      return succeed(undefined)
    },
    restore() {
      ctx.restore()
      return succeed(undefined)
    },
    lineTo(p) {
      ctx.lineTo(p[0], p[1])
      return succeed(undefined)
    },
    moveTo(p) {
      ctx.moveTo(p[0], p[1])
      return succeed(undefined)
    },
    beginPath() {
      ctx.beginPath()
      return succeed(undefined)
    },
    closePath() {
      ctx.closePath()
      return succeed(undefined)
    },
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;Finally, the &lt;code&gt;renderTo&lt;/code&gt; function ties everything together. It retrieves the canvas element, adjusts its size for the device pixel ratio, gets the 2D context, and runs the rendering effect:&lt;/p&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const renderTo = (f: (size: Size) =&amp;gt; Drawing, canvasId: string): void =&amp;gt;
  pipe(
    // 1. Obtain a canvas.
    getCanvasElementById(canvasId),
    andThen(canvas =&amp;gt; {
      // 2. Scale the canvas according to the device pixel ratio.
      const rect = canvas.getBoundingClientRect()
      return dpr.pipe(
        andThen(dpr =&amp;gt; {
          canvas.width = rect.width * dpr
          canvas.height = rect.height * dpr
          canvas.style.width = `${rect.width}px`
          canvas.style.height = `${rect.height}px`
          return pipe(
            // 3. Get the 2D context.
            getContext2D(canvas),
            andThen(ctx =&amp;gt; {
              ctx.scale(dpr, dpr)
              // 4. Compute the Drawing by calling f(size) and render it on the context.
              return render(f({ height: canvas.height / dpr, width: canvas.width / dpr }), ctx)
            })
          )
        })
      )
    })
  ).pipe(runSync) // 5. Perform the side-effect on the canvas.
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Rendering P-161&lt;/h5&gt;
&lt;pre&gt;&lt;code class="ts language-ts"&gt;const renderP161 = (canvasId: string, bgColor: string) =&amp;gt;
  renderTo(size =&amp;gt; drawP161(size, hex(bgColor)), canvasId)
&lt;/code&gt;&lt;/pre&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">Recreating the iconic artwork of Manfred Mohr the hard way, by rolling my own 3D graphics DSL in TypeScript with Effect.</media:description><media:content xmlns:media="http://search.yahoo.com/mrss/" fileSize="187977" height="800" medium="image" type="image/jpeg" url="https://tetsuo.github.io/images/zkm-01-0134-02-0427.jpg" width="1015"/></item><item><title>Triggers and notifications in PostgreSQL</title><link>https://tetsuo.github.io/triggers-and-notifications-in-postgresql.html</link><category>sql</category><category>c</category><category>rust</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Sun, 17 Nov 2024 00:00:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2024-11-17:/triggers-and-notifications-in-postgresql</guid><description>&lt;blockquote&gt;
&lt;p&gt;Building a pub/sub application with libpq to batch-process account onboarding jobs on a self-managed PostgreSQL-backed job queue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/tetsuo/mailroom/"&gt;&lt;strong&gt;mailroom&lt;/strong&gt;&lt;/a&gt; uses PostgreSQL &lt;a href="https://www.postgresql.org/docs/current/sql-createtrigger.html"&gt;triggers&lt;/a&gt; and &lt;a href="https://www.postgresql.org/docs/current/sql-notify.html"&gt;notification events&lt;/a&gt; to detect account status changes and send relevant email notifications.&lt;/p&gt;
&lt;p&gt;In this post, we'll explore the architecture in detail, starting with the schema and triggers for tracking account updates in a PostgreSQL-backed queue, then building a collector in C with &lt;code&gt;libpq&lt;/code&gt; to consume those events and batch-process action tokens.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;The system comprises these actors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User&lt;/strong&gt;: Responsible for creating and activating accounts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin&lt;/strong&gt;: Can suspend accounts (not the main focus though).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Components include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accounts&lt;/strong&gt;: A table for storing users and their lifecycle states.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tokens&lt;/strong&gt;: A table for managing activation and recovery tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Triggers&lt;/strong&gt;: Automate processes like status updates, notifications, and timestamp modifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the sequence diagram outlining the workflows:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Workflows" src="https://tetsuo.github.io/images/mercury-postgresql-workflows.svg"/&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Accounts table&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;accounts&lt;/code&gt; table manages user data and tracks account lifecycle states.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;CREATE TYPE account_status AS ENUM (
    'provisioned',
    'active',
    'suspended'
);

CREATE TABLE accounts (
    id                  BIGSERIAL PRIMARY KEY,
    email               VARCHAR(254) UNIQUE NOT NULL,
    status              account_status DEFAULT 'provisioned' NOT NULL,
    login               VARCHAR(254) UNIQUE NOT NULL,
    created_at          INTEGER DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
    status_changed_at   INTEGER,
    activated_at        INTEGER,
    suspended_at        INTEGER,
    unsuspended_at      INTEGER
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;status&lt;/code&gt; field tracks the current state of the account (&lt;code&gt;provisioned&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, or &lt;code&gt;suspended&lt;/code&gt;), while timestamps like &lt;code&gt;status_changed_at&lt;/code&gt; and &lt;code&gt;activated_at&lt;/code&gt; capture important lifecycle events, helping to maintain the &lt;code&gt;status&lt;/code&gt; field correctly during transitions and ensuring accurate tracking of account states over time.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Tokens table&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;tokens&lt;/code&gt; table tracks actionable tokens, such as those used for activation or password recovery.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;CREATE TYPE token_action AS ENUM (
    'activation',
    'password_recovery'
);

CREATE TABLE tokens (
    id          BIGSERIAL PRIMARY KEY,
    action      token_action NOT NULL,
    secret      BYTEA DEFAULT gen_random_bytes(32) UNIQUE NOT NULL,
    code        VARCHAR(5) DEFAULT LPAD(TO_CHAR(RANDOM() * 100000, 'FM00000'), 5, '0'),
    account     BIGINT NOT NULL,
    expires_at  INTEGER DEFAULT EXTRACT(EPOCH FROM NOW() + INTERVAL '15 minute') NOT NULL,
    consumed_at INTEGER,
    created_at  INTEGER DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,

    FOREIGN KEY (account) REFERENCES accounts (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Key columns:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;action&lt;/code&gt; – Specifies the token type (&lt;code&gt;activation&lt;/code&gt; or &lt;code&gt;password recovery&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret&lt;/code&gt; – A unique and secure token string.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt; – A short, human-readable security code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expires_at&lt;/code&gt; – Defines the expiration time for tokens, defaulting to 15 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This table complements the &lt;code&gt;accounts&lt;/code&gt; table by managing token-based actions, with relationships maintained through the foreign key &lt;code&gt;account&lt;/code&gt;.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Triggers&lt;/h2&gt;
&lt;p&gt;PostgreSQL triggers allow us to automate processes in response to data changes. Below are the triggers to ensure seamless management of account status transitions, token consumption, and notifications.&lt;/p&gt;
&lt;h3&gt;1. &lt;strong&gt;Before account insert&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event&lt;/strong&gt;: Before an account is inserted into the &lt;code&gt;accounts&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: Automatically creates an activation token when a new account is provisioned.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="plpgsql language-plpgsql"&gt;CREATE OR REPLACE FUNCTION trg_before_account_insert()
RETURNS TRIGGER AS $$
BEGIN
    IF (NEW.status = 'provisioned') THEN
        INSERT INTO
        tokens
            (account, action)
        VALUES
            (NEW.id, 'activation');
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER before_account_insert
    BEFORE INSERT ON accounts
    FOR EACH ROW
    EXECUTE FUNCTION trg_before_account_insert ();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h5&gt;Why not an AFTER trigger?&lt;/h5&gt;
&lt;p&gt;While it may seem logical to create the token &lt;em&gt;after&lt;/em&gt; confirming the account's existence (since the token is ultimately tied to the account), this approach has a critical flaw: if the token insertion fails, we could end up with an account that lacks a corresponding activation token, which would break downstream processes.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;BEFORE&lt;/code&gt; trigger ensures that token creation and account insertion are part of the same transaction, guaranteeing the consistency we need. If token creation fails, the entire transaction will be rolled back, preventing the system from entering an invalid state.&lt;/p&gt;
&lt;p&gt;This is why the &lt;code&gt;DEFERRABLE INITIALLY DEFERRED&lt;/code&gt; constraint is applied to the &lt;code&gt;tokens&lt;/code&gt; table. It allows a token to be inserted even before the associated account is created, provided both operations occur within the same transaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. &lt;strong&gt;Before account status change&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event&lt;/strong&gt;: Before an account's &lt;code&gt;status&lt;/code&gt; is updated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: Updates timestamps for key status changes (e.g., activated, suspended, unsuspended).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="plpgsql language-plpgsql"&gt;CREATE OR REPLACE FUNCTION trg_before_account_status_change ()
    RETURNS TRIGGER
    AS $$
DECLARE
    ts integer := extract(epoch FROM now());
BEGIN
    IF (NEW.status = OLD.status) THEN
        RETURN NEW;
    END IF;

    NEW.status_changed_at = ts;

    IF (NEW.status = 'active') THEN
        IF (OLD.status = 'provisioned') THEN
            NEW.activated_at = ts;
        ELSIF (OLD.status = 'suspended') THEN
            NEW.unsuspended_at = ts;
            NEW.suspended_at = NULL;
            -- Revert status to 'provisioned' if never activated
            IF (OLD.activated_at IS NULL) THEN
              NEW.status = 'provisioned';
            END IF;
        END IF;
    ELSIF (NEW.status = 'suspended') THEN
        NEW.suspended_at = ts;
        NEW.unsuspended_at = NULL;
    END IF;
    RETURN new;
END;
$$
LANGUAGE plpgsql;

CREATE TRIGGER before_account_status_change
    BEFORE UPDATE OF status ON accounts
    FOR EACH ROW
    EXECUTE FUNCTION trg_before_account_status_change ();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. &lt;strong&gt;After token consumed&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event&lt;/strong&gt;: After a token's &lt;code&gt;consumed_at&lt;/code&gt; field in &lt;code&gt;tokens&lt;/code&gt; is updated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: Activates the associated account when an activation token is consumed.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="plpgsql language-plpgsql"&gt;CREATE OR REPLACE FUNCTION trg_after_token_consumed ()
    RETURNS TRIGGER
    AS $$
BEGIN
    IF (NEW.action != 'activation') THEN
        RETURN NULL;
    END IF;
    -- Activate account
    UPDATE
        accounts
    SET
        status = 'active'
    WHERE
        id = NEW.account
        AND status = 'provisioned';
    RETURN NULL;
END;
$$
LANGUAGE plpgsql;

CREATE TRIGGER after_token_consumed
    AFTER UPDATE OF consumed_at ON tokens
    FOR EACH ROW
    WHEN (NEW.consumed_at IS NOT NULL AND OLD.consumed_at IS NULL)
    EXECUTE FUNCTION trg_after_token_consumed ();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. &lt;strong&gt;After token inserted&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event&lt;/strong&gt;: After a token is inserted into the &lt;code&gt;tokens&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purpose&lt;/strong&gt;: Notifies external services that a new token has been created.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="plpgsql language-plpgsql"&gt;CREATE OR REPLACE FUNCTION trg_after_token_inserted()
    RETURNS TRIGGER
    LANGUAGE plpgsql
AS $$
BEGIN
    NOTIFY token_insert;
    RETURN NULL;
END;
$$;

CREATE TRIGGER after_token_inserted
    AFTER INSERT ON tokens
    FOR EACH ROW
    EXECUTE FUNCTION trg_after_token_inserted ();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Let's try it out!&lt;/h2&gt;
&lt;p&gt;Follow these steps to test the triggers and notifications in action:&lt;/p&gt;
&lt;h3&gt;Setting your environment&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;(Skip this section if you've already set up the tables and triggers.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Clone the &lt;a href="https://github.com/tetsuo/mailroom"&gt;&lt;code&gt;tetsuo/mailroom&lt;/code&gt;&lt;/a&gt; repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;git clone https://github.com/tetsuo/mailroom.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the following command to create a new database in PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;createdb mailroom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, navigate to the &lt;code&gt;migrations&lt;/code&gt; folder and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;psql -d mailroom &amp;lt; 0_init.up.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, you can use &lt;a href="https://github.com/golang-migrate/"&gt;go-migrate&lt;/a&gt; which is often my preference.&lt;/p&gt;
&lt;hr/&gt;
&lt;h3&gt;Inspect the initial state&lt;/h3&gt;
&lt;p&gt;Before adding any data, let's take a look at the initial state of the &lt;code&gt;jobs&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;psql -d mailroom -c "SELECT * FROM jobs;"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see one row with &lt;code&gt;job_type&lt;/code&gt; set to &lt;code&gt;mailroom&lt;/code&gt; and &lt;code&gt;last_seq&lt;/code&gt; set to zero:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; job_type | last_seq
----------+----------
 mailroom |        0
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Create a new account&lt;/h3&gt;
&lt;p&gt;Insert a new account into the &lt;code&gt;accounts&lt;/code&gt; table. This should automatically generate an activation token.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;INSERT INTO accounts (email, login)
    VALUES ('user@example.com', 'user123');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; To insert three records with randomized email and login fields, use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;printf "%.0sINSERT INTO accounts (email, login) VALUES ('user' || md5(random()::text) || '@fake.mail', 'user' || substr(md5(random()::text), 1, 20));\n" {1..3} | \
    psql -d mailroom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new account with &lt;code&gt;status = 'provisioned'&lt;/code&gt; is added to &lt;code&gt;accounts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An activation token is automatically inserted into the &lt;code&gt;tokens&lt;/code&gt; table, linked to the account.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT * FROM accounts WHERE id = 1;
SELECT * FROM tokens WHERE account = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's an example &lt;code&gt;account&lt;/code&gt; record:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-[ ACCOUNT 1 ]-------------------------------------------------------------------
id                | 1
email             | usere3213152e8cdf722466a011b1eaa3c98@fake.mail
status            | provisioned
login             | user85341405cb33cbe89a5f
created_at        | 1735709763
status_changed_at |
activated_at      |
suspended_at      |
unsuspended_at    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The corresponding &lt;code&gt;token&lt;/code&gt; record generated by the trigger function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-[ TOKEN 1 ]---------------------------------------------------------------------
id          | 1
action      | activation
secret      | \x144d3ba23d4e60f80d3cb5cf25783539ba267af34aecd71d7cc888643c912fb7
code        | 06435
account     | 1
expires_at  | 1735710663
consumed_at |
created_at  | 1735709763
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Consume the activation token&lt;/h3&gt;
&lt;p&gt;Simulate token consumption by updating the &lt;code&gt;consumed_at&lt;/code&gt; field in the &lt;code&gt;tokens&lt;/code&gt; table.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;UPDATE
    tokens
SET
    consumed_at = extract(epoch FROM now())
WHERE
    account = 1
    AND action = 'activation';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The account's &lt;code&gt;status&lt;/code&gt; in &lt;code&gt;accounts&lt;/code&gt; should change to &lt;code&gt;active&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;activated_at&lt;/code&gt; timestamp should be updated in &lt;code&gt;accounts&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT * FROM accounts WHERE id = 1;
SELECT * FROM tokens WHERE account = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Suspend the account&lt;/h3&gt;
&lt;p&gt;Change the account's status to &lt;code&gt;suspended&lt;/code&gt; to test the suspension flow.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;UPDATE accounts SET status = 'suspended' WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The account's &lt;code&gt;suspended_at&lt;/code&gt; timestamp is updated.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;unsuspended_at&lt;/code&gt; field is cleared.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT * FROM accounts WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Unsuspend the account&lt;/h3&gt;
&lt;p&gt;Restore the account's status to &lt;code&gt;active&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;UPDATE accounts SET status = 'active' WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The account's &lt;code&gt;unsuspended_at&lt;/code&gt; timestamp is updated.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;suspended_at&lt;/code&gt; field is cleared.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT * FROM accounts WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Observe notifications&lt;/h3&gt;
&lt;p&gt;Listen for token creation notifications on the &lt;code&gt;token_insert&lt;/code&gt; channel using &lt;code&gt;LISTEN&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;LISTEN token_insert;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, insert some dummy data into the &lt;code&gt;accounts&lt;/code&gt; table (or directly into &lt;code&gt;tokens&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Expected outcome&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;LISTEN&lt;/code&gt; session should immediately display a notification like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Asynchronous notification "token_insert" with payload "" received.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; might need a little nudge (empty &lt;code&gt;;&lt;/code&gt;) to display notifications:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mailroom=# LISTEN token_insert;
LISTEN
mailroom=# ;
Asynchronous notification "token_insert" received from server process with PID 5148.
Asynchronous notification "token_insert" received from server process with PID 5148.
Asynchronous notification "token_insert" received from server process with PID 5148.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;These notifications signal that new tokens have arrived—it's time to start processing them.&lt;/em&gt;&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Jobs table&lt;/h2&gt;
&lt;p&gt;Now we need to build a mechanism to collect newly added tokens. To do that, we'll define a query that manages their progression through the queue.&lt;/p&gt;
&lt;p&gt;We use the jobs table to maintain a cursor that advances through tokens. This table simply tracks the last processed token (&lt;code&gt;last_seq&lt;/code&gt;) for each job type:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;CREATE TYPE job_type AS ENUM (
    'mailroom'
);

CREATE TABLE jobs (
    job_type job_type PRIMARY KEY,
    last_seq BIGINT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Initialize the mailroom queue:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;INSERT INTO
jobs
    (last_seq, job_type)
VALUES
    (0, 'mailroom');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Retrieving pending jobs&lt;/h3&gt;
&lt;p&gt;The following query retrieves relevant job data (tokens and account details), ensuring only valid, unexpired, and unprocessed tokens are selected, with accounts in the correct status for the intended action.&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT
    t.account,
    t.secret,
    t.code,
    t.expires_at,
    t.id,
    t.action,
    a.email,
    a.login
FROM
    jobs
    JOIN tokens t
        ON t.id &amp;gt; jobs.last_seq
        AND t.expires_at &amp;gt; EXTRACT(EPOCH FROM NOW())
        AND t.consumed_at IS NULL
        AND t.action IN ('activation', 'password_recovery')
    JOIN accounts a
    ON a.id = t.account
    AND (
      (t.action = 'activation' AND a.status = 'provisioned')
      OR (t.action = 'password_recovery' AND a.status = 'active')
    )
WHERE
    jobs.job_type = 'mailroom'
ORDER BY
    id ASC
LIMIT 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Joins &amp;amp; filters explained:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jobs table:&lt;/strong&gt; We filter for rows where &lt;code&gt;job_type&lt;/code&gt; is &lt;code&gt;mailroom&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tokens table:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;We join tokens with jobs using the condition &lt;code&gt;tokens.id &amp;gt; jobs.last_seq&lt;/code&gt;, which ensures we only process tokens that haven't been handled yet.&lt;/li&gt;
&lt;li&gt;We further filter tokens to include only those that are not expired (&lt;code&gt;expires_at&lt;/code&gt; is in the future), have not been consumed (&lt;code&gt;consumed_at&lt;/code&gt; is NULL), and have an action of either &lt;code&gt;activation&lt;/code&gt; or &lt;code&gt;password_recovery&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accounts table:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;We join accounts on &lt;code&gt;accounts.id = tokens.account&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For tokens with the &lt;code&gt;activation&lt;/code&gt; action, the account must be in the &lt;code&gt;provisioned&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;For tokens with the &lt;code&gt;password_recovery&lt;/code&gt; action, the account must be &lt;code&gt;active&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dequeueing and advancing the cursor&lt;/h3&gt;
&lt;p&gt;Next, we integrate this query into a common table expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;WITH token_data AS (
    -- Insert SELECT query here
),
updated_jobs AS (
  UPDATE
    jobs
  SET
    last_seq = (SELECT MAX(id) FROM token_data)
  WHERE
    EXISTS (SELECT 1 FROM token_data)
  RETURNING last_seq
)
SELECT
  td.action,
  td.email,
  td.login,
  td.secret,
  td.code
FROM
  token_data td
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This accomplishes two key tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Retrieves tokens&lt;/strong&gt; generated after the current &lt;code&gt;last_seq&lt;/code&gt; along with the corresponding user data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Updates the &lt;code&gt;last_seq&lt;/code&gt; value&lt;/strong&gt; to prevent processing the same tokens again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Output example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-[ RECORD 1 ]--------------------------------------------------------------
action | activation
email  | usere3213152e8cdf722466a011b1eaa3c98@fake.mail
login  | user85341405cb33cbe89a5f
secret | \x144d3ba23d4e60f80d3cb5cf25783539ba267af34aecd71d7cc888643c912fb7
code   | 06435
-[ RECORD 2 ]--------------------------------------------------------------
action | activation
email  | user41e8b6830c76870594161150051f8215@fake.mail
login  | user2491d87beb8950b4abd7
secret | \x27100e07220b62e849e788e6554fede60c96e967c4aa62db7dc45150c51be23f
code   | 80252
-[ RECORD 3 ]--------------------------------------------------------------
action | activation
email  | user7bb11e235c85afe12076884d06910be4@fake.mail
login  | user91ab8536cb05c37ff46a
secret | \xa9763eec727835bd97b79018b308613268d9ea0db70493fd212771c9b7c3bcb2
code   | 31620
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Indexes&lt;/h4&gt;
&lt;p&gt;To optimize the query performance, the following composite indexes are recommended:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;CREATE INDEX accounts_id_status_idx ON accounts (id, status);

CREATE INDEX tokens_id_expires_consumed_action_idx ON tokens
    (id, expires_at, consumed_at, action);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indexing Strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Equality Conditions First&lt;/strong&gt;: Since columns used in equality conditions (&lt;code&gt;=&lt;/code&gt; or &lt;code&gt;IN&lt;/code&gt;) are typically the most selective, they should come first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Range Conditions Next&lt;/strong&gt;: Columns used in range conditions (&lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;) should follow.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2&gt;Collector behavior&lt;/h2&gt;
&lt;p&gt;Instead of continuously polling the database for new batches, a lightweight service is set up to subscribe to a notification channel, monitor incoming events, and trigger the previously defined job retrieval query when either a certain &lt;strong&gt;row limit&lt;/strong&gt; is reached (based on received notifications) or a &lt;strong&gt;timeout&lt;/strong&gt; occurs.&lt;/p&gt;
&lt;p&gt;Here's how the job retrieval and batch execution are controlled:&lt;/p&gt;
&lt;h4&gt;Batch limit&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The maximum number of email destinations in a single batch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A collector queries the database for at most &lt;strong&gt;N&lt;/strong&gt; tokens at a time (where &lt;strong&gt;N&lt;/strong&gt; is the &lt;strong&gt;batch limit&lt;/strong&gt;). Even if 500 tokens are waiting in the database, the collector will only take, say, 10 at a time. This imposes a hard cap on the throughput of tokens that can leave the database at once.&lt;/p&gt;
&lt;h4&gt;Batch timeout&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The time to wait for accumulating enough notifications to fill a batch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A collector waits up to &lt;strong&gt;X&lt;/strong&gt; milliseconds before processing incoming notifications (where &lt;strong&gt;X&lt;/strong&gt; is the &lt;strong&gt;batch timeout&lt;/strong&gt;). If fewer than the batch limit have arrived during that period, the collector will still dequeue whatever did arrive, but it won't pull more immediately. In effect, this sets an upper limit on how long new tokens can linger before being handed over to the email sender.&lt;/p&gt;
&lt;h5&gt;Example&lt;/h5&gt;
&lt;p&gt;If you set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A batch &lt;strong&gt;timeout&lt;/strong&gt; of 30 seconds.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;limit&lt;/strong&gt; of 10 notifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If 10 notifications arrive in quick succession, the batch is triggered immediately.&lt;/li&gt;
&lt;li&gt;If fewer than 10 arrive over 30 seconds, the batch is triggered when the timeout ends.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Keep in mind that a collector doesn't impose rate limiting;&lt;/strong&gt; it primarily controls database roundtrips and batch size. A large influx of notifications will keep triggering the batch limit, effectively bypassing the timeout, so the overall token throughput downstream remains largely unaffected.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Implementation details&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;collector&lt;/code&gt; is written in C and interacts with PostgreSQL via &lt;a href="https://www.postgresql.org/docs/current/libpq.html"&gt;&lt;strong&gt;libpq&lt;/strong&gt;&lt;/a&gt;. It is responsible solely for collecting jobs and writing them to output in a structured format.&lt;/p&gt;
&lt;h3&gt;Dequeuing jobs&lt;/h3&gt;
&lt;p&gt;When the connection is established, &lt;code&gt;collector&lt;/code&gt; issues a &lt;code&gt;LISTEN&lt;/code&gt; command on the specified channel and creates the prepared statements for subsequent queries.&lt;/p&gt;
&lt;p&gt;As notifications arrive, it fetches tokens &lt;strong&gt;in batches&lt;/strong&gt; and writes the results directly to stdout. Processing continues until all queued tokens are exhausted or an error occurs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128196; &lt;strong&gt;See the full implementation in &lt;a href="https://github.com/tetsuo/mailroom/blob/master/collector/src/db.c#L234C1-L252C2"&gt;&lt;code&gt;db.c&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Batch output structure&lt;/h3&gt;
&lt;p&gt;The results are output as &lt;strong&gt;line-delimited batches&lt;/strong&gt;, formatted as &lt;strong&gt;comma-separated values&lt;/strong&gt; in the following order:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;action,email,username,secret,code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each batch is represented as a single line, where every row follows this schema:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;action&lt;/code&gt; – Numeric representation of the email action type (e.g., &lt;code&gt;1&lt;/code&gt; for activation, &lt;code&gt;2&lt;/code&gt; for password recovery).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt; – Recipient's email address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;username&lt;/code&gt; – Recipient's login name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret&lt;/code&gt; – A base64 URL-encoded string containing the signed token.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt; – (Optional) Numeric code (e.g., for password recovery).&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Example output&lt;/h5&gt;
&lt;p&gt;In this example, the first line contains a batch of three jobs, including both password recovery and account activation. The second line contains a single activation job:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2,john.doe123@fakemail.test,johndoe,0WEKrnjY_sTEqogrR6qsp7r7Vg4SQ_0iM_1La5hHp5p31nbkrHUBS0Cz9T24iBDCk6CFqO7tJTihpsOVuHYgLg,35866,1,jane.smith456@notreal.example,janesmith,BfQXx31qfY2IJFTtzAp21IdeW0dDIxUT1Ejf3tYJDukNsfaxxOfldwL-lEfVy4SEkZ_v18rf-EWsvWXH5qgvIg,24735,1,emma.jones789@madeup.mail,emmajones,jxrR5p72UWTQ8JiU2DrqjZ-K8L4t8i454S9NtPkVn4-1-bin3ediP0zHMDQU2J_iIyzH4XmNtzpXZhjV0n5xcA,25416
1,sarah.connor999@unreal.mail,resistance1234,zwhCIthd12DqpQSGB57S9Ky-OXV_8H0e8aHOv_kWoggIuAZ2sc-aQVpIoQ-M--PjwVfdIIxiXkv_WjRjGI57zA,38022
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Signing and validating tokens&lt;/h3&gt;
&lt;p&gt;During the dequeue operation, the token's secret is signed using HMAC-SHA256 and encoded in URL-safe Base64 format. This enables the frontend to verify the authenticity of a token without performing an immediate database lookup.&lt;/p&gt;
&lt;p&gt;The encoded secret consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;path name&lt;/strong&gt; (e.g., &lt;code&gt;/activate&lt;/code&gt; or &lt;code&gt;/recover&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;original secret&lt;/strong&gt; (and &lt;strong&gt;code&lt;/strong&gt;, in the case of recovery).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cryptographic signature&lt;/strong&gt; generated from the secret.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="c language-c"&gt;static size_t construct_signature_data(char *output, const char *action,
                                       const unsigned char *secret, const char *code)
{
  size_t offset = 0;

  if (strcmp(action, "activation") == 0)
  {
    memcpy(output, "/activate", 9); // "/activate" is 9 bytes
    offset = 9;
    memcpy(output + offset, secret, 32);
    offset += 32;
  }
  else if (strcmp(action, "password_recovery") == 0)
  {
    memcpy(output, "/recover", 8); // "/recover" is 8 bytes
    offset = 8;
    memcpy(output + offset, secret, 32);
    offset += 32;
    memcpy(output + offset, code, 5); // code is 5 bytes
    offset += 5;
  }

  return offset; // Total length of the constructed data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ℹ️ &lt;em&gt;If you'd like to see how verification works on the backend, check out &lt;a href="https://github.com/tetsuo/mailroom/blob/master/etc/verifyHmac.js"&gt;&lt;code&gt;verifyHmac.js&lt;/code&gt;&lt;/a&gt; in the repo.&lt;/em&gt;&lt;/p&gt;
&lt;h5&gt;Security considerations&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Handle expired tokens properly.&lt;/strong&gt; One approach is to include &lt;code&gt;expires_at&lt;/code&gt; in the payload so expiration can be checked without a DB call. For stronger protection, cache consumed tokens until they naturally expire to prevent reuse within their validity window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regularly rotate your signing key.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Main loop logic&lt;/h3&gt;
&lt;!-- In [`main.c`](https://github.com/tetsuo/mailroom/blob/master/collector/src/main.c), you'll find references to environment variables such as `MAILROOM_BATCH_TIMEOUT`, `MAILROOM_BATCH_LIMIT`, and `MAILROOM_SECRET_KEY` (a 32-byte random value, represented as a 64-character hex string). Refer to the [`README`](https://github.com/tetsuo/mailroom/blob/master/README.md#environment-variables) file for the full list. --&gt;
&lt;p&gt;At a high level, the main loop continuously:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&#128260; &lt;strong&gt;Dequeues and processes&lt;/strong&gt; ready batches&lt;/li&gt;
&lt;li&gt;&#128233; &lt;strong&gt;Checks for new notifications&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Waits on&lt;/strong&gt; &lt;code&gt;select()&lt;/code&gt; &lt;strong&gt;for database activity or a timeout&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&#129658; &lt;strong&gt;Performs periodic health checks&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&#128268; &lt;strong&gt;Reconnects&lt;/strong&gt; to the database if needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the &lt;strong&gt;batch limit&lt;/strong&gt; is reached or the &lt;strong&gt;timeout expires&lt;/strong&gt;, the collector executes the &lt;strong&gt;dequeue query&lt;/strong&gt;. If a broken connection is detected, it attempts to &lt;strong&gt;reconnect and resume processing&lt;/strong&gt; once stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pseudo-code representation:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// &#127775; Main processing loop
WHILE the application is running &#128260;
    // &#128268; Handle reconnection if needed
    IF the connection is not ready ❌ THEN
        reconnect to the database &#128260;
        initialize the connection ✅
        reset counters &#128290;
        CONTINUE to the next iteration ⏩
    END IF

    // &#128230; Process ready batches
    IF ready for processing ✅ THEN
        dequeue and process a batch of items &#128228;
        reset state for the next cycle &#128257;
        CONTINUE to the next iteration ⏩
    END IF

    // &#128718;️ Handle pending notifications
    process all incoming notifications &#128229;
    IF notifications exceed the batch limit &#128680; THEN
        mark ready for processing ✅
        CONTINUE to the next iteration ⏩
    END IF

    // ⏱️ Wait for new events or timeout
    wait for activity on the connection &#128225; or timeout ⌛
    IF interrupted by a signal &#128680; THEN
        handle the signal (e.g., shutdown) ❌
        CONTINUE to the next iteration ⏩
    ELSE IF timeout occurs ⏳ THEN
        IF notifications exist &#128203; THEN
            mark ready for processing ✅
            CONTINUE to the next iteration ⏩
        END IF
        perform periodic health checks &#129658;
    END IF

    // &#128736;️ Consume available data
    consume data from the connection &#128246;
    prepare for the next cycle &#128257;
END WHILE
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128196; &lt;strong&gt;See the full implementation in &lt;a href="https://github.com/tetsuo/mailroom/blob/master/collector/src/main.c#L203"&gt;&lt;code&gt;main.c&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href="https://man7.org/linux/man-pages/man2/select.2.html"&gt;&lt;code&gt;select()&lt;/code&gt;&lt;/a&gt; system call plays a key role in the program's operation. It is a UNIX mechanism that monitors file descriptors (e.g., sockets) to check if they are ready for I/O operations like reading or writing. In this code, &lt;code&gt;select()&lt;/code&gt; is used to monitor the socket for new notifications and &lt;strong&gt;enforce a timeout&lt;/strong&gt; for batch processing.&lt;/p&gt;
&lt;p&gt;Once &lt;code&gt;select()&lt;/code&gt; signals that data is available, &lt;code&gt;PQconsumeInput&lt;/code&gt; is called to read incoming data into libpq's internal buffers. Then &lt;code&gt;PQnotifies&lt;/code&gt; is invoked to retrieve any pending notifications and update the counter.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128214; &lt;strong&gt;Learn more about &lt;a href="https://www.postgresql.org/docs/current/libpq-async.html"&gt;libpq's async API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr/&gt;
&lt;h2&gt;Compile &amp;amp; run&lt;/h2&gt;
&lt;p&gt;To compile, verify that you have &lt;code&gt;openssl@3&lt;/code&gt; and &lt;code&gt;libpq@5&lt;/code&gt; installed, then use the provided &lt;code&gt;Makefile&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Run the collector&lt;/h3&gt;
&lt;p&gt;Use the following command to build and run the &lt;code&gt;collector&lt;/code&gt; executable with example configuration variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;make &amp;amp;&amp;amp; \
  MAILROOM_BATCH_LIMIT=3 \
  MAILROOM_BATCH_TIMEOUT=5000 \
  MAILROOM_DATABASE_URL="dbname=mailroom" \
  MAILROOM_SECRET_KEY="cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe" \
  ./collector
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Refer to the &lt;a href="https://github.com/tetsuo/mailroom/blob/master/README.md#environment-variables"&gt;&lt;code&gt;README&lt;/code&gt;&lt;/a&gt; file for the full list of environment variables&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once configured and started, it will log its activity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024/04/20 13:37:00 [PG] configured; channel=token_insert queue=mailroom limit=3 timeout=5000ms healthcheck-interval=270000ms
2024/04/20 13:37:00 [PG] connecting to host=/tmp port=5432 dbname=mailroom user=ogu sslmode=disable
2024/04/20 13:37:00 [PG] connected
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Insert accounts and observe batching&lt;/h3&gt;
&lt;p&gt;In another terminal, insert 5 accounts:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sh language-sh"&gt;printf "%.0sINSERT INTO accounts (email, login) VALUES ('user' || md5(random()::text) || '@fake.mail', 'user' || substr(md5(random()::text), 1, 20));\n" {1..5} | \
    psql -d mailroom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You'll observe that &lt;code&gt;collector&lt;/code&gt; immediately process the first batch of three items. After a 5-second delay (as defined by the &lt;code&gt;MAILROOM_BATCH_TIMEOUT&lt;/code&gt;), it processes the remaining two in a second batch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024/04/20 13:37:00 [PG] NOTIFY called; waking up
2024/04/20 13:37:00 [PG] processing 3 rows... (max reached)
1,userb183abb7a25d04027061e6b8d8d8e7fa@fake.mail,userb0bf075b82b892f53d97,gVRNesi-opSvs3ntPfr9DzSn_JwbOD04VVIurQSCOFzzd3BOM3WBDL3SOtDjMxKLd6csSn8_p9hemXHIUxIjPg,78092,1,user43b01ba9686c886473e526429dd2c672@fake.mail,userf420078dba4fd5a91de2,--DTy5LsbDeLP_AweXIPSjL3_avQMT5cH_bRxPy1uxQLVhXKaw7Oxd7NYkcJ6MZmnnqWqTcBPHA5z7bqunXEAA,25778,1,user46f81dfd34b91a1904ac4524193575aa@fake.mail,user6d91baab56d2823b326d,ryooWewe3OTxIGF1Gjl5Vvl8BsXoqWVbCAt1t6J--_KX1SM4DbyCes4yn75OWVe60G4MMZdv4byRh1wy-Clvxw,78202
2024/04/20 13:37:00 [PG] NOTIFY called; waking up
2024/04/20 13:37:05 [PG] processing 2 rows... (timeout)
1,user12d2722e1c07b0a531ea69ae125d4697@fake.mail,user853ae29eefc5d44a6bc6,4pmew2o2EOAZBDHWvJBcixJftpRCb8uyXZhzN12EOcrLBmzc4ic9avwd9dla09pIiKIoqW5iIwMfoXLEM3_LGw,38806,1,user9497d0e033019fcf3198eecb053ba40e@fake.mail,userfcde338dba96cc419613,ANLMa-1y37VLCDqK0wnfEFhUVzHsWpaNGV2ttI8m3o6_lbbYOKmp3hP7Q8H8ZQRNMPAj4xsSqC26nesfVZLgzQ,89897
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h3&gt;Testing reconnect behavior&lt;/h3&gt;
&lt;p&gt;To simulate a dropped connection, open another terminal, connect to the &lt;code&gt;mailroom&lt;/code&gt; db via &lt;code&gt;psql&lt;/code&gt;, and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE
  datname = 'mailroom'
  AND pid &amp;lt;&amp;gt; pg_backend_pid();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the connection is killed, &lt;code&gt;select()&lt;/code&gt; wakes up, causing &lt;code&gt;PQconsumeInput()&lt;/code&gt; to fail with an error. The process logs a reconnect attempt, and once reconnected, it resumes processing without losing track of queued tokens during the downtime.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024/04/20 13:37:42 [PG] WARN: error consuming input: server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.

2024/04/20 13:37:42 [PG] connecting to host=/tmp port=5432 dbname=mailroom user=ogu sslmode=disable
2024/04/20 13:37:42 [PG] connected
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Further improvements&lt;/h2&gt;
&lt;p&gt;Building on this foundation, you can extend your triggers to support more complex workflows and fine-tune the collector to operate under stricter constraints, all while keeping the database at the core of event processing.&lt;/p&gt;
&lt;p&gt;Before we conclude, let's explore some potential improvements, starting with support for multiple worker setups.&lt;/p&gt;
&lt;h3&gt;1. Multiple workers&lt;/h3&gt;
&lt;p&gt;When you update &lt;code&gt;last_seq&lt;/code&gt;, PostgreSQL locks the &lt;code&gt;jobs&lt;/code&gt; row being updated, preventing other processes from modifying it until the transaction is complete. However, PostgreSQL &lt;strong&gt;does not prevent multiple processes from attempting to read the same cursor&lt;/strong&gt; before one updates it. This can lead to duplicate processing if you're not careful.&lt;/p&gt;
&lt;p&gt;If there's any chance of concurrent execution, using &lt;code&gt;FOR UPDATE&lt;/code&gt; is essential:&lt;/p&gt;
&lt;pre&gt;&lt;code class="sql language-sql"&gt;...
    FROM
        jobs
        -- Lock the `jobs` record to prevent concurrent access
        FOR UPDATE
        JOIN tokens t ON t.id &amp;gt; jobs.last_seq
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Without locking:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Consumer A reads &lt;code&gt;jobs.last_seq = 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Consumer B also reads &lt;code&gt;jobs.last_seq = 100&lt;/code&gt; before A updates it.&lt;/li&gt;
&lt;li&gt;Both consumers select tokens where &lt;code&gt;t.id &amp;gt; 100&lt;/code&gt;, potentially processing the same tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;With FOR UPDATE:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Consumer A locks the &lt;code&gt;jobs&lt;/code&gt; record and reads &lt;code&gt;last_seq = 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Consumer B tries to read &lt;code&gt;jobs.last_seq&lt;/code&gt; but is blocked until Consumer A's transaction completes.&lt;/li&gt;
&lt;li&gt;Consumer A updates &lt;code&gt;last_seq&lt;/code&gt; to, say, &lt;code&gt;150&lt;/code&gt; and releases the lock.&lt;/li&gt;
&lt;li&gt;Consumer B then reads the updated &lt;code&gt;last_seq = 150&lt;/code&gt;, processing the next set of tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alternatively, to efficiently handle &lt;strong&gt;multiple consumers&lt;/strong&gt;, you might consider &lt;strong&gt;eliminating the &lt;code&gt;jobs&lt;/code&gt; table altogether&lt;/strong&gt;. Instead, add a new field, such as &lt;code&gt;processed_at&lt;/code&gt;, to the &lt;code&gt;tokens&lt;/code&gt; table. This field will indicate when a token has been processed. By updating &lt;code&gt;processed_at&lt;/code&gt; during token retrieval, you can use &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; to support a multi-consumer setup in a safe fashion.&lt;/p&gt;
&lt;p&gt;However, if you're certain that only a single consumer runs this query at any given time, I recommend sticking with the &lt;code&gt;jobs&lt;/code&gt; table as a single point of reference. This approach avoids the need for complex locking mechanisms, and you can further enhance the &lt;code&gt;jobs&lt;/code&gt; table to keep a history of job executions, parameters, and statuses, which can be valuable for auditing purposes.&lt;/p&gt;
&lt;h3&gt;2. Priority queues&lt;/h3&gt;
&lt;p&gt;The current queueing mechanism processes tokens without distinguishing between their types and lacks the ability to prioritize critical ones, such as password recovery, over less urgent emails like account activations. At present, '10 emails per second' could mean 10 emails of the same type or a mix, depending on the batch. While effective, this design leaves room for improvement, such as introducing prioritization or smarter batching strategies.&lt;/p&gt;
&lt;h3&gt;3. Adaptive batching&lt;/h3&gt;
&lt;p&gt;User activity is rarely consistent—there are bursts of high traffic that may far exceed daily or hourly quotas, followed by periods of minimal activity. Rather than using fixed limits and timeouts, batch size and timeout values can be dynamically adjusted based on real-time conditions. During low-traffic periods, the batch size can be increased to improve efficiency. During peak hours, it can be reduced to minimize delays.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;What's next?&lt;/h2&gt;
&lt;p&gt;⏭ While not covered in this post, a &lt;strong&gt;Rust-based&lt;/strong&gt; AWS SES email sender is available in the &lt;code&gt;sender&lt;/code&gt; folder of the &lt;a href="https://github.com/tetsuo/mailroom/tree/master/sender"&gt;repository&lt;/a&gt;. It consumes the collector's output to send bulk emails.&lt;/p&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">Building a pub/sub application with libpq to batch-process account onboarding jobs on a self-managed PostgreSQL-backed job queue.</media:description><media:content xmlns:media="http://search.yahoo.com/mrss/" fileSize="11008" height="531" medium="image" type="image/svg+xml" url="https://tetsuo.github.io/images/mercury-postgresql-workflows.svg" width="671"/></item><item><title>Making sense of Haskell</title><link>https://tetsuo.github.io/making-sense-of-haskell.html</link><category>haskell</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Tue, 30 May 2023 12:41:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2023-05-30:/making-sense-of-haskell</guid><description>&lt;blockquote&gt;
&lt;p&gt;An attempt at a beginner-friendly explanation of denotational semantics and how it maps to Haskell code &#128556;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is a function?&lt;/h2&gt;
&lt;p&gt;Functions in Haskell are &lt;em&gt;pure&lt;/em&gt;, meaning they're fixed mappings from inputs to outputs with no side effects like performing I/O or throwing exceptions.&lt;/p&gt;
&lt;p&gt;Here is an example demonstrating the Fibonacci sequence:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;fib :: Integer -&amp;gt; Integer
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This defines &lt;code&gt;fib&lt;/code&gt; as a function that takes an integer &lt;code&gt;n&lt;/code&gt; and returns the nth Fibonacci number. Each match corresponds to an equation defining the function's &lt;em&gt;meaning&lt;/em&gt; in terms of input-output relationships.&lt;/p&gt;
&lt;p&gt;This view of a function is called &lt;strong&gt;denotational&lt;/strong&gt;, in contrast to &lt;strong&gt;operational semantics&lt;/strong&gt;, where functions are sequences of &lt;em&gt;operations&lt;/em&gt; executed over time, like in imperative languages.&lt;/p&gt;
&lt;h2&gt;Denotational semantics&lt;/h2&gt;
&lt;p&gt;Denotational semantics is powerful because it turns programs into &lt;em&gt;mathematical objects&lt;/em&gt; in some &lt;strong&gt;semantic domain&lt;/strong&gt;, be it numbers, functions, sets, or even stranger things.&lt;/p&gt;
&lt;p&gt;Formally, a language's meaning is defined by its &lt;strong&gt;semantic function&lt;/strong&gt;, which maps program syntax to a chosen semantic domain. Think of it as a box &lt;code&gt;⟦⟧&lt;/code&gt; where you place a syntactic expression inside and get back its value in that domain. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⟦E⟧ : V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;means the &lt;strong&gt;expression&lt;/strong&gt; &lt;code&gt;E&lt;/code&gt; is assigned a &lt;strong&gt;value&lt;/strong&gt; in the semantic domain &lt;code&gt;V&lt;/code&gt; (which could be numbers, functions, etc.).&lt;/p&gt;
&lt;h3&gt;Example: Calculator&lt;/h3&gt;
&lt;p&gt;Consider arithmetic expressions written in prefix notation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⟦add 1 2⟧ = 1 + 2
⟦mul 2 5⟧ = 2 × 5
     ⟦42⟧ = 42
 ⟦neg 42⟧ = -42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can define the &lt;strong&gt;abstract syntax&lt;/strong&gt; for these expressions using Backus-Naur Form (BNF):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n ∈ Int ::= ... | -1 | 0 | 1 | 2 | ...
e ∈ Exp ::= add e e
         |  mul e e
         |  neg e
         |  n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, every expression evaluates to an integer, so the semantic domain is ℤ (the set of all integers). In Haskell, we might denote this with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;type ℤ = Integer

-- | An expression representing a numeral structure.
data Exp = Lit ℤ         -- Literal integer
         | Neg Exp       -- Negation of an expression
         | Add Exp Exp   -- Addition of two expressions
         | Mul Exp Exp   -- Multiplication of two expressions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;em&gt;valuation function&lt;/em&gt; then assigns a mathematical meaning to each expression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      ⟦Exp⟧ : ℤ
⟦add e1 e2⟧ = ⟦e1⟧ + ⟦e2⟧
⟦mul e1 e2⟧ = ⟦e1⟧ × ⟦e2⟧
    ⟦neg e⟧ = -⟦e⟧
        ⟦n⟧ = n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or, in Haskell:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;eval :: Exp -&amp;gt; ℤ
eval (Lit n)     = n                   -- ⟦n⟧ = n
eval (Neg e)     = - (eval e)          -- ⟦neg e⟧ = -⟦e⟧
eval (Add e1 e2) = eval e1 + eval e2   -- ⟦add e1 e2⟧ = ⟦e1⟧ + ⟦e2⟧
eval (Mul e1 e2) = eval e1 * eval e2   -- ⟦mul e1 e2⟧ = ⟦e1⟧ × ⟦e2⟧
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Move language&lt;/h2&gt;
&lt;p&gt;Consider &lt;em&gt;Move&lt;/em&gt;, a made-up DSL for controlling a robot.&lt;/p&gt;
&lt;p&gt;The Move language specifies commands such as &lt;code&gt;go E 3&lt;/code&gt;, which instruct a robot to move a given number of steps in a specified direction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go E 3; go N 4; go S 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Each &lt;code&gt;go&lt;/code&gt; command constructs a &lt;code&gt;Step&lt;/code&gt;, representing an n-unit movement in one of the cardinal directions.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Move&lt;/code&gt; is a sequence of steps separated by semicolons.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The abstract syntax for the Move language might be defined as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n ∈ Nat  ::= 0 | 1 | 2 | ...
d ∈ Dir  ::= N | S | E | W
s ∈ Step ::= go d n
m ∈ Move ::= ε | s ; m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can explore two interpretations (semantic domains) for Move programs:&lt;/p&gt;
&lt;h3&gt;1. Total distance calculation&lt;/h3&gt;
&lt;p&gt;In this interpretation, the semantic domain is ℕ (the natural numbers), representing the total distance traveled.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;Step&lt;/code&gt; expressions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  S⟦Step⟧ : Nat
S⟦go d k⟧ = k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For &lt;code&gt;Move&lt;/code&gt; expressions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  M⟦Move⟧ : Nat
   M⟦ε⟧ = 0
 M⟦s;m⟧ = S⟦s⟧ + M⟦m⟧
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Target position calculation&lt;/h3&gt;
&lt;p&gt;Here, the semantic domain is the set of functions that map a starting position &lt;code&gt;(x, y)&lt;/code&gt; to a final position. We denote this using lambda calculus (λ-calculus):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⟦Expr⟧ : Pos → Pos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each &lt;code&gt;Step&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  S⟦Step⟧ : Pos → Pos
S⟦go N k⟧ = λ(x,y).(x,y+k)
S⟦go S k⟧ = λ(x,y).(x,y−k)
S⟦go E k⟧ = λ(x,y).(x+k,y)
S⟦go W k⟧ = λ(x,y).(x−k,y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;Move&lt;/code&gt; expression composes these functions in sequence. For an empty &lt;code&gt;Move&lt;/code&gt;, the function simply returns the starting position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;M⟦Move⟧ : Pos → Pos
   M⟦ε⟧ = λp.p
 M⟦s;m⟧ = M⟦m⟧ ∘ S⟦s⟧
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementing Move in Haskell&lt;/h2&gt;
&lt;p&gt;Here's the thing, the Move DSL can be implemented directly in Haskell by mirroring its BNF grammar with algebraic data types and defining its semantics as pure functions.&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;-- Abstract syntax
data Dir = N | S | E | W

data Step = Go Dir Int

data Move
  = Empty
  | Seq Step Move

-- Semantics: total distance
stepDist :: Step -&amp;gt; Int
stepDist (Go _ k) = k

moveDist :: Move -&amp;gt; Int
moveDist Empty       = 0
moveDist (Seq s m)   = stepDist s + moveDist m

-- Semantics: final position
type Pos = (Int, Int)

stepPos :: Step -&amp;gt; Pos -&amp;gt; Pos
stepPos (Go N k) (x, y) = (x    , y + k)
stepPos (Go S k) (x, y) = (x    , y - k)
stepPos (Go E k) (x, y) = (x + k, y    )
stepPos (Go W k) (x, y) = (x - k, y    )

movePos :: Move -&amp;gt; Pos -&amp;gt; Pos
movePos Empty       p = p
movePos (Seq s m)   p = movePos m (stepPos s p)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test it, save the code as &lt;code&gt;Move.hs&lt;/code&gt; and run &lt;code&gt;ghci Move.hs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then define a program:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;let prog = Seq (Go E 3) (Seq (Go N 4) (Seq (Go S 1) Empty))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Total distance traveled:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;moveDist prog
-- 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Final position from the origin:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;movePos prog (0, 0)
-- (3, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Final position from an arbitrary point:&lt;/p&gt;
&lt;pre&gt;&lt;code class="haskell language-haskell"&gt;movePos prog (10, -2)
-- (13, 1)
&lt;/code&gt;&lt;/pre&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">An attempt at a beginner-friendly explanation of denotational semantics and how it maps to Haskell code &#128556;</media:description></item><item><title>Building a full-stack web app with fp-ts</title><link>https://tetsuo.github.io/building-a-full-stack-web-app-with-fp-ts.html</link><category>typescript</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Wed, 25 Jan 2023 12:41:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2023-01-25:/building-a-full-stack-web-app-with-fp-ts</guid><description>&lt;p&gt;&lt;a href="https://tetsuo.github.io/wr/flixbox.html"&gt;&lt;img alt="flixbox - Search movie trailers" src="https://tetsuo.github.io/images/flixbox.jpg"/&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In this post, we take a quick tour of &lt;a href="https://www.github.com/tetsuo/flixbox"&gt;&lt;strong&gt;flixbox&lt;/strong&gt;&lt;/a&gt;, a movie trailer search app built entirely with &lt;a href="https://github.com/gcanti/fp-ts"&gt;&lt;strong&gt;fp-ts&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://gcanti.github.io/fp-ts/ecosystem/"&gt;libraries&lt;/a&gt; from the fp-ts community.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Server&lt;/h2&gt;
&lt;p&gt;The flixbox server is powered by &lt;a href="https://github.com/DenisFrezzato/hyper-ts"&gt;&lt;strong&gt;hyper-ts&lt;/strong&gt;&lt;/a&gt;, which is a partial porting of &lt;a href="https://hyper.wickstrom.tech/"&gt;Hyper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Internally, a set of middlewares is defined like &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;movie&lt;/code&gt;, and &lt;code&gt;results&lt;/code&gt; for interacting with the &lt;a href="https://www.themoviedb.org/"&gt;&lt;strong&gt;TMDb&lt;/strong&gt;&lt;/a&gt; API and managing caching. These functions are arranged into pipelines that can short-circuit on failure, handling things like input validation, TMDb errors, or missing resources.&lt;/p&gt;
&lt;h3&gt;Example: Movie middleware&lt;/h3&gt;
&lt;p&gt;The following middleware handles &lt;code&gt;/movie/ID&lt;/code&gt; requests.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;/movie/3423&lt;/code&gt; is called:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&#128450;️ Check the internal cache:
&lt;ul&gt;
&lt;li&gt;✅ Return cached data if available.&lt;/li&gt;
&lt;li&gt;&#128260; Otherwise, fetch data from TMDb, store it in the cache, and return the result.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&#128230; Respond with a JSON object.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;import * as H from 'hyper-ts/lib/Middleware'

// ...

function getMovieMiddleware(
  tmdb: TMDb,
  store: Storage&amp;lt;Document&amp;gt;
): (route: MovieRoute) =&amp;gt; H.Middleware&amp;lt;StatusOpen, ResponseEnded, AppError, void&amp;gt; {
  return route =&amp;gt;
    pipe(
      GET, // ensure this is a GET request
      H.apSecond(
        pipe(
          // try to get the movie from the cache
          get(store, `/movies/${String(route.id)}`),
          H.map(entry =&amp;gt; entry.value),
          // if not found, fetch from TMDb and cache it
          H.orElse(() =&amp;gt;
            pipe(
              movie(tmdb, route.id),
              H.chain(value =&amp;gt;
                pipe(
                  put(store, `/movies/${String(route.id)}`, value),
                  H.map(entry =&amp;gt; entry.value)
                )
              )
            )
          )
        )
      ),
      // respond with JSON
      H.ichain(res =&amp;gt;
        pipe(
          H.status&amp;lt;AppError&amp;gt;(200),
          H.ichain(() =&amp;gt; sendJSON(res))
        )
      )
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128196; &lt;strong&gt;See the full implementation in &lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/server/Flixbox.ts"&gt;&lt;code&gt;server/Flixbox.ts&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr/&gt;
&lt;h2&gt;Shared modules&lt;/h2&gt;
&lt;p&gt;On both client and server, flixbox utilizes a set of common modules including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gcanti/logging-ts/"&gt;&lt;strong&gt;logging-ts&lt;/strong&gt;&lt;/a&gt; for structured logging&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gcanti/io-ts"&gt;&lt;strong&gt;io-ts&lt;/strong&gt;&lt;/a&gt; for runtime type validation&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gcanti/monocle-ts"&gt;&lt;strong&gt;monocle-ts&lt;/strong&gt;&lt;/a&gt; for optics support&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gcanti/fp-ts-routing"&gt;&lt;strong&gt;fp-ts-routing&lt;/strong&gt;&lt;/a&gt; for declarative route parsing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Among these, &lt;a href="https://github.com/gcanti/io-ts/"&gt;&lt;strong&gt;io-ts&lt;/strong&gt;&lt;/a&gt; is especially valuable for robust type validation throughout the application, with applications such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/app/Model.ts"&gt;Defining client-side application state&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/tree/0.0.7/src/tmdb/model"&gt;Modeling TMDb API data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/server/Error.ts#L17"&gt;Reporting validation errors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/app/Router.ts#L5"&gt;Matching URL queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/app/components/Layout.tsx#L77"&gt;Validating React component props&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/server/index.ts#L72"&gt;Ensuring correctness of environment variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/gcanti/io-ts"&gt;io-ts&lt;/a&gt; is highly recommended even for projects that do not fully adopt fp-ts.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Extending fp-ts modules to new effect types&lt;/h3&gt;
&lt;p&gt;While these modules integrate smoothly within the fp-ts v2 ecosystem, certain scenarios may require explicit type-level configuration.&lt;/p&gt;
&lt;p&gt;For instance, when using &lt;code&gt;logging-ts&lt;/code&gt; with a custom effect type, you must provide an instance of the &lt;code&gt;Logger&lt;/code&gt; algebra that conforms to that effect. &lt;code&gt;logging-ts&lt;/code&gt; facilitates this by exposing &lt;code&gt;getLoggerM&lt;/code&gt;, which abstracts over any monad.&lt;/p&gt;
&lt;p&gt;To integrate &lt;code&gt;logging-ts&lt;/code&gt; with the effects flixbox generates, a new HKT &lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/logging/TaskEither.ts"&gt;&lt;code&gt;LoggerTaskEither&lt;/code&gt;&lt;/a&gt; is defined and registered in &lt;code&gt;fp-ts&lt;/code&gt;'s &lt;code&gt;URItoKind2&lt;/code&gt;, thereby allowing type class instance support for logging within the &lt;code&gt;TaskEither&lt;/code&gt; context, the most frequently used effect type in the project.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2&gt;Client&lt;/h2&gt;
&lt;p&gt;The client uses &lt;a href="https://github.com/gcanti/elm-ts"&gt;&lt;strong&gt;elm-ts&lt;/strong&gt;&lt;/a&gt;, which provides an fp-ts adaptation of &lt;a href="https://elm-lang.org/"&gt;Elm&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Elm shares conceptual similarities with &lt;a href="https://redux.js.org/understanding/history-and-design/prior-art"&gt;Redux&lt;/a&gt;. Messages in Elm correspond to Redux actions, and the Elm &lt;code&gt;update&lt;/code&gt; function closely mirrors Redux reducers, responsible for state changes.&lt;/p&gt;
&lt;p&gt;Here are the message types used in the flixbox UI:&lt;/p&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;type Msg =
  | Navigate
  | PushUrl
  | UpdateSearchTerm
  | SubmitSearch
  | SetHttpError
  | SetNotification
  | SetSearchResults
  | SetPopularResults
  | SetMovie
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Elm architecture in a nutshell&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&#128196; &lt;strong&gt;Initial state&lt;/strong&gt;: You define an initial application state, the model.&lt;/li&gt;
&lt;li&gt;&#128444;️ &lt;strong&gt;View function&lt;/strong&gt;: A view function renders visual elements based on the current state.&lt;/li&gt;
&lt;li&gt;&#128257; &lt;strong&gt;Update function&lt;/strong&gt;: When user interaction triggers a message (e.g., clicking a link triggers a Navigate message), the update function is called.
&lt;ul&gt;
&lt;li&gt;&#128229; It receives the message and the current state as its inputs.&lt;/li&gt;
&lt;li&gt;⚙️ It processes the current state and returns a new state and potentially a new message.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&#128260; &lt;strong&gt;State updates&lt;/strong&gt;: The new state is sent to subscribers (like the view function).&lt;/li&gt;
&lt;li&gt;&#127744; &lt;strong&gt;Continuous processing&lt;/strong&gt;: New actions are processed until no further actions remain.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&#128196; &lt;strong&gt;See the full implementation in &lt;a href="https://github.com/tetsuo/flixbox/blob/0.0.7/src/app/Effect.ts"&gt;&lt;code&gt;app/Effect.ts&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Optics for state updates&lt;/h3&gt;
&lt;p&gt;The client also uses &lt;a href="https://www.github.com/gcanti/monocle-ts"&gt;&lt;strong&gt;monocle-ts&lt;/strong&gt;&lt;/a&gt;, a port of &lt;a href="https://www.optics.dev/Monocle/"&gt;Monocle&lt;/a&gt;, allowing composable structures like &lt;a href="https://gcanti.github.io/monocle-ts/modules/Lens.ts.html"&gt;&lt;code&gt;Lens&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://gcanti.github.io/monocle-ts/modules/Traversal.ts.html"&gt;&lt;code&gt;Traversal&lt;/code&gt;&lt;/a&gt; for state updates without mutations.&lt;/p&gt;
&lt;p&gt;Consider the following comparison to &lt;a href="https://immerjs.github.io/immer/"&gt;Immer.js&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Immer.js example&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="javascript language-javascript"&gt;import produce from "immer"

const toggleTodo = produce((draft, id) =&amp;gt; {
    const todo = draft.find(todo =&amp;gt; todo.id === id)
    todo.done = !todo.done
})

const nextState = toggleTodo(baseState, "Immer")
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;monocle-ts equivalent&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="typescript language-typescript"&gt;import * as _ from 'monocle-ts/lib/Traversal'

type Todo = { id: number; done: boolean }

type Todos = ReadonlyArray&amp;lt;Todo&amp;gt;

const toggleTodoDone = (id: number) =&amp;gt;
  pipe(
    _.id&amp;lt;Todos&amp;gt;(),
    _.findFirst(todo =&amp;gt; todo.id === id),
    _.prop('done'),
    _.modify(done =&amp;gt; !done)
  )

const nextState = toggleTodoDone(42)(baseState)
&lt;/code&gt;&lt;/pre&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">In this post, we take a quick tour of flixbox, a movie trailer search app built entirely with fp-ts and libraries from the fp-ts community.</media:description><enclosure length="-1" type="application/pdf" url="https://elm-lang.org/assets/papers/concurrent-frp.pdf"/><itunes:explicit>no</itunes:explicit><itunes:subtitle>In this post, we take a quick tour of flixbox, a movie trailer search app built entirely with fp-ts and libraries from the fp-ts community. Server The flixbox server is powered by hyper-ts, which is a partial porting of Hyper. Internally, a set of middlewares is defined like get, put, movie, and results for interacting with the TMDb API and managing caching. These functions are arranged into pipelines that can short-circuit on failure, handling things like input validation, TMDb errors, or missing resources. Example: Movie middleware The following middleware handles /movie/ID requests. When /movie/3423 is called: &#128450;️ Check the internal cache: ✅ Return cached data if available. &#128260; Otherwise, fetch data from TMDb, store it in the cache, and return the result. &#128230; Respond with a JSON object. import * as H from 'hyper-ts/lib/Middleware' // ... function getMovieMiddleware( tmdb: TMDb, store: Storage&amp;lt;Document&amp;gt; ): (route: MovieRoute) =&amp;gt; H.Middleware&amp;lt;StatusOpen, ResponseEnded, AppError, void&amp;gt; { return route =&amp;gt; pipe( GET, // ensure this is a GET request H.apSecond( pipe( // try to get the movie from the cache get(store, `/movies/${String(route.id)}`), H.map(entry =&amp;gt; entry.value), // if not found, fetch from TMDb and cache it H.orElse(() =&amp;gt; pipe( movie(tmdb, route.id), H.chain(value =&amp;gt; pipe( put(store, `/movies/${String(route.id)}`, value), H.map(entry =&amp;gt; entry.value) ) ) ) ) ) ), // respond with JSON H.ichain(res =&amp;gt; pipe( H.status&amp;lt;AppError&amp;gt;(200), H.ichain(() =&amp;gt; sendJSON(res)) ) ) ) } &#128196; See the full implementation in server/Flixbox.ts. Shared modules On both client and server, flixbox utilizes a set of common modules including: logging-ts for structured logging io-ts for runtime type validation monocle-ts for optics support fp-ts-routing for declarative route parsing Among these, io-ts is especially valuable for robust type validation throughout the application, with applications such as: Defining client-side application state Modeling TMDb API data Reporting validation errors Matching URL queries Validating React component props Ensuring correctness of environment variables io-ts is highly recommended even for projects that do not fully adopt fp-ts. Extending fp-ts modules to new effect types While these modules integrate smoothly within the fp-ts v2 ecosystem, certain scenarios may require explicit type-level configuration. For instance, when using logging-ts with a custom effect type, you must provide an instance of the Logger algebra that conforms to that effect. logging-ts facilitates this by exposing getLoggerM, which abstracts over any monad. To integrate logging-ts with the effects flixbox generates, a new HKT LoggerTaskEither is defined and registered in fp-ts's URItoKind2, thereby allowing type class instance support for logging within the TaskEither context, the most frequently used effect type in the project. Client The client uses elm-ts, which provides an fp-ts adaptation of Elm. Elm shares conceptual similarities with Redux. Messages in Elm correspond to Redux actions, and the Elm update function closely mirrors Redux reducers, responsible for state changes. Here are the message types used in the flixbox UI: type Msg = | Navigate | PushUrl | UpdateSearchTerm | SubmitSearch | SetHttpError | SetNotification | SetSearchResults | SetPopularResults | SetMovie Elm architecture in a nutshell &#128196; Initial state: You define an initial application state, the model. &#128444;️ View function: A view function renders visual elements based on the current state. &#128257; Update function: When user interaction triggers a message (e.g., clicking a link triggers a Navigate message), the update function is called. &#128229; It receives the message and the current state as its inputs. ⚙️ It processes the current state and returns a new state and potentially a new message. &#128260; State updates: The new state is sent to subscribers (like the view function). &#127744; Continuous processing: New actions are processed until no further actions remain. &#128196; See the full implementation in app/Effect.ts. Optics for state updates The client also uses monocle-ts, a port of Monocle, allowing composable structures like Lens and Traversal for state updates without mutations. Consider the following comparison to Immer.js: Immer.js example: import produce from "immer" const toggleTodo = produce((draft, id) =&amp;gt; { const todo = draft.find(todo =&amp;gt; todo.id === id) todo.done = !todo.done }) const nextState = toggleTodo(baseState, "Immer") monocle-ts equivalent: import * as _ from 'monocle-ts/lib/Traversal' type Todo = { id: number; done: boolean } type Todos = ReadonlyArray&amp;lt;Todo&amp;gt; const toggleTodoDone = (id: number) =&amp;gt; pipe( _.id&amp;lt;Todos&amp;gt;(), _.findFirst(todo =&amp;gt; todo.id === id), _.prop('done'), _.modify(done =&amp;gt; !done) ) const nextState = toggleTodoDone(42)(baseState)</itunes:subtitle><itunes:author>noemail@noemail.org (Onur Gündüz)</itunes:author><itunes:summary>In this post, we take a quick tour of flixbox, a movie trailer search app built entirely with fp-ts and libraries from the fp-ts community. Server The flixbox server is powered by hyper-ts, which is a partial porting of Hyper. Internally, a set of middlewares is defined like get, put, movie, and results for interacting with the TMDb API and managing caching. These functions are arranged into pipelines that can short-circuit on failure, handling things like input validation, TMDb errors, or missing resources. Example: Movie middleware The following middleware handles /movie/ID requests. When /movie/3423 is called: &#128450;️ Check the internal cache: ✅ Return cached data if available. &#128260; Otherwise, fetch data from TMDb, store it in the cache, and return the result. &#128230; Respond with a JSON object. import * as H from 'hyper-ts/lib/Middleware' // ... function getMovieMiddleware( tmdb: TMDb, store: Storage&amp;lt;Document&amp;gt; ): (route: MovieRoute) =&amp;gt; H.Middleware&amp;lt;StatusOpen, ResponseEnded, AppError, void&amp;gt; { return route =&amp;gt; pipe( GET, // ensure this is a GET request H.apSecond( pipe( // try to get the movie from the cache get(store, `/movies/${String(route.id)}`), H.map(entry =&amp;gt; entry.value), // if not found, fetch from TMDb and cache it H.orElse(() =&amp;gt; pipe( movie(tmdb, route.id), H.chain(value =&amp;gt; pipe( put(store, `/movies/${String(route.id)}`, value), H.map(entry =&amp;gt; entry.value) ) ) ) ) ) ), // respond with JSON H.ichain(res =&amp;gt; pipe( H.status&amp;lt;AppError&amp;gt;(200), H.ichain(() =&amp;gt; sendJSON(res)) ) ) ) } &#128196; See the full implementation in server/Flixbox.ts. Shared modules On both client and server, flixbox utilizes a set of common modules including: logging-ts for structured logging io-ts for runtime type validation monocle-ts for optics support fp-ts-routing for declarative route parsing Among these, io-ts is especially valuable for robust type validation throughout the application, with applications such as: Defining client-side application state Modeling TMDb API data Reporting validation errors Matching URL queries Validating React component props Ensuring correctness of environment variables io-ts is highly recommended even for projects that do not fully adopt fp-ts. Extending fp-ts modules to new effect types While these modules integrate smoothly within the fp-ts v2 ecosystem, certain scenarios may require explicit type-level configuration. For instance, when using logging-ts with a custom effect type, you must provide an instance of the Logger algebra that conforms to that effect. logging-ts facilitates this by exposing getLoggerM, which abstracts over any monad. To integrate logging-ts with the effects flixbox generates, a new HKT LoggerTaskEither is defined and registered in fp-ts's URItoKind2, thereby allowing type class instance support for logging within the TaskEither context, the most frequently used effect type in the project. Client The client uses elm-ts, which provides an fp-ts adaptation of Elm. Elm shares conceptual similarities with Redux. Messages in Elm correspond to Redux actions, and the Elm update function closely mirrors Redux reducers, responsible for state changes. Here are the message types used in the flixbox UI: type Msg = | Navigate | PushUrl | UpdateSearchTerm | SubmitSearch | SetHttpError | SetNotification | SetSearchResults | SetPopularResults | SetMovie Elm architecture in a nutshell &#128196; Initial state: You define an initial application state, the model. &#128444;️ View function: A view function renders visual elements based on the current state. &#128257; Update function: When user interaction triggers a message (e.g., clicking a link triggers a Navigate message), the update function is called. &#128229; It receives the message and the current state as its inputs. ⚙️ It processes the current state and returns a new state and potentially a new message. &#128260; State updates: The new state is sent to subscribers (like the view function). &#127744; Continuous processing: New actions are processed until no further actions remain. &#128196; See the full implementation in app/Effect.ts. Optics for state updates The client also uses monocle-ts, a port of Monocle, allowing composable structures like Lens and Traversal for state updates without mutations. Consider the following comparison to Immer.js: Immer.js example: import produce from "immer" const toggleTodo = produce((draft, id) =&amp;gt; { const todo = draft.find(todo =&amp;gt; todo.id === id) todo.done = !todo.done }) const nextState = toggleTodo(baseState, "Immer") monocle-ts equivalent: import * as _ from 'monocle-ts/lib/Traversal' type Todo = { id: number; done: boolean } type Todos = ReadonlyArray&amp;lt;Todo&amp;gt; const toggleTodoDone = (id: number) =&amp;gt; pipe( _.id&amp;lt;Todos&amp;gt;(), _.findFirst(todo =&amp;gt; todo.id === id), _.prop('done'), _.modify(done =&amp;gt; !done) ) const nextState = toggleTodoDone(42)(baseState)</itunes:summary><itunes:keywords>typescript</itunes:keywords></item><item><title>CouchDB design document bundler</title><link>https://tetsuo.github.io/couchdb-design-document-bundler.html</link><category>js</category><author>noemail@noemail.org (Onur Gündüz)</author><pubDate>Mon, 2 Jan 2023 14:55:00 GMT</pubDate><guid isPermaLink="false">tag:tetsuo.github.io,2023-01-02:/couchdb-design-document-bundler</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/tetsuo/couchilla"&gt;&lt;strong&gt;couchilla&lt;/strong&gt;&lt;/a&gt; is a bundler for packing design documents for CouchDB with CommonJS support.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;In &lt;a href="https://couchdb.apache.org/"&gt;CouchDB&lt;/a&gt;, &lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html"&gt;design documents&lt;/a&gt; are special database entries that contain JavaScript functions, such as &lt;em&gt;view&lt;/em&gt; and &lt;em&gt;update&lt;/em&gt; functions. These functions, executed on demand, generate secondary indexes, often termed MapReduce views.&lt;/p&gt;
&lt;p&gt;JavaScript support in CouchDB is based on the Mozilla SpiderMonkey engine (and, starting with version 3.4.1, also QuickJS). However, because design functions are language-independent, CouchDB does not include a dedicated tool for creating them.&lt;/p&gt;
&lt;p&gt;That's where couchilla comes in. It scans a directory of JavaScript files containing view and filter functions, then builds a JSON design document that's ready to deploy.&lt;/p&gt;
&lt;h2&gt;Example directory layout&lt;/h2&gt;
&lt;p&gt;For instance, your directory might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── filters
│   └── quu.js
├── views
│   ├── foo.map.js
│   └── bar.reduce.js
└── validate_doc_update.js
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#view-functions"&gt;View functions&lt;/a&gt; reside in the &lt;code&gt;views&lt;/code&gt; directory. Files with &lt;code&gt;.map.js&lt;/code&gt; (or simply &lt;code&gt;.js&lt;/code&gt;) are converted into &lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#map-functions"&gt;map functions&lt;/a&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#reduce-and-rereduce-functions"&gt;Reduce functions&lt;/a&gt; are defined in files with &lt;code&gt;.reduce.js&lt;/code&gt; extensions.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#filter-functions"&gt;Filter functions&lt;/a&gt; belong in the &lt;code&gt;filters&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;View functions&lt;/h2&gt;
&lt;h3&gt;Map functions&lt;/h3&gt;
&lt;p&gt;Emit key/value pairs to store them in a view.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;views/foo.map.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;export default doc =&amp;gt; emit(doc._id, 42)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reduce functions&lt;/h3&gt;
&lt;p&gt;Take sum of mapped values:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;views/foo.reduce.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;export default (keys, values, rereduce) =&amp;gt; {
  if (rereduce) {
    return sum(values)
  } else {
    return values.length
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Builtin reduce functions&lt;/h4&gt;
&lt;p&gt;You can opt to use &lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#built-in-reduce-functions"&gt;Erlang native functions&lt;/a&gt; using the &lt;code&gt;builtin&lt;/code&gt; annotation. For example the &lt;code&gt;sum&lt;/code&gt; function above can be rewritten using &lt;code&gt;_sum&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;views/foo.reduce.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;/* builtin _sum */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During compilation this will be replaced with a call to the builtin &lt;a href="https://docs.couchdb.org/en/stable/ddocs/ddocs.html#sum"&gt;&lt;code&gt;_sum&lt;/code&gt;&lt;/a&gt; function.&lt;/p&gt;
&lt;h2&gt;Filter functions&lt;/h2&gt;
&lt;p&gt;Filter by field:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filters/foo.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;export default (doc, req) =&amp;gt; {
  if (doc &amp;amp;&amp;amp; doc.title &amp;amp;&amp;amp; doc.title.startsWith('C')) {
    return true
  }
  return false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Validate document update functions&lt;/h2&gt;
&lt;p&gt;Log incoming requests and respond with forbidden:&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;export default (newDoc, oldDoc, userCtx, secObj) =&amp;gt; {
  log(newDoc)
  log(oldDoc)
  log(userCtx)
  log(secObj)
  throw { forbidden: 'not able now!' }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr/&gt;
&lt;h2&gt;Requiring other modules&lt;/h2&gt;
&lt;p&gt;All code, including &lt;code&gt;require()&lt;/code&gt; statements, must be enclosed within the exported default function.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;views/gamma.map.js&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="js language-js"&gt;export default doc =&amp;gt; {
  const gamma = require('gamma')

  emit(doc._id, gamma(doc.value))
}
&lt;/code&gt;&lt;/pre&gt;
</description><media:description xmlns:media="http://search.yahoo.com/mrss/" type="html">couchilla is a bundler for packing design documents for CouchDB with CommonJS support.</media:description></item></channel></rss>