<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Hasen Judi]]></title><description><![CDATA[Computer and Web Programming]]></description><link>https://hasen.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!Az4m!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd038f74a-8d40-45f8-92cf-f96bf834430b_350x350.png</url><title>Hasen Judi</title><link>https://hasen.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 03 Sep 2026 22:44:17 GMT</lastBuildDate><atom:link href="/__u/hasen.substack.com/feed" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><webMaster><![CDATA[hasen@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[hasen@substack.com]]></itunes:email><itunes:name><![CDATA[Hasen Judi]]></itunes:name></itunes:owner><itunes:author><![CDATA[Hasen Judi]]></itunes:author><googleplay:owner><![CDATA[hasen@substack.com]]></googleplay:owner><googleplay:email><![CDATA[hasen@substack.com]]></googleplay:email><googleplay:author><![CDATA[Hasen Judi]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[How Shirei retains component identity and state]]></title><description><![CDATA[The API is immediate mode, but components have consistent identity and retain their state across frames]]></description><link>https://hasen.substack.com/p/how-shirei-retains-component-identity</link><guid isPermaLink="false">https://hasen.substack.com/p/how-shirei-retains-component-identity</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Wed, 19 Aug 2026 01:38:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Az4m!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd038f74a-8d40-45f8-92cf-f96bf834430b_350x350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://judi.systems/shirei">Shirei</a> is a practical GUI framework for writing GUI apps as native Go programs &#8212; not web pages.</p><p>I used to very proactively describe it as &#8220;immediate mode&#8221;, but recently I am starting to use the term &#8220;declarative&#8221; instead, because it deviates from several &#8220;de facto standard&#8221; conventions about what &#8220;immediate mode&#8221; means (or implies).</p><p>For one thing, it&#8217;s not a C++ library, and it&#8217;s not meant to be used with game engines.</p><p>In fact, we don&#8217;t even use GPU for rendering &#8212; at least at the time of this writing.</p><p>More importantly though, the API we expose is not about &#8220;drawing&#8221; things to the screen &#8220;immediately&#8221;.</p><p>Rather, what you do &#8220;immediately&#8221; is create a container tree that describes what the UI should look like at the end of this rendering cycle.</p><p>The containers do not even have their sizes and positions &#8220;resolved&#8221; when you create them; that gets resolved later by the core engine.</p><p>In a way, it&#8217;s similar to the React model, at least on a superficial level.</p><p>I still think it qualifies as &#8220;immediate mode&#8221;, because the way you use the API, your code does not keep widget objects around to pass to the API.</p><p>If you have a complex data structure that you want to reflect in the UI, you do not need to create and maintain a mirror widget structure. You just write procedural code to traverse or iterate your data structure.</p><p>The UI is not created by gluing together a bunch of widget objects.</p><p>The UI is built instead by running a view function that builds a container tree, and then having the system run the layout algorithm on that tree and turning it to a graphical interface to display on the screen.</p><h2>Procedurally building a container tree</h2><p>The biggest difference between Shirei view functions and React style functions, is the function signature.</p><p>A react style function takes one argument: a &#8220;props&#8221;, and returns a JSX Element, which is essentially a description of what a &#8220;subtree&#8221; of the dom is supposed to look like.</p><pre><code><code>type MyComponentProps = {
    ...
}

function MyComponent(props: MyComponentProps): JSX.Element {
    return &lt;div&gt;
        ...
    &lt;/div&gt;
}
</code></code></pre><p>Shirei, on the other hand, does not pass around container trees as objects. Instead, you build the container tree procedurally: opening a new container, setting attributes on it, and creating child containers. The child containers are also built the same way: procedurally.</p><p>So, if you have an html element tree like this:</p><pre><code><code>    &lt;div style="background: hsl(0, 10%, 90%); padding: 10px"&gt;
        &lt;div style="width: 100px; height: 80px; background: hsl(0, 50%, 50%)"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
</code></code></pre><p>We build it procedurally this way (conceptually):</p><pre><code><code>open container
    set attribute: background=/__u/hasen.substack.com/(0, 10, 90, 1)
    set attribute: padding = (10, 10, 10, 10)
    open container
        set attribute: min width = 100
        set attribute: max width = 100
        set attribute: min height = 80
        set attribute: max height = 80
        set attribute: background=/__u/hasen.substack.com/(0, 50, 50, 1)
    close container
close container
</code></code></pre><p>This requires there to be an invisible &#8220;builder&#8221; object that is keeping track of the tree structure as it is being built.</p><p>This is another aspect of being &#8220;immediate&#8221;: you&#8217;re manipulating a data structure, you are issuing commands to a system.</p><p>Building the tree procedurally allows us to freely change the steps being performed based on current conditions. For example, if we are being hovered, we can change the background.</p><pre><code><code>open container
    ...
    open container
        ...
        set attribute: background=/__u/hasen.substack.com/(0, 50, 50, 1)
        if hovered() {
            set attribute: background=/__u/hasen.substack.com/(0, 50, 70, 1)
        }
    close container
close container
</code></code></pre><p>In Shirei, the actual code looks like this:</p><pre><code><code>Container(Attrs(Background(0, 10, 90, 1), Pad(10)), func() {
    Container(Attrs(FixSize(100, 80), Background(0, 50, 50, 1)), func() {
        if IsHovered() {
            ModAttrs(Background(0, 50, 70, 1))
        }
    })
})
</code></code></pre><p>The function <code>Container</code> takes as a parameter the initial attribute set and the &#8220;builder&#8221; function. Implicitly, the <code>open container</code> happens at the start of the builder function, and the <code>close container</code> happens at the end of it.</p><p><code>Attrs</code> is a function that allows specifying the attributes ergonomically using a set of helper functions. It&#8217;s only there for &#8220;aesthetic&#8221; reasons, to avoid the code looking like this:</p><pre><code><code>Container(Attributes{
    Background: Vec4{0, 10, 90, 1},
    Padding: Vec4{10, 10, 10, 10},
}, ...)
</code></code></pre><p><code>FixSize</code> is a helper function that sets both min size and max size at the same time. Here is the actual, full implementation of it:</p><pre><code><code>func FixSize(w, h float32) AttrsFn {
&#9;return func(a *AttrSet) {
&#9;&#9;a.MaxSize = Vec2{w, h}
&#9;&#9;a.MinSize = Vec2{w, h}
&#9;}
}
</code></code></pre><p>Most of these helpers don&#8217;t do much of &#8220;computational work&#8221; at runtime; they are there to serve aesthetic purposes.</p><p>Opening a container and setting the attributes are usually the same operation, but changing the attributes has to happen inside the builder function, because we cannot check if the current container is hovered unless we first &#8220;open&#8221; it.</p><h2>Container Types</h2><p>Unlike the DOM, there is only <em>one</em> container type: <code>Container</code>.</p><p>We do have a function to draw a button. It&#8217;s basically <code>Button(icon, label)</code>.</p><p>But <code>Button</code> is not a container type. It&#8217;s just a function that builds out a container tree that, when rendered to the screen, looks like a button. The function also handles input and changes the appearance of the button based on the user interaction: highlighting it when hovered, pressing it when pressed.</p><p>This is not a &#8220;quirk&#8221; in the framework; it&#8217;s a deliberate architecture design decision.</p><p>By treating all containers as the same, we greatly simplify the code that performs container layout. We just walk through the container tree and run the same logic on every level. We never had to check the container type to apply special &#8220;rules&#8221; to it. All the &#8220;rules&#8221; are present in the attribute set.</p><p>Whether we are a button or a text input or a text label or a scrollbar - is a concern of the UI builder code.</p><p>Once the container tree is built, Shirei&#8217;s layout engine takes over to size and position all the elements, route events, retain identities, etc.</p><p>Now, the astute reader might have noticed the function <code>IsHovered()</code> and wondering what it means and how it works.</p><p>If the ui building code only declares the shape the container tree, not the sizing or positioning, how does it know whether the current container is being hovered?</p><p>The answer is that it&#8217;s based on where this container was positioned in the previous frame.</p><p>But this assumes we know where this container was on the previous frame, which assumes that we know the &#8220;identity&#8221; of the current container, relative to the containers created on the previous frame.</p><p>But how does that work? Doesn&#8217;t immediate mode mean we build the UI, render it to the screen, then throw away all the data?</p><p>Well, yes and no. We do throw away the container tree, but we do keep around a <em>parallel</em> tree, which we use to resolve container identities and remember their states.</p><h2>Container identity</h2><p>A <code>Container</code> has a continuous identity across frames based on its position in the tree.</p><p>How can this work?</p><p>Let&#8217;s step out of Shirei for a bit and think more abstractly. Imagine a node tree where each node has a &#8220;key&#8221; that identifies its type.</p><p>Imagine two node trees, one produced at frame N, and the other produced at frame N+1</p><p>Tree at frame N:</p><pre><code><code>- Root
    - A
    - B
    - C
</code></code></pre><p>Tree at frame N+1:</p><pre><code><code>- Root
    - A
    - B
    - D
    - C
</code></code></pre><p>We can easily see that a container like &#8220;Root -&gt; A&#8221; is the same: the node has the same name, and its parent is also the same.</p><p>&#8220;Root -&gt; C&#8221; is also easily identifiable as the same container. Even though its absolute position relative to its parent changes (at frame N it&#8217;s the third child, but at frame N+1 it&#8217;s the fourth child).</p><p>How do we know it&#8217;s the same? Because we use the node type as part of positioning; in both frames, it&#8217;s the first C node relative to its parent.</p><p>I said earlier that containers do not have &#8220;types&#8221;, but we <em>can</em> identify the container by its location in the codebase. We refer to this in an abstract way as an &#8220;implicit type&#8221;.</p><p>Now, if we render containers in a loop, then all containers will have the same &#8220;implicit&#8221; type, which might create problems if the ordering of the loop can change across frames.</p><p>In cases like this, we can use an explicit key per container, which is the same solution React resorts to:</p><pre><code><code>for idx := range list {
    item := &amp;list[idx]
    ContainerWithKey(item.id, Attrs(...), func() {
        ....
    })
}
</code></code></pre><p>So, let&#8217;s consider the following snippet</p><pre><code><code>attrs := Attrs(Expand, Pad(10), Corners(6), Background(145, 25, 92, 1))
if options.ShowEmail {
    Container(attrs, func() {
        Label(contact.Email)
    })
}
if options.ShowPhone {
    Container(attrs, func() {
        Label(contact.Phone)
    })
}
</code></code></pre><p>We have two <code>Container</code> calls, guarded by an <code>if</code> statement. If the first conditional changed from true to false, the position of the phone container, relative to its parent, is going to change, but, that container has a distinct implicit key from the email container, so the implicit id is easy to resolve correctly.</p><h2>Retained state</h2><p>We use the retained id in order to retain the layout information: the size and position of this container, so we can query it at later frames.</p><p>More than that, we allow retaining arbitrary &#8220;state&#8221; related to the current component.</p><p>This allows components to embody complex behavior without the caller having to retain any state themselves.</p><p>The &#8220;API&#8221; presented appears &#8220;immediate&#8221;, but behind the scenes there is retained state.</p><p>Here&#8217;s a demo of a &#8220;special label&#8221; component. It features &#8220;two&#8221; retained state variables:</p><ul><li><p>Has this component ever been hovered?</p></li><li><p>What is the requested text size?</p></li></ul><p>It &#8220;hides&#8221; the label until the component has been hovered at least once. It shows small text control buttons that increase or decrease the text size.</p><pre><code><code>func SpecialLabel(text string) {
&#9;Container(Attrs(Corners(4), Gap(10), Pad(20), BorderColor(0, 50, 50, 1), BorderWidth(1)), func() {
&#9;&#9;type MyState struct {
&#9;&#9;&#9;hoveredOnce bool
&#9;&#9;&#9;sizeInc     int
&#9;&#9;}

&#9;&#9;// retained component state
&#9;&#9;s := Use[MyState]("my-state")

&#9;&#9;// ephemeral frame state
&#9;&#9;var hoveredNow = false

&#9;&#9;if IsHovered() {
&#9;&#9;&#9;hoveredNow = true
&#9;&#9;&#9;s.hoveredOnce = true
&#9;&#9;}

&#9;&#9;textColor := Vec4{0, 0, 0, 0}
&#9;&#9;if s.hoveredOnce {
&#9;&#9;&#9;textColor[3] = 1
&#9;&#9;}

&#9;&#9;if hoveredNow {
&#9;&#9;&#9;Container(Attrs(Row, Float(2, 2), Gap(4)), func() {
&#9;&#9;&#9;&#9;if CtrlButton(SymPlus, "", true) {
&#9;&#9;&#9;&#9;&#9;s.sizeInc += 5
&#9;&#9;&#9;&#9;}
&#9;&#9;&#9;&#9;if CtrlButton(SymMinus, "", true) {
&#9;&#9;&#9;&#9;&#9;s.sizeInc -= 5
&#9;&#9;&#9;&#9;}
&#9;&#9;&#9;})
&#9;&#9;}

&#9;&#9;Label(text, FontSize(20+float32(s.sizeInc)), TextColorVec(textColor))
&#9;})
}

func root() {
&#9;ModAttrs(Spacing(20))

&#9;SpecialLabel("Hello")
&#9;SpecialLabel("World")
}
</code></code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;f94acb7b-fc83-4812-8511-9d8555b0e712&quot;,&quot;duration&quot;:null}"></div><h2>Multi-pass layout rendering</h2><p>Getting the position data from the retained state has a caveat: on the first frame, there is no size or position data, as the element has not had its position and size determined yet. To work around this, we can run the UI building code several times even within the same frame cycle. This means, the resolved size and position of this container will be available on the second pass.</p><p>A second layout pass can also be triggered when an element moves.</p><p>See, the rendering cycle goes roughly through the following stages:</p><ol><li><p>Collect input data from platform layer</p></li><li><p>Run user provided UI builder code to produce the container tree</p></li><li><p>Resolve all sizes and positions of containers in the tree</p></li><li><p>Produce rendering primitives (Surfaces)</p></li><li><p>Render Surface list onto platform provided window surface buffer</p></li></ol><p>Steps 2 and 3 are what can run &#8220;multiple times&#8221;.</p><p>The catch is that we limit how many times we do this. Currently we only do it once, meaning we can run a second pass, but we will not run a third pass.</p><p>So, even though the UI builder code will be dealing with data from the previous frame, in practice, most of the time, there will be no noticeable glitch.</p><h2>Hovering mechanics</h2><p>Since I used <code>IsHovered()</code> as the hook for this article, I feel the need to clarify one important aspect of hovering detection logic: it&#8217;s not just about whether the mouse is over the container&#8217;s rectangle on the screen; it also requires understanding that containers can overlap, and we need to know which containers are in front of other containers.</p><p>So at the end of each frame, we record the list of hoverable containers in <em>paint order</em>: if two or more containers overlap, the <em>latest</em> one to be painted is the one most in front.</p><p>Then, at the start of the next frame, we walk that list backwards, and the first rect to contain the current mouse position &#8220;wins&#8221; as the container that is being directly hovered. All of its parent containers are then also considered as &#8220;hovered&#8221;, though indirectly.</p><p>During UI building, when we call <code>IsHovered()</code> we do not yet have the full picture of what the situation regarding container ordering will look like. We need to wait for the UI building to finish for this frame, and for sizing, positioning, and ordering to all be resolved.</p><p>This is why we must use the data from the previous frame, and why this mechanism cannot work<em>unless</em> we do retain enough state so that we can <em>identify</em> containers across frames.</p>]]></content:encoded></item><item><title><![CDATA[Announcing an Alpha (Test) release of my self-hosting program: Sprouts/Gardener]]></title><description><![CDATA[Desktop application that makes it possible to self-host your website on a VPS, without cloud lock-in, and without linux sysadmin skills]]></description><link>https://hasen.substack.com/p/announcing-sprouts-gardener</link><guid isPermaLink="false">https://hasen.substack.com/p/announcing-sprouts-gardener</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Wed, 01 Oct 2025 04:54:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6b09d0fc-3561-4ca6-93ef-75e9cbfb63b0_730x522.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Available here for public testing: <a href="https://judi.systems/sprouts/">https://judi.systems/sprouts/</a></p><p>Over three years ago, I wrote a post about the lack of tools to make self-hosting easy without sysadmin skills.</p><p>The intention was always to work on a project to solve this problem! Things did not go to plan, and the project got derailed several times for various reasons, until about 4 months ago when I started working in earnest on releasing this first version.</p><p>This first iteration focuses on static content. While the intention was always to make a website where you can interact with your visitors, static content was the lowest hanging fruit and I decided to go for it as a starting point.</p><p>I am now using this program to publish my own static website: <a href="https://judi.systems">Judi Systems</a></p><p>Here&#8217;s a one minute demo of publishing a website to a domain name on the internet, on a freshly provisioned VPS linux box from Vultr</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;f6867cc7-8c06-4ed0-a443-59ba054872d3&quot;,&quot;duration&quot;:null}"></div><p>Head over here to learn more <a href="https://judi.systems/sprouts/">https://judi.systems/sprouts/</a></p><p>If you give it a try, I would love to hear your feedback!</p><p>Thank you for your attention to this matter!</p>]]></content:encoded></item><item><title><![CDATA[Never ending stream of bugs]]></title><description><![CDATA[How to stop it]]></description><link>https://hasen.substack.com/p/never-ending-stream-of-bugs</link><guid isPermaLink="false">https://hasen.substack.com/p/never-ending-stream-of-bugs</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Mon, 17 Feb 2025 09:18:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Az4m!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd038f74a-8d40-45f8-92cf-f96bf834430b_350x350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>"Never ending stream of bugs" is a surprisingly common problem in many software development shops.</p><p>It might seem like an intractable problem, but it&#8217;s actually not.</p><p>It&#8217;s solvable, but you have to tackle it on several different fronts:</p><ul><li><p>Application Specs</p></li><li><p>Code Architecture</p></li><li><p>QA Automation</p></li><li><p>Development Environment</p></li></ul><p>Most bugs happen because:</p><ul><li><p>Specs are not well understood</p></li><li><p>Architecture is confusing</p></li></ul><p>Most bugs can't get fixed because:</p><ul><li><p>Manual testing is near impossible</p></li></ul><h2>(1) Application Specs</h2><p>If no one knows what the program is supposed to do, forget it. Bugs will never get fixed, and QA will never get automated. This is what you should focus on first.</p><p>Specs should be clear but not verbose. No one wants to read half a page when a sentence would suffice.</p><p>Do not let business people write them! The chief architect or tech lead should be writing them.</p><p>Specification documents are an artifact that engineering is responsible for.</p><p>The specs will derive both the code architecture and QA automation.</p><h2>(2) Code Architecture</h2><p>The ground truth about computers is they can only do two things: (a) Process data (b) Move it around (I/O)</p><p>Code Architecture should reflect this. No crazy abstractions. No service providers. No business domain.</p><p>Do not model the problem. Model the solution.</p><p>Programmers need to understand how data is flowing through the system. It should be crystal clear. No obfuscation.</p><p>Every little thing you add to obfuscate data flow will be a time bomb waiting to explode into an intractable bug.</p><h2>(3) QA Automation</h2><p>Forget about unit tests. They are mostly a waste of time.</p><p>Allocate time and resource for an automated end-to-end suite.</p><p>How do you know whether the tests are good? Here are some heuristics:</p><ul><li><p>They simulate accurately how a user interacts with the program/system</p></li><li><p>They do not deal with system state. Only inputs that a user can give and outputs that a user can see.</p></li><li><p>Zero mocks! The test executes all the relevant code paths in the system in exactly the same way that would happen in production.</p></li><li><p>Never once do you need to change the test as a result of refactoring the system internals. Tests only change when you make changes to the user interactions (inputs/outputs).</p></li></ul><p>Given the above, it's easy to fix bugs with confidence.</p><ul><li><p>You can check the spec to see what's the expected behavior of these other things</p></li><li><p>You can run the test suite to see if anything broke as a result of your fix</p></li></ul><p>But there&#8217;s one last piece to the puzzle!</p><h2>(4) Development Environment</h2><p>All the above is almost useless if it's not easy for anyone on the team to run the QA test suite with one button (or one simple command).</p><p>Local development should be seamless. Every single programmer on the team should be able to run the entire system with one simple command. They should have a quick edit/run/debug cycle.</p><p>QA should also be trivially easy to run on staging too.</p><div><hr></div><p>When you don't have all these points taken care of, an endless stream of bugs is just par for the course. There's nothing you can do about it unless you fix the root cause.</p>]]></content:encoded></item><item><title><![CDATA[HCF EP 007: Prototyping with imported data]]></title><description><![CDATA[2025.01.16]]></description><link>https://hasen.substack.com/p/hcf-ep-007-prototyping-with-imported-data</link><guid isPermaLink="false">https://hasen.substack.com/p/hcf-ep-007-prototyping-with-imported-data</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Fri, 17 Jan 2025 10:52:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nUQt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Happy new year everyone. I hope you had a nice holiday and may this year of 2025 bring you great achievements in your life.</p><p>This is Episode 7 of HandCraftedForum.</p><p>In the past few episodes we talked about the project's basic structured and discussed some technical points.</p><p>In this episode I want to start prototyping the conversation view.</p><p>I would like to try out some ideas that allows us to have threaded conversations but still have a linear view of all the replies to a post.</p><p>Something like Twitter/X, where when you open a post, you see its parents, and its descendants (replies).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!nUQt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!nUQt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg" width="1199" height="716" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:716,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!nUQt!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45dca8f8-aa26-4e59-b006-e4f57aebb36e_1199x716.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>X is not very good at showing you all the replies, specially when they are threaded. You have to keep drilling. Instead I think we could show all the replies to a post, flattened.</p><p>I don't like threaded views line Reddit and Hacker News, because replies to top comments get shown before replies that would otherwise rank higher; whether you are sorting by date or by some kind of score.</p><p>However, I do like having comments that are replies to other comments, show a link to the comment they are replying to. I think Discourse does that. I think it's a good idea and we will steal it.</p><h2><strong>Data Model</strong></h2><p>To keep track of parent-child relationships between posts and their replies, we'll add a ParentPostId to the Post struct, and we'll use an index to keep track of replies.</p><p>Since we have no real data yet, we don't need to migrate anything; we just add a migration process to reset the posts bucket, and now we can change the packing function for Posts without incrementing the version.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!X2eZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!X2eZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg" width="1200" height="292" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:292,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!X2eZ!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34f85e5c-080e-48ad-9966-ff93520cd6d4_1200x292.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">app.go</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Qdj7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Qdj7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg" width="1170" height="766" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:766,&quot;width&quot;:1170,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Qdj7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd26cb6b-5c21-4245-9f2b-6dafd51cfbb4_1170x766.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">posts.go</figcaption></figure></div><p>To keep track of parent child relationships, we declare an index from int to int.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!9MoF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!9MoF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg" width="1200" height="429" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:429,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!9MoF!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F412bbda6-9236-4aa1-87fa-b862f354638f_1200x429.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">posts.go</figcaption></figure></div><p>This index can be used to get both all the ancestors of a post and all of its descendants, iteratively instead of recursively.</p><p>Remember that the index is a two way multi-map between terms and targets: If the term points to the direct parent id, then the target will point to the direct children ids. Since the term points to all its ancestors, then the target will point to all of its descendants.</p><p>To explain a bit further (feel free to skip this paragraph if you already understand): <br>- The parent id is always on the post<br>- When we first post a comment A without a parent, there's nothing for it in the index.<br>- When we then post a reply B to A, we store [A] as the term for B.<br>- When we post C as a reply to B, we get the list of ancestors of B, which is [A], and add [B] to it, and store it as the list of parents for C.<br>- When we post D as a reply to C, we get the list of ancestors of C, which is [A, B], and we add C to it and store it as the list of ancestors for D. Storing a list of terms for a target basically adds entries of the form (term, priority, target) for each of the terms. If we ignore the priority and the ordering, you can think of the index this way:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!jtc7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!jtc7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg" width="1199" height="853" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:853,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!jtc7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c8d2c13-543f-4888-8a23-5a5516020758_1199x853.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Think of it as a table that we iterate and filter. It's clear that given key 'B' we can find all the ancestors and descendants by just iterating and filtering.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!KBjM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!KBjM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg" width="390" height="519.6725440806046" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1058,&quot;width&quot;:794,&quot;resizeWidth&quot;:390,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!KBjM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b2d07f8-38e2-4920-87af-cb5ec45b291f_794x1058.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So to implement writing to this index, we look at the parent id, and if it's set, we retrieve the list of the parents parents from the index, add the direct parent id to the list, then write that as the set of terms for the post id.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!2-33!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!2-33!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg" width="1200" height="568" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:568,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!2-33!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5eed1c3-fc1e-4d39-a81f-0819b787b840_1200x568.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>populating the post replies index</p><p>We will see below how to query this index for the ancestors and replies.</p><h2><strong>Importing Data</strong></h2><p>Now, in order to start prototyping the UI, we need to have some conversation data. I don't want to create the conversations manually, nor do I want to generate them.</p><p>I did a bit of research to see if we can find a dump of some conversation data from HN or some mailing list. Turns out, HN has an API to get conversation data:</p><p><a href="https://github.com/HackerNews/API">https://github.com/HackerNews/API</a></p><p>I wrote some code to "import" a thread recursively by the id of the root post. The code does the import in two stages: first stage downloads all posts from the API, second stage populates our DB with all the downloaded posts.</p><p>We keep the post ids from HN. Since this is temporary data, we don't care about maintaining an auto increment id for the posts. This also allows us to import multiple times; for example, if we fix our import code, or our index updating code, we don't have to do anything to the posts themselves.</p><p>The only other note worthy thing item here is that I also download posts locally so that we don't touch the API more than needed. If we already downloaded a post, we don't have to download it again.</p><p>The importer is an executable package <code>forum/hn_importer</code>. The command takes one argument: the post to download.</p><pre><code><code>go run forum/hn_importer 24649786</code></code></pre><h2><strong>API / UI to view a single post</strong></h2><p>Having imported some posts, let's verify it by implementing a simple view page.</p><p>On the server side, we'll define a function that retrieves a single post by id</p><pre><code><code>
type PostQuery struct {
    PostId int
}

type PostResponse struct {
    Post Post
}

var PostNotFound = errors.New("PostNotFound")

func GetPost(ctx *vbeam.Context, req PostQuery) (resp PostResponse, err error) {
    if !vbolt.Read(ctx.Tx, PostsBkt, req.PostId, &amp;resp.Post) {
        err = PostNotFound
    }
    return
}
</code></code></pre><p>On the client side we add a router entry so that navigating to <code>&#8220;/item/:id&#8221;</code> shows a page with the post's content.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!AQ4K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!AQ4K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg" width="1200" height="229" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:229,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!AQ4K!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5e3ce2d5-d749-42c3-8aed-0e9fdee7cf6f_1200x229.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>We'll define two functions: one to fetch the post, one to view it.</p><pre><code><code>export const itemHandler = {
    fetch: fetchPostItem,
    view: viewPostItem
}

async function fetchPostItem(route: string, prefix: string) {
    const postId = vlens.intUrlArg(route, prefix)
    return server.GetPost({PostId: postId})
}

const clsPostPlain = vlens.cssClass("post-plain", {
    // .. snipped
})

function viewPostItem(route: string, prefix: string, data: server.PostResponse) {
    return &lt;div class={clsPostPlain}&gt;
        &lt;p&gt;
            {data.Post.Content}
        &lt;/p&gt;
        &lt;a class="permalink" href=/__u/hasen.substack.com/%7B%22/item/%22 + data.Post.Id}&gt;Permalink: {data.Post.Id}&lt;/a&gt;
    &lt;/div&gt;
}</code></code></pre><p>Now we can navigate to <code>&#8220;/item/&lt;id&gt;&#8221;</code> for some id to check how the content looks like.</p><p>Unfortunately I did not create a screenshot of what that would look like, so I can't really show it. I could show you the API response but you can already imagine what it would look like.</p><p>Originally I had intended to stream myself programming this. The idea was that I'd later cut things from the stream and share them here. However, the stream was not setup properly: half the screen was out of view, and it got cut abruptly half way through.</p><p>At any rate, the above is just the basic skeleton. We want to expand it so that the page shows not only the post itself, but all of its ancestors and all of its descendants.</p><h2><strong>API/UI to view all parents and replies</strong></h2><p>One of the major points I want to impart on you in this article is how we design the API response.</p><p>Take a look at the very first sketch in this article and consider what kind of data we need to implement it.</p><p>What we have is a list of posts. Depending on what we decide to add to the UI, we'll need some additional metadata about the post. For now, I want to display the username and the number of replies so the user can decide whether they want to "zoom in" on the specific comment or not.</p><p>Here's my API design:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!zKH8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!zKH8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg" width="1200" height="504" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:504,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!zKH8!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F94a4b94f-f007-4fef-a03a-8634dc0739bb_1200x504.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Notice the response is just a container for basic types we already have.</p><p>This is in contrast to what most people would do, which is design a post object modeled around the UI.</p><p>They will think: the UI to view the post needs to know the username and the number of replies so let's create this response model this way:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!0ueu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!0ueu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg" width="1200" height="465" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:465,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!0ueu!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc07a1be9-428d-4c65-94cb-8665ed95aa20_1200x465.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Notice the comment in scary uppercase telling you that this is a bad idea.</p><p>Why is it a bad idea? Because it's creating additional work for yourself, with no return on investment. You have to write code on the server side to transform a regular Post object into a UIPostModel. This is not a useful data transformation. The data is already available in the Post object. Creating this parallel object that is very similar but also subtly different serves no purpose. The data provided is the same; it's just arranged in a different way.</p><p>What's the purpose of this re-arrangement? Nothing.</p><p>So many people hold in their minds a bunch of bogus notions about the problem domain and how your object model should reflect the business rules. Just pure non-sense.</p><p>The UI needs to display a list of posts in a specific order, so just provide the order in a list, and then provide the id -&gt; post mapping.</p><p>The post has a user id and you need to get the username? Just slap another map. You need the number of replies to each post? Just slap another map.</p><p>Now the implementation is pretty straight forward. The list of posts is as follows:</p><ul><li><p>Parent posts</p></li><li><p>Self (the requested post id)</p></li><li><p>Replies</p></li></ul><p>So we start by reading these post ids off the database in that order (the post replies index)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!SgDz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!SgDz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg" width="1200" height="730" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:730,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!SgDz!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924c270e-25c5-434b-8c88-ce721105ad39_1200x730.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now <code>resp.PostIds</code> is the list of post ids we need to load. So we just load them! We already saw in a previous episode that we have a helper function that does just that.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!hpAM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!hpAM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg" width="1200" height="356" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:356,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!hpAM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F39505337-20a3-435c-9928-d1a8200ed7f7_1200x356.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now we use the counting feature from the index to get the number of replies.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!UrXO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!UrXO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg" width="1199" height="597" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/af354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:597,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!UrXO!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf354252-f632-4c03-819e-34d0eb911a43_1199x597.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now we load the users. This is the "trickiest" part of the code because it tries to avoid loading a user that's already been loaded.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!t2vV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!t2vV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg" width="1200" height="527" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:527,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!t2vV!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab52fe92-74ab-4a9a-ad19-430139d39084_1200x527.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This gives us all the data we need for the UI as we are prototyping.</p><p>Here's what the prototype UI looks like</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!m2lj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 424w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 848w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 1272w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!m2lj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png" width="422" height="479.18029115341545" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/69033786-09f5-4387-a8da-79099c653009_893x1014.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1014,&quot;width&quot;:893,&quot;resizeWidth&quot;:422,&quot;bytes&quot;:258601,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 424w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 848w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 1272w, /__u/substackcdn.com/image/fetch/$s_!m2lj!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69033786-09f5-4387-a8da-79099c653009_893x1014.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Conversation UI prototype (data imported from HN)</figcaption></figure></div><p>The UI code is pretty straight forward and I don't think there's anything about it that is new or remarkable to make it worthy of showing here.</p><p>Refer to the code attached to this episode.</p><p>Here's a demo of importing posts from HN and viewing it in the UI</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;7b766739-b455-49c9-a22c-39c44bec02cd&quot;,&quot;duration&quot;:null}"></div><h2><strong>The perils of Clean Code adjacent paradigms</strong></h2><p>What do you notice about our implementation?</p><p>The same "Post" type is used for the storage layer, the application code, the API, the UI.</p><p>This should not come off as remarkable, it's just straight forward common sense.</p><p>However, a lot of "programming education" material out there teaches people a very different thing.</p><p>They teach people to completely isolate the different components of the system, and to separate the data types across the different components, so each component has its own representation of the data. They also teach you to use a "Data Transfer Object" to move data between layers.</p><p>So you would have a "Repository" layer that has its own representation of "Post" that it stores and loads from the database, and a "PostModel" object that it exposes to the outside world.</p><p>You would also have a "Domain" layer with its own representation of "Post". It would use the "repository.PostModel" to communicate with the Repository layer, and use another "api.PostModel" to communicate with the outside world through the API.</p><p>The UI will also have its own representation of the post, perhaps even several representations depending on context: PostViewModel, PostEditForm. It will have a convertor for each such type to and from the api PostModel.</p><p>If you follow these ideas, you will have to write tons of code. All of it noisy boilerplate that happens to be a great minefield for bugs to hide.</p><p>We're having none of it. This alone means we can be several times more productive than someone who programs using such paradigms. At least 3x more productive, as a conservative estimate.</p><div><hr></div><p>Download the code: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP007.zip">EP007.zip</a></p><p>View the code online: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP007">HandCraftedForum/tree/EP007</a></p>]]></content:encoded></item><item><title><![CDATA[HFC EP 006: Collapsing code paths]]></title><description><![CDATA[2024.12.18]]></description><link>https://hasen.substack.com/p/hfc-ep-006-collapsing-code-paths</link><guid isPermaLink="false">https://hasen.substack.com/p/hfc-ep-006-collapsing-code-paths</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Sun, 05 Jan 2025 15:14:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is Episode 6 of HandCraftedForum.</p><p>In the previous episode (<a href="/__u/hasen.substack.com/p/hcf-ep-005-cursor-based-pagination">EP005</a>), I pointed out that we have a problem with code duplication when it comes to fetching posts by hashtag and by userid.</p><p>This again was not one of the planned topics for the series but I think it's worth a dedicated episode.</p><p>We have two almost identical code paths that are only different in a couple of small details.</p><p>We have two features:</p><ul><li><p>Find posts by hashtag</p></li><li><p>Find posts by userid</p></li></ul><p>For each we use a special Index object. The hash tag index uses the hashtag (string) as the key, while the user posts index uses the userid (int) as the key.</p><p>Because the index object is different, we have two different RPCs to call:</p><ul><li><p>PostsByHashtag</p></li><li><p>PostsByUser</p></li></ul><p>They both return the same thing, but have different inputs.</p><p>The input for PostsByUser looks like this:</p><pre><code><code>struct {
    UserId int
    Cursor []byte
}</code></code></pre><p>The input for PostsByHashtag looks like this:</p><pre><code><code>struct {
    Hashtag string
    Cursor  []byte
}</code></code></pre><p>As you can see, each has two parameters: cursor, and the input to the index. Each chooses to call the input by a different name, and of course, the input has a different type: for one it's a string, for the other it's an int.</p><p>The functions themselves are almost identical though. They only differ in the name of the input parameters, the index they use, and the query term they pass to the index.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Q9Z_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Q9Z_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg" width="1200" height="446" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:446,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Q9Z_!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a2f3076-5001-42d7-8c00-037352808329_1200x446.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now, this divergence on the server side will cause a divergence on the client side.</p><p>The frontend code has two divergent but almost identical blocks of code to implement the fetching of posts either by userid or by hashtag.</p><p>Fortunately, the response from both RPCs is the same, allowing us to use the same state object to track the post listing itself:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!FUKf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!FUKf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg" width="928" height="786" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:786,&quot;width&quot;:928,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!FUKf!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15738834-f99b-4482-aa44-7b7445aa6d02_928x786.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But the fetching of more posts is duplicated in two almost identical functions</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!_KOu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!_KOu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg" width="1200" height="298" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:298,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!_KOu!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6846d8ff-cafe-407c-b1cd-8ec7019959cc_1200x298.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The fragment where we display posts and show the "More" button is also almost identical but divergent</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!zB_K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!zB_K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg" width="1080" height="607" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:607,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!zB_K!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F689be44e-0c7c-4f22-b7cb-869f74f1400e_1080x607.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The starting point for this divergence was using two different index objects to answer each query, and each index using a different type for the query term. This resulted in two different server side procedures to perform the query, each takes a different request object that specifies the query term using a differently named field using a different type. This results in the client side having to implement "fetch more" in two separate functions, one for each backend procedure. Finally culminating in the post list and "more" button fragment to also diverge in two place, each one binding itself to its corresponding callback handler.</p><h2><strong>Naive approach</strong></h2><p>The naive approach is to notice the duplicate pattern, extract helper functions that take parameters with "holes" in them, and the caller fills the hole with the appropriate information.</p><p>In other words, create parameterized versions of all the above duplicated code fragments.</p><p>Something like this:</p><pre><code><code>function viewPostsWithFetchMore&lt;P&gt;(form: Form, param: P, fetchFn: (p: P, form: Form) =&gt; any) {
    return &lt;&gt;
        {viewPosts(form.posts)}
        {form.cursor &amp;&amp; &lt;button disabled={form.sending}
            onClick={vlens.cachePartial(fetchFn, param, form)}&gt;More&lt;/button&gt;}
    &lt;/&gt;
}</code></code></pre><p>If you do this, the call sites can transform this way:</p><pre><code><code>// Inside the hashtag search view function
viewPostsWithFetchMore(form, hashtag, fetchMoreByHashtag)

// Inside the user posts view function
viewPostsWithFetchMore(form, userId, fetchMoreUserPosts)</code></code></pre><p>Now, the process can be repeated for the fetch function itself. We extract the common bits to a function with holes, and we supply the holes</p><pre><code><code>async function fetchMoreUserPosts(userId: number, form: Form) {
    return fetchMoreFromServer(form, { UserId: userId, Cursor: form.cursor }, server.PostsByUser)
}

async function fetchMoreByHashtag(hashtag: string, form: Form) {
    return fetchMoreFromServer(form, { Hashtag: hashtag, Cursor: form.cursor }, server.PostsByHashtag)
}

async function fetchMoreFromServer&lt;P&gt;(form: Form, params: P, remoteFn: (p: P) =&gt; Promise&lt;rpc.Response&lt;server.Posts&gt;&gt;, ) {
    form.sending = true
    let [resp, err] = await remoteFn(params)
    form.sending = false
    vlens.scheduleRedraw()
    if (resp) {
        form.posts.push(...resp.Posts)
        form.cursor = resp.Cursor
    } else {
        form.error = err
    }
}</code></code></pre><p>The same thing can be done on the server side: extract the common parts of the duplicated code into a function that takes parameters for the different parts that you fill at the call site with the appropriate values:</p><pre><code><code>type ByUserReq struct {
    UserId int
    Cursor []byte
}

func PostsByUser(ctx *vbeam.Context, req ByUserReq) (resp Posts, err error) {
    return PostsBy(ctx, req.Cursor, req.UserId, UserPostsIdx)
}

type ByHashtagReq struct {
    Hashtag string
    Cursor  []byte
}

func PostsByHashtag(ctx *vbeam.Context, req ByHashtagReq) (resp Posts, err error) {
    return PostsBy(ctx, req.Cursor, req.Hashtag, HashTagsIdx)
}

const Limit = 2
func PostsBy[T comparable](ctx *vbeam.Context, cursor []byte, term T, index *vbolt.IndexInfo[int, T, time.Time]) (resp Posts, err error) {
    var window = vbolt.Window{
        Limit:     Limit,
        Direction: vbolt.IterateReverse,
        Cursor:    cursor,
    }
    var postIds []int
    resp.Cursor = vbolt.ReadTermTargets(
        ctx.Tx,   // the transaction
        index,    // the index
        term,     // the query term
        &amp;postIds, // slice to store matching targets
        window,   // query windowing
    )
    vbolt.ReadSlice(ctx.Tx, PostsBkt, postIds, &amp;resp.Posts)

    generic.EnsureSliceNotNil(&amp;resp.Posts)
    generic.EnsureSliceNotNil(&amp;resp.Cursor)
    return&#9;
}</code></code></pre><p>You could go even <em>crazier</em> with the object oriented approach, create a base class and inherit into sub classes. I hate to even <em>think</em> about what that would look like.</p><p>Anyway, this approach, of extracting common code parts into a function with holes, is not very good actually. On one hand, yes, we are reducing code duplication, but on the other hand, we are creating new concepts, and now, someone trying to read the code has to understand why we have these generic higher order functions that take other functions and parameters as inputs.</p><p>Before, we had two parallel code paths. After, we still effectively have two code paths, but they are heavily intertwined.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!DUcb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!DUcb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg" width="1200" height="994" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:994,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DUcb!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1159573-94db-4472-a14d-0403ff6bbba2_1200x994.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Simple approach</strong></h2><p>We can notice the reason for the code paths divergence is us having two different index objects, using a different type for the query term.</p><p>This can all go away if we just use the same index for both types of queries. After all, there's nothing special about querying by user id that makes it different from querying by hashtag. The fact that user ids are numeric is largely irrelevant, because we will serialize both to a byte buffer before we pass it to the index. The only thing we need to worry about is avoiding overlap between the two types of queries, and this is trivial to achieve by using a prefix. This allows us to use different types of prefixes for different types of queries that we will add in the future.</p><p>For instance, we can use '#' for hashtags and '@' for user ids, or we can use 't:' for hashtags and 'u:' for user ids. It does not really matter what we use. It just matters that we are consistent.</p><p>So, we start by unifying the two index objects into one:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!iVdc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!iVdc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg" width="1199" height="264" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:264,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!iVdc!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6199c73f-9ac3-4342-a9c4-83680711ee99_1199x264.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Then, when it comes time to create the post and update the index, we combine the tags terms and the user term into one list and set them on the new index:</p><pre><code><code>terms := make([]string, 0, len(post.Tags)+1)
generic.Append(&amp;terms, fmt.Sprintf("u:%d", post.UserId))
for _, tag := range post.Tags {
    generic.Append(&amp;terms, "t:"+tag)
}
priority := post.CreatedAt
vbolt.SetTargetTermsUniform(
    ctx.Tx,   // transaction
    PostsIdx, // index reference
    post.Id,  // target
    terms,    // terms (slice)
    priority, // priority (same for all terms)
)</code></code></pre><p>Since we want to collapse the code paths, we also update the querying procedure, along with its inputs and outputs, in a way that helps the client code be more uniform.</p><pre><code><code>type PostsQuery struct {
    Query  string
    Cursor []byte
}

type PostsResponse struct {
    Posts      []Post
    NextParams PostsQuery
}</code></code></pre><p>Now the posts query is basically the same as the previous two functions, except it does not care at all what type of terms you're looking for: the operation is all the same.</p><pre><code><code>func QueryPosts(ctx *vbeam.Context, req PostsQuery) (resp PostsResponse, err error) {
    var window = vbolt.Window{
        Limit:     Limit,
        Direction: vbolt.IterateReverse,
        Cursor:    req.Cursor,
    }
    var postIds []int
    resp.NextParams = req
    resp.NextParams.Cursor = vbolt.ReadTermTargets(
        ctx.Tx,    // the transaction
        PostsIdx,  // the index
        req.Query, // the query term
        &amp;postIds,  // slice to store matching targets
        window,    // query windowing
    )
    vbolt.ReadSlice(ctx.Tx, PostsBkt, postIds, &amp;resp.Posts)
    generic.EnsureSliceNotNil(&amp;resp.Posts)
    generic.EnsureSliceNotNil(&amp;resp.NextParams.Cursor)
    return
}</code></code></pre><p>Next, we update the client side code to use 't:' and 'u:' prefixes on initial page load</p><pre><code><code>export async function fetchUserPosts(route: string, prefix: string) {
    const params = vlens.urlParams(route);
    const userId = vlens.intParam(params, "user_id", 0);
    return server.QueryPosts({ Query: 'u:' + userId, Cursor: "" })
}

export async function fetchByHashtag(route: string, prefix: string) {
    const params = vlens.urlParams(route);
    const hashtag = params.get("hashtag") ?? "";
    return server.QueryPosts({ Query: 't:' + hashtag, Cursor: "" });
}</code></code></pre><p>Now because the response contains `NextParams`, the client side "fetch more" can be really simple. It doesn't need to care about the search type, or what page we are currently on.</p><p>It just stores the NextParams on initialization, and keeps storing it after every "fetch more" operation</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!DiH6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!DiH6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg" width="1008" height="774" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:774,&quot;width&quot;:1008,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DiH6!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb3ec3a6-5158-49a3-a469-acbb73af2699_1008x774.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The callback function for the "More" button can be really simple now</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!J7o3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!J7o3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg" width="1199" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!J7o3!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6485b053-3f82-44ff-9683-3c45670c9c5a_1199x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The fragment to display the list of posts followed by the "More" button is now also greatly simplified.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!qc86!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!qc86!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg" width="1199" height="646" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:646,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!qc86!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1021d2ea-47fb-430d-a484-3788e5a15f83_1199x646.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now we've not only eliminated duplicate code; we've collapsed the code paths. The divergent part is very small: just the fetching function for different pages: fetchUserPosts and fetchHashtagPosts. Everything else after that is uniform.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!HYnC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!HYnC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg" width="1200" height="609" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:609,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!HYnC!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c512201-6050-460f-b526-558fd9ad4942_1200x609.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This way of collapsing code paths is really powerful but it is hardly taught anywhere. It allows us to add more features without adding any complication.</p><p>Suppose, for instance, that we wanted to also index posts by year and year-month combinations. With this approach to collapsing code paths, we can easily just add more terms this way:</p><pre><code><code>    generic.Append(&amp;terms, fmt.Sprintf("y:%d", post.CreatedAt.Year()))
    generic.Append(&amp;terms, fmt.Sprintf("m:%s", post.CreatedAt.Format("2006.01")))</code></code></pre><p>Where as, if we had taken the naive approach described above, where we painstakingly create generic functions with holes to be filled in by the caller, it would be more complicated: we'd have to create a new index (or two), new functions to query by dates, new "fetch more" client side functions to query by dates, etc. We'd just have a lot more boilerplate code to write. Which means increased friction towards anything we want to do to improve the querying function.</p><p>For example, we might consider supporting queries by "range". Since the Index is backed by a B-Tree, it's not difficult to see how we can query by a range of query terms, for example from 'm:2024.05' to 'm:2024.10'.</p><p>We can, for example, change the PostsQuery to be more like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!0P3X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 424w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 848w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 1272w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!0P3X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png" width="668" height="274" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:274,&quot;width&quot;:668,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 424w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 848w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 1272w, /__u/substackcdn.com/image/fetch/$s_!0P3X!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2f11436-8176-41b7-9fa4-76eb35e2186d_668x274.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We'd interpret `To` being unset to mean it's not a query range but a normal query. And because the response object will include the `NextParams`, the client code for "fetch more" does not have to change at all.</p><p>Having just one index object and one function to query it, means we can add features of this type without a lot of trouble.</p><p>Had we kept around many index objects and many querying functions, it would be a lot more effort to support this for all of them. We'd have more functions to change, more "query input" types to change, and more UI code to change.</p><p>Next, imagine if we wanted to support querying by multiple terms, some of which could be a range, and some of which would be just normal terms. For example, let's say we combine 'u:2' with 'm:2024.12' to get all the posts by the given user in the given month.</p><p>Again, we'd just have one type to change and one function to update. The input parameters could be change to look like this:</p><pre><code><code>type QueryTerm struct {
    From string
    To   string
}

type PostsQuery struct {
    Terms  []QueryTerm
    Cursor []byte
}</code></code></pre><p>The function logic would be somewhat involved, but it would be tractable, because it only deals with one index.</p><p>Had we gone the naive approach and kept several index objects and several query param structs for each query type, it would be a lot more complicated. It would probably take a Herculean effort to to implement a function that lets you query by multiple query terms, because each query term would hit a different index, and you'd have to keep track of the type of each query parameter and the index associated with it.</p><p>Maybe something crazy like this:</p><pre><code><code>type HashtagQuery struct {
    Hashtag string
}

type ByUserQuery struct {
    UserId int
}

type ByDateQuery struct {
    From time.Time
    To   *time.Time
}

type PostQuery interface {
    IsPostQuery() bool
    CountItems(tx *bolt.Tx) int
    PerformQuery(tx *bolt.Tx, cursor []byte, limit int) []int
    // TODO: add whatever other methods are needed
    // for the implementation of QueryPosts to work
    // with a list of PostQuery items
}

// TODO: implement PostQuery for HashtagQuery, ByUserQuery, ByDateQuery

func ParseQueryString(query string) []PostQuery {
    // somehow parse a complex query string into a list 
    // of query items that implement the interface
}

func QueryPosts(tx *vbolt.Tx, query []PostQuery) []Post {
    // perform the query
}</code></code></pre><p>While this example is hypothetical, it's not mere speculation.</p><p>This is actually the kind of programming that happens in the wild. This is what senior engineers in most web companies spend their time doing. A team of ~6 engineers would spend two~three months to make this kind of query work.</p><p>As a side note: the interface PostQuery exists as a placeholder (a template with holes to be filled), and the content of the interface depends on the implementation details of QueryPosts as it attempts to combine multiple queries. I put `CountItems` in it because in my head I imagine it as part of the implementation, but maybe what we would need is something else.</p><p>Now, I'm not saying that implementing "query by multiple query terms" would otherwise be easy. It might be easy or it might be difficult. But either way, implementing it on <em>one</em> index object with one unified query type, is <em>for sure</em> going to be an order of magnitude simpler than implementing it on multiple indexes and multiple types of queries.</p><p>With our uniform structure, we only have to solve the basic problem: if we can do it on one index with one type of query term, we can trivially do it for all type combinations.</p><p>Where as, with the naive approach, we first have to figure out how to combine multiple query terms for one index, and then after we implement it and test it, we have to figure out how to generalize it to querying multiple indexes.</p><p>Should we actually try to implement this? I haven't thought about this problem that deeply, but since it's an interesting challenge, I'll give it some thought. We'll need to have a good deal of sample posts to work with, because it'd be difficult to assess the effectiveness of our implementation when we only have ~20 posts or so.</p><p>Originally my plan was to delay the implementation of "Search" as far back as possible, but something is urging me to focus on it sooner. Maybe I'll try it for the next episode! No promises, but stay tuned!</p><h2><strong>Migration</strong></h2><p>We changed how the index stores and queries data, but the data we've already saved and indexed is not using this new index, so if we just run the local server now and try to fetch posts by users or by hashtags, we'd see nothing, because the index has no data.</p><p>We have two ways to solve this:</p><p>(1) We can delete the database file and let the server create a new one when it starts up. At this stage we don't have a lot of data, so we can just manually recreate some fake sample data like we did before.</p><p>(2) We can run a migration process on startup to re-index all the existing posts.</p><p>I chose the latter because I have a system for it. I add some code to the OpenDB function to "apply db processes". I give each migration/process a unique label prefixed with 'year-monthday-'. The migration system uses the label to ensure the migration process is only ever executed once on the database, so this function is safe to keep around for a long time. I'll try to cover the migration system a bit more in depth at a later time.</p><pre><code><code>    vbolt.ApplyDBProcess(db, "2024-1217-unify-post-index", func() {
        // delete old indexes
        vbolt.WithWriteTx(db, func(tx *vbolt.Tx) {
            tx.DeleteBucket([]byte("user-posts"))
            tx.DeleteBucket([]byte("hashtags"))
            tx.Commit()
        })
        log.Println("Deleted old indexes")
        // populate the new index
        vbolt.TxWriteBatches(db, PostsBkt, BatchSize, func(tx *vbolt.Tx, batch []Post) {
            for _, post := range batch {
                UpdatePostIndex(tx, post)
            }
            vbolt.TxCommit(tx)
        })
    })</code></code></pre><h2><strong>Demo</strong></h2><p>With that, here's a demo of the application after our changes. You can see from the network tab that we are using the new unified procedure.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;cdf84c51-b792-4282-bc25-4a38973c1205&quot;,&quot;duration&quot;:null}"></div><p>Note: in the process of making this work, I had to fix a few bugs in vbolt. Make sure to run `go mod download` to pull in the dependencies.</p><p>Download the code: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP006.zip">EP006.zip</a></p><p>View the code online: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP006">HandCraftedForum/tree/EP006</a></p>]]></content:encoded></item><item><title><![CDATA[HCF EP 005: Cursor based pagination]]></title><description><![CDATA[2024.12.16]]></description><link>https://hasen.substack.com/p/hcf-ep-005-cursor-based-pagination</link><guid isPermaLink="false">https://hasen.substack.com/p/hcf-ep-005-cursor-based-pagination</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Sat, 04 Jan 2025 08:23:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!bBAx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is Episode 5 of HandCraftedForum.</p><p>In these first few episodes we're doing an introduction to our homegrown mini framework and data storage layer that we'll be using through out the project.</p><p>In the previous episode, we wrote the backend code for creating users and posts and indexing posts by hashtag, but we did not implement the UI.</p><p>In between the last episode and this episode I implemented the basic UI and fixed a minor bug along the way.</p><p>There isn't anything special in the UI code that we didn't previously cover so I did not think it's worth much to explain in detail. The code will be attached at the end of the article as usual, so make sure to check it out. Here's a quick demo of the UI.</p><ul><li><p>Creating a user account</p></li><li><p>Creating a post on behalf of the user</p></li><li><p>Finding posts by hashtags</p></li><li><p>Finding posts by users</p></li></ul><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;e6c6894d-4603-4d01-bcea-48689641e8fe&quot;,&quot;duration&quot;:null}"></div><p>One small change I did to the backend code is to read entries off the index in reverse. We want to see newest posts first, but the index sorts by the creation time in ascending order.</p><p>To make this happen, I had to update VBolt to support iterating the index in reverse order.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!bBAx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!bBAx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg" width="1199" height="651" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:651,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!bBAx!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc157b03a-88e2-4178-89d2-1c0ad991688f_1199x651.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When the client creates a new post, it just pushes it to the top of the list:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!BtOh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!BtOh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg" width="1200" height="463" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:463,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!BtOh!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ff94002-74bd-410a-93d8-2c372f7b5c66_1200x463.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The topic of this episode was not planned, but I think it's a neat little thing that just comes out of using B-Trees for indexing, and being able to directly make use of its properties, and would probably be quite painful to mimic using higher level concepts like relational tables.</p><p>In the previous episode I did a quick overview of the concept behind the "Index" storage and explained that it stores tuples in a list that gets sorted by the underlying B-Tree.</p><p>Here's one way to visualize how the sorted term -&gt; target mapping is stored in the B-Tree. It's basically a list of `[]byte` keys, each key is internally composed of a three-tuple: (term, priority, target). They are arranged such that when the B-Tree sorts these keys in byte order, it's equivalent to sorting the list of tuples in order.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!DpuP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!DpuP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg" width="1200" height="812" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:812,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DpuP!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9ea5464-db4f-49f3-8cfd-a54e1c2b443c_1200x812.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To find targets for term 'A', we iterate the B-Tree on keys that start with "A" by seeking to the first key that has the prefix (A ...), then iterating one by one until we find an entry that does not have that prefix.</p><p>We can stop the iteration after N steps, and ask the B-Tree to return the full byte representation of the key it stopped at.</p><p>This will serve as a "cursor". We can encode it to base64 (or any ascii representation really) and use it later to continue the iteration where it stopped.</p><p>Compared to pagination by passing a page number, this has some advantages and some disadvantages. The advantage is that it performs better if you have thousands of elements to skip; it's more work for the computer to read hundreds of entries from the B-Tree only to drop them.</p><p>The B-Tree is really good at <em>jumping</em> to a particular key, but it's not very good at jumping N keys ahead. It has to move there one step at a time.</p><p>On the other hand, using a cursor means that as an end-user, we do not have random access: we can only grab the next page. If for some reason the user wants to skip to page 20, they have to hit "next page" 20 times, which is a lot more wasteful than the computer skipping 20 pages when reading the B-Tree.</p><p>Another advantage of the cursor is that it's more <em>stable</em>. It's like you left a bookmark where you were reading so you can come grab it again next time to continue where you left off. Even if items got added or deleted somewhere else in the list, you will still continue exactly where you left off.</p><p>If you are interested in the topic, you can lookup "cursor pagination vs offset pagination". Here's a Grok summary:</p><p><a href="https://x.com/i/grok/share/6pA5jISvoRiKDFrdwQerQkiNV">https://x.com/i/grok/share/6pA5jISvoRiKDFrdwQerQkiNV</a></p><p>The most suitable problem for cursor navigation is "infinite scrolling".</p><p>Now, here's how we can incorporate cursor pagination into our UI:</p><ul><li><p>Add a cursor field to the response and the request `Cursor: []byte`</p></li><li><p>Pass the cursor to form the request to the query function `vbolt.ReadTermTargets` and pass output cursor back to the response.</p></li><li><p>UI retains the cursor value</p></li><li><p>"fetch more" button sends the cursor value</p></li></ul><p>Here are the changes to the backend code:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!peal!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!peal!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg" width="1200" height="1147" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1147,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!peal!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5ef6d9a8-a36b-4502-9fed-7e218c7702f5_1200x1147.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We also update the PostsByHashtag function in the same way (not shown for brevity).</p><p>Here are the changes to the UI code:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!qjRW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!qjRW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg" width="1200" height="460" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:460,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!qjRW!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc4e557e-eea8-4bbb-be3e-03c9dbcb18f2_1200x460.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Notice how the callbacks for fetching by userid and fetching by hashtags are almost identical.</p><p>This is not very good. We'll discuss how to collapse these duplicate functions in the next episode, which although was not planned, is still a very good topic that deserves its own independent treatment, and not be shoved as a side quest in an article about cursor pagination.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;bd015901-c8a1-4ded-81ef-7e8727bef77c&quot;,&quot;duration&quot;:null}"></div><p>Now, if you watch closely, there's a bug: fetching more does not seem to be working for hashtags! Even though the response does contain the correct data!</p><p>What gives?</p><p>A little bit of debugging reveals that the code for listing the posts was using `data.Posts`, which is the posts from the initial page fetch, when it should have been using `form.posts`, which is the list we retain in the UI state.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!HNS7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!HNS7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg" width="1200" height="384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:384,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!HNS7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F473bab9e-da4d-425d-8304-4da9ab0b7ecc_1200x384.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is one of those problems that happens when you have mostly duplicate code doing the same things with slight variations! As you keep making changes, you have to remember to keep updating both places, and it's easy to forget things. Things are still small now so the mistakes are harmless and easy to find, but you can imagine how things would get out of hand as the combinations of code path increases exponentially.</p><p>In the next episode, we'll show how we collapse the code paths without resorting to OOP like abstractions. The solution will be so much better and so much simpler! Stay tuned!</p><div><hr></div><p>Here's the code for today's episode.</p><p>Note: I updated vbeam and vbolt. If you are pulling from github, make sure to run `go mod tidy` after pulling.</p><p>Download the code:</p><p><a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP005.zip">EP005.zip</a></p><p>View the code online:</p><p><a href="https://github.com/hasenj/HandCraftedForum/tree/EP005">HandCraftedForum/tree/EP005</a></p>]]></content:encoded></item><item><title><![CDATA[HCF EP 004: Indexing and Querying]]></title><description><![CDATA[2024.12.12]]></description><link>https://hasen.substack.com/p/hcf-ep-004-indexing-and-querying</link><guid isPermaLink="false">https://hasen.substack.com/p/hcf-ep-004-indexing-and-querying</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Fri, 13 Dec 2024 15:29:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cVdz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is Episode 4 of HandCraftedForum.</p><p>Last time I showed how to persist simple user account data to the database. This time I want to demonstrate indexing and querying, and since this is a "forum" project, let's do this with basic posts.</p><p>We'll do a very barebones kind of post. Think a "Tweet". Each post has nothing but the id of the user who posted, when he posted it, and the content of the post.</p><p>We'll use two indexes: one to keep track of posts by user id, and one to enable querying posts by hashtags, like the old Twitter.</p><p>We don't have proper sessions yet, so we'll make the username part of the data we submit, and we will not validate the authorization.</p><p>On the user list page, we'll turn each user to a link, when you click on it, it takes you to a page where you can post on that user's behalf. The page will have a list of recent posts by the user, and a textbox on the top to create a new post.</p><p>Then for each post, we'll extract hash tags, and create a link for each. When you click it, you see a page listing all posts that use this hashtag</p><p>We have basically two new pages, each one needs its own RPC to fetch data:</p><ul><li><p>List latest posts by user id</p></li><li><p>Find posts by hash tag</p></li></ul><p>We'll also need one procedure to create a post.</p><p>We'll start with the backend code first, because it's surprisingly straight forward.</p><p>First, define the post struct</p><pre><code><code>type Post struct {
    Id        int
    UserId    int
    CreatedAt time.Time

    Content string
}</code></code></pre><p>We also define a method to extract hash tags from content. You can checkout the implementation from the codebase attached at the end of this article.</p><p>Now we define three vbolt objects:</p><ul><li><p>A bucket to hold the posts themselves</p></li><li><p>An index to group posts by user id</p></li><li><p>An index to query posts by hash tag</p></li></ul><pre><code><code>var PostsBkt = vbolt.Bucket(&amp;dbInfo, "posts", vpack.FInt, PackPost)

// UserPostsIdx term: user id. priority: timestamp. target: post id
var UserPostsIdx = vbolt.IndexExt(&amp;dbInfo, "user-posts", vpack.FInt, vpack.UnixTimeKey, vpack.FInt)

// HashTagsIdx term: hashtag, priority: timestamp, term: post id
var HashTagsIdx = vbolt.IndexExt(&amp;dbInfo, "hashtags", vpack.StringZ, vpack.UnixTimeKey, vpack.FInt)</code></code></pre><p>One way to think of an index is as a bidirectional multi-map. You want to use a query term to find matching targets. Thus we use the terminology:</p><ul><li><p>Term: the search term you want to use to query for data</p></li><li><p>Target: the matching item that you're targeting for queries</p></li></ul><p>It's a multi-map, because each term can have multiple matching targets.</p><p>It's bidirectional, because you can go from term to targets, or from target to terms.</p><p>Structurally, it's a collection of entries, sorted in ascending order, each entry is composed of three elements: (term, priority, target). For each such entry, there's a mirror entry with (target, term, priority).</p><p>We can think of an index as an accelerator structure to help us quickly find items matching a query term, or we can think of it as a way to group elements under a group key. If we choose to think of it as a collection, the grouping key would be the term.</p><p>(NOTE: I have recently introduced the concept of "collection" into vbolt, but it's still in flux, and it overlaps very much with Index, so it might go away, or the API might get merged with the Indexing API, so I choose for now to not talk about it much).</p><p>Now, when we create a new Post, we save it to the posts bucket, and we also add its entry in the user posts index and the hashtags index.</p><p>In both indexes, we use the creation timestamp as the priority index. This means when we iterate matching posts, they will be ordered by creation time.</p><p>Updating the index happens by setting all the terms for a target, along with their priorities. This is the basic concept, but we have a few different functions for doing that, depending on the use case:</p><ul><li><p>We can set several terms, associating a different priority to each</p></li><li><p>We can set several terms, with the same priority</p></li></ul><p>We also have helpers to set one term (so we don't have to create a slice when calling the function), and we can ignore the priority parameter (using the zero value).</p><p>Here's the actual code to write the data to the bucket and indexes:</p><pre><code><code>vbolt.Write(ctx.Tx, PostsBkt, post.Id, &amp;post)

vbolt.SetTargetSingleTermExt(
    ctx.Tx,         // transaction
    UserPostsIdx,   // index reference
    post.Id,        // target
    post.CreatedAt, // priority
    post.UserId,    // term (single)
)

tags := ExtractHashTags(post.Content)
vbolt.SetTargetTermsUniform(
    ctx.Tx,         // transaction
    HashTagsIdx,    // index reference
    post.Id,        // target
    tags,           // terms (slice)
    post.CreatedAt, // priority (same for all terms)
)</code></code></pre><p>I agree the API is not very clean or consistent; hopefully it will be cleaned up in a new future release.</p><p>Now, to find posts by user id, we do the following:</p><pre><code><code>const Limit = 100
var window = vbolt.Window{Limit: Limit}
var postIds []int
vbolt.ReadTermTargets(
    ctx.Tx,       // the transaction
    UserPostsIdx, // the index
    req.UserId,   // the query term
    &amp;postIds,     // slice to store matching targets
    window,       // query windowing
)
vbolt.ReadSlice(ctx.Tx, PostsBkt, postIds, &amp;resp.Posts)</code></code></pre><p>We read the matching post ids to a list of numbers, then use this list of numbers to read the list of posts to a slice of <code>[]Post</code>.</p><p>Querying the index is basically the same:</p><pre><code><code>var postIds []int
vbolt.ReadTermTargets(
    ctx.Tx,      // the transaction
    HashTagsIdx, // the index
    req.Hashtag, // the query term
    &amp;postIds,    // slice to store matching targets
    window,      // query windowing
)
vbolt.ReadSlice(ctx.Tx, PostsBkt, postIds, &amp;resp.Posts)</code></code></pre><p>This is all the code we need to index and query data.</p><p>There's no "SQL" layer that the code has to go through. There's no "ORM". We don't <em>need</em> an ORM. We don't need to create a "Repository" interface; vbolt is already a suitable programmatic interface, with reasonably designed building blocks. We don't need to mock the database for testing.</p><p>If we want to do some automated testing, we can write test code that uses a temporary file as the database, and then let it run the code. Then we can verify the inputs and outputs of the system as a whole.</p><p>In fact, let's do just that. (I was not going to do it until I wrote the above paragraph).</p><p>First we setup the temporary test database.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!cVdz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!cVdz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg" width="1100" height="448" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:448,&quot;width&quot;:1100,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!cVdz!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F660645ce-7a07-4ede-97be-86f0ec3490fe_1100x448.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In case you are wondering what that OpenDB function is, I simply extracted the few lines we had inside MakeApplication</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!DIbj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!DIbj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg" width="950" height="534" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:534,&quot;width&quot;:950,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!DIbj!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F720f4402-3c6f-4b6c-b14b-e8d9293a87dd_950x534.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Back to the test, we specify a few test cases and a set of expected outputs:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!tHH9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!tHH9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg" width="1026" height="730" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:730,&quot;width&quot;:1026,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!tHH9!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa46b1b80-3794-4501-b103-017f0b781561_1026x730.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Then we execute the inputs and check the outputs</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!mNyf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!mNyf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg" width="1200" height="660" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:660,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!mNyf!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb389a93e-627f-4774-87bc-978830dbe305_1200x660.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We can run the tests, and they pass!!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!m7AH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 424w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 848w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 1272w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!m7AH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png" width="794" height="622" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:622,&quot;width&quot;:794,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 424w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 848w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 1272w, /__u/substackcdn.com/image/fetch/$s_!m7AH!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb73bc7ba-20fb-4f78-b8c0-7591976e064a_794x622.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now, you may wonder if it's really executing the tests! OK, we can easily add some logging and run the test command with `-v`</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!PT4G!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!PT4G!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg" width="1200" height="586" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:586,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!PT4G!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8e85bc-0a80-4de6-a3cd-52075257dae5_1200x586.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Then the output will be:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!sfom!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!sfom!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg" width="1200" height="1183" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1183,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!sfom!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbece3160-c074-4f5d-8d6e-1d7bd748bf27_1200x1183.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now that we have an automated test that confirms the code is working, we can start coding the frontend.</p><p>It should be pretty obvious what we need to do. There's nothing new here so I'm not sure how much explanation is required for the frontend code.</p><p>Just like last time, we define a route, we fetch data for said route, and we display the data.</p><p>We'll do that next episode, and we will add some basic session token management as well.</p><p>Hope you enjoyed this episode and found it useful.</p><p>Download the code: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP004.zip">EP004.zip</a></p><p>View the code online: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP004">HandCraftedForum/tree/EP004</a></p>]]></content:encoded></item><item><title><![CDATA[HCR EP 003: Data persistence API]]></title><description><![CDATA[2024.12.09]]></description><link>https://hasen.substack.com/p/hcr-ep-003-data-persistence-api</link><guid isPermaLink="false">https://hasen.substack.com/p/hcr-ep-003-data-persistence-api</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Tue, 10 Dec 2024 15:10:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Aj6R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is Episode 3 of the HandCraftedForum series, where I document the creation of a forum system using a mini web framework.</p><p>In the previous episode I walked you through setting up an empty project and creating a simple RPC on the server the the client can call, as well as basic UI interaction and state management.</p><p>In this episode I will show you how we do data persistence by continuing the basic auth implementation.</p><p>The purpose of this exercise is to demonstrate the basic structure of applications written in this framework. Once we are done this demonstration, we will begin building out the project at a faster pace, and I will not necessarily be showing all the steps in their tiny details, as writing up about everything is going to take considerable effort and slow down the project work itself.</p><p>So in these first few episodes, I want to give you the tools to understand the basic way we do things, so that you can follow along as we proceed further.</p><h1><strong>Native data persistence, indexing, and querying</strong></h1><p>Unlike most web frameworks, we are not going to be using an external database server that we connect to, instead, our database engine is a <em>library</em> that is embedded into the program itself. Sort of like SQLite, but not.</p><p>Also unlike most web frameworks, our data model is not relational, and our querying model is not SQL. Instead, we just call regular functions to persist data, persist indexing information, and load/query it.</p><p>In the SQL model, you send commands in a textual query language, which you must construct first, and send to the storage engine. Even in SQLite, that's what you do. The SQL engine then parses the query you sent and turns it into an execution plan, and executes it, usually in some kind of byte code virtual machine; at least, that seems to be what SQLite does.</p><p>Our data persistence models is completely different. We use a B-Tree based storage engine (BoltDB) to persist data in <em><strong>buckets</strong></em>, <em><strong>indexes</strong></em>, and <em><strong>collections</strong></em>.</p><p>You can think of a bucket as a persisted map: you put in the object Id, you get back a copy of the object.</p><p>An index is like a persisted bidirectional multi-map, but with a sorting key, and the ability to set all the search terms (keys) for a particular target.</p><p>A collection is a way to group a list of keys under a parent key, with an ordering key.</p><p>We have regular function to interact with these storage blocks:</p><ul><li><p>Buckets: Store/Load/Delete items by id</p></li><li><p>Collection: Add/remove (key, order) by id</p></li><li><p>Index: Set target terms, and iterate matching targets for a search term.</p></li></ul><p>A few other utility functions exist, but the above constitutes the core of our persistence API, and it suffices for almost everything you want to do in a web application.</p><p>To allow data to be persisted, it needs to be serialized. We define a serialization function using VPack.</p><p>This is what a typical serialization function would look like:</p><pre><code><code>type User struct {
    Id       int
    Username string
    Email    string
    IsAdmin  bool
}

func PackUser(self *User, buf *vpack.Buffer) {
    vpack.Version(1, buf)
    vpack.Int(&amp;self.Id, buf)
    vpack.String(&amp;self.Username, buf)
    vpack.String(&amp;self.Email, buf)
    vpack.Bool(&amp;self.IsAdmin, buf)
}
</code></code></pre><p>The <code>PackUser</code> function is used for both Serialization and Deserialization. You do not need to define these two separately.</p><p>Now that we have a type and a serialization function, we can define a bucket:</p><pre><code><code>var dbInfo vbolt.Info

var UsersBkt = vbolt.Bucket(&amp;dbInfo, "users", vpack.FInt, PackUser)
</code></code></pre><p>The first argument is an object that collects information about the database file: the list of buckets, indexes, and collections. This information will be used to initialize the database. We will see how in a bit.</p><p>The second parameter is a name for the bucket. You will almost never use this directly, but we still choose to give a short but meaningful name. The only actual requirement is for it to be unique.</p><p>The third and fourth parameters define the types of the "key" and "value" objects, and their serialization functions. <code>vpack.FInt</code> is the serialization function for integers that uses fixed width (8 bytes), as opposed to the regular <code>vpack.Int</code> which uses a special scheme that allows using the minimum number of bytes to store small number values.</p><p>The Bucket function takes packing functions, not types, as its parameters. The types for Keys and Values are derived from the packing functions.</p><p>Now, let's say we want to define an RPC to create a user with a username, email, and password.</p><p>We need a few more things in addition to the above:</p><p>We need a bucket store the password hash. Note: we do not put the hash in the User struct; we don't want that data to be sent to the browser when we read stuff from the <code>UsersBkt</code>.</p><pre><code><code>var PasswdBkt = vbolt.Bucket(&amp;dbInfo, "passwd", vpack.FInt, vpack.ByteSlice)</code></code></pre><p>We also need to keep track of usernames that are taken so we can prevent duplicate usernames, and we also want to be able to retrieve the user id for a given user name.</p><pre><code><code>var UsernameBkt = vbolt.Bucket(&amp;dbInfo, "username", vpack.StringZ, vpack.Int)</code></code></pre><p>Now there's an important step when you define a set of buckets: we must create them if they don't exist on program startup.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Aj6R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Aj6R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg" width="1040" height="622" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:622,&quot;width&quot;:1040,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!Aj6R!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86f4ada3-0b90-44c6-9c6e-f1c2f3aebc94_1040x622.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Without this bit of initialization code, any attempt to read or write to buckets would panic at runtime.</p><p>Note: we don't return errors for reads or writes to the database; we only panic. It's the programming equivalent of reading/writing to a null or invalid pointer, or to outside of array bounds.</p><p>When they panic, the response handler for the RPC returns a special "Server Error" response.</p><p>Now we can implement AddUser and GetUsers in terms of the buckets.</p><p>Here's a utility function to fetch all the users from the UsersBkt (without pagination)</p><pre><code><code>func fetchUsers(tx *vbolt.Tx) (users []User) {
    vbolt.IterateAll(tx, UsersBkt, func(key int, value User) bool {
        generic.Append(&amp;users, value)
        return true
    })
    return
}</code></code></pre><p>We'll use this at the end of both the AddUser and ListUsers.</p><p>Now, let's take a look at how I implement adding a user. It's a bit difficult to explain everything in a blog format, so I took a screenshot of the code and annotated it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!c159!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!c159!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg" width="1456" height="1293" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1293,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!c159!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9179cf7c-d0a9-46db-aa98-9b21732e8537_1737x1542.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We can make a few changes to the frontend code, they should be easy to guess, and we have an interface to add users:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;cb33bfda-4c45-45ab-8f19-65b6039ac43e&quot;,&quot;duration&quot;:null}"></div><p>I hope this serves as a good introduction to my style of programming with database: it's just a set of building blocks and functions you call to manipulate data on buckets, indexes, and collections. We haven't seen how to use indexes yet, but when the time comes and you see it, hopefully you will not be surprised.</p><p>There's a lot to talk about in terms of what the storage layer can do and how to use it. I'll introduce more aspects of it gradually as we move along.</p><p>Download the code: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP003.zip">EP003.zip</a></p><p>View the code online: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP003">HandCraftedForum/tree/EP003</a></p>]]></content:encoded></item><item><title><![CDATA[HCF EP 002: RPC Wiring & UI State Management]]></title><description><![CDATA[2024.12.09]]></description><link>https://hasen.substack.com/p/hcf-ep-002-rpc-wiring-and-ui-state</link><guid isPermaLink="false">https://hasen.substack.com/p/hcf-ep-002-rpc-wiring-and-ui-state</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Tue, 10 Dec 2024 15:01:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!2MBh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous episode, we setup a mostly empty project, and now we want to start building out the project iteratively bit by bit.</p><p>The core feature of a forum is the post. If you cannot read posts and reply to them, you don't have a forum.</p><p>Can you have a forum without categories? You can.</p><p>Can you have a forum without user auth? Yes you can. Everyone can be anonymous. If they can read and participate in discussions, it's a forum.</p><p>Can you have a forum without search? Yes you can. People can just share links, or external sites can index the content and expose search feature.</p><p>So normally I'd say: ignore everything and start with the discussion. I'm even trying to work out a UI design:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!2MBh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!2MBh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg" width="1134" height="1434" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1434,&quot;width&quot;:1134,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!2MBh!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31712c68-f153-4dcf-98f2-b8c329fab0ed_1134x1434.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>However, I was to start by introducing the basic features of our mini framework:</p><ul><li><p>RPC Wiring</p></li><li><p>UI State Management</p></li><li><p>Data persistence &amp; Indexing</p></li></ul><p>So we'll work on something simple that allows us to showcase how our framework works.</p><p>To this end, I decided to start with a basic, bare bones, user registration system.</p><h2><strong>Features of a basic auth system</strong></h2><ul><li><p>Form to create an account; nothing but username, email, and password</p></li><li><p>Form to login to account; nothing but username and password</p></li><li><p>Session management using tokens</p></li><li><p>Distinguishing "Admin" accounts</p></li><li><p>Page to list all accounts</p></li><li><p>Persisting account and session data</p></li><li><p>[DEBUG] Button to login as any account (if you are admin)</p></li></ul><p>Things we will not do now:</p><ul><li><p>Email confirmation</p></li><li><p>Pagination for the user list page</p></li><li><p>User Profile &amp; Bio</p></li></ul><h2><strong>Stage 1: Volatile anonymous "accounts"</strong></h2><p>We start with the most basic thing: an input box an a submit button. You can create as many accounts as you want, and the server keeps track of them.</p><p>We start by creating a procedure on the server side that is exposed to the client. The input is the new username, the output is a list of all current usernames.</p><pre><code><code>// global (but volatile) list of usernames
var usernames []string

type AddUserRequest struct {
    Username string
}

type UserListResponse struct {
    AllUsernames []string
}

func AddUser(ctx *vbeam.Context, req AddUserRequest) (resp UserListResponse, err error) {
    usernames = append(usernames, req.Username)
    resp.AllUsernames = usernames
    return
}</code></code></pre><p>This procedure does not do much other than appending the given name to a list and returning the list.</p><ul><li><p>Input and Output params must be structs</p></li><li><p>First input param is <code>*vbeam.Context</code></p></li><li><p>Second output param is error</p></li><li><p>The content is regular code that does not care about HTTP or JSON.</p></li></ul><p>Now, to expose this code to the client, we call <code>vbeam.RegisterProc(app, AddUser)</code> inside the <code>MakeApplication</code> function:</p><pre><code><code>func MakeApplication() *vbeam.Application {
    vbeam.RunBackServer(cfg.Backport)
    db := vbolt.Open(cfg.DBPath)
    var app = vbeam.NewApplication("HandCraftedForum", db)
    vbeam.RegisterProc(app, AddUser)    //    &lt;&lt;====   Added!
    return app
}</code></code></pre><p>Great, now, how can we test that this works? Do we need to build the UI first?</p><p>Actually no. There's a much simpler way.</p><p>Inside <code>main.tsx</code> add a line to import the generated <code>server</code> module, and expose it on <code>window</code></p><pre><code><code>import * as vlens from "vlens";
import * as server from "@app/server"     //   &lt;==== added

async function main() {
    vlens.initRoutes([
        vlens.routeHandler("/", () =&gt; import("@app/home")),
    ]);
}

main();

(window as any).server = server           //   &lt;==== added
</code></code></pre><p>Now, we can call it from the browser console!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!TiO7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 424w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 848w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 1272w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!TiO7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png" width="502" height="510.5665529010239" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1192,&quot;width&quot;:1172,&quot;resizeWidth&quot;:502,&quot;bytes&quot;:1062153,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 424w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 848w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 1272w, /__u/substackcdn.com/image/fetch/$s_!TiO7!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F263b8545-0f4a-470d-937e-0d6f55dd541c_1172x1192.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Just like the Go function returns a response and an error, so does the generated binding function. It returns a response and an error, except the error is a string type. When there's an error, the response is set to null and the error is the returned error message.</p><p>Since it goes over the network, it needs to be awaited</p><p>typescript</p><pre><code><code>let [resp, err] = await server.AddUser({Username: "admin"})</code></code></pre><p>This is the basic pattern for communication between the client and the server.</p><p>Now let's create the client side code. This will be a special page so let's create a new route, for instance, `/users`</p><p>The idea is to show the current list of users, let you add new ones, and then "select" a user to login as.</p><p>We start by adding the route, and note that we have to add it <em>before</em> the '/' route, because we match like Go's http routes, by prefix, and the first route that matches as a prefix for the current location will be picked.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!tzbc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!tzbc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg" width="1082" height="248" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:248,&quot;width&quot;:1082,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!tzbc!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F617b3d0b-5a5d-4fc3-bb52-6329d3fc3c9e_1082x248.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The initial content of <code>users.tsx</code> is to just list the current user names</p><pre><code><code>import * as preact from "preact"

import * as server from "@app/server";

export async function fetch(route: string, prefix: string) {
    return server.ListUsers({})
}

export function view(route: string, prefix: string, data: server.UserListResponse): preact.ComponentChild {
    return &lt;div&gt;
        &lt;h3&gt;Users&lt;/h3&gt;
        {data.AllUsernames.map(name =&gt; &lt;div key={name}&gt;{name}&lt;/div&gt;)}
    &lt;/div&gt;
}
</code></code></pre><p>The fetch function now calls <code>ListUsers</code>, which I haven't shown, but you can easily imagine how we added it.</p><p>The view function takes that initial response and just renders the list of names.</p><p>NOTE: We have to restart the server after defining new RPC functions in order to generate the typescript bindings.</p><p>When we run this we face a problem: because the server has restarted, the username list is cleared, but it was initialized to nil, so the response list will be null.</p><p>This is a weakness of the binding system: it does <em>not</em> set the type of slices to nullable. This is on purpose: I do not want to litter the code will nullable array types. It's borderline retarded. Instead, I just do a bit of extra work to ensure all lists in responses are not null. Sometimes I can miss some cases, but I see it as low risk. Ideally the JSON encoder never encodes a nil slice as a JSON null, but that day is not today.</p><p>For now I will just init <code>usernames</code> to an actual empty slice.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!n79u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!n79u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg" width="1199" height="638" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:638,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!n79u!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56de6dd0-e17d-4c70-ab96-7ef4fddbabe9_1199x638.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That should do it, but it does not change the fact that the list is empty.</p><p>We can again just add names from the dev console. When we do it and reload the page, we see those names, indicating the TSX code shown above works!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!S7RQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!S7RQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg" width="968" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:968,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="/__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!S7RQ!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4def2fc-253d-49f2-9692-b5d4e32f7941_968x720.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now we want to show a form where you add a new name and the list is auto refreshed. To do this we will need to store some state:</p><ul><li><p>The current name input</p></li><li><p>The latest known user list</p></li></ul><p>This state must be "stable" across rendering cycles, but we are:</p><ul><li><p>Not using class components</p></li><li><p>Not using React Hooks</p></li></ul><p>So how do we make a "stable" state object across rendering cycles? Caching</p><p>vlens ships with a simple caching system that lets you specify:</p><ul><li><p>The cache key (usually a list of things)</p></li><li><p>The computation function if no hit for key</p></li></ul><p>There happens to be <em>no</em> limit to how many keys you can cache. We don't have an LRU. We just expect you not to fill the cache with tons of things. Which is arguably not a great cache API, but it lets us rely on the cache to allow for creating "stable" objects that are "hooked" to other "stable" objects.</p><p>Here's our type definition for the state object:</p><pre><code><code>type Form = {
    data: server.UserListResponse
    name: string
    error: string
}
</code></code></pre><p>Now, we can create a function that retrieves a stable reference to it this way:</p><pre><code><code>function getForm(data: server.UserListResponse): Form {
    function create(): Form {
        return {
            data, name: "", error: "",
        }        
    }
    const key = [getForm, vlens.cacheById(data)]
    return vlens.cacheGet(key, create)
}</code></code></pre><p>When this is called for the first time with the given parameters, there will be nothing that matches the given key, so the <code>create</code> inner function will be called to create the instance, and the reference to that instance will be stored in the cache.</p><p>Next time we call getForm with the same reference to <code>data</code>, it will not create a new instance of <code>Form</code>, instead, it will return a reference to the instance previously created.</p><p>This effectively creates a hook, but not in the React style. It hooks one object reference B to another object reference A. As long as the reference to A is stable, the reference to B will also be stable.</p><p>Now, since this pattern is so desirable, we have a specialized interface for it: <code>declareHook</code>. It takes a function that receives parameters, and hooks the output to those parameters.</p><pre><code><code>const useForm = vlens.declareHook((data: server.UserListResponse): Form =&gt; ({
    data, name: "", error: ""
}))</code></code></pre><p>This is functionally the same as the previous code: it create a function (we called it <code>useForm</code> instead of <code>getForm</code> here) that returns a reference hooked to its input.</p><p>Our plan is that when we submit the new name and get the update user list, we update the list in the Form object, and leave the original `data` object untouched, because it's what we got from the initial page load, and we would like to treat it as effectively immutable.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!JSFt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!JSFt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg" width="1200" height="601" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:601,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!JSFt!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3eb36fdd-6e59-4c95-b9ba-370507717591_1200x601.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So now we add the input form, and this is a chance to introduce another powerful feature of vlens: binding input to fields without explicitly writing callbacks</p><pre><code><code>&lt;input type="text" {...events.inputAttrs(vlens.ref(form, "name"))} /&gt;</code></code></pre><p>There are two features in this snippet:</p><ul><li><p><code>vlens.ref</code>: an object that acts like a pointer to a field.</p></li><li><p><code>events.inputAttrs</code>: set the <code>value</code> and <code>onInput</code> attributes to bind the input field to the given <code>ref</code></p></li></ul><p>The ref is, in reality, a poorly implemented 'pointer' from C. What we really want to do is say something like:</p><pre><code><code>input({ .ref = &amp;form.name })</code></code></pre><p>It's just a bit too verbose because of the language and the underlying DOM API.</p><p>Now, just having this, we want a simple way to check that the code does indeed bind the input field to the <code>form.name</code> field, so we just print it:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!L1cM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!L1cM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg" width="1199" height="491" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:491,&quot;width&quot;:1199,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!L1cM!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a8994ad-5795-4c58-a6eb-c45b78dff84f_1199x491.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here we confirm that it works:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;ebb04de6-2e05-4c58-b6c9-10102ba97738&quot;,&quot;duration&quot;:null}"></div><p>Next, we need to add a button that, when clicked, calls the <code>AddUser</code> proc on the server, and replace the username list with the one we get from the response.</p><p>Well, we can easily define a function that does exactly that:</p><pre><code><code>async function onAddUserClicked(form: Form) {
    let [resp, err] = await server.AddUser({Username: form.name})
    if (resp) {
        form.name = ""
        form.data = resp
        form.error = ""
    } else {
        form.error = err
    }
}
</code></code></pre><p>But this function takes a form as a parameter; we can't just pass it to an <code>onClick</code> .. we'd have to use a closure &#129300;</p><p>The problem with closures in JSX attributes is they are not stable references, and to make good use of the virtual dom, we would really like to keep all the callback references stable.</p><p>vlens has just the right tool for that, again using the caching module. We have a utility that lets us return a <em>stable closure reference</em>!!</p><p>So instead of doing this:</p><pre><code><code>&lt;button onClick={() =&gt; onAddUserClicked(form)}&gt;Add&lt;/button&gt;</code></code></pre><p>We do this:</p><pre><code><code>&lt;button onClick={vlens.cachePartial(onAddUserClicked, form)}&gt;Add&lt;/button&gt;</code></code></pre><p><code>vlens.cachePartial(fn, a, b, c)</code> returns a stable reference to what you would get from calling <code>fn.partial(a, b, c)</code>. However, for this to work properly, the input function itself must not be a closure that depends on variables in its environment with an unstable reference.</p><p>Now, one last thing to know about: because none of the variables we're working with has any special status in the context of the UI library, we have to explicitly tell the UI to refresh after we are done with the response:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!mGr-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!mGr-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg" width="1200" height="401" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:401,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!mGr-!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe16e9d1d-9303-4cbc-9a8c-7fa435c6edeb_1200x401.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>`vlens.scheduleRedraw()` does not cause an immediate re-rendering. It just tells the system to re-render in the next animation frame. It's safe to call this as many time as you want per rendering-cycle. It's idempotent.</p><p>Here's testing the UI:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;f70297e2-db98-4633-b26c-23a2a40ac1d3&quot;,&quot;duration&quot;:null}"></div><p>We can try adding some error checking for fun, but it's too trivial to cover in great detail here, and we can leave it as an exercise to the reader. For instance, we can check for duplicate entries, and refuse an entry if it already exists. We can enforce only alphanumeric characters and underscore, dash, and dot.</p><p>Now, more importantly, the next step is to persist the data to a database that survives server restarts.</p><p>But, I think we've covered enough ground in this episode already, so we'll save data persistence to the next episode.</p><p>Download the source code: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP002.zip">EP002.zip</a></p><p>View the source code online: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP002">HandCraftedForum/tree/EP002</a></p>]]></content:encoded></item><item><title><![CDATA[HCF EP 001: Empty Project Setup]]></title><description><![CDATA[2024.12.06]]></description><link>https://hasen.substack.com/p/hcf-ep-001-empty-project-setup</link><guid isPermaLink="false">https://hasen.substack.com/p/hcf-ep-001-empty-project-setup</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Tue, 10 Dec 2024 09:37:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mJ4Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2024.12.06</p><p>This is the first article about the development of "HandCraftedForum"</p><p>Repository URL: <a href="https://github.com/hasenj/HandCraftedForum">https://github.com/hasenj/HandCraftedForum</a></p><p>HandCraftedForum is a web project that will use a mini framework I have developed over the course of the last couple of years.</p><p>As such, the base/skeleton project setup will reflect the framework.</p><p>The framework overall does not have a particular name, but it's composed of the following components:</p><p>VBolt: a storage layer built on top of BoltDB and VPack, a serialization library.</p><p>VBeam: the server side web framework. It mostly consists of an RPC system that lets the client side call functions on the server side without thinking about ReST or HTTP.</p><p>VLense: a client side framework, consist mostly of routing and some utilities to help create UIs (p)react style without callbacks or "hooks". (We provide our own version of what hooks are meant to do; more on that later).</p><p>The overall theme is straight forward programming with data and procedures. The server side code uses VBolt to store and retrieve data. VBeam provides an interface to the client to communicate with the server side code. The client code renders the UI using the data it obtained from the server + transformation applied via user interaction.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!mJ4Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 424w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 848w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 1272w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!mJ4Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png" width="732" height="535" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:535,&quot;width&quot;:732,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 424w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 848w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 1272w, /__u/substackcdn.com/image/fetch/$s_!mJ4Z!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F423e312c-250a-4b1c-b075-fe8e08c477ed_732x535.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The skeleton of the project will let us run a web server locally and opens a mostly empty page with a welcome message rendered via client side code.</p><h2><strong>License</strong></h2><p>Before we start, we should choose a license. This is mostly a technicality but has implications.</p><p>I'm generally in favor of open source for libraries and code snippets, but not for final products. I believe software products should be sold for money.</p><p>I asked Claude if there's a license I can use that allows people to freely use the source code and study from it while prohibiting people from taking the product as-is and rebranding it as their own. I didn't think there was such an option, but to my surprise, turns out there is: the</p><p><a href="https://commonsclause.com/">commons clause</a></p><p> combined with any other license.</p><p>So I combined the commons clause with the MIT license.</p><p>The license does not comply with the OSI definition of "open source", but we don't care.</p><p>The license lets you use the code freely in all the way that matters. It just does not late you take the final product and make money from it.</p><p><a href="https://commonsclause.com/#faq">Check the linked website for the FAQ regarding the license.</a></p><h2><strong>Server side code</strong></h2><p>First we create a module using <code>go mod init forum</code></p><pre><code><code>module forum

go 1.22.1</code></code></pre><p>Then we create <code>app.go</code>:</p><p>go</p><pre><code><code>package forum

import (
    "forum/cfg"

    "go.hasen.dev/vbeam"
    "go.hasen.dev/vbolt"
)

func MakeApplication() *vbeam.Application {
    vbeam.RunBackServer(cfg.Backport)
    db := vbolt.Open(cfg.DBPath)
    var app = vbeam.NewApplication("HandCraftedForum", db)
    return app
}
</code></code></pre><p>This creates a mostly empty application.</p><p>The <code>MakeApplication</code> function is mostly a convention I use. Note that this package is not the <code>main</code> package, so there will be no <code>main</code> function here. Instead, we will have two different <code>main</code> packages: one that we run locally, and one that we run in production.</p><p>The local and production version differ in the following ways:</p><ul><li><p>The local server also bundles the frontend code and performs type checking on it</p></li><li><p>It generates the server side bindings for the client (more on that later)</p></li><li><p>It uses different paths for database file and static folder</p></li><li><p>The local server serves the frontend code from the local file system, while the production server <em>embeds</em> the generated frontend bundle and serves it from RAM. In fact, the code is written to expect the frontend bundle to have been already generated in a prior step.</p></li></ul><p>Let's look at this function line by line:</p><p>go</p><pre><code><code>vbeam.RunBackServer(cfg.Backport)</code></code></pre><p>This innocent looking line does something very important: it tells any previously running instance of this server to shutdown.</p><p>A "back server" is a private backdoor that lets us control the server. The way we do that is by using a specific port number that no other program uses. If you have multiple servers running and they all use this framework, you need to pick these port numbers such that they do not conflict.</p><p>The first thing a back server does is send a "shutdown" command to the given port number. If a server is already listening there, it will shutdown.</p><p>This allows nearly instant deployment with zero shut down time.</p><p>The next line create a db instance. For now we have nothing on the db, so it doesn't matter what we do with it. It's just that the db is required for the next line: the vbeam function that creates a new application and sets up the RPC system.</p><p>Again this is the empty skeleton application so we will just return it as-is.</p><p>We also create a <code>cfg</code> package to store some code level configuration variables that vary between local and production. In this case, we see <code>cfg.DBPath</code>.</p><p>There's also a variable that does not vary between local and production: the backport number. I just chose this number arbitrarily.</p><p>cfg/cfg.go</p><pre><code><code>package cfg

const Backport = 12832
</code></code></pre><p>To distinguish local from production, we use build tags.</p><p>cfg/local.go</p><pre><code><code>//go:build !release

package cfg

const IsRelease = false
const DBPath = ".serve/data/db.bolt"
const StaticDir = ".serve/static/"
</code></code></pre><p>cfg/release.go</p><pre><code><code>//go:build release

package cfg

const IsRelease = true
const DBPath = "data/db.bolt"
const StaticDir = "static/"
</code></code></pre><p>Next we create the command that runs the local development server. This will mostly follow a template.</p><p>First we create a directory <code>local</code> and create <code>local.go</code> and name the package <code>main</code> so it can be executed:</p><p><code>local/local.go</code></p><pre><code><code>package main

func main() {

}</code></code></pre><p>Now, before writing out the full content, I want to first show the function that launches the server:</p><pre><code><code>import (
    "fmt"
    "forum"
    "net/http"
    "os"

    core_server "go.hasen.dev/core_server/lib"

    "go.hasen.dev/vbeam"

    "forum/cfg"
)

const Port = 5212
const Domain = "forum.localhost"
const FEDist = ".serve/frontend"

func StartLocalServer() {
    defer vbeam.NiceStackTraceOnPanic()

    app := forum.MakeApplication()
    app.Frontend = os.DirFS(FEDist)
    app.StaticData = os.DirFS(cfg.StaticDir)
    vbeam.GenerateTSBindings(app, "frontend/server.ts")

    var addr = fmt.Sprintf(":%d", Port)
    var appServer = &amp;http.Server{Addr: addr, Handler: app}

    core_server.AnnounceForwardTarget(Domain, Port)
    appServer.ListenAndServe()
}</code></code></pre><p>Notice that after we call <code>MakeApplication</code> that we created above, we set the Frontend and StaticData fields on it.</p><p>In the production <code>main</code>, we will set those variables differently. We will see when the time comes.</p><p>Next we generate the client side bindings. We don't have "RPC" yet, but when we make one, a binding module will be generated automatically.</p><p>This assumes that we will put all the frontend code in the <code>frontend</code> directory.</p><p>Next we prepare an http server from the Go standard library and set the handler to the <code>app</code> we just created, because the <code>vbeam.Application</code> object implements the http server interface.</p><p>The next line is important for allowing us to open the website using a domain on port 80 instead of <code>localhost:5212</code></p><pre><code><code>core_server.AnnounceForwardTarget(Domain, Port)</code></code></pre><p>This assumes we have <code>core_server</code> running.</p><p>Core server is a very small reverse proxy that is configured only via UDP messages. This is the most important message: it maps a domain to a port. Meaning, when a request comes for the given domain, it reverse proxies to the given port on localhost.</p><p>The core server can be installed this way:</p><pre><code><code>go install go.hasen.dev/core_server@latest</code></code></pre><p>Once installed, run it this way:</p><pre><code><code>nohup core_server &amp; disown</code></code></pre><p>You don't need to worry about running it multiple times; it's idempotent. If you run it again, it will shutdown the previous instance before starting.</p><p>Keep in mind: this is entirely optional. If you don't have <code>core_server</code>, you can always access the server using the <code>localhost:port</code> combination.</p><p>Next we show the main function for the local server:</p><pre><code><code>
var FEOpts = esbuilder.FEBuildOptions{
    FERoot: "frontend",
    EntryTS: []string{
        "main.tsx",
    },
    EntryHTML: []string{"index.html"},
    CopyItems: []string{
        "images",
    },
    Outdir: FEDist,
    Define: map[string]string{
        "BROWSER": "true",
        "DEBUG":   "true",
        "VERBOSE": "false",
    },
}

var FEWatchDirs = []string{
    "frontend",
    "frontend/images",
}

func main() {
    os.Mkdir(".serve", 0644)
    os.Mkdir(".serve/static", 0644)
    os.Mkdir(".serve/frontend", 0644)

    var args local_ui.LocalServerArgs
    args.Domain = Domain
    args.Port = Port
    args.FEOpts = FEOpts
    args.FEWatchDirs = FEWatchDirs
    args.StartServer = StartLocalServer

    local_ui.LaunchUI(args)
}
</code></code></pre><p>This setups the frontend builder configuration as well as the list of directories to watch for changes in order to typecheck and rebuild.</p><p>This won't make sense without talking about the frontend first.</p><h2><strong>The frontend</strong></h2><p>The frontend consists of an index.html file that loads the entry typescript file, which will start a client side router, and then render the current page depending on the URL.</p><p>frontend/index.html</p><pre><code><code>&lt;!doctype html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
    &lt;title&gt;HandCraftedForum&lt;/title&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;script src="/__u/hasen.substack.com/main.tsx" type="module"&gt;&lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></code></pre><p>Nothing special here, except the fact that we are referring to <code>/main.tsx</code>. The frontend bundler will replace this with the path that resulted from the bundling operation.</p><p>We need to first setup the dependencies</p><pre><code><code>npm install --save preact vlens
npm install --save-dev typescript</code></code></pre><p>We also setup a basic tsconfig.json file and configure it to use preact for jsx.</p><p>json</p><pre><code><code>{
    "compilerOptions": {
        "module": "es2020",
        "target": "es2020",
        "lib": ["es2020", "dom"],
        "moduleResolution": "node",
        "esModuleInterop": true,
        "skipLibCheck": true,
        "noEmit": true,
        "strict": true,
        "jsx": "react",
        "jsxFactory": "preact.h",
        "jsxFragmentFactory": "preact.Fragment",
        "baseUrl": "frontend",
        "noImplicitAny": true,
        "paths": {
            "@app/*": ["./*"]
        }
    },
}
</code></code></pre><p>Notice I've set it up so that we can use the prefix <code>@app</code> to refer to our own codebase.</p><p>Now we can setup a skeleton main.tsx:</p><pre><code><code>import * as vlens from "vlens";

async function main() {
    vlens.initRoutes([
        vlens.routeHandler("/", () =&gt; import("@app/home")),
    ]);
}

main();
</code></code></pre><p>This initialize the app and sets up a single route mapping.</p><p>Client side routing works by matching a route with a fetching function and view function.</p><p>The fetch function must return a promise for some data. This data will be stored and passed to the view function. The view function always takes this data as input. The reference to this piece of data will be stable across rendering cycles.</p><p>This is a crucial feature of this mini framework: there's no state management. There's just data. Data can be used to reference other data. The "entry point" to all the data you need is the object returned from the <code>fetch</code> function above.</p><p>You can associate "side" data ("hooked" data) by relying on the stability of the reference. We will show how later in the project.</p><p>The <code>routeHandler</code> is a helper function that sets the route by dynamically importing a module and looking for "magic" names <code>fetch</code> and <code>view</code>.</p><p>Actually the second argument does not need to be a module. It can be any function that returns a promise for an object satisfying the interface:</p><pre><code><code>export type RouteHandler&lt;Data = any&gt; = {
    fetch: (route: string, prefix: string) =&gt; Promise&lt;rpc.Response&lt;Data&gt;&gt;;
    view: (route: string, prefix: string, data: Data) =&gt; preact.ComponentChild;
}</code></code></pre><p>You can just as easily do something more complicated than just loading a module, but for now we will simplify our life by just creating a module for each route with a <code>fetch</code> and <code>view</code> functions.</p><p>For the home route, we will again just make the bare minimum empty fetch and view functions:</p><pre><code><code>import * as preact from "preact"
import * as rpc from "vlens/rpc";

type Data = {}

export async function fetch(route: string, prefix: string) {
    return rpc.ok&lt;Data&gt;({})
}

export function view(route: string, prefix: string, data: Data): preact.ComponentChild {
    return &lt;div&gt;
        &lt;h2&gt;Hand Creafted Forum&lt;/h2&gt;
        &lt;img src="/__u/hasen.substack.com/images/framework.png" /&gt;
    &lt;/div&gt;
}
</code></code></pre><p>The fetch function fetches no data per se; it just returns an empty object as if it was an "ok" response from the server. While the empty object is generally useless, it's not <em>entirely</em> useless: it has a reference id, and that reference id is stable.</p><p>We will not use that yet, but it's worth keeping in mind.</p><p>Our view function just returns basically static html (as jsx).</p><p>In the jsx, we refer to an image. We create an <code>images</code> directory inside <code>frontend</code> and place the image file there.</p><p>We are now ready to run our local server. Here's a video of me doing that:</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;815db0ae-ad33-4a95-92c2-9d6cb88251d2&quot;,&quot;duration&quot;:null}"></div><p>Here's the resulting page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!udb9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!udb9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg" width="1200" height="907" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:907,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!udb9!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F62088e2d-4f66-47ee-aa00-ba34ff514c8a_1200x907.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We have successfully created an empty skeleton project using our framework.</p><p>Note: in the video you can see me edit the jsx code and then reload the page to see the results. This is not "hot module reload". You still have to reload manually. We are just saving you the bundling step by doing it automatically when you save the code file.</p><h2><strong>Code base for Episode 001:</strong></h2><p>Web view: <a href="https://github.com/hasenj/HandCraftedForum/tree/EP001">HandCraftedForum/tree/EP001</a></p><p>Zip download: <a href="https://github.com/hasenj/HandCraftedForum/archive/refs/tags/EP001.zip">EP001.zip</a></p><h2><strong>Follow along</strong></h2><p>Download the code base, see if you can run it locally.</p><p>If you face any problem, report it to me, either by opening issues on github or replying here on substack.</p>]]></content:encoded></item><item><title><![CDATA[Announcement: Hand Crafted Forum]]></title><description><![CDATA[2024.12.05]]></description><link>https://hasen.substack.com/p/announcement-hand-crafted-forum</link><guid isPermaLink="false">https://hasen.substack.com/p/announcement-hand-crafted-forum</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Tue, 10 Dec 2024 09:10:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4HSe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!4HSe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!4HSe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg" width="1200" height="480" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:480,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!4HSe!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2460749f-d82c-4e20-bc2c-256909c46295_1200x480.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>2024.12.05</p><p>I've been meaning for a while to build a web project in public.</p><p>After some deliberation, I'm settling on a forum. For various reasons:</p><ul><li><p>There does not seem to exist a very good one, or at least, not many choices are available, so if the one or two famous ones do not fit your needs, you are out of luck.</p></li><li><p>It's familiar enough that most people know what to expect.</p></li><li><p>It's simple enough on the surface, but more advanced features can contain quite a bit of complexities.</p></li></ul><p>Until we settle on a product/brand name, I'm going to call it "Hand Crafted Forum" (because "hand made" is taken!)</p><p>I want to use it to demonstrate a few things I care about in web development:</p><ul><li><p><strong>Straight forward programming. Just data and functions. </strong>This means there's not a hint of "OOP". No adapters, no providers, no services, no repositories. It also means we take into account what the computer has to do to accomplish the requested task: we will try to minimize waste, to a point, without requiring deep expertise in low level programming. This implies the data model and transformations are guided by the desired product features, not by some idealized version of what the data is supposed to represent.</p></li><li><p><strong>Simplified development environment &amp; deployment process.</strong> This means not much to install or configure other than language compiler(s). Most things will be embedded libraries instead of external programs. If something *must* be an external program (say, ffmpeg), it will be zero configuration, or we put all the configuration necessary in our own code base. If you have the codebase and the compiler, you can start the local server in one command, and you can deploy to staging/production with one command.</p></li><li><p><strong>Inside out iteration process</strong> We start with the core functionality and add side features and ornamentations later. The UI design will visually suck at first, and gradually get better over time. We will dedicate a lot of time to the UX, because the UX is the product.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!M_q-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_webp, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!M_q-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg" width="1200" height="475" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:475,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Image&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Image" title="Image" srcset="/__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_424, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_848, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_1272, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!M_q-!, /__u/hasen.substack.com/w_1456, /__u/hasen.substack.com/c_limit, /__u/hasen.substack.com/f_auto, /__u/hasen.substack.com/q_auto:good, /__u/hasen.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4464470f-bb2b-4955-8ff0-0933b2a34d3b_1200x475.jpeg 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Why you should avoid Scrum, and what to do instead]]></title><description><![CDATA[How to manage software projects]]></description><link>https://hasen.substack.com/p/scrum</link><guid isPermaLink="false">https://hasen.substack.com/p/scrum</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Tue, 06 Aug 2024 06:41:35 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c7730b22-6ef0-49ba-a8df-ce600d379ff3_1728x688.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2024.08</p><h2>Overview of Scrum</h2><p>Scrum is both a process and a meta process.</p><p>It's a process because it has strongly defined roles and activities: the scrum master "SM", the product owner "PO", the daily scrum, the retrospective, the backlog, etc.</p><p>It's a meta process because it doesn't define the actually important roles well enough and instead tell you to define your own:</p><ul><li><p>Who is responsible for architecture?</p></li><li><p>What are the different roles for senior, intermediate, and junior programmers?</p></li><li><p>What happens if there's disagreement about important architecture decisions?</p></li></ul><p>These are the actually important questions that you need a framework in place in order to answer. "Talking about it as a team" is not a framework.</p><p>Scrum pretends that the answers to these questions is a mystery and you can only discover it by doing the Scrum process. But the answer is not a mystery at all, and by avoiding the answer to these important questions, Scrum provides no value at all.</p><p>Scrum's idea for project management starts with the "PO" creating a backlog of tickets. On what basis are the tickets created? That's left up to them!</p><p>The PO then talks to their team of programmers to select tickets to complete in a "Sprint", which is a time-box where the team "commits" to a specific "goal".</p><p>After the sprint is complete, the teams stops to evaluate the situation and adjust accordingly.</p><h2>Starting on the wrong foot</h2><p>Quote from the scrum guide:</p><blockquote><p>1. A Product Owner orders the work for a complex problem into a Product Backlog.</p></blockquote><p>Scrum's very first step is already a mistake. You can't just have one non-technical person competently break down the product into a series of tickets.</p><p>The PO, according to scrum rules, should not be a technical person. As such, they don't really know what's the best way to break down the problem into a list of tickets. Whatever they come up with is probably not of good quality.</p><p>By the time the programmers get to have a look at the backlog, it's too late to fix.</p><p>The PO has already been put in too much effort into creating all these elaborate user stories, and they are not going to fundamentally change how the tickets are structured.</p><p>The implicit assumption in Scrum is that programmers are incapable of performing the work of breaking down a large goal into a series of small steps, because it's a kind of soft skill that programmers don't have.</p><p>This is of course misguided. Many programmers have those skills. The better the programmer is, the more likely it is that they have a deeper understanding of how to best break down a project into a series of steps.</p><h3>Phases and Milestones</h3><p>You can't actually start at the level of an individual ticket, or even a user story.</p><p>You have to start at a somewhat higher level: breaking it down into a series of phases, each encompassing a meaningful set of features that can be released, either to the public, or to a select group of test users. It should be something that can be completed in a few months.</p><p>Each phase is then broken further down into a series of milestones that can be reasonably completed within a few weeks.</p><p>You may notice that "few weeks" is vague. This is on purpose. You need some amount of flexibility in terms of scope and time. This will be explained further on the section titled "The Sprint".</p><p>Arriving at meaningful phases and milestones requires collaboration between technical leaders and stakeholders.</p><p>A tech leader <em>can</em> come up with milestones on their own, but these milestones might not make business sense. Similarly, the business owner can come up with arbitrary milestones, but they might not make sense from a technical perspective. That's why it should be a collaboration between the two sides.</p><p>Now, to reduce communication overhead, one or two people from each side would usually suffice. Adding more people to the mix does not usually result in a better understanding of the problem. Instead it would just increase the communication overhead. It would also increase the probability of personal conflicts, as each member tries to prove how valuable they are to the process by suggesting new ideas and thinking of new potential obstacles.</p><p>The goal is not to arrive at the <em>perfect</em> plan. We just need something reasonable; a rough plan that allows us to start moving forward.</p><p>How to break the project down into a series of milestones and how to prioritize the work is itself an important topic that needs its own treatment.</p><p>You have to start by building a shared understanding (between the business side and the tech side) of the high level goals and the vision behind the project.</p><p>Based on these goals, you think of a <em>smaller</em> version of the product that aligns with the ultimate goals and vision.</p><p>In each phase, you <em>increase</em> the scope of the project so that it comes closer and closer to fulfilling all of its goals.</p><p>One common mistake I've seen some managers make, specially when operating under the tacit assumptions of scrum, is the following: they break down the project by slicing it down vertically; for example: their plan is to develop the product one page at a time. Each time, they aim to get all the details for that page fleshed out in terms of graphic design and implementation. If the page is too large to develop in one sprint, they break it further down (again vertically) into sections, each of which they expect to be developed fully in a sprint.</p><p>It's as if you plan the construction of a house by building one room at a time, each time everything about that room is fleshed out: all the electric sockets, the windows, the water pipes, etc, but only for that room! And then you break that down to one wall at a time, again with all the windows and electricity sockets and water pipes for that wall!</p><p>This is not a good way to manage the project's milestones.</p><p>Instead, start with a broad sketch; a skeleton. Then flesh out the details gradually and iteratively.</p><p>Start with a rough sketch of the core feature. Use fake data to compensate for lack of real data.</p><p>For example, if the the current phase of the project is mainly about booking and managing appointments, start with the calendar page. It doesn't have to look good. It doesn't even have to look correct; it's fine at first to pretend that all months just have 30 days.</p><p>What matters at this stage is nailing the feel and interaction.</p><p>Since you haven't yet built anything to input the data, you will have to fake it: fake users, fake sessions, fake appointments, etc.</p><p>You try various ideas for how to interact with the calendar until you find something that seems to work well enough. Then you can start filling out the other details: getting the month display to be correct, working out the graphic design, the colors, the shadows, etc.</p><p>If you have more programmers, you can let them work on sketching out the other more peripheral pages, or do various types of independent UX experiments, etc.</p><p>Think of how you would go about putting in screws on a table. You don't put it one screw at a time. You put in all the screws first but keep them loose, then you gradually start tightening them up, one by one, but all together: never tightening one screw too much while leaving others too loose.</p><h2>Planning the sprint</h2><p>Quote from the scrum guide:</p><blockquote><p>2. The Scrum Team turns a selection of the work into an Increment of value during a Sprint.</p></blockquote><p>The second step in the scrum process is also deeply misguided. It assumes that a PO, having created a backlog of tickets without much engineering considerations, can just share the top items from the list with his team of programmers, and they can just grab a bunch of tickets that fit into a sprint and just start working on them!</p><p>That's not how software development works. You cannot just pickup a ticket and "implement it" as if it's an isolated unit.</p><p>A lot of upfront preparatory work is usually required, specially if we assume that most tickets are created such that each can be completed by a single engineer in a couple of days.</p><p>At the very least, you need to define the interface between the different components in the system, but more broadly, you need to define the data model and outline the list of processes that need to be implemented.</p><p>This is especially required if you have a team of multiple people that are expected to work in tandem on different components of the same feature or related set of features.</p><p>Scrum provides no guidance here. Instead, it's often a hinderance, as it primes the PO into thinking that the programmers just need to break down the tickets into new, smaller tickets if necessary, and that's it!</p><p><em>As if</em> features can just be built onto the air, with no foundation in place.</p><p>When the programmers manage to convince the PO that they do in fact need to build some foundation first, the PO and the SM would ask the programmers to build the minimum amount of foundation necessary for the next sprint, and nothing more!</p><p>To see how absurd this is, imagine a construction crew being asked to build a house in sprints, each sprint they build just one wall, and each sprint they are only allowed to build the foundation for that wall only and nothing else!</p><p>Building software is not like building physical buildings, but the analogy here stands: you need to build the lower layers first, you need some foundation to build on, and there are good and bad ways of breaking down the work into stages. Building a house one wall at a time is nonsensical, but if you've never seen how houses are built, you wouldn't know.</p><p>How do you to take a series of abstract user requirements, and produce an actionable plan that would bring them to reality? This is one of most important problems in project management, but Scrum provides no guidance here what so ever. It is left as an exercise for the reader.</p><p>The PO <em>just</em> creates a list of backlog items. The team <em>just</em> makes a plan to implement an increment for the next sprint.</p><p>If you are looking into scrum as a solution to project management, this should be the biggest red flag!</p><p>Quote from the Scrum Guide</p><blockquote><p>Topic Three: How will the chosen work get done?</p><p>For each selected Product Backlog item, the Developers plan the work necessary to create an Increment that meets the Definition of Done. This is often done by decomposing Product Backlog items into smaller work items of one day or less. How this is done is at the sole discretion of the Developers. No one else tells them how to turn Product Backlog items into Increments of value.</p></blockquote><p>Notice the strong implication that developer's planning stage mostly involves breaking down backlog tickets into small tickets that can be completed in a day or two.</p><p>It assumes the creation of the backlog tickets was already done in a way that is appropriate from an engineering standpoint.</p><p>The programmers, according to scrum, do not get to tell the PO that the current way the work is broken down makes no sense from a technical point of view.</p><p>That is not their job!</p><p>They are just to figure out how to make it work! If the PO says we need to build the west facing wall with a window in it that can be opened and closed, you are not allowed to tell the PO that this is <em>not</em> how houses are built. Your job is to figure out <em>how</em> to just build the west facing wall with the window on it. You can break the work down into two stages: first you build the west facing wall with a hole for the window, and then you attach the window with the handles.</p><h2>The missing Architect</h2><p>There's no one in Scrum who plays the role of an Architect.</p><p>Quote from the Scrum Guide:</p><blockquote><p>Scrum Team</p><p>The fundamental unit of Scrum is a small team of people, a Scrum Team. The Scrum Team consists of one Scrum Master, one Product Owner, and Developers. Within a Scrum Team, there are no sub-teams or hierarchies. It is a cohesive unit of professionals focused on one objective at a time, the Product Goal.</p><p>Scrum Teams are cross-functional, meaning the members have all the skills necessary to create value each Sprint. They are also self-managing, meaning they internally decide who does what, when, and how.</p></blockquote><p>Scrum purports to be a software development process, yet it has no place and makes no room for an architect. It does not even <em>pretend</em> that it addresses the question of how to handle architectural decisions.</p><p>This is by far the worst aspect of this process: the diffusion of leadership roles - other than the SM and the PO.</p><p>This can be very appealing to business people, but it's disastrous for a software project.</p><p>A well functioning engineering team needs a well defined hierarchy. The senior engineers and junior engineers need to understand their role within the team. Responsibilities must be assigned individually, and individual incentives must be aligned.</p><p>Scrum diffuses responsibility, and thus destroys incentives.</p><p>Under Scrum, the responsibility for architecture is assigned to "the team". There's might be a de-facto architect: a team member who demonstrates more knowledge and experience to his peers, and maybe gets some tacit recognition as such; but never formal recognition, even if they end up doing the majority of architectural work.</p><p>The PO and the SM have well defined roles, so they receive visibility and recognition, while individual developers, even those making critical decisions or making important contributions, often end up underappreciated. This can lead to misaligned incentives, and may open the door for unintentional sabotage amongst team members.</p><p>A well functioning company would have a structure where the senior engineers, the architects and the tech leads get the visibility and the recognition they need to align their incentives.</p><h4>The importance of hierarchy</h4><p>Leadership roles within a software development team should be clearly defined. If there's no leader, infighting and bitterness ensues, as everyone tries to be a leader.</p><p>There has to be a leader who makes decision and everyone follows along. This is not to say that team members have no say in the matter. It's of course important that people can state their opinions and objections, and the leader does have a duty to listen to them, but at the end of the day, the buck stops with him; he makes the call.</p><p>Without leadership roles, politics take over: people who want power and can tolerate disagreement and social conflict will often get their way, not because of any merit, but because other people give in to keep the peace.</p><p>Without leadership roles, a team composed of mostly junior programmers will block the senior architect from making the required decisions because they disagree with him or don't understand the reasoning behind his decisions.</p><p>It gets worse if there are multiple senior or intermediate engineers with overlapping responsibilities, as they can disagree about how to do things or second guess each other's decisions.</p><p>Someone has to be responsible for making the decision at the end of the day, and when that role is well defined, most engineers would have no problem deferring to him even if they disagree with the final decision.</p><h3>Engineering Roles</h3><p>This is a rough outline of what the engineering roles ought to be and what the responsibilities and limits of each role are.</p><ul><li><p>Tech Lead / Architect</p><ul><li><p>The most senior engineer on the team.</p></li><li><p>Makes technical decisions after consulting with seniors.</p></li><li><p>Produces design documents as needed to ensure everyone understands how the system is supposed to work.</p></li><li><p>Responsible for ensuring a coherent technical design for the product as a whole.</p></li></ul></li><li><p>Senior Engineers</p><ul><li><p>Each responsible for a relatively large sub-system within the project.</p></li><li><p>Work with the tech lead to design the system as a whole and the sub-system they are responsible for in particular.</p></li><li><p>Implement the core and most critical features of their respective sub-system.</p></li><li><p>Mentor the junior members</p></li></ul></li><li><p>Junior Engineers</p><ul><li><p>Each assigned to a Senior</p></li><li><p>Learn how the system is supposed to work</p></li><li><p>Implement all the peripheral and non-critical features</p></li><li><p>Handle the mundane details to ensure their Seniors can focus on the important aspects of the system</p></li><li><p>Aim to improve at their craft and eventually attain the requisite skill level to be recognized as a senior.</p></li></ul></li></ul><h4>Committing and Reviewing Code</h4><p>Seniors commit/merge to the master branch without waiting for code review, unless they have doubts and need some help.</p><p>Juniors at first must get approval from their Senior before merging their changes into the master branch. After a while they get permission to merge without review.</p><p>Code reviews can occur after the merging has already been done. For example, Senior engineers can hold code review sessions with their juniors if they notice a drop in the quality of code being committed.</p><h2>The Sprint</h2><p>Scrum requires programmers to commit to completing a fixed amount of work in a fixed amount of time. This is the "core" of scrum. It appeals to managers because the consistent delivery of small improvements in fixed cycles creates the impression of consistent progress. But it's mostly an illusion.</p><p>The scrum sprint is very strict. You have to make 100% commitment to completing the work. If you end up only completing 95% of it, Scrum would view this as a failure! During the "Retrospective", the PO and the SM will tell the programmers that they failed to deliver the promised work this sprint, and they need to work on improving their delivery, their estimates, or their ticket refinements. Sometimes, an entire hour, or even two, would be spent discussing why the sprint failed, and the programmers would be required to criticize their own efforts and add new rules to the process to make sure such a travesty (of having completed only 95% of the work instead of 100%) does not happen again.</p><p>The reality is, unless you set to yourself very "easy" goals, it's impossible to consistently deliver on the estimated time.</p><p>An estimate and a commitment are not the same thing. When you estimate that a thing can be done in three days, you cannot commit to completing it in three days. If you have to commit, you need to add in some slack.</p><p>When you do scrum, your team are always spending a week to finish work that could be basically completed in two days, because if they don't, they will have to suffer through the next retrospective again!</p><h3>Setting fuzzy goals</h3><p>If one wanted to actually be agile, they would not fix the scope nor the time. To be agile, you need flexibility. You must allow some "fuzziness" into the process: we will aim to get roughly this much work done in roughly this much time. Maybe we'll end up getting 90% of the work done in 90% of the time, but the rest would take much longer than expected, so we stop here.</p><p>This is good and normal. A healthy work environment would see it as positive progress towards the goal.</p><p>Fuzziness is important because you can't actually know how far ahead you would progress without actually doing the work. By the time you get near the end of milestone, you'll have a better idea about what can be reasonably completed in the remaining time, what would take a bit more time but can still be completed soon, and what would take a lot more than originally anticipated.</p><p>You can then decide whether it's worth to extend the milestone a little bit in order to give the engineers the time needed to get more work done.</p><p>This kind of flexibility is made impossible in Scrum, but it's essential for agility.</p><p>Imagine a predator chasing its prey, but at the very start they set a timer for themselves: I will catch it in 10 seconds! If 10 seconds pass and they haven't caught the prey yet, they halt the chase! Full stop! Even if they were just two seconds away from actually catching the prey! This cannot be described as "agile". In the real world, an agile predator would only halt the chase if they determined it to be futile because the prey has outpaced them and ran too far for them to catch up to.</p><h2>Closing words</h2><p>We've outlined some problems with Scrum and introduced a set of alternative ideas for project management. I'm not aware of any name for these set of ideas. They're just the obvious way to produce software, and I've seen them practiced informally in several companies I've worked with.</p><p>A part of me feels a little bit of unease about trying to outline these ideas. They feel so natural and obvious.</p><p>However, we live in a world were bad ideas like Scrum are being touted as "agile" and being adopted by companies trying to figure out how to manage their software projects, so we have no choice to counter act this except to expose its problems and outline the obvious better way.</p>]]></content:encoded></item><item><title><![CDATA[Coordinating multiple web servers on the same machine]]></title><description><![CDATA[Without complexities]]></description><link>https://hasen.substack.com/p/coordinating-multiple-web-servers</link><guid isPermaLink="false">https://hasen.substack.com/p/coordinating-multiple-web-servers</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Fri, 07 Jun 2024 11:02:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fd2a04ef-ac46-4b42-9fc6-ca31adafb26d_1456x816.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em><strong>TL;DR</strong> I&#8217;m releasing a simple reverse-proxy that is very easy to use and configure programmatically. It&#8217;s published as Go package with a vanity domain: <a href="https://pkg.go.dev/go.hasen.dev/core_server">go.hasen.dev/core_server</a></em></p><div><hr></div><p>A web server is just a program that listens to incoming tcp connections on port 80 (or 443, if you want to support https, which you should).</p><p>The only problem with this statement is that you can only have one program listening on the port.</p><p>Now, mitigating this would be easy if the OS would let you listen on a domain:port combination.</p><p>The OS provides a mechanism for different programs to listen on different ports, and even on different IP:port combinations, however, it does not provide a mechanism for different web servers to listen on different <em>domains</em>.</p><p>This is because the TCP/IP protocol does not know about domain names. They are separate systems.</p><p>The HTTP protocol is aware of domain names though; via the Host header. When you visit <code>example.com</code>, the browser sets the `<code>Host: example.com</code>` header. But that information is at the http level, not at the tcp level, and as far as I understand, the Linux kernel does not have builtin http support.</p><p>So while it is possible <em>in principle</em> to multiplex incoming requests to different programs based on the requested domain, the OS does not provide a standard mechanism for this.</p><p>Instead, you need a user-land process to fill this role: it would listen on the http and https ports, handles the TLS handshake, parses the request header, and then forwards the request to another program, based on the Host header.</p><p>This kind of program is called a reverse-proxy. A regular proxy (in internet parlance) is used by a client to hide their address: your server sees the reqeust coming from the proxy, instead of the end user. But here we&#8217;re doing the opposite: the user sends a request to your server, but unbeknownst to them, your server process is behind a proxy.</p><p>The most popular program that does this is <code>nginx</code>. It&#8217;s very versatile and can do many more things than just act as a reverse proxy. It&#8217;s just not very easy to configure programmatically: you have to find out where and how it manages its configuration files, and then edit them to add an entry to reverse proxy to your program, and then send it a signal to reload the configuration files.</p><p>Another popular tool in recent years appears to be <a href="https://doc.traefik.io/traefik/getting-started/quick-start/">Traefik</a>. I have no experience with it, but their official tutorial reveals that they depend on Docker, and require configurations with weird and obscure options.</p><p>A recent addition is <a href="https://caddyserver.com/docs/quick-starts/reverse-proxy">Caddy</a>. It&#8217;s particularly good because it handles https and uses Let&#8217;s Encrypt to automatically issue certificates. It&#8217;s written in Go and has an command line interface as well as an http json api. However, it&#8217;s not obvious how to do it programmatically via the json API. Essentially, just like nginx, it does many more things than just reverse-proxying.</p><div><hr></div><p>The problem is actually very simple, and the solution does not need to be that complicated. At least, not complicated to setup and use. </p><p>Internally it might involve some complications: handling https, issuing certificates, forwarding requests.</p><p>The usage pattern should be very simple:</p><ul><li><p>Launch it via the command line</p></li><li><p>Safe to launch multiple times (e.g. new version)</p><ul><li><p>Previous process gracefully shuts down</p></li><li><p>Configuration is carried forward by the new process</p></li></ul></li><li><p>Exposes a command that accepts two parameters: the domain and the port</p><ul><li><p>Easy to call this command programmatically from any programming language</p></li></ul></li><li><p>Does not output anything to the command line</p></li><li><p>Log filepath is well defined and easy to discover</p></li></ul><div><hr></div><p>I&#8217;m releasing a Go program that does just that. Check it out at <a href="https://pkg.go.dev/go.hasen.dev/core_server">go.hasen.dev/core_server</a></p><p>It&#8217;s only a few hundred lines of code. The way to configure the mapping is to send the following kind of message to the UDP port 40608</p><pre><code>add example.com 5555</code></pre><p>The structure of the message is simple:</p><ul><li><p>the word add</p></li><li><p>the domain name</p></li><li><p>the port number</p></li></ul><p>That&#8217;s it.</p><p>If you use it, I&#8217;d love to hear your feedback.</p><div><hr></div><p>P.S. I asked an LLM to provide the implementation of a simple function that sends a string message over a udp port, and here&#8217;s the output:</p><p>Python:</p><pre><code>import socket

def send_udp_message(port, message):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(message.encode(), ('localhost', port))
    sock.close()</code></pre><p>Ruby:</p><pre><code>require 'socket'

def send_udp_message(port, message)
  sock = TCPSocket.new('localhost', port)
  sock.send(message, 0)
  sock.close
end</code></pre><p>PHP:</p><pre><code>function send_udp_message($port, $message) {
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
    socket_sendto($sock, $message, strlen($message), 0, 'localhost', $port);
    socket_close($sock);
}</code></pre><p>Node.JS (with modifications; perhaps needs further cleanup)</p><pre><code>const dgram = require('dgram');

async function sendUDPMessage(port, message) {
  return new Promise(resolve =&gt; {
    const client = dgram.createSocket('udp4');
    client.send(message, port, 'localhost', (err) =&gt; {
      client.close();
      resolve(err)
    });
  })
}</code></pre><p>And here&#8217;s my Go implementation:</p><pre><code>func SendUDP(port int, cmd string) error {
&#9;var udpAddress = net.UDPAddr{Port: port}
&#9;conn, err := net.DialUDP("udp", nil, &amp;udpAddress)
&#9;if err != nil {
&#9;&#9;return err
&#9;}
&#9;defer conn.Close()
&#9;_, err = io.WriteString(conn, cmd)
&#9;return err
}</code></pre><p>&#8212;</p><p></p>]]></content:encoded></item><item><title><![CDATA[View Counters Design Document]]></title><description><![CDATA[How to count impressions, reach, and graph them over time]]></description><link>https://hasen.substack.com/p/view-counters-design-document</link><guid isPermaLink="false">https://hasen.substack.com/p/view-counters-design-document</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Fri, 31 May 2024 11:27:45 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/aa89a629-1a69-4e8a-a00a-8edc10ca2967_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>2024.05</p><h2>Background</h2><p>We are building a website where people make "posts" and see other people's posts. People can "follow" certain accounts and some kind of algorithm "recommends" posts for accounts.</p><h2>Problem statement</h2><p>We want to see how many views our posts are getting. We also want to distinguish between total impressions vs reach.</p><p>How many unique people saw this post? If people come to the post again and again, that's valuable information, but we do want to distinguish between impressions and reach.</p><p>We also want to see a graph of the growth of impressions and reach over time.</p><h2>How to model the data</h2><p>In previous designed documents, I just presented the data model as a given. This time I want to try something different. I want to walk through the problem solving process.</p><h3>Views</h3><p>How do we know the number of views? Well, everytime someone views the post, we increment a counter. So that should be pretty easy.</p><p>Now the problem might be that of a definition: how do we decide that someone has viewed the post? If the only way to view a post was by opening the page that holds it, that would be easy. But what if the post shows up on their news feed? Does that count as a view? What if we sent the post to the browser, but it was the 10th post in the feed and the user never scrolled down to it! Do we count it when we send it, or do we wait for a signal from the client that the user has scrolled past it?</p><p>What if the post is long and the thing that appears in the feed is just a summary, do we count that as a view, or do we keep separate counts of views of the summary vs views of the details?</p><p>We can create two counters: summary_view and detail_view</p><p>What if the post is short, and the summary and the detail are the same thing? Well we should still count the detail_view separately because it does have significance: expanding the post details is not just about viewing its content, it's also about posting a reply or seeing what replies other people have left.</p><p>So, the solution is, for each post, have two counts:</p><ul><li><p>summaryViews</p></li><li><p>detailViews</p></li></ul><p>Simply increment them when there's a viewing event.</p><p>Now we need to answer the question: how do we count a view event? I think we can just let the client tell us when the post is brought into view.</p><p>This of course leaves the possibility that a client can just spam view requests to artificially inflate the view count on a post, hoping that this would increase its odds of being recommended to more people by the algorithm.</p><p>Perhaps we don't care because that kind of spamming would actually decrease the odds of the post being recommended as it decreases the engagement rate. But then this opens the door for the opposite kind of attack: artificially decreasing the engagement rate for a post you don't like by spamming view requests to increase its total view count.</p><p>At first we probably don't care, but if we do care, perhaps several months down the road, we can guard against this kind of behavior by adding another check before counting a view: only count a view from the same user if the previous view from that user was more than 10 minutes ago.</p><p>How do we know when was the last time a user viewed a post? Well we could maintain a mapping where the key is the tuple (user_id, post_id) and the value is (view_timestamp), and if we get a request, we first check the mapping: if a record exists for the same (user_id, post_id) pair, we check the time stamp. If the timestamp is not older than 10 minutes, we ignore the request. We don't even update the time stamp! Or perhaps, we could increment a counter, so the value is not just the view_timestmap, but also how many view requests arrived in the timeframe since view_timestmap. This allows to detect spam events: if for example we get 100 or more view requests within 10 minutes, there's a high probability the user is spamming requests to increase the view count. Now, if the the view timestamp is older than 10 minutes, then we overwrite it and reset counter to 1, and we finally increase the view count on the post indicated by post_id.</p><p>Having this kind of mechanism in place should also make it very clear how to count <em>reach</em>. That is, views by unique users.</p><p>Simply speaking, if we see no entry at all for (user_id, post_id), then this is the first time this user sees this post, and we can increase the reach counter.</p><p>We can do the same thing with detail views, but this makes it likely that we will duplicate all that book keeping code.</p><p>So to avoid that, we add another parameter to the tuple above: (user_id, post_id, mode) where mode is a constant that indicates whether we are handling the summary views or the detail views. We can also use it for other things, like 'comments', 'reposts', etc. The output from the process are two bits of information: should this count as a new view? and should it count as an increase in reach?</p><p>Thinking about this in go, we can define the following structs:</p><pre><code><code>type PostViewMode int
const (
    SummaryView PostViewMode = 0
    DetailView  PostViewMode = 1

    MaxViewMode PostViewMode = 2
)

type PostViewParams struct {
    UserId int
    PostId int
    Mode PostViewMode
}

type PostViewVerifyResult struct {
    IsValid bool
    IsUnique bool
}

func VerifyPostView(params PostViewParams) PostViewVerifyResult

type UserPostViews struct {
    Timestamp timestamp
    Counter int
}

type PostAnalytics struct {
    Totals  [MaxViewMode]int
    Uniques [MaxViewMode]int
}

bucket UserPostViewsFilter(PostViewParams, UserPostViews)
bucket PostAnalyticsBucket(postId int, PostAnalytics)
</code></code></pre><p>You may have noticed the PostAnalytics struct uses an array instead of a named fields like `TotalViews, UniqeViews, TotalDetailViews, UniqueDetailViews, etc...`.</p><p>This streamlines the processing of verifying and updating data.</p><p>Simply put, given a mode, we update the counts this way:</p><pre><code><code>
result := VerifyPostView(params)
if result.IsValid {
    analytics.Totals[params.Mode] += 1
    if result.IsUnique {
        analytics.Uniques[params.Mode] += 1
    }
}
</code></code></pre><p>This is generally a good way to keep the number of codepaths small, speically when they'd be mostly identical.</p><p>Imagine if we had to do this instead, with named fields, and no "mode" parameter to VerifyPostView.</p><pre><code><code>
// code path for handling summary view
result := VerifyPostSummaryView(userId, postId)
if result.IsValid {
    summaryAnalytics.Totals += 1
    if result.IsUnique {
        summaryAnalytics.Uniques += 1
    }
}

// different (but identical) code path for handling detail views
result := VerifyPostDetailView(userId, postId)
if result.IsValid {
    detailAnalytics.Totals += 1
    if result.IsUnique {
        detailAnalytics.Uniques += 1
    }
}

// as we add more modes, we duplicate the code more and more</code></code></pre><h3>Visualizing</h3><p>To draw a chart, what do we need? We need data points on a scale that makes sense for the chart's dimensions.</p><p>If we're drawing data from the last 7 days and the chart has some width in it, it might make sense to take data grouped by the hour: every dot on the chart represents the value of the views counter at that point in time, one dot for the views on 2024-05-26 14:00, the next dot is the value of the views counter on 2024-05-26 15:00, and so on.</p><p>If we are viewing the chart over the range of a year, it doesn't really make sense to use the granularity of an hour. A day would make more sense: what was the views counter set to at 2024-05-26 0:00</p><p>If we imagine viewing the chart with the range of 10 years, the granulairty of a single week might be enough.</p><p>So let's go with three levels of granularity: hour, day, week.</p><p>That should satisfy most conditions reasonably well.</p><p>What if you want to see analytics in a finer detail? We could keep 10 minute granularity view counts only for the 72 hours, for example. Otherwise it'll just waste too much data.</p><p>The expected usage pattern is the user wants to visualize the exposure for some post he made over time. In the same chart, we want to show the impressions, the reach, and the engagement numbers over a period of time. The granularity will be decided by the time range. I'm not worried too much right now about who will decide the granularity or how.</p><p>The query parameters will probably include the post id, the time range, the metric (the view mode from above), and the granularity. The output is a list of pairs, each pair is a timestamp and the view counts for that timestamp.</p><p>The timestamp in the output can be chosen either to represent the beginning or the end of the "time window" specified by the granularity. For example, if the granulairty is a day, and the timestamp is "2024-05-26" and the views is 550, does that mean the post had 550 at the start of the day or the end of the day? Does the timestamp represent the start or the end of the time period?</p><p>It doesn't matter which convention we pick, as long as we remain consistent and stick to it throughout the entire codebase.</p><p>So what should we pick? I think we should pick the choice that simplifies the process of storing and querying.</p><p>How do we "store" the data? Everytime we register a new view event, we have to know to which time bucket it belongs to, for each granularity we care about, and update the count for that granularity.</p><p>It's easier to define the granularity if we can "truncate" the timestamp to find the appropriate time value.</p><p>For example, let's say the time is 2024-05-26 14:12</p><p>Hour granularity: 2024-05-26 14:00</p><p>Day Granularity: 2024-05-26 00:00</p><p>What about the week granularity? I think we can just use the Go standard library time trimming function.</p><pre><code><code>package main

import (
&#9;"fmt"
&#9;"time"
)

func main() {
&#9;t := time.Date(2024, 5, 26, 14, 12, 0, 0, time.UTC)
&#9;fmt.Println(t)
&#9;fmt.Println(t.Truncate(time.Hour))
&#9;fmt.Println(t.Truncate(time.Hour * 24))
&#9;fmt.Println(t.Truncate(time.Hour * 24 * 7))
}</code></code></pre><p>Outputs:</p><pre><code>2024-05-26 14:12:00 +0000 UTC
2024-05-26 14:00:00 +0000 UTC
2024-05-26 00:00:00 +0000 UTC
2024-05-20 00:00:00 +0000 UTC</code></pre><p>For the week, it seems to truncate it to a Monday. It probably has to do with the underlying representation.</p><p>We can try in another language. Let's say, Javascript.</p><p>The time truncate works by doing (time - time % duration). It should be pretty easy to replicate in Javascript using just unix timestamps (JS timestamps are to millisecond accuracy).</p><pre><code><code>let t = Date.parse("2024-05-26T14:12") // 1716700320000

let s = 1000
let m = s * 60
let h = m * 60
let d = h * 24
let w = d * 7

// truncate to hour
new Date(t - t % h).toJSON() // 2024-05-26T05:00:00.000Z

// truncate to day
new Date(t - t % d).toJSON() // 2024-05-26T00:00:00.000Z

// truncate to week
new Date(t - t % w).toJSON() // 2024-05-23T00:00:00.000Z</code></code></pre><p>Notice the week start point is now different. Go does not use unix timestamps internally, so that's probably why.</p><p>For general consistency it's probably better to stick to unix timestamps.</p><p>The answer is clear now: the timestamp represents the start of the time window for this bucket in this granularity.</p><p>Now, it should be very clear how to update counts:</p><p>Everytime we encounter a view, after updating the totals, we also update the totals in the current bucket for each time granularity</p><p>Remember the above line where we did:</p><pre><code><code>analytics.Totals[params.Mode] += 1</code></code></pre><p>This means we already know the totals, so all we have to do is store in the specific time bucket for each granularity.</p><p>Note however that this process does not guarantee that we will have entries for all the time buckets.</p><p>To give an example, let's say there are the following entries in our database. Assume they are all for the same post, for the same granularity (hour), for the same metric (total impressions).</p><pre><code> Timestamp           Value
-------------------|-------------------
 2024-05-26 13:00  | 2454
 2024-05-26 14:00  | 2465
 2024-05-26 17:00  | 2470
 2024-05-26 18:00  | 2493
 2024-05-26 19:00  | 2509
 2024-05-26 20:00  | 2538
 2024-05-26 21:00  | 2552
-------------------|-------------------</code></pre><p>Notice there are no entries for hours 15:00 and 16:00. Why does this happen? Because the process we perform is to set the total values in the bucket for the current time window. If no event occurs during the specific time window, there will be no entry.</p><p>How do we deal with this?</p><p>Well, if the client queries for views from, say 15:00 to 20:00, we have nothing to show for 15:00, so we have to find the last entry before 15:00 and use its value. In this case, the entry is the 14:00 entry, and the view count at the end of that entry is the view count at the end of 15:00 (aka the view count at 16:00). How do you get the last entry directly before the 15:00 entry depends on your database engine. If it's a low level storage engine that can iterate on entries by order with a cursor, you just move the cursor back one step at the beginning.</p><p>With that, we can now describe the data model.</p><p>Note: all of the above was kind of a thinking process. I'm not necessarily going to use everything as-is. Don't be too surprised if the next section uses slightly different names or even structures than what I described above. I tried to streamline it a little bit more.</p><h2>Data Model</h2><p>As always, no actual code has been written yet, so think of this as a starting point to get you started, not as the final destination to arrive to.</p><pre><code><code>type Metric int
const (
    SummaryViews Metric = 0
    DetailViews  Metric = 1
    Engagements  Metric = 2

    MaxMetric    Metric = 3
)

type Granularity int
const (
    AllTime Granularity = 0
    Weekly  Granularity = 1
    Daily   Granularity = 2
    Hourly  Granularity = 3

    MaxGranularity Granularity = 4
)

type UserPostKey struct {
    UserId int
    PostId int
    Metric Metric
}

type UserRepeatTracking struct {
    Timestamp timestamp
    Count int
}

type PostMetricsKey struct {
    PostId int
    Granularity Granularity
    Metric Metric
    Timestamp timestamp
}

bucket UserViews(UserPostKey, UserRepeatTracking)
bucket PostMetricsTotal(PostMetricsKey, int)
bucket PostMetricsUnique(PostMetricsKey, int)</code></code></pre><h2>Processes</h2><h3>Handling an analytic event</h3><p>Some user interacted in some what with a post:</p><ul><li><p>View</p></li><li><p>Expand</p></li><li><p>Engagement (like, comment, etc)</p></li></ul><p>The input is: user id, post id, metric type.</p><p>In response to this event, we want to record some analytics.</p><ul><li><p>Check the values in the `UserViews` bucket that corresponds to the given input. If a value already exists, check the time stamp. If the activity is within a certain threshold (e.g. 10 minutes), increase the counter and don't proceed with any anlytics recording. If the timestamp is older than the threshold, or there's no entry, reset the entry with the current timestamp and a counter value of 1. It's important to note whether there was an entry at all or not. If there was no entry, this counts as a new unique event. If not, it's just a regular event but not unique</p><ul><li><p>For future consideration: If the value of the counter is way too high (e.g. 1000 events) it might be a suspicious activity. (For now we don't specify how handle it)</p></li></ul></li><li><p>Compute the appropriate timestamps for all granularities:</p><ul><li><p>AllTime: The zero time</p></li><li><p>Weekly: Now, truncated by a 7 day duration</p></li><li><p>Daily: Now, truncated by a day duration</p></li><li><p>Hourly: Now, truncated by an hourly duration</p></li></ul></li><li><p>If we are to add the event to the total counter:</p><ul><li><p>Fetch the counter from the PostMetricsTotal buckets that corresponds to the AllTime granularity.</p></li><li><p>Increment it by one</p></li><li><p>Store the number in the PostMetricsTotal bucket for all the given granularities (using the timestamps we computed)</p></li><li><p>If we are to consider this event a unique event, do the same thing for the PostMetricsUnique bucket</p></li></ul></li></ul><h3>Displaying metrics for a post</h3><p>Given a specific post, the user wants to see how many total views it got, and how many unique impressions it's got. He also wants to see the total and unique values for the detail expands and engagements.</p><p>The input is the post id.</p><ul><li><p>Construct a PostMetricsKey query key that has the post id set to the input, while the granularity and timestamps are kept to the zero value.</p></li><li><p>Loop over the metrics values (0..&lt;MaxMetric), and for each one, copy the key, set the Metric to the value from the loop, and load the value from both the PostMetricsTotal and PostMetricsUnique.</p><ul><li><p>Alternatively, instead of looping explicitly, utilize the appropriate functionality in your database system that loads all entries where for the given post it at granularity 0 and timestamp 0. In other words, allow the database system to naturally load all the metrics without you explicitly looping over the metric values.</p></li></ul></li><li><p>Collect the result into the appropriate structure for a response. Something like this could work:</p></li></ul><pre><code><code>    type PostMetrics struct {
         Uniques [MaxMetric]int
         Totals  [MaxMetric]int
    }</code></code></pre><h3>Displaying post metrics in a chart</h3><p>To show the various engagement metrics change (grow) over time.</p><p>Input: Post ID, time range (start, end), granularity, the list of metrics.</p><ul><li><p>For each listed matreic, construct the appropriate PostMetricsKey, using the given post id and granularity and start time. Iteratively all the entries for this metric that fall within the given time range.</p><ul><li><p>If there's a significant gap between the timestamp in the first entry that matches the query and the requested range's start time, find the one entry immediately before the requested start time.</p></li></ul></li><li><p>Return the data to the client to be displayed using the appropriate UI component (out of scope for this document).</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Paywall Design Document]]></title><description><![CDATA[Multiple products, multiple access modes]]></description><link>https://hasen.substack.com/p/paywall-design-document</link><guid isPermaLink="false">https://hasen.substack.com/p/paywall-design-document</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Fri, 10 May 2024 11:31:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f48c6f5f-1f55-46c3-9acf-40504d019d8f_1728x688.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is part of a series of data modeling design documents.</p><p>Keep in mind that this kind of document only serves as a starting point, not as a final design.</p><p>As you start to implement this in actual code, it's almost certain that you would need to make some changes and adjustments.</p><p>If this document helps you get started and get along far enough to a working solution, then it has served its purpose.</p><h2>Background</h2><p>We are running a website and we want to provide some products and services that require payment to access.</p><p>At first we're not worrying about the actual payment process. Instead, we'll focus first on managing the data that represents user's license to access the products.</p><p>I'll introduce interfacing with some payment provider in the later part of this document.</p><p>One thing I want to bring to your attention is that we will not be using an "abstraction" for the payment processor service.</p><p>Instead we'll just be managing our own data, and at key points interact with the external service to verify conditions and update our data.</p><h2>Requirements</h2><ul><li><p>Allow multiple separate products</p></li><li><p>Allow access by:</p><ul><li><p>subscription</p></li><li><p>usage credits</p></li><li><p>a combination thereof</p></li></ul></li><li><p>Allow trial access</p></li><li><p>Allow lifetime access</p></li><li><p>Allow multiple tiers of access (basic, pro, premium, etc)</p><ul><li><p>With different credit point cost for features, depending on the tier</p></li></ul></li></ul><h2>Thinking about the data model</h2><p>With these requirements in mind, it's tempting to think about "modes" of access, and data to describe product "packages", etc. But I think we can build a simple core model that supports all the requirements.</p><p>Instead of thinking about disjoint features, I want to think about their union.</p><p>It should be possible for the product to be subscription based but also limited by usage. The subscription gives you base level access to the features, but if you use too much you'll need to make additional payments to recharge your credits.</p><p>It took me a few iterations to arrive at this design, so don't be surprised if it appears counter-intuitive.</p><pre><code><code>type UserLicense struct {
    Id int
    UserId int
    ProductId int

    RequiresCredits bool
    Credits int

    HasExpiration bool
    Expiration timestamp

    AutoRenews bool
    RenewPeriod Period
}

type Period struct {
    Years int
    Months int
    Days int
}

type UserProductKey struct {
    UserId int
    ProductId int
}

bucket UserLicenses(Id int, UserLicense)
bucket LicenseLookup(UserProductKey, Id int) // user-product =&gt; license-id</code></code></pre><p>This design models the access right of a certain user to a certain product.</p><p>It does not describe a "package".</p><p>To lookup what access a user has to a product, we use the LicenseLookup bucket to find the license id and then load the license from the UserLicenses bucket.</p><p>In principle, it's possible for different users to have different access modes to the same product: one user has life-time access, another user has a subscription based access, another user has credit-based access, etc.</p><p>The flags <code>RequiresCredits</code> and <code>HasExpiration</code> determine the access mode.</p><p>If <code>RequiresCredit</code> is set to true, then credit charges may apply. If it's false, then credit charges will never apply.</p><p>If <code>HasExpiration</code> is set, then the user cannot access the product past the expiration date.</p><p>If <code>AutoRenew</code> is set, then we expect the expiration date to be extended by <code>RenewalPeriod</code> after the expiration date has passed. This means when checking for expiration, we should allow a grace period for the payment to go through and for our system to update its records. (I'm not sure if RenewalPeriod belongs here; this is a point to revisit when I go to actually implement this).</p><p>If <code>RequiresCredit</code> and <code>HasExpiration</code> are both false, then the user has unlimited lifetime access to the product. Notice how we don't have an explicit flag for it. It just emerges naturally.</p><p>Similarly, trial access can be implemented by granting the user access to the product with a short expiration period, and we can apply some restrictions such as making sure that they have never had access to the product in the past.</p><p>If they did have access to the product in the past but it expired, the entry would be still be in the UserLicenses bucket, and it would be still be found using the LicenseLookup bucket.</p><h3>Product ID</h3><p>The product id has no inherent meaning. It's just a numeric value that we use across the code and the database to denote a specific product or service.</p><p>The only thing that matters is that this never changes, ever, and that two separate products never ever have the same id.</p><h3>Access Tiers</h3><p>The data model itself has no concept of tiers, but tiers can simply be encoded as different product ids.</p><pre><code>const TierA = 10
const TierB = 11
const TierC = 12</code></pre><p>Verifying access right to a service can be done by verifying all relevant tiers, and using the first one that matches.</p><h3>Credit Points</h3><p>Credit points are currency-like values for use within our system. They are integers only and have no fractions.</p><h3>Using a Product</h3><h4>Verifying and Granting Access</h4><pre><code><code>type ProductAccessCost struct {
    ProductId int
    CreditCost int
}

type ProductAccessResult struct {
    Allowed bool
    BalanceBefore int
    BalanceAfter int
}

func VerifyProductAccess(tx Tx, UserId int, cost []ProductAccessCost) ProductAccessResult
func ChargeProductAccess(tx Tx, UserId int, cost []ProductAccessCost) ProductAccessResult</code></code></pre><p>Notice that VerifyProductAccess and ChargeProductAccess take a list of ProductAccessCost. This is to support the concept of "Access Tiers" as explained above.</p><p>For example, if you are on a basic tier, this feature costs 20 points, but if you are on a pro tier, it costs 5 points.</p><p>The result would have the <code>Allowed</code> flag set to true if the user's access has not expired, and if he can fulfill the credit costs (that is, that his credit points &gt;= the credit cost, but this restriction only applies if his access has the <code>RequiresCredits</code> flag).</p><p>VerifyProductAccess would just check the user can access one of the product ids listed, and it would report how his credits would change if he were to use this 'set'.</p><p>ChargeProductAccess would actually charge the credit points against the user. It can potentially do other things we haven't covered here, such as logging the user's access to the product.</p><h2>Interfacing with an external payment service</h2><p>We can use an external payment provider like Paypal, Stripe, LemonSqueezy, Paddle, GumRoad, or some other service.</p><p>We need two essential features from such a service:</p><ul><li><p>A way to verify some proof of payment</p></li><li><p>A way to verify subscription status</p></li></ul><p>Both boil down of course to verifying payment at different points in time.</p><h3>LemonSqueezy</h3><p>To simplify the interface with LemonSqueezy, I'll base the design on the assumption that they issue a LicenseKey per product.</p><p><a href="https://docs.lemonsqueezy.com/help/licensing/license-api#post-v1-licenses-validate">LemonSqueezy License Key Validation API</a></p><p>LemonSqueezy has its own version of productId, and it also has a variantId to handle different tiers or access modes.</p><p>Mapping between our internal product ids and LemonSqueezy's product-and-variant-id can be done in a variety of ways, but in my opinion we can just use a switch statement or a series of if-else blocks.</p><p>The variantId basically determines the payment method (free, onetime, recurring) and the assumed access mode. For example, if you offer a trial, a basic membership, and a premium membership, they would each be considered a variant of the same product.</p><pre><code><code>type LicenseSource struct {
    Id int // same as UserLicense.Id
    Source string // e.g. "LemonSqueezy" or "Stripe"
}

type LemonSqueezyOrder struct {
    Id int // same as UserLicense.Id
    OrderItemId int
    LicenseKey string // if applicable
    SubscriptionId int // if applicable
}

bucket LicenseSources(Id int, LicenseSource)
bucket LemonSqueezyOrders(Id int, LemonSqueezyOrder)
index ActiveSubscriptions(Expiration date, LicenseId)</code></code></pre><h3>License Key Verification</h3><p>We provide a UI for the user to enter his License Key that he acquired via LemonSqueezy.</p><p>The user sends us his key, and we verify it using the validate API (linked above).</p><pre><code><code>POST https://api.lemonsqueezy.com/v1/licenses/validate license_key=1234-1234-abcd-abcd</code></code></pre><p>If the license is valid, the response will have have <code>valid: true</code> and it will also contain some meta data. We care about the following:</p><ul><li><p>store_id</p></li><li><p>product_id</p></li><li><p>variant_id</p></li><li><p>order_item_id</p></li><li><p>customer_email</p></li></ul><p>If the store_id does not match our store, then the key is invalid from our point of view.</p><p>If the customer_email does not match the logged in user, we reject this key and send an appropriate error message to the user to let them that they need to login with the same email address.</p><p>If store_id and customer_email are valid, we create (or update) the user license information to grant them access to the product based on the product_id and variant_id. For the purposes of this document, I'm assuming this will be done manually via code. For example:</p><pre><code><code>switch variantId {
    case Lemon_ProductA_TierB:
        userAccess.ProductId = ProductA_TierB
        userAccess.HasExpiration = true
        userAccess.ExpirationDate = time.Now().AddDate(0, 1, 0)
        userAccess.AutoRenew = true
        userAccess.RenewPeriod.Months = 1
    case .....:
        .... etc
}</code></code></pre><p>We use the order_item_id to find the subscription object</p><pre><code><code>GET https://api.lemonsqueezy.com/v1/subscriptions?filter[order_item_id]=123456</code></code></pre><p>Now, we update the LicenseSources and LemonSqueezyOrders buckets to contain the relevant information.</p><h3>Renew Subscription</h3><p>This requires a daily process. The simplest way is have a goroutine (or thread, or equivalent) scheduled to run at a set time everyday, e.g. 1am. Have it query all licenses that have expired but expected to auto renew.</p><p>The query uses the <code>ActiveSubscriptions</code> index. Since keys are sorted from lowest, the first key is the <em>oldest</em> expired subscription. We start iterating from the first key and stop as soon as we find a key that is in the future.</p><p>In a SQL database, this could roughly look something like:</p><pre><code><code>select license_id form active_subscriptions
where expiration_date &lt; today + 1
order by expiration_date</code></code></pre><p>For each item, verify the status of the subscription with the merchant service.</p><p>It would be nice if LemonSqueezy provided us an API that takes a list of subscription ids and reports the status for all of them at once, but for now I'm afraid we'll have to do it one at a time.</p><p><a href="https://docs.lemonsqueezy.com/api/subscriptions">https://docs.lemonsqueezy.com/api/subscriptions</a></p><p>Check the <code>status</code> field. If it's <code>"active"</code>, then we can assume the renewal payment was successful, and we can update the expiration date on our database (including updating the corresponding entry on the <code>ActiveSubscriptions</code> index).</p><p>If it's <code>"past_due"</code>, we check the grace period. If we're past the grace period, we cancel the subscription from our end and send a request to LemonSqueezy to cancel it as well:</p><p><a href="https://docs.lemonsqueezy.com/api/subscriptions#cancel-a-subscription">https://docs.lemonsqueezy.com/api/subscriptions#cancel-a-subscription</a></p><p>Similarly, if it's any of <code>"unpaid"</code>, <code>"cancelled"</code>, or <code>"expired"</code>, then we consider it cancelled, and update our data accordingly.</p><p>Note: when we cancel a subscription on our end, we also remove its entry from the <code>ActiveSubscriptions</code> index.</p>]]></content:encoded></item><item><title><![CDATA[The Straight Forward Programming Manifesto]]></title><description><![CDATA[It's all about the data]]></description><link>https://hasen.substack.com/p/straight-forward-programming</link><guid isPermaLink="false">https://hasen.substack.com/p/straight-forward-programming</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Sat, 20 Apr 2024 15:06:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/444e0db4-7c89-4c85-9b8f-9bbb9e677f15_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Software engineering is about data modeling: Specifying the structure of the data, and specifying how it is processed.</p><p>All behaviors we associate with computer applications emerge from how data is processed over time.</p><p>The only thing computers can do is data processing. They can't do anything else.</p><div><hr></div><p>It makes no sense to talk about "business logic" as if it's a separate thing from the data processing.</p><div><hr></div><p>The purpose of reading code is to deduce how data is being processed. When code is written in such a way to purposely obfuscate what the data is and how it's processed, we say the code is hard to read.</p><div><hr></div><p><strong>We advocate</strong> straight forward programming: writing code in the most simple and direct way to perform its duty:</p><ul><li><p><strong>Process the data correctly</strong></p><p>This is the essence of programming. If you can't do this you're not a programmer.</p></li><li><p><strong>Lend itself to be easy to understand</strong></p><p>If you can't deduce the data model and the operations being performed on it, then the code is not easy to understand. (skill issues non-withstanding)</p></li></ul><div><hr></div><p>The purpose of software architecture is to model data processing such that:</p><ul><li><p><strong>Building blocks</strong> allow a wide range of desired behaviors</p><p>You get the desired behavior by composing building blocks together in different ways. You can experiment until you find what works well</p></li><li><p><strong>Code paths</strong> can be simplified and collapsed</p><p>The data model is rich enough to describe a wide range of behaviors that can be implemented by very few code paths</p></li></ul><div><hr></div><p><strong>We oppose</strong> architectures that:</p><ul><li><p><strong>Hide</strong> the data and <strong>obfuscates</strong> its processing<br>Making systems opaque and difficult to reason about is not a virtue</p></li><li><p><strong>Overfit</strong> to the current iteration of the requirements</p><p>A good architecture is flexible, allowing exploration of the design space</p></li><li><p>Emphasize <strong>abstract processes</strong> that are not defined in terms of data<br>Good abstractions are defined in terms of data.<br>Bad abstractions are defined in terms of abstract objects.</p></li></ul><div><hr></div><p><strong>We Encourage</strong> proven techniques to untangle code complexities </p><ul><li><p>Separate data processing into <strong>stages<br></strong>Each stage prepares the data to feed to the next stage.</p></li><li><p><strong>Semantic </strong>Compression<br>Extract common code patterns into reusable functions that operate on data</p></li><li><p>Collapsing <strong>code paths</strong><br>When the same code path can process many cases without modification</p></li></ul><div><hr></div><p>We <strong>de-emphasize</strong> irrelevant concerns</p><ul><li><p>How to organize code in files and folders?</p></li><li><p>How big should files be?</p></li><li><p>How long should functions be?</p></li><li><p>How to format code?</p></li></ul><p>It doesn&#8217;t matter. Use your taste. Find what works for you.</p>]]></content:encoded></item><item><title><![CDATA[Authentication Design Document]]></title><description><![CDATA[User registration, login, sessions]]></description><link>https://hasen.substack.com/p/authentication-design-document</link><guid isPermaLink="false">https://hasen.substack.com/p/authentication-design-document</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Sun, 14 Apr 2024 15:43:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b963a8bb-e322-4c34-a64e-44f5bdf9dc4f_1344x896.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm trying a new series where I take some hypothetical feature and explain what it takes to implement it. (In the context of web programming).</p><p>There will not be much in the way of actual code, but there should be enough details for most programmers to be able to implement the feature on their own as an exercise, even if they are not experienced enough to come up with the design on their own.</p><p>If you are experienced, everything here probably seems trivial. You might even wonder what's the point.</p><p>There's a phenomenon where experienced people don't realize the knowledge and experience they take for granted is actually not available to most people. I've noticed it when working with other people: things that seem obvious to me are puzzling to others. Likewise, some things are puzzling to me, but appear obvious to those proficient at them.</p><p>Authentication is not a particularly difficult problem. If anything, it's among the easiest problems you'll ever encounter. But it feels like a good starting point for the series, because almost any problem you want you solve will require you to have users and accounts first.</p><h2>Background:</h2><p>What does it mean for a user to register and login? It means they reserve a handle (e.g. a username, email address) to identify themselves, and they can use it to interact with the service as themselves.</p><p>This document will be limited to handling the following operations:</p><ul><li><p>Registration</p></li><li><p>Sessions</p><ul><li><p>Login</p></li><li><p>Logout</p></li></ul></li><li><p>Email Verification</p></li><li><p>Password Reset</p></li></ul><p>Almost all operations require the use of "tokens".</p><h2>Outline of Operations:</h2><p><strong>Registration</strong></p><p>The user reserves an identifier and specifies a password. Potentially providing other information, such as an email address and a phone number.</p><p><strong>Email Verification</strong></p><p>The user tells us back a secret token or short code that we sent to his email (or phone number).</p><p><strong>Login</strong></p><p>The user provides his username and password to prove that he is the one sitting on the machine, and as a result, a session is created.</p><p><strong>Session</strong></p><p>The website remembers that the user has logged in, without requiring him to re-enter his password all the time to verify himself.</p><p>The server issues a secret token to associate with the user id, the client keeps this token and sends it along with every request.</p><p><strong>Password reset</strong></p><p>If the user forgets his password, a fallback mechanism is provided via email.</p><p>Usually done by producing a password-reset token and sending an email with a special URL that, when clicked, opens a page that allows resetting the form.</p><p><strong>Logout</strong></p><p>The client forgets the session token, and optionally requests the server to expire the token</p><h3>Important Considerations</h3><p><strong>The Session</strong></p><p>The session token counts as a form of authentication, but it's weaker than the password. It's possible that some time after the user has logged in, someone else is now sitting at the machine.</p><ul><li><p>Some operations should require the user to re-enter his password.</p><ul><li><p>Viewing sensitive information</p></li><li><p>Attempting to change the password</p></li><li><p>Making a payment, specially if a large sum</p></li></ul></li><li><p>Tokens need to expire after a while to minimize risks.</p></li></ul><h3>Building Blocks</h3><p><strong>Passwords</strong></p><p>Passwords are to never be stored in plain text as-is. They must be encrypted using a "one way" encryption scheme, such that:</p><ul><li><p>It's not practically possible to deduce the password given the encrypted form</p></li><li><p>It's very easy to validate a specific password against a specific hash</p></li></ul><p>The consensus seems to be on the "bcrypt" algorithm.</p><p>Here's the Go implementation: <a href="https://pkg.go.dev/golang.org/x/crypto/bcrypt">https://pkg.go.dev/golang.org/x/crypto/bcrypt</a></p><p><strong>Username and User Id:</strong></p><p>The system should be designed to allow the user to change their username without breaking anything else in the system.</p><p>Now, whether you want to allow the user to change his identifier is a separate question.</p><p>The implication is that the actual user identification used throughout the system is a numerical id, and the user-facing handler is merely a way to get the actual user id.</p><p><strong>Generating Tokens</strong></p><p>Tokens are easy to generate: fill a 16 byte buffer with random data from the OS provided high entropy random data source (/dev/random on linux).</p><p>Then encode the buffer to base64 (or hex, or any other text encoding)</p><p><strong>Verifying and using tokens</strong></p><p>Some operations require sending the user a URL with a token.</p><p>When the page is first loaded, the server verifies the token: that it exists, has not expired, and is useable for the given purpose (password reset, etc).</p><p>When the form on the page is submitted, the token is submitted with it as well, and the server again verifies the token.</p><p>If the token is one-time use, the server expires the token after performing the requested operation.</p><p><strong>Token expiration and deletion</strong></p><p>Tokens are only useful for specific operations. There's no point in keeping old tokens around forever. Once a token is expired, it can either be deleted immediately, or left alone for some other process to delete old tokens.</p><p>For example, you can run a daily process to delete any token that has expired more than 10 days ago.</p><p><strong>Sessions</strong></p><p>A session is merely a token that identifies a user.</p><p>The client and the server agree on a special identifier name to use in request headers (or cookies) to identify the session token.</p><h2>Data Design</h2><p>To support all the process needed for authentication, we will define types, buckets, and indices.</p><p>Refer to the <a href="/__u/hasen.substack.com/p/data-spec">Data Design Spec</a> for a brief explanation of the notation used.</p><pre><code><code>
type Account struct {
&#9;Id int // auto generated
&#9;Username string
&#9;Email string
&#9;EmailVerified bool
&#9;PasswordHash string
&#9;Created time.Time
}

type AccountToken struct {
&#9;Token string // unique (id)
&#9;Type string // session, email_verification, etc
&#9;AccountId int
&#9;Created timestamp
&#9;Expires timestamp // or can be defined as a duration
}

bucket Accounts(Id int, Account)
bucket Usernames(Username string, AccountId int)
bucket Emails(Email string, AccountId int)
bucket Tokens(Token string, AccountToken) // string to object
index  AccountTokens(AccountId int, Token string) // iterates tokens associated with account

// Valid session token types
const TokenTypeSession = "session"
const TokenTypePasswordReset = "password_reset"
const TokenTypeEmailVerification = "email_verification"</code></code></pre><h2>Operations</h2><p>Her's a basic description of operations in plain language.</p><h3>New Account Registration</h3><ul><li><p>User provides desired username, email, and password</p></li><li><p>Verify that no existing account uses the username</p><ul><li><p>By checking the Usernames index does not have an entry for the given username (and the same for the email, if you want)</p></li></ul></li><li><p>Hash the password using bcrypt</p></li><li><p>Generate a new Id on the AccountsBucket</p><ul><li><p>Each bucket has an auto incrementing sequence</p></li></ul></li><li><p>Generate a verification token for the provided email, and send an email (as a background task) containing a verification link</p></li><li><p>Store the Account in the Accounts bucket.</p></li><li><p>Create a session token and send it to the client.</p></li></ul><h2>Verify Email:</h2><ul><li><p>A verification URL with a token is already sent to the email address</p></li><li><p>When the user opens the URL, the token will be sent to the server</p></li><li><p>The server looks up the token, makes sure it has not expired, and that it is</p></li></ul><p>in fact an email verification token</p><ul><li><p>The email associated with the token is now considered verified. Update the <code>EmailVerified</code> flag on the account.</p></li><li><p>This token is one time use, so expire it and schedule it for deletion</p></li></ul><h3>Change Email:</h3><p>When the user changes their email address, the following steps must be taken:</p><ul><li><p>Set the <code>EmailVerified</code> flag on the account to <code>false</code></p></li><li><p>Use the <code>AccountTokens</code> index to find any "pending" email verification token associated with the account and expire it (or delete it). This is crucial because the account is only associated with the account id rather than the email.</p></li><li><p>Create a new verification token and send it (as a URL) to the provided email address. (same as what we did during registration)</p></li></ul><h3>Login:</h3><ul><li><p>User enters his username and password</p></li><li><p>The password is sent to the server in plain text</p></li><li><p>Use the <code>Usernames</code> bucket to find the account id</p></li><li><p>Load the account and compare the password against the <code>PasswordHash</code> on the account, using the bcrypt algorithm.</p><ul><li><p>Note: an empty password hash does not match any password!</p></li></ul></li><li><p>If account exists and password matches, create a session token and return it to the client along with basic user information</p></li><li><p>If the client is SPA based, store the session token in localStorage and add it to the headers in every request using a special header name that both the client and the server agree on.</p></li></ul><h3>Password Reset:</h3><p>If the user has forgotten their password, they are allowed to request a password reset.</p><ul><li><p>The user enters their username or their email, which ever they happen to remember</p></li><li><p>The server finds the account id associated with the account, creates a password reset token, and sends it to the email associated with the account.</p></li><li><p>When the user clicks the URL, he is taken to a form for entering a new password</p></li><li><p>The new password and the token are sent to the server</p></li><li><p>The server verifies the token: that it has not expired, and that it is of the appropriate type</p></li><li><p>The password is hashed, and a session token is generated and returned to the client.</p></li></ul><h3>Password Change</h3><p>If the user wants to change his password while logged in, we require him to enter his original password, because as mentioned above, the session is a weaker form of authentication than the password.</p><ul><li><p>User enters existing password and new password</p></li><li><p>Existing password is validated against hashed password stored on the account</p></li><li><p>New password is hashed, and its hash replaces the old password hash on the account</p></li></ul><h3>Session Invalidation</h3><p>This is an admin function: if there's suspicion that a user account has been hacked, it should be possible for an admin to invalidate all current sessions associated with the user account.</p><ul><li><p>Use the <code>AccountTokens</code> index to find all tokens associated with the account</p></li><li><p>If the token is not expired, expire it immediately</p><ul><li><p>No need to check for token type. We actually should expire all tokens of all types, because each one can be used to gain illegitimate access to the account.</p></li></ul></li></ul><h3>Session Recognition</h3><p>You want a certain class of request handlers on the server side to require a valid session before proceeding with handling the request.</p><p>The exact requirements vary from one handler to another, but at a minimum, we want to return a "401 unauthorized" if the request does not have a session.</p><ul><li><p>Grab the session token from the specified location: either a special http header, a special token name, or both</p></li><li><p>Find the token object, make sure its type is "session", and that is is not expired.</p></li><li><p>Find the account associated with it and make sure it exists.</p></li><li><p>If any of the above is not fulfilled, return a 401 response and stop end the handler</p></li><li><p>If all conditions are satisfied, keep around the session and account information so the rest of the request handler has access to them.</p></li></ul><h2>Conclusion</h2><p>I have mixed feelings about writing this document. On one hand, I think this kind of data modelling and informal explanation of basic operations is useful in general. On the other hand, I'm not sure of:</p><ul><li><p>How useful is it for this particular "feature":</p><ul><li><p>To me it seems very basic. A few months ago I would have assumed that every programmer just has enough skills to come up with this on their own.</p></li></ul></li><li><p>How detailed should I go:</p><ul><li><p>I tried to keep it high level and abstract. I made almost no assumptions about programming language, framework, or database.</p></li><li><p>I kept it focused on the server side data model</p></li></ul></li><li><p>How actionable this information is:</p><ul><li><p>Would a team of beginners, having no prior experience implementing this feature, be able to use this document to actually create a fully working implementation?</p></li></ul></li><li><p>Noise to signal ratio:</p><ul><li><p>How much of the document is actually useful? How much of it is just blabber that makes it difficult to access the useful information hidden in it? (assuming there's any at all)</p></li></ul></li></ul>]]></content:encoded></item><item><title><![CDATA[Data Spec]]></title><description><![CDATA[Conventions for data modelling]]></description><link>https://hasen.substack.com/p/data-spec</link><guid isPermaLink="false">https://hasen.substack.com/p/data-spec</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Sun, 14 Apr 2024 15:41:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6f6d09c6-6642-40f3-aedf-2d2dddc620c9_1680x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This document describes the convention used to describe data in feature design documents.</p><h2>Data as structs</h2><p>The core convention is describing data in terms of structs and slices, in the syntax of the Go programming language.</p><p>This will be in contrast to the more common convention of describing data in terms of relational tables.</p><p>This choice is deliberate. You can of course use relational tables to store the data. You can also use "json" documents to store the data.</p><p>The basic data types are going to be: int, float, string, boolean.</p><p>In addition, I will often refer to 'date' and 'timestamp' as if they were built-in types.</p><p>A 'date' is usually a string in the format 'yyyy-mm-dd'. The idea is that it has no time component, and no timezone. It's a little bit abstract in that sense.</p><p>A timestamp is probably stored as a 64bit millisecond with respect to some epoch (usually the unix epoch), along with a timezone (either as an identifier, or as a difference from utc). The details might vary from system to system, but the core assumption will be that it can be converted to some specific local time for the purpose of input/output on the user's machine.</p><p>Example:</p><pre><code><code>type StoreItem struct {
&#9;Id int
&#9;Slug string // url slug; one per store item
&#9;Title string
&#9;Subtitle string
&#9;Description string
&#9;BrandId int // reference to some brand
&#9;Images []string // urls relative to the user images endpoint
&#9;UnitPrice int
&#9;UnitCurrency string
}

type Brand struct {
&#9;Id int
&#9;Name string
&#9;Logo string // image url
&#9;Header string // image url
&#9;Homepage string // company url
&#9;CatchPhrase string
&#9;Description string
}</code></code></pre><h2>Storage Bucket</h2><p>Defining a type does not mean that it will be stored as-is in the database. Things are only stored when they are put in a bucket. A bucket uses a lookup key to retrieve a type of object</p><p>Examples:</p><pre><code><code>bucket StoreItems(Id int, StoreItem)
bucket Brands(Id int, Brand)
bucket StoreItemSlugs(Slug string, Id int)</code></code></pre><p>In a relational database, you may choose to implement a bucket in terms of a table, and the lookup key may be understood as the primary key for the table.</p><h2>Lookup Index</h2><p>A lookup index is also a mapping, but instead of being one to one, it's basically many to many, mapping a lookup term to a list of targets.</p><p>Examples:</p><pre><code><code>index BrandItems(BrandId int, StoreItemId int)
index CurrencyItems(Currency string, StoreItemId int)</code></code></pre><p>The BrandItems index allows us to iterate over all the store items that are from a certain brand.</p><p>The CurrencyItems index allows us to iterate over all the store items that define their price in a certain currency.</p><p>In a relational database, the index may be assumed to be some kind of optimization, since SQL allows you to use arbitrary queries with arbitrary lookup conditions.</p><p>However, when modelling data, it's important to be explicit about these lookup indices.</p><p>When using the lookup index, you provide a key and iterate on matches.</p><p>When setting the lookup index, you provide a match and a list of keys. It's the opposite.</p><p>Think of the index on the back of a book. If you edit a page and want to update the index, the operation requires that you provide the page number and a list of important terms that appear on the page. The system will then find each term on the index, and update it by adding the given page number. It will also need to find keys on the index that used to point to the given page number but no longer need to, and remove the page number from their matches.</p>]]></content:encoded></item><item><title><![CDATA[Trying something new - feature designs]]></title><description><![CDATA[The hardest part in programming is modelling data and defining operations on it]]></description><link>https://hasen.substack.com/p/trying-something-new-feature-designs</link><guid isPermaLink="false">https://hasen.substack.com/p/trying-something-new-feature-designs</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Mon, 01 Apr 2024 08:01:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Az4m!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd038f74a-8d40-45f8-92cf-f96bf834430b_350x350.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m going to try something new for a while. On a bi-weekly basis, I&#8217;m going to publish design documents, where I take some hypothetical feature, and then describe a data model that would enable implementing it. I will also define the operations on the data, and how they contribute to bringing this feature to reality.</p><p>The amount of code will be minimal. The idea is that any programmer, even if they are not very experienced, can use this design document to implement the feature on their own.</p><p><strong>The first item</strong> I&#8217;m thinking of is basic authentication: account creation, email verification, password encryption, password reset, login, validating sessions, etc.</p><p>I&#8217;ve noticed a lot of people don&#8217;t know how to implement it and instead rely on third party services, which frankly I think is pathetic. It&#8217;s not that hard, and if you can&#8217;t bother to do it properly, I have to question whether you can get anything at all done.</p><p><strong>The next item</strong> I&#8217;m think of is basic integration with LemonSqueezy: managing product ids, validating &amp; activating license keys, using the license key as an alternative form of email verification, supporting multiple modes: subscription, lifetime access, credit based access, and trial period access.</p><p>If you&#8217;re interested in these kinds of topics, stay tuned.</p>]]></content:encoded></item><item><title><![CDATA[Forms without callbacks in (p)react]]></title><description><![CDATA[General technique for binding input fields to object field references]]></description><link>https://hasen.substack.com/p/react-forms-without-callbacks</link><guid isPermaLink="false">https://hasen.substack.com/p/react-forms-without-callbacks</guid><dc:creator><![CDATA[Hasen Judi]]></dc:creator><pubDate>Thu, 21 Dec 2023 08:27:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5dfc69c0-d993-4ad7-a0c2-6075bb20896a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There's an interesting idea in imgui libraries based on C where the input field takes a pointer to the value it's supposed to edit:</p><pre><code><code>    input_field("label", &amp;object.field)
</code></code></pre><p>We can take inspiration from this idea to define input components in (p)react without using callbacks.</p><p>We want to write this:</p><pre><code><code>    &lt;Input ref={ref(object, 'field')} /&gt;
</code></code></pre><p>instead of this:</p><pre><code><code>    &lt;input value={object.field}
        onChange={e =&gt; object.field = e.target.value}
    /&gt;
</code></code></pre><p>Here's why:</p><ul><li><p>Zero callbacks</p></li><li><p>Code volume smaller</p></li><li><p>No duplicating <code>object.field</code></p></li></ul><p>Contemporary react based apps overuse closures. We should reduce the amount of closures as much as we can. Assume by default that closures come with performance penalties and find other default ways of achieving tasks.</p><p>For implementing this idea, we still need a callback, but it will be a single regular function, not a dynamic closure at every usage site.</p><p>We need a way to pass the ref to the input element without closures. The easy way to do this is to encode the reference as a string into the DOM attributes of the input element.</p><h2><strong>Object ID</strong></h2><p>We can easily come up with numeric ids for object references, but we need them to be stable; they should not change between render cycles.</p><p>Javascript does not give us the "address" of object references, but internally they are addresses, and the builtin Map object uses those addresses.</p><p>Here's a pair of functions to get an id for an object and also get an object back from the id.</p><pre><code><code>let idmap = new Map&lt;unknown, number&gt;()
let objmap = new Map&lt;number, unknown&gt;()

export function objectId(obj: unknown): number {
    let v = idmap.get(obj)
    if (v === undefined) {
        v = nextObjectId()
        idmap.set(obj, v)
        objmap.set(v, obj)
    }
    return v
}

export function objectById&lt;T = any&gt;(id: number): T | null {
    return objmap.get(id) as T ?? null;
}
</code></code></pre><h3><strong>Leaking memory</strong></h3><p>Whenever we call <code>objectId(...)</code> on something, we hold a strong reference to it in the <code>idmap</code> and <code>objmap</code>. This means we should never call it on "temporary" local objects.</p><p>It also means we need a function to clear the idmap and objmap that we can call at the appropriate time. If the application we're developing is an SPA (single page application) with client side routing, we can call this function when the route changes.</p><pre><code><code>export function clear() {
    idmap.clear()
    objmap.clear()
}
</code></code></pre><p>One possible alternative is to use <code>WeakMap</code> instead of map, but then it can't work with strings, which we want to be able to work with.</p><h2><strong>Field Ref</strong></h2><p>For the field ref, we can define a type with two keys: <code>obj</code> and <code>key</code>.</p><p>This is the closest thing to an arbitrary C pointer in Javascript (in terms of what we can do with it): we can use it to read and write arbitrary data without having to know anything about where that data is coming from.</p><pre><code><code>export function ref&lt;T, K extends keyof T&gt;(obj: T, key: K): Ref&lt;T[K]&gt; {
    return { obj, key } as Ref&lt;T[K]&gt;
}

export function get&lt;T&gt;(r: Ref&lt;T&gt;): T {
    return r.obj[r.key]
}

export function set&lt;T&gt;(r: Ref&lt;T&gt;, value: T) {
    r.obj[r.key] = value
}
</code></code></pre><p>The definition of <code>Ref&lt;T&gt;</code> is constructed such that T is the type stored in <code>obj.key</code>.</p><p>Making this type in Typescript requires an ugly hack:</p><pre><code><code>declare const _ref_type: unique symbol;
export type Ref&lt;T = any&gt; = RawRef &amp; {
    [_ref_type]: T
}

export type RawRef&lt;T = any&gt; = {
    obj: T;
    key: keyof T;
}
</code></code></pre><p>When we call <code>ref(object, "field")</code>, the Typescript checker will:</p><ul><li><p>Ensure that <code>object.field</code> is valid</p></li><li><p>Deduce the type of the returned ref</p></li></ul><p>Example code:</p><pre><code><code>type Person = {
    name: string;
    age: number;
}

let p: Person = { name: "Hello", age: 20 }

let x = ref(p, "name")
let y = ref(p, "age")
</code></code></pre><p>The Typescript checker automatically deduces that <code>x</code> has type <code>Ref&lt;string&gt;</code> and that <code>y</code> has type <code>Ref&lt;number&gt;</code>.</p><p>If you type a bad field name like this, it will report an error:</p><pre><code><code>let z = ref(p, "inv")
</code></code></pre><h2><strong>Ref String Encoding</strong></h2><p>When we call <code>ref(....)</code> the object we get back will be a temporary local object. It will not have a stable id, so we can't just use <code>objectId(...)</code> on it; we'd get something different every time, and we'd leak memory, as discussed above.</p><p>Instead, we can get the id of the object and the field name. String literals have stable references, so we can call <code>objectId</code> on them.</p><pre><code><code>function encodeRefAttr(ref: refs.Ref): string {
    let objectId = refs.objectId(ref.obj);
    let fieldId = refs.objectId(ref.key);
    return `${objectId}:${fieldId}`;
}

function decodeRefAttr(sref: string): refs.Ref {
    let [objectIdStr, fieldIdStr] = sref.split(":");
    return refs.ref(
        refs.objectById(parseInt(objectIdStr)),
        refs.objectById(parseInt(fieldIdStr)),
    );
}
</code></code></pre><p>Note the absence of any "validation" code in the decoder. We just assume the references are valid. If someone puts garbage in, they just get garbage out, similar to trying to pass garbage to a C function that expects a pointer.</p><p>Now, here's another helper to read the attribute of a specific DOM element.</p><pre><code><code>function getAttr(el: HTMLElement | EventTarget | null, attr: string): string {
    if (el instanceof HTMLElement) {
        return el.getAttribute(attr) ?? "";
    } else {
        return "";
    }
}
</code></code></pre><p>It does not seem like much but it removes edge cases we don't care about, and we can also pass <code>event.target</code> to it without Typescript complaining.</p><h2><strong>The input handler</strong></h2><p>Now we have all the building blocks to create our <code>Input</code> component and define the input handler callback:</p><pre><code><code>
const INPUT_REF_ATTR = "input-ref";

function onInput(event: Event) {
    let target = event.target as HTMLInputElement;
    let ref = decodeRefAttr(getAttr(target, INPUT_REF_ATTR));
    let refValue = refs.get(ref);
    let value: any = target.value;
    refs.set(ref, value);
    core.scheduleRedraw();
}

export function inputAttrs(ref?: refs.Ref): any {
    if (!ref) {
        return {};
    }
    return {
        [INPUT_REF_ATTR]: encodeRefAttr(ref),
        value: refs.get(ref),
        onInput: onInput,
    };
}

export function Input(props: { ref: refs.Ref&lt;string&gt; }) {
    return &lt;input {...inputAttrs(ref)} /&gt;
}

</code></code></pre><p>This is a "bare-bones" implementation, but I think it illustrates the idea.</p>]]></content:encoded></item></channel></rss>