<?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[Spontaneous Productivity : The Tech Behind Nestful]]></title><description><![CDATA[How Nestful is built using novel web technologies]]></description><link>https://blog.nestful.app/s/the-tech-behind-nestful</link><image><url>https://substackcdn.com/image/fetch/$s_!aZOI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13336315-f7a5-451b-ae0a-2cf7a1f81850_1024x1024.png</url><title>Spontaneous Productivity : The Tech Behind Nestful</title><link>https://blog.nestful.app/s/the-tech-behind-nestful</link></image><generator>Substack</generator><lastBuildDate>Thu, 07 May 2026 12:14:33 GMT</lastBuildDate><atom:link href="https://blog.nestful.app/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Nestful]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[nestful@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[nestful@substack.com]]></itunes:email><itunes:name><![CDATA[Nestful]]></itunes:name></itunes:owner><itunes:author><![CDATA[Nestful]]></itunes:author><googleplay:owner><![CDATA[nestful@substack.com]]></googleplay:owner><googleplay:email><![CDATA[nestful@substack.com]]></googleplay:email><googleplay:author><![CDATA[Nestful]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[How we dropped Vue for Gleam and Lustre ]]></title><description><![CDATA[Nestful is now exclusively powered by Gleam and Lustre. Vue was completely removed and TypeScript is only used for glue code and FFI.]]></description><link>https://blog.nestful.app/p/how-we-dropped-vue-for-gleam-and</link><guid isPermaLink="false">https://blog.nestful.app/p/how-we-dropped-vue-for-gleam-and</guid><dc:creator><![CDATA[Nestful]]></dc:creator><pubDate>Wed, 28 Jan 2026 19:30:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!aZOI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13336315-f7a5-451b-ae0a-2cf7a1f81850_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It took a while, but as of today, if you visit <a href="https://nestful.app">Nestful</a> you are going to be served by Gleam &#8212; a friendly, type-safe, functional language compiling to Erlang and JavaScript &#8212; and Lustre &#8212; its front end, Elm-like framework.</p><p>This is the conclusion of a transition that started more than a year ago, stemming from my frustration with TypeScript and the state of front-end frameworks. Gleam and Lustre were and still are a breath of fresh air and I am glad to have made the move.</p><p>This post will detail how those early motivations measure up in hindsight, and what it means (and feels) to maintain tens of thousands of lines of Gleam code.</p><h2>You could do it too</h2><p>Nestful is what I call &#8220;almost done&#8221; software. It&#8217;s not quite the completeness that are SQLite or Sublime Text, but is also not as ever-changing as something like Windows, a bastion of agile development these days. This positioning makes Nestful able to survive such a rewrite.</p><p>Nestful is valuable to users as is, and so they are more tolerant of features not being a part of every browser refresh. Some are happy with the low amount of jarring changes. Not every app has this luxury, with some pushed by their market segment to chase competition. Thankfully Nestful is a very opinionated tool in a niche without much innovation, so could easily afford the rewrite.</p><p>However, a rewrite is not the only avenue. Since Gleam is not a pure language, it&#8217;s very easy to interact with existing JavaScript or TypeScript code. New code and much-needed refactors could be done in Gleam with little to no hassle. In fact, this is how Nestful started, with Vue SFCs written in Gleam, and later with Lustre taking over state management while Vue kept on rendering.</p><p>You could do the same in your company, implementing carefully and incrementally, and enjoy the benefits described in the blog post for those new sections of code.</p><h2>Of joy and trouble</h2><p>Clearly, Nestful does change. &#8220;Almost done&#8221; is very different from &#8220;done&#8221;, with improvements added in a calculated manner. As of writing, the next planned big feature is showing items from third party services. This requires a non-trivial amount of architecture to then be able to arbitrarily add tasks from other services and have them bubble up next to one another for the user to compare priorities.  </p><p>To do that, the code base needed to support changes maintainability-wise, but also the enjoyment of writing them. The thing is &#8212; those are intertwined. Yes, there is an aspect of enjoyment that stems solely from the writing, in isolation. But there&#8217;s also the mental load that comes from bad maintainability which kills that joy of coding.</p><p>In my <a href="https://blog.nestful.app/p/why-i-rewrote-nestful-in-gleam">original post</a> about rewriting in Gleam, I mentioned some of the language specifics that were a joy &#8212; simple, exhaustiveness checks, pattern matching, friendly syntax. Those all turned out to be true. A stark contrast to TypeScript&#8217;s complexity, where one has to write two programs; one serving logic and the other types. All I wanted was to make sure there aren&#8217;t any null references.</p><p>Difficulties on the front end most often boil down to managing state. jQuery closures, Angular services, React and Vue hooks and stores and of course, the resurgence of back-end-centric solutions like Phoenix &#8212; those are all attempts trying to make sure incrementing a counter won&#8217;t turn on the user&#8217;s toaster.</p><p>Elm and its architecture almost entirely solved state issues like the aforementioned by getting state soundness in exchange for boilerplate. You write more defining your state model, the actions that can be performed on it, and the side effects they may incur and in return you get significantly fewer bugs and, most importantly, developer clarity of how state flows. That is a fine exchange. Boilerplate is much easier to live with than being ever-vigilant about state racing like it&#8217;s Daytona.</p><p>Along the years I&#8217;ve advised companies on how to replicate this architecture with TypeScript, solving issues like the that &#8220;spooky actions at a distance&#8221;, unpredictable reactivity, and unruly side effects. I&#8217;ve done it with Nestful. However, there was always this feeling of trying to fit a square peg in a round hole. It&#8217;s well worth the benefits, but could I have done better?</p><p>That&#8217;s where <a href="https://hexdocs.pm/lustre/">Lustre</a> &#8212; Gleam&#8217;s implementation of The Elm Architecture &#8212; shines as the true star of the show.</p><p>Explicit state handling and not having to fight off reactivity made Nestful much faster. The architecture also avoids over-design by allowing the developer to easily compose and recompose state and its handlers, Gleam and Lustre do make refactors easy and clean, because in the end, it&#8217;s all just regular functions with strong type guarantees.</p><p>This all concludes to a single word: fun. Programming Gleam is fun, and I do not intend on going back to the old, cold days of TypeScript if I can help it.</p><h2>Lessons Learned</h2><p>The following is a short summary of some lessons learned along the way, in no particular order (except for that first one). I hope that they can be useful to you.</p><h3>The AI Elephant in the Room</h3><p>I know I&#8217;ll get a lot of replies saying how Gleam is too new for AI and therefore productivity is reduced compared to something like React. Well, I&#8217;ve been using Opus 4.5 for some of the rewrite and I can tell you two things:</p><ol><li><p>It is much better at Gleam even compared to Sonnet 4.5 and Codex 5.2</p></li><li><p>LLMs are very knowledgeable idiots</p></li></ol><p>Given their stupidity as their main weakness and their vast knowledge as their main strength, LLMs need very precise guidance and supervision to crank out what I&#8217;d consider acceptable code. In that context, having more knowledge about a specific framework or language results in diminishing returns, especially with tools like Claude Code or OpenCode which give the LLM more information.</p><p>Instead, it&#8217;s much preferable to guide the LLM to avoid its stupidity.  The constraints that are inherent to Gleam and Lustre do that perfectly. Yes, the LLM might try to concatenate a conditional CSS class instead of using <code>attribute.classes</code>, or rename variables weirdly because it has PTSD from JavaScript name collisions. That is much easier to deal with than having it RNG an architecture like we&#8217;re playing The Sims.</p><h3>Forget view hierarchy</h3><p>This is more of an Elm architecture generality but is useful even if you don&#8217;t adopt a framework that follows it. Due to components holding state, React and Vue make it extremely easy to model your state hierarchy identically to your view hierarchy.</p><p>This is often not only counterproductive but downright harmful. State should be modeled completely separately from the view. Drawing state encapsulation lines that match view encapsulation will just mean you&#8217;ll be ever-busy trying to circumvent your own architecture as you discover state cross-requirements.</p><h3>Design late</h3><p>JavaScript and to some extent TypeScript are both landmine languages and therefore incur significant refactor cost, as refactoring doesn&#8217;t only mean avoiding pitfalls in the new code, but also breakage that is caused by removing the old code.</p><p>This is almost entirely absent in Gleam, which has your back. This makes the drawbacks of designing early &#8212; making incorrect assumptions about the future &#8212; more harmful than the logic inconsistencies that still can sneak into Gleam code, especially since it&#8217;s not pure.</p><p>Instead, just build really well for what you have now. The code will tell you when you need to change the design, which will be a mostly painless process. </p><h3>Accept the boilerplate</h3><p>Lustre requires you to commit to more boilerplate. It is a fact of its design. Fighting it by being &#8220;smart&#8221; will come back to bite you down the line.</p><h3>Consider your FFI boundaries carefully</h3><p>FFI is by far the most bug-prone area of the code. This is understandable. Be careful when writing it and its glue code to Gleam.</p><p>I hope that tooling in the future could detect type inconsistencies between TypeScript types and the types declared in Gleam for the external function. </p><h2>Have fun</h2><p>I may be repeating myself &#8212; well, I am repeating myself &#8212; but remember to have fun. Slight adjustments to the things you do can go a long way in making you enjoy them. Even if you don&#8217;t like Gleam or Lustre (how?!) &#8212; incorporate what you do like, even if in a small way. I haven&#8217;t done my research but I&#8217;d bet people who enjoy the work do it more productively.</p><p>What more do you need?</p>]]></content:encoded></item><item><title><![CDATA[How Nestful uses CRDTs to sync your data with (almost) no backend]]></title><description><![CDATA[Nestful has quite the interesting use case &#8211; it is an offline first app, yet it supports cross-device sync.]]></description><link>https://blog.nestful.app/p/how-nestful-uses-crdts-to-sync-your</link><guid isPermaLink="false">https://blog.nestful.app/p/how-nestful-uses-crdts-to-sync-your</guid><dc:creator><![CDATA[Nestful]]></dc:creator><pubDate>Mon, 07 Apr 2025 16:48:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cz__!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Nestful has quite the interesting use case &#8211; it is an offline first app, yet it supports cross-device sync. This means it should somehow consolidate data even if it&#8217;s the result of multiple different offline sessions coming back online at different times.</p><p>Most of you are already aware that the most common way to do this today is Conflict-free Replicated Data Types (CRDTs). As their name suggests, they are replicated data structures that can be combined, with algorithms guaranteeing that the data will eventually converge.</p><p>Nestful uses Yjs, a fast JavaScript CRDT. Yjs is truly a magnificent piece of software. Each user has its own Yjs Document where most data is stored. For local persistence that data is synced into IndexedDB, the in-browser database, using y-indexeddb, which is part of Yjs&#8217; <em>provider</em> ecosystem.</p><p>Providers are pluggable pieces of code that hook into a Yjs document for added functionality. This is mostly in the form of sync to and from various places, from our local IndexedDB database to a middle ground in the form of y-websocket, to a full-blown solution like y-sweet.</p><p>Although I would have been glad to find a ready-made provider and integrate that into Nestful, things were not so easy. Commercial providers like y-sweet were in their infancy, so I didn&#8217;t even bother evaluating them. The most battle-tested solution was y-websocket, but that was just the communication layer. I would have had to write a non-negligible amount of backend code, which development (and maintenance) bandwidth did not allow for.</p><p>What do we do, then? To understand our options we need to figure out where Nestful was at the time, and what it is that we need to sync.</p><h2>Nestful&#8217;s legacy</h2><p>Historically Nestful was written as a generic, simple CRUD app using PostgreSQL via Supabase. I thought to myself &#8211; it&#8217;s a personal-use todo app, why would I need anything else? I&#8217;d say that for most todo apps that decision would have been correct (and it surely was correct at the time, for other reasons). The thing is, Nestful has two very important traits that change most of everything:</p><ol><li><p>Nestful is offline-first, which now requires constructing a sync mechanism</p></li><li><p>Nestful uses a tree-structure which the traversal of can cause significant recursion</p></li></ol><p>Don&#8217;t let anyone tell you otherwise &#8211; both are perfectly solvable and maintainable using PostgreSQL, even if they lend themselves better to something like a document-based CRDT.</p><p>Nestful&#8217;s other circumstances, including the way it was coded and my annoyance with my then local DB (a story for another time about how not to maintain FOSS) pushed me to switch.</p><p>So now we know that Nestful is hosted on Supabase, relying on its Auth and its Database, and most important of all &#8211; Nestful does not deploy any backend. This means that adding one will not only incur the complexity of the functionality we want to add, but of the expanded codebase, its deployment, and the server maintenance.</p><p>Doing that is all well and good, we are a software company after all, but let&#8217;s do better.</p><h2>Update here, update there</h2><p>That way sync works with Yjs (and most other CRDTs) is by using an update mechanism. Yjs allows the developer to obtain binary updates, representing a data diff, then send those over the wire. There are 3 main ways to do that:</p><ol><li><p>Listen to changes</p></li><li><p>Diff against another version of the data</p></li><li><p>Get everything</p></li></ol><p>Those updates can then be consumed in any order and they will converge to the same final state. Now that we know we want to sync updates, the questions are where do we save them and how will they get there.</p><p>Remember Nestful using Supabase? Well, Supabase has an S3 equivalent called Supabase Storage. The other side of the coin of provider lock-in is of course, blissful integration. After a minimal setting of permissions, end users would be able to push updates to Storage, and fetch updates that are already there.</p><p>Of course, just fetching all the updates all the time can be wasteful (and slow), so we&#8217;ll have to do some tweaking.</p><h2>Almost converged</h2><p>To not have the client download a humongous amount of files, which is as slow over these &#8220;serverless&#8221; platforms almost as how stupid the term &#8220;serverless&#8221; is, we could write a small &#8220;serverless&#8221; function to consolidate the updates for us.</p><p>That function will receive a date from the client, and will send over a single file.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cz__!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cz__!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 424w, https://substackcdn.com/image/fetch/$s_!cz__!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 848w, https://substackcdn.com/image/fetch/$s_!cz__!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!cz__!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cz__!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png" width="1448" height="1192" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/153561d2-2979-422a-a437-163a9cb72881_1448x1192.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1192,&quot;width&quot;:1448,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&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="" srcset="https://substackcdn.com/image/fetch/$s_!cz__!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 424w, https://substackcdn.com/image/fetch/$s_!cz__!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 848w, https://substackcdn.com/image/fetch/$s_!cz__!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!cz__!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F153561d2-2979-422a-a437-163a9cb72881_1448x1192.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 role="img" 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><title></title><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 a client wants to get the latest data, he sends a date to the just-a-function-not-a-server. Upon receiving the combined update, the client diffs its own state against it, and uploads a diff directly to the just-object-storage-not-a-server.</p><p>This is called a full sync. At this point, the client and server are fully in sync. Any further update on the client is:</p><ol><li><p>Uploaded directly to object storage</p></li><li><p>Broadcasted to peers using Supabase, to avoid a roundtrip</p></li></ol><p>Using this scheme also has the added benefit of being able to restore data to a point in time, even if it was garbage collected by Yjs.</p><p>And that&#8217;s how Nestful syncs your data. This simple, client-dependent approach serves us well. It works, is stable, low maintenance, but&#8230; it&#8217;s slow. Nestful does <em>not</em> in fact require fast sync. It is not a real-time collaborative platform like other products using Yjs, and so sync can happen in the background, take a few seconds, and no one would notice.</p><p>But&#8230; we can&#8217;t settle for that, can we?</p><h2>The future ahead</h2><p>Even though it is fine for now (and for a long time from now) that a full sync is slow, we may need a bit more performance in the future. Although this may require getting an actual server and setting up an actual backend, the current design will stay mostly the same.</p><p>Since end-to-end encryption is planned for Nestful, the server will not be able to consolidate updates. Instead, a client will have to do that periodically and upload an encrypted checkpoint, which will serve as the new starting point for the next consolidation.</p><p>For this we&#8217;re going to need to cache both the metadata and data since the latest checkpoint for the update files in object storage to be able to quickly and efficiently serve whatever is needed, but that is for the future.</p><p>Check out the marvel of CRDT syncing by <a href="https://nestful.app/signup">trying Nestful</a> now, and also try Nestful itself while you&#8217;re at it.</p><p>Frequent readers will remember Nestful is written using Gleam and will probably wonder how we call Yjs from inside our lovely Gleam code. Here&#8217;s a <a href="https://hexdocs.pm/ygleam/">YGleam link</a> for you.</p><p>You may also be interested in <a href="https://blog.nestful.app/p/its-tea-time-nestful-now-manages">our latest story about moving to an Elm-like state management</a>.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.nestful.app/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[It's TEA time: Nestful now manages state with Gleam and Lustre]]></title><description><![CDATA[Does a frontend framework matter?]]></description><link>https://blog.nestful.app/p/its-tea-time-nestful-now-manages</link><guid isPermaLink="false">https://blog.nestful.app/p/its-tea-time-nestful-now-manages</guid><dc:creator><![CDATA[Nestful]]></dc:creator><pubDate>Mon, 31 Mar 2025 17:07:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fcbdda7c-b8dc-44c1-97e1-ee7ac6f36091_900x630.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Does a frontend framework matter?</p><p>As much as you might have heard otherwise, the answer is an astounding yes. Those who fight on the hills pretending it&#8217;s all about the code are missing one very crucial point:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Spontaneous Productivity ! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Frameworks shape the code you write.</p><p>To a certain extent, they define what&#8217;s possible, but more important than that &#8211; they set a path of least resistance. Yes, frameworks coming and going is one of the most frequent occurrences in nature, but that doesn&#8217;t mean it&#8217;s wasted effort to choose the right one or invest in it. The waste only appears when the frameworks are of the same kind, and introduce similar tradeoffs.</p><p>The clearest example of this is Elm. It&#8217;s not a coincidence that Elm manages state differently than how you&#8217;d normally do it in JS-land. Even Redux, which is an implementation of The Elm Architecture (also known as Model-View-Update), could not give the same streamlined experiences developers expect from Elm. Even if that experience is better than what&#8217;s otherwise available in the JavaScript ecosystem, you have to be fairly disciplined to keep it going.</p><p>Alas, there is only so much time in this world for us to try and fit a square into a peg. At this point some would fall back to the usual default: &#8220;Elm is great, but it&#8217;s too far from JavaScript. And there&#8217;s really nothing very different about all the JavaScript options, so I&#8217;ll just &#8211; &#8221;</p><p>But wait. What if I told you there is another option? As robust as Elm, with a familiar syntax, and that you could learn in a day?</p><p>Grab yourself some tea, for this is the story of how adopting the Gleam language out of pure disdain for TypeScript led to much better state management for Nestful.</p><p>Before we delve into the details, let me bring you up to speed.</p><p>About a year since I started experimenting with <a href="https://gleam.run">Gleam</a>, a delightful functional language that compiles to both Javascript and Erlang. I&#8217;ve decided to rewrite Nestful in Gleam to alleviate <a href="https://blog.nestful.app/p/why-i-rewrote-nestful-in-gleam">some of the terrible slog</a> that is TypeScript.</p><p>The first major part was the introduction of <a href="https://github.com/vleam/vleam">Vleam</a>, a set of tools that allows one to write Gleam inside Vue SFCs. Nestful is written with Vue and incremental adoption is a must when working on such a large-scale rewrite.</p><p>In November I launched <a href="https://blog.nestful.app/p/gleams-lustre-is-frontend-developments">OmniMessage</a>, an experimental library for client-server communication. OmniMessage, you see, is an extension of Lustre (please bear with the buzzwords), Gleam&#8217;s Elm-inspired frontend framework. It allows one to define messages that are common to both the client and the server, thus streamlining communication.</p><p>Whether that&#8217;s a good idea or not, time will tell. What I do know is that Lustre was a breath of fresh air. But why? What is it about Lustre that&#8217;s missing from Redux (except for my strong, deep, unwavering dislike of React, that is)?</p><p>The answer is Gleam. Writing MVU-style apps in JavaScript is a bit like driving in a zigzag pattern &#8212; possible, but unless you&#8217;re 100% diligent, a crash is coming.</p><p>Luckily, I try to avoid premature optimization as much as possible and state management was no different. Nestful&#8217;s was simply done, by hooking up different kinds of Vue composables and utilizing Yjs&#8217; event system. Some people may gasp at the lack of structure, but hindsight has revealed this to be a good decision. Much better than committing to what some would consider &#8220;proper&#8221; state management, spreading its tentacles into every corner of Nestful, making change a labor of desperation.</p><p>Instead, having a simple, unstructured system allowed it to be promptly removed when the learnings were sufficient and time had come to replace it.</p><p>After adopting Gleam for the language it is, Nestful could continue to enjoy its ecosystem, including Elm-inspired Lustre.</p><h2>Why you should have some TEA</h2><p>The Elm Architecture (TEA), also known as Model-View-Update (MVU), is a centralized form of state management made popular by &#8212;you guessed it &#8212;The Elm programming language.</p><p>As its alternative name suggests, it involves a centralized model that is sent for rendering to a view function, and is updated by an update function. If it sounds simple, that&#8217;s because it is. Here&#8217;s the state management diagram straight out of Elm&#8217;s docs:<br></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qh51!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qh51!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 424w, https://substackcdn.com/image/fetch/$s_!qh51!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 848w, https://substackcdn.com/image/fetch/$s_!qh51!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 1272w, https://substackcdn.com/image/fetch/$s_!qh51!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qh51!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png" width="1456" height="920" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:920,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:160157,&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;:&quot;https://blog.nestful.app/i/160270568?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qh51!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 424w, https://substackcdn.com/image/fetch/$s_!qh51!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 848w, https://substackcdn.com/image/fetch/$s_!qh51!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.png 1272w, https://substackcdn.com/image/fetch/$s_!qh51!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe0189ec-2ba9-484b-88f8-abab3227ef8f_2500x1580.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 role="img" 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><title></title><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>Here&#8217;s the counter example from Lustre, Gleam&#8217;s frontend framework, which also implements TEA:</p><pre><code>type Msg {

  Incr  

  Decr

}



fn update(model, msg) {

  case msg {

    Incr -&gt; model + 1

    Decr -&gt; model - 1

  }

}



fn view(model) {

  let count = int.to_string(model)

  div([], [

    button([on_click(Incr)], [text(" + ")]),

    p([], [text(count)]),

    button([on_click(Decr)], [text(" - ")])

  ])

}</code></pre><p></p><p>That&#8217;s it, kids. For most web applications, you can pack Pinia and Zustand up, thank them for their service, and promptly show them out to the nearest commit.</p><p>You see, those popular solutions are popular for good reason. They are a low common denominator that, with all honesty, works quite well. The problem is they&#8217;re not as good as it gets. They break at the edges, if you will. A local maxima.</p><p>For Nestful, The Elm Architecture has the edge on them in three crucial places.</p><h3>Preventing state-creep</h3><p>State creep is the bane of my existence. When components are stateful, one of two things must happen:</p><ol><li><p>You must hold constant, unwavering diligence, keeping local state local and global state global.</p></li><li><p>You despair as global and local states are inevitably mixed into a soup of misery.</p></li></ol><p>I personally am not a fan of either constantly looking over my shoulder for the state boogeyman, nor of holding the mental model the size of the Tokyo rail system of how state flows through an application.</p><p>Instead, using TEA, I have a lovely, detailed map of all the paths data flows in. That map is an entire overview of the application, caging in the global state, only to be seen &#8212; never mutated.</p><h3>A pure heart</h3><p>Another great advantage of strictly enforcing TEA is that it makes it very easy to structure an application using &#8220;Functional Core, Imperative Shell&#8221;, a design pattern in which I/O is separated out to distinct parts at the start and end of a flow, while the center, the business logic processing that I/O, is kept pure. It always returns the same output for a given input, and does not involve side effects.</p><p>As it so happens, that is exactly how TEA pushes you to architect your state. Both the update and view functions are pure, and will return the same output for a given model and message.</p><p>Side effects are then handled by an effect system, which takes side-effect tasks returned by the update function and executes them out of the update function&#8217;s context. <br><br>This approach isolates the important bit of the application, the business logic, from the ever-changing, hardly testable, outside world.</p><h3>Works great with Gleam</h3><p>Gleam lends itself to this model quite significantly. Being a functional, type-safe language is a great usability gain for TEA, especially thanks to discriminate unions, who allow us to write the apps message type like this:<br></p><pre><code>type Msg {

  Incr

  Decr

}</code></pre><p>And the case statement, that allows for exhaustive matching on that message type:<br></p><pre><code>fn update(model, msg) {

  case msg {

    Incr -&gt; model + 1

    Decr -&gt; model - 1

  }

}</code></pre><p>Other implementations, like Redux, Dartea, or Vuex to some extent, while admirable, all fall short. After a developer has chosen a paradigm, tools need to not only enforce it, but encourage that paradigm. They need to not only make it the path of least resistance, but make the other paths hard in comparison.</p><p>A tool that requires diligence to keep going is failing at its job.</p><p>An added bonus from those three advantages is they make it extremely easy to refactor. They allow razing fields of old code to the ground, building skyscrapers above them, exchanging the fear of imminent crumble for the guarantees of type-safety and robust defaults.</p><p>But wait, isn&#8217;t Nestful a Vue app? How am I going to fit Gleam-written TEA into it? Am I going to have to settle for a TEA-like experience, similar to what I could conjure up using existing TypeScript solutions?</p><h2>Incrementally adopting a frontend framework</h2><p>So I&#8217;ve adopted Gleam and can now write it inside Vue files instead of TypeScript and look to use it to implement TEA and replace the current haphazardly-managed state.</p><p>To do that, two main tasks need to be completed:</p><ol><li><p>Construct a runtime capable of executing the TEA state machine.</p></li><li><p>Somehow integrate that runtime into existing Gleam and TypeScript Vue components.</p></li></ol><p>The first one already exists, it&#8217;s just Lustre (but without a view).</p><p>There are two ways of going about incorporating Lustre incrementally. The first is to carve out portions of Nestful and write those exclusively in Lustre. The second is to launch a Lustre app and render nothing, using just its state management capabilities.</p><p>Since the goal here is to have comprehensive state management, the second solution is more appealing, but a closer look will reveal that it&#8217;s actually the only solution possible.</p><p>Nestful, you see, is a giant item graph that&#8217;s traversed for specific views or actions. To enable offline-only capabilities, the entirety of the graph is stored on the client in the form of a single Yjs document, and if enabled, is synced on changes.</p><p>This means that not only is there very little local state to manage, the global nature of Nestful&#8217;s state will greatly benefit from collecting the static logic into a single, coherent, state machine. For Nestful, it makes much more sense to embed Vue in Lustre rather than Lustre in Vue.</p><p></p><pre><code>// /src/compositions/useNestful.ts

import { computed, shallowRef, watch } from 'vue';

import { UseNestful } from '../nestful/compositions.gleam';

import { nestful } from '../nestful.gleam';



const modelRef = shallowRef();

const modelComputed = computed(() =&gt; modelRef.value);



const [initialModel, dispatch] = nestful((model) =&gt; {

  modelRef.value = model;

});



modelRef.value = initialModel;



export function useNestful() {

  return new UseNestful(modelComputed, dispatch);

}





// /src/nestful/compositions.gleampub type UseNestful {

  UseNestful(model: vue.Computed(Model), dispatch: fn(Msg) -&gt; Nil)

}



@external(javascript, "/src/compositions/useNestful", "useNestful")

pub fn use_nestful() -&gt; UseNestful</code></pre><p></p><p>This is the glue code that allows Vue components, written either in Gleam or TypeScript, to read the model and dispatch messages. It is supported by helper functions in nestful.gleam to avoid having to import the different Gleam types into TypeScript.</p><p>Vue components are then treated as functions composing a Lustre app&#8217;s view function.</p><p>But there&#8217;s a catch.</p><p>The Elm Architecture&#8217;s centrally managed state trades off component composability to the point where Elm&#8217;s docs <a href="https://guide.elm-lang.org/webapps/structure">outright discourage them</a>. While I don&#8217;t consider this blasphemy like some React folks may, I can see how different parts of an app lose on not being completely separate.</p><p>All things considered, it&#8217;s not an endemic issue similar to those I am trying to solve with the move to Gleam. That same page on the Elm docs discouraging components lists much more important things right before that that are benefits of TEA.</p><p>Which brings us to today.</p><h2>A fun future</h2><p>Except for two pending files, no global state is managed outside Lustre. State holding composables have been reduced significantly, and I hope to remove them completely in the following months.</p><p>The adoption of TEA has made Nestful faster, more maintainable and easier to develop features for. Most importantly, however, those advantages are now the path of least resistance in Nestful&#8217;s development cycle.</p><p>I hope that this transition will continue to bear fruit as time passes. By reducing the need for testing, by encouraging better design and architecture, and most importantly &#8212; by being fun.</p><p>This post was mostly about the technical benefits of Gleam and Lustre, but there&#8217;s also an emotional one &#8212; programming in them is a much, much, much more enjoyable experience than the TS alternative.</p><p>As we put our best foot forward in an attempt to increase value for our customers we sometimes forget about ourselves, and if we can &#8212; we can and should enjoy the process. Programming can and should be fun.</p><p>Subscribe and stay tuned for more unique takes on personal productivity and novel(-ish) web technologies.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Spontaneous Productivity ! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Gleam's Lustre is Frontend Development's Endgame]]></title><description><![CDATA[How a combination of language, architecture and ecosystem lead to more maintainable code and a more enjoyable experience.]]></description><link>https://blog.nestful.app/p/gleams-lustre-is-frontend-developments</link><guid isPermaLink="false">https://blog.nestful.app/p/gleams-lustre-is-frontend-developments</guid><dc:creator><![CDATA[Nestful]]></dc:creator><pubDate>Wed, 20 Nov 2024 13:06:58 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/393eaf80-4223-4d50-9eaf-48a81c1f336a_1456x1048.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a launch post for a tool I built called OmniMessage, which aspires to be the final nail in coffin of client-server state synchronizing problems.</p><p>OmniMessage is written in <a href="https://gleam.run">Gleam</a>, a language that compiles to both Erlang and JavaScript, and is an extension of  <a href="https://hexdocs.pm/lustre/">Lustre</a>, Gleam&#8217;s major frontend framework.</p><p>To understand the benefits of OmniMessage and how it works, we first need we&#8217;ll first need explore how Gleam and Lustre provide one of the best developer experiences you can find for the web these days, and how building on top of them can improve that experience even more.</p><p>If you&#8217;re already familiar with The Elm Architecture and functional languages, you can <a href="https://blog.nestful.app/i/151830772/introducing-omnimessage">click here to skip to the OmniMessage part</a>, however going through the Lustre tutorial really puts the problem it solves in perspective.</p><h2>A Functional C</h2><p>Gleam&#8217;s website says:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V5zS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V5zS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 424w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 848w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 1272w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V5zS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png" width="927" height="407" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:407,&quot;width&quot;:927,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:50398,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&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="" srcset="https://substackcdn.com/image/fetch/$s_!V5zS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 424w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 848w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 1272w, https://substackcdn.com/image/fetch/$s_!V5zS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e2c2dcd-7423-4666-ad39-120c4893efa6_927x407.png 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 role="img" 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><title></title><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>Gleam powers the app this blog is for, <a href="https://nestful.app">Nestful</a>. Which I am <a href="https://blog.nestful.app/p/why-i-rewrote-nestful-in-gleam">incrementally rewriting</a>.</p><p>That other blog post already covers a lot of Gleam&#8217;s advantages and the specific considerations and tradeoffs as they pertain to Nestful. Those still apply, but when we look at frontend development in general, one key trait of Gleam is crucial: </p><p><strong>Gleam is a functional language with a C-style syntax.</strong></p><p>That is an indispensable advantage on the journey to ecosystem nirvana.</p><p>Here&#8217;s some for you to bask in. It is a very pleasant language:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://tour.gleam.run/flow-control/list-patterns/" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CG1J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 424w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 848w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 1272w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CG1J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png" width="1260" height="939" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:939,&quot;width&quot;:1260,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:119673,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://tour.gleam.run/flow-control/list-patterns/&quot;,&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="https://substackcdn.com/image/fetch/$s_!CG1J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 424w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 848w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.png 1272w, https://substackcdn.com/image/fetch/$s_!CG1J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f7f5c0d-b035-4a3f-8de0-123a435be818_1260x939.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 role="img" 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><title></title><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">Straight from Gleam&#8217;s tour! Click to jump to the code</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://tour.gleam.run/functions/pipelines/" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dyaz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 424w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 848w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 1272w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dyaz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png" width="1255" height="897" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:897,&quot;width&quot;:1255,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:119842,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://tour.gleam.run/functions/pipelines/&quot;,&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="https://substackcdn.com/image/fetch/$s_!Dyaz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 424w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 848w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.png 1272w, https://substackcdn.com/image/fetch/$s_!Dyaz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F540e386b-b59c-4f2e-b94b-b8b41e7de5be_1255x897.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 role="img" 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><title></title><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">Here&#8217;s another. Check out those pipes</figcaption></figure></div><p>We know that frontend developers like functional paradigms. TypeScript functional-like libraries continue to pop up,  some developers really like React, and even when they dislike those, they still argue about it <a href="https://github.com/airbnb/javascript/issues/1271">on AirBnB&#8217;s style guide repository</a>.</p><p>Even with all the that functional engagement in mind, however you slice it, frontend development is all in JavaScript-land, and JavaScript has a C-style syntax.</p><p>In my opinion, this is a significant part of why previous attempts like Elm, Reason/ReScript and PureScript did not reach the success they should have. It is also why Flutter made such huge strides with frontend developers. Flutter&#8217;s developer experience is excellent, and Dart sure does feel a lot like TypeScript.</p><p>The fact that Gleam has that kind of syntax will help the most crucial part of going mainstream &#8212; ecosystem growth. It&#8217;s going to be nice to have a functional language that&#8217;s not only simple and type safe, but also has a large ecosystem.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/p/gleams-lustre-is-frontend-developments?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/p/gleams-lustre-is-frontend-developments?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.nestful.app/p/gleams-lustre-is-frontend-developments?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>Enter Lustre</h2><p>If I had to describe Lustre with only a few words, I would say it is Gleam&#8217;s Elm and LiveView.</p><p>Yes, Elm <strong>and</strong> LiveView. While not a 1-to-1 match, Lustre is a Model-View-Update framework that can run both as a single-page application, like Elm, on the server like LiveView, and with OmniMessage &#8212; well, you&#8217;ll see.</p><p>I am not going to define Model-View-Update. Instead, I&#8217;m going to teach you some Lustre. By the time we&#8217;re done you&#8217;ll know exactly what it means.</p><p>We&#8217;ll start with a counter example, which is small and contained, then continue to build a (very, very minimal) chat app. Follow along with the code, it&#8217;s nice seeing how the different pieces fall into place.</p><p>First, let&#8217;s keep our data in a model, and have a function initializing it:</p><pre><code>type Model {
  Model(count: Int)
}

fn init(initial_count: Int) -&gt; Model {
  Model(int.max(0, initial_count))
}</code></pre><p>A <code>Model</code> type that contains a single integer, which we initialize to a given value, but not lower than zero. Fairly simple.</p><p>Next, we&#8217;ll define messages that can operate on that model:</p><pre><code>import gleam/int

import lustre/effect.{type Effect}

type Msg {
  Increment
  Decrement
}

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    Increment -&gt; #(Model(model.count + 1), effect.none())
    Decrement -&gt; #(Model(int.max(0, model.count - 1)), effect.none())
  }
}</code></pre><p>Our update function returns a new count based on the message it receives, adding or substracting from the original. Don&#8217;t mind the <code>effect.none()</code> part for now &#8212; we&#8217;ll get to that later.</p><p>Finally, we&#8217;ll have a function for displaying a user interface. It&#8217;s important to note that all the view functions in this post use <code>lustre_pipes</code>, which is an extension of Lustre&#8217;s view utilities that I consider easier to read:</p><pre><code>import lustre_pipes/attribute
import lustre_pipes/element.{type Element}
import lustre_pipes/element/html
import lustre_pipes/event

fn view(model: Model) -&gt; Element(Msg) {
  let count = int.to_string(model.count)

  html.div()
  |&gt; attribute.class("h-full w-full flex justify-center items-center")
  |&gt; element.children([
    html.button()
      |&gt; event.on_click(Decrement)
      |&gt; element.text_content("-"),
    html.p()
      |&gt; element.text_content(count),
    html.button()
      |&gt; event.on_click(Increment)
      |&gt; element.text_content("+"),
  ])
}</code></pre><p>When we hand the <code>init</code>, <code>update</code>, and <code>view</code> functions to Lustre starts a runtime that:</p><ol><li><p> Calls <code>init</code> with an initial value</p></li><li><p> Uses the resulting model to render a view</p></li><li><p> Listens to events and calls <code>update</code> with any dispatched messages, then back to #2</p></li></ol><p>Model, view, update.</p><p>Because views are pure functions, meaning they do not perform any side-effects, they can be used anywhere. Every time we&#8217;ll hand the same model into that function, we&#8217;re going to get the same view. Every. Single Time.</p><p>This makes features like hydration very simple. Since this is a deterministic state machine, all we need is the current state. In Lustre, hydration means simply sending the model alongside the rendered HTML. Lustre will use that model to create a view of its own and compare it to the prerendered HTML. If they match, the app is &#8220;hydrated&#8221;.</p><p>But I digress. We&#8217;re here to talk about client-server state management, so let&#8217;s continue, building a small chat app.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>A LiveView Single Page Application</h2><p>Our app should be able to:</p><ol><li><p>Send chat messages</p></li><li><p>Display chat messages and their status (sending, sent, received, etc)</p></li><li><p>Have a button that scrolls down to the latest chat message</p></li><li><p>Show the amount of active users in the chat room</p></li></ol><p>We&#8217;re missing some key features such as receiving messages, chat room creation and user authentication. It doesn&#8217;t matter. Even this confined use case of a single room with a single user will show us the benefits of managing state with Lustre.</p><p>We&#8217;re going to build a single page application first, then use the fundamentals for the server as well. The conversion will be trivial.</p><p>First, our model:</p><pre><code>// For date handling
import birl

pub type Model {
  Model(
    messages: List(ChatMessage),
    draft_content: String,
  )
}

pub type ChatMessage {
  ChatMessage(
    id: String,
    content: String,
    status: ChatMessageStatus,
    sent_at: birl.Time,
  )
}

pub type ChatMessageStatus {
  ClientError
  ServerError
  Sent
  Received
  Sending
}</code></pre><p>Note that to avoid confusing chat messages and Lustre messages, the former will be strictly called a chat message.</p><p>We&#8217;ll initialize our model with an empty draft and empty chat messages.</p><pre><code>fn init(_) -&gt; #(Model, effect.Effect(Msg)) {
  #(Model([], draft_content: ""), effect.none())
}</code></pre><p>Again, don&#8217;t worry about that <code>effect.none()</code> for now. </p><p>To define our <code>Msg</code> type, first let&#8217;s think through what we need to do. Lustre suggests using a SubjectVerbObject (SVO) structure:</p><ol><li><p><code>UserUpdateDraftContent</code></p></li><li><p><code>UserSendChatMessage</code></p></li><li><p><code>UserScrollToLatest</code></p></li><li><p><code>ServerSentChatMessages</code></p></li></ol><p>Number 4 is for getting back messages we sent to the server, not for other user&#8217;s messages, which is out of scope. This is more of a bring-your-own-refresh kind of app.</p><p>Having those messages written in SVO not only makes it easy to reason about how the app works, but also makes it easier to debug later, if and when needed.</p><p>Here is an update function that takes care of all the simple bits:</p><pre><code>// For uuid generation
import gluid


pub type Msg {
  UserUpdateDraftContent(String)
  UserSendChatMessage
  UserScrollToLatest
  ServerSentChatMessages(List(ChatMessage))
}

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
<strong>    </strong>UserUpdateDraftContent(draft_content) -&gt; #(
      Model(..model, draft_content:),
      effect.none(),
    )
    UserSendChatMessage -&gt; {
      let chat_msg =
        Message(
          id: gluid.guidv4() |&gt; string.lowercase(),
          content: model.draft_message,
          status: shared.Sending,
          sent_at: birl.utc_now(),
        )

      let chat_msgs = [chat_msg, ..model.chat_msgs]

      #(Model(..model, chat_msgs:), effect.none())
    }
  }
}</code></pre><p>How lovely is that <code>case</code>, which the Gleam compiler should mark as an error. Gleam makes sure <code>case</code> is exhaustive &#8212; it has our back in guaranteeing we address all of the different messages that the <code>update</code> function should be able to handle.</p><p>But wait! There&#8217;s a design problem. We need to clean the draft after sending it, but that doesn&#8217;t make sense to do in the <code>UserSendChatMessage</code> branch &#8212; chat messages can come from other places. A &#8220;scheduled messages&#8221; queue, for example. We need another message, <code>UserSendDraft</code>:</p><pre><code>pub type Msg {
  UserSendDraft
  UserUpdateDraftContent(String)
  UserSendChatMessage(String)
  UserScrollToLatest
  ServerSentChatMessages(List(ChatMessage))
}

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    UserUpdateDraftContent(content) -&gt; #(
      Model(..model, draft_content: content),
      effect.none(),
    )
    UserSendChatMessage(chat_msg) -&gt; {
      let chat_msgs = [chat_msg, ..model.chat_msgs]

      #(Model(..model, chat_msgs:), effect.none())
    }
    UserSendDraft -&gt; #(
      Model(..model, draft_content: ""),
      effect.from(fn(dispatch) {
        Message(
          id: gluid.guidv4() |&gt; string.lowercase(),
          content: model.draft_content,
          status: shared.Sending,
          sent_at: birl.utc_now(),
        )
        |&gt; UserSendChatMessage
        |&gt; dispatch
      }),
    )
  }
}</code></pre><p><code>UserSendDraft</code> returns not only the model, but also something called an <code>Effect</code>.</p><p>An effect is a type that instructs Lustre&#8217;s runtime to perform tasks on our behalf. In this case we ask Lustre to dispatch <code>UserSendChatMessage</code> with the message, by returning that effect alongside our model. <code>effect.none()</code> simply tells Lustre that there&#8217;s nothing to do, which is the case for the rest of our <code>case</code> branches.</p><p>Side effects are a must in most apps, especially on the web. To be able to use them while keeping our update function pure (meaning, having the same output every time it receives the same input), MVU delegates their execution to the runtime. If we want a side effect to happen, we have to ask the runtime to perform it.</p><p>Now that we know effects exist, we&#8217;ll use them to implement <code>UserScrollToLatest</code>. </p><p>For that we need need a container to scroll. Enter, a view function:</p><pre><code>fn status_string(status: ChatMessageStatus) {
  case status {
    ClientError -&gt; "Client Error"
    ServerError -&gt; "Server Error"
    Sent -&gt; "Sent"
    Received -&gt; "Received"
    Sending -&gt; "Sending"
  }
}

fn chat_message_element(chat_msg: ChatMessage) {
  html.div()
  |&gt; element.children([
    html.p()
    |&gt; element.text_content(
      status_string(chat_msg.status) &lt;&gt; ": " &lt;&gt; chat_msg.content,
    ),
  ])
}

fn sort_chat_messages(chat_msgs: List(ChatMessage)) {
  use a, b &lt;- list.sort(chat_msgs)
  birl.compare(a.sent_at, b.sent_at)
}

fn view(model: Model) -&gt; element.Element(Msg) {
  let sorted_chat_msgs =
    model.chat_msgs
    |&gt; sort_chat_messages

  html.div()
  |&gt; attribute.class(
    "h-full flex flex-col justify-center items-center gap-y-5"
  )
  |&gt; element.children([
    html.div()
      |&gt; attribute.id("chat-msgs")
      |&gt; attribute.class(
      "h-80 w-80 overflow-y-auto p-5 border border-gray-400 rounded-xl",
      )
      |&gt; element.keyed({
        use chat_msg &lt;- list.map(sorted_chat_msgs)
        #(chat_msg.id, chat_message_element(chat_msg))
      }),
    html.form()
      |&gt; attribute.class("w-80 flex gap-x-4")
      |&gt; event.on_submit(UserSendDraft)
      |&gt; element.children([
        html.input()
          |&gt; event.on_input(UserUpdateDraftContent)
          |&gt; attribute.type_("text")
          |&gt; attribute.value(model.draft_content)
          |&gt; attribute.class(
            "flex-1 border border-gray-400 rounded-lg p-1.5")
          |&gt; element.empty(),
        html.input()
          |&gt; attribute.type_("submit")
          |&gt; attribute.value("Send")
          |&gt; attribute.class(
      "border border-gray-400 rounded-lg p-1.5 text-gray-700 font-bold",
          )
          |&gt; element.empty(),
      ]),
  ])
}
</code></pre><p>Note how event handlers must return a <code>Msg</code> for our update function to handle. </p><p><code>event_onsubmit</code> accepts a straight up <code>Msg</code>, so we just put <code>UserSendDraft</code>.</p><p><code>on_input</code> accepts a function of the type <code>fn(String) &#8594; Msg</code>, which is exactly what <code>UserUpdateDraftContent</code> is. The following is identical:</p><pre><code>|&gt; event.on_input(fn(value: String) {
  UserUpdateDraftContent(value)
})</code></pre><p>If you know HTML, the rest is fairly straightforward except for two parts: <code>use</code> and <code>element.keyed</code>.</p><p><code>use</code> is syntactic sugar for a final-argument callback. Everything before the arrow is the callback&#8217;s arguments, lines below the <code>use</code> are the callback&#8217;s body.</p><p>This means these two are equivalent:</p><pre><code>fn sort_chat_messages(chat_msgs: List(ChatMessage)) {
  use a, b &lt;- list.sort(chat_msgs)
  birl.compare(a.sent_at, b.sent_at)
}

fn sort_chat_messages(chat_msgs: List(ChatMessage)) {
  chat_msgs
  |&gt; list.sort(fn(a, b) {
    birl.compare(a.sent_at, b.sent_at)
  })
}</code></pre><p>You know a language is simple when <code>use</code> is its most &#8220;complicated&#8221; part.</p><p><code>element.keyed</code> creates an element whose children have a unique identifier (the key) attached to them such that when Lustre has to re-render the list itself, it knows which elements changed and which didn&#8217;t. This is similar to React&#8217;s or Vue&#8217;s <code>key</code> property and is done in Lustre by giving the <code>element.keyed()</code> function a list of tuples in the form of <code>#(key, Element)</code>.</p><p>Now that we have our view, we can handle <code>UserScrollToLatest</code>:</p><pre><code># for handling possible errors
import gleam/result
# for interacting with the DOM
import plinth/browser/element as plinth_element

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    // other handlers omitted for brevity
    UserScrollToLatest -&gt; #(model, scroll_to_latest_message())
  }
}

const msgs_container_id = "chat-msgs"

fn scroll_to_latest_message() {
  effect.from(fn(_dispatch) {
    let _ =
      document.get_element_by_id(msgs_container_id)
      |&gt; result.then(fn(container) {
        plinth_element.scroll_height(container)
        |&gt; plinth_element.set_scroll_top(container, _)
        Ok(Nil)
      })

    Nil
  })
}</code></pre><p>This code uses <code>plinth</code>, an library to interact with browser APIs, to first find the container by its id (<code>get_element_by_id</code>), and if found (<code>result.then</code>), scroll.</p><p>By extracting this effect to a separate function, we can include it in other places, like automatically scrolling on <code>UserSendChatMessage</code>.</p><p>By now you should experience how Gleam and Lustre make for this very structured, harmonious development experience, where everything has a place in the render loop.</p><p>Now that we have the client side taken care of, let&#8217;s address the server. As you&#8217;ve probably guessed, talking to the server is a side effect, meaning we&#8217;ll instruct Lustre to make the talking on our behalf. Luckily there is a package that does just that:</p><pre><code>import lustre_http as http

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    // other handlers omitted for brevity
    ClientMessage(shared.UserSendChatMessage(chat_msg)) -&gt; {
      let chat_msgs = [chat_msg, ..model.chat_msgs]

      #(
        Model(..model, chat_msgs:),
        effect.batch([
          scroll_to_latest_message(),
          http.post(
            "/chat-message",
            [chat_msg] |&gt; chat_msgs_to_json,
            http.expect_json(
              chat_msgs_from_json,
              ServerSentChatMessages,
            ),
          ),
        ]
      )
    }
  }
}</code></pre><p>This takes care of creating the message on the server. We still update the model with the new chat message in <code>Sending</code> state, and we continue with <code>effect.batch</code>.</p><p><code>effect.batch</code> takes several effects and combines them to a single one for our <code>update</code> function. The first one is our scroll effect that&#8217;ll happen after sending a message. The second will HTTP POST that message to the path <code>/chat_message</code>.</p><p>That effect is from the <code>lustre_http</code> library (that we import as <code>http</code>), that can create effects of HTTP requests. The arguments for creating the POST effect are:</p><ol><li><p>The path to POST to, in our case <code>/chat-message</code></p></li><li><p>The body of the post request, in our case a <code>JSON.stringify</code>-ed chat message</p></li><li><p>A description of the result, (JSON) and its handler (<code>ServerSentChatMessages</code>).</p></li></ol><p>In a production chat app it would have been better to use websockets. The websocket effect works very similarly but requires more setup due to the nature of websockets. Since boilerplate does not add to our learning, we demonstrate using regular HTTP.</p><p>When Lustre will execute this effect, the following will happen:</p><ol><li><p>It will post our encoded chat message to <code>/chat-message</code></p></li><li><p>It will try parsing it as JSON using <code>chat_msgs_from_json</code></p></li><li><p>On success, it will dispatch <code>ServerSendChatMessages(Ok(messages))</code></p></li><li><p>On error, it will dispatch <code>ServerSendChatMessages(Error(error))</code></p></li></ol><p>Gleam, you see, has errors as values. That means that except for problematic FFI, functions never throw. You must deal with errors as they come or consciously defer their handling. This pairs fantastically with <code>case</code>&#8217;s exhaustiveness checks:</p><pre><code>pub type Msg {
  // accepts a `Result` from `lustre_http`'s effect:
  ServerSentChatMessages(Result(List(ChatMessage), http.HttpError))
}

fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    // Gleam will make sure both variants are present. Lovely.
    ServerSentChatMessage(Ok(List(ChatMessage)) -&gt; todo
    ServerSentChatMessage(Error(error)) -&gt; todo
  }
}</code></pre><p>Again, we have a design problem. The server is the source of truth for chat messages, so we&#8217;d like it to override our local copies (that&#8217;s how a <code>Sending</code> chat message will become a <code>Sent</code> chat message). However doing that for a list can be quite costly.</p><p>Let&#8217;s change our state to hold chat messages in a dictionary, instead:</p><pre><code>pub type Model {
  Model(
    messages: Dict(String, ChatMessage),
    draft_content: String,
  )
}</code></pre><p>And implement <code>ServerSendChatMessage</code>:</p><pre><code>fn update(model: Model, msg: Msg) -&gt; #(Model, Effect(Msg)) {
  case msg {
    ServerSentChatMessage(Ok(server_chat_msgs)) -&gt; {
      let chat_msgs =
        model.chat_msgs
        |&gt; dict.merge(server_chat_msgs)

      #(Model(..model, chat_msgs:), effect.none())
    }
    ServerSentChatMessage(Error(error)) -&gt; {
      // this is where you'd show, say, an error toast
      #(model, effect.none())
    }
    // changes to other branches available in the full code below
  }
}</code></pre><p>As with any project, the more time we spend writing its code, the more we learn about it and about the solutions it demands. With MVU, those changes are easy to adapt to since the state mechanism stays the same. By keeping the state handling mechanism completely separate from our project&#8217;s design choices, we avoid having to change it when we inevitably discover we made poor ones.</p><p>To complete our server-communication portion for chat messages, let&#8217;s fetch on <code>init</code>:</p><pre><code>fn init(_) -&gt; #(Model, effect.Effect(Msg)) {
  #(
    Model(dict.new(), draft_content: ""),
    http.get(
      "/chat-message",
      http.expect_json(
        chat_msgs_from_json,
        ServerSentChatMessages,
      ),
    ),
  )
}</code></pre><p>So far this should have been a relatively pleasant experience of writing a very regular app, with all the pros and cons that come with it. Our state handling approach forever puts us on alert, having to make sure our local copy of the chat is up to date with the server&#8217;s copy &#8212; the source of truth. This is a very common SPA issue.</p><p>I can hear the LiveView gang collectively yelling into the past as I&#8217;m typing. &#8220;Keep everything on the server&#8221;, they say. Well, we&#8217;re about to. &#8212; &#8220;and get rid of that REST mess!&#8221;. Ok, I heard you, we&#8217;re about to.</p><p>Our first step of evolution will be to implement our final missing feature:</p><blockquote><p>Show the amount of active users in the chat room</p></blockquote><p>I sneakily did not include a <code>Msg</code> for handling this when we built our <code>update</code> function. This information is strictly server-side, with no interaction, and most importantly &#8212; <strong>it is meaningless when we&#8217;re offline.</strong></p><p>It&#8217;s a no-brainer to run it exclusively on the server. This is where the LiveView part of &#8220;Elm <strong>and</strong> LiveView&#8221; comes in. We can take this Lustre component:</p><pre><code>type Model {
  Model(user_count: Option(Int))
}

fn init(count_listener: fn(fn(Int) -&gt; Nil) -&gt; Nil) {
  #(
    Model(None),
    effect.from(fn(dispatch) {
      listen(fn(new_count) { 
        dispatch(GotNewCount(new_count))
      })
    })
  )
}

type Msg {
  GotNewCount(Int)
}

fn update(model: Model, msg: Msg) {
  case msg {
    GotNewCount(new_count) -&gt; #(Model(new_count), effect.none())
  }
}

fn view(model: Model) {
  let count_message =
    model.user_count
    |&gt; option.map(int.to_string)
    |&gt; option.unwrap("Getting user count...")

  html.p()
    |&gt; element.text_content(count_message)
}</code></pre><p>Run it on the server, serve it via websockets on <code>/user-count</code>, then add the following to our client&#8217;s view function:</p><pre><code>server_component.component()
  |&gt; server_component.route("/user-count")
  |&gt; element.children([
    html.p()
      |&gt; element.text_content("Getting user count...") 
  ])</code></pre><p>Lustre will make it happen so that our user count will travel from the server into the client, accurately rendered. No need to sync state, everything comes from the source.</p><p>In my opinion, even after considering all the good that is Gleam, MVU, and Lustre&#8217;s implementation of it, this is the biggest advantage of them all. </p><p>Whenever you use HTMX or LiveView, there always comes a time when you need to &#8220;sprinkle some JavaScript&#8221;. While sprinkling some JavaScirpt is much better than writing everything in JavaScript, writing none is best.</p><p>This is the power of Gleam&#8217;s Lustre. You have all the advantages of a single page application, <em>and</em> of server side components. You pick the right approach, and use the same exact tool to fulfill it.</p><p>I hear you, HTMX people calling into the past: &#8220;make the chat messages server side too&#8221;. Nope. Not going to happen. At least not the way you think it will.</p><p>You see, the chat <strong>is meaningful even when the user is offline</strong>. Even if you make it online-only, there&#8217;s a problem moving the chat messages functionality to the server. How will we handle a chat message that was just sent? The server can&#8217;t render a <code>Sending</code> state because it&#8217;s still sending &#8212; it doesn&#8217;t know it exists!</p><p>&#8220;Sprinkle some JavaScript!&#8221;</p><p>Yes, the current solution is to have some mix of client and server side code, and do the syncing manually, just for that one bit.</p><p>Or is it?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Introducing OmniMessage</h2><p>OmniMessage is a library created to answer this exact problem. It is composed of a Lustre extension on the client side, and server utilities (including another, optional, Lustre extension) on the server side.</p><p>After you set those two up a subset of messages, at your discretion, will dispatch in <strong>both the client and the server</strong>. You can think of this as a more blissful RPC.</p><p>Here&#8217;s the boilerplate:</p><pre><code>import omnimessage_lustre as omniclient

// This just converts to/from JSON
let encoder_decoder =
  omniclient.EncoderDecoder(
    // this converts TO JSON
    fn(msg) {
      case msg {
        // Encode a certain subset of messages
        ClientMessage(message) -&gt; Ok(shared.encode_client_message(message))
        // Return Error(Nil) for messages you don't want to send out
        _ -&gt; Error(Nil)
      }
    },
    // this converts FROM JSON
    fn(encoded_msg) {
      shared.decode_server_message(encoded_msg)
      |&gt; result.map(ServerMessage)
    },
  )

// This creates an extended Lustre component
omniclient.component(
  init,
  update,
  view,
  dict.new(),
  encoder_decoder,
  transports.http("http://localhost:8000/omni-http", option.None, dict.new()),
  TransportState,
)</code></pre><p>The &#8220;biggest&#8221; chunk of work is encoding and decoding, but that&#8217;s something you need to do anyway. Even pure server components eventually need to save to a database.</p><p>If we setup OmniMessage like that, our original Lustre app could reuse <strong>all </strong>the client-side messages we wrote at the beginning of this post, and the server will reply and override our <code>Sending</code> chat messages when they are received.</p><p>Yes, yes, I know you want to see the code, but it really is the same. Here, look:</p><pre><code>// MODEL ---------------------------------------------------------------

pub type Model {
  Model(chat_msgs: dict.Dict(String, ChatMessage), draft_content: String)
}

fn init(_initial_model: Option(Model)) -&gt; #(Model, effect.Effect(Msg)) {
  #(Model(dict.new(), draft_content: ""), effect.none())
}

// UPDATE --------------------------------------------------------------

pub type Msg {
  UserSendDraft
  UserScrollToLatest
  UserUpdateDraftContent(String)
  ClientMessage(ClientMessage)
  ServerMessage(ServerMessage)
  TransportState(transports.TransportState(json.DecodeError))
}

fn update(model: Model, msg: Msg) -&gt; #(Model, effect.Effect(Msg)) {
  case msg {
    // Good old UI
    UserUpdateDraftContent(content) -&gt; #(
      Model(..model, draft_content: content),
      effect.none(),
    )
    UserSendDraft -&gt; {
      #(
        Model(..model, draft_content: ""),
        effect.from(fn(dispatch) {
          shared.new_chat_msg(model.draft_content, shared.Sending)
          |&gt; shared.UserSendChatMessage
          |&gt; ClientMessage
          |&gt; dispatch
        }),
      )
    }
    UserScrollToLatest -&gt; #(model, scroll_to_latest_message())
    // Shared messages
    ClientMessage(shared.UserSendChatMessage(chat_msg)) -&gt; {
      let chat_msgs =
        model.chat_msgs
        |&gt; dict.insert(chat_msg.id, chat_msg)

      #(Model(..model, chat_msgs:), scroll_to_latest_message())
    }
    // The rest of the ClientMessages are exlusively handled by the server
    ClientMessage(_) -&gt; {
      #(model, effect.none())
    }
    // Merge strategy
    ServerMessage(shared.ServerUpsertChatMessages(server_messages)) -&gt; {
      let chat_msgs =
        model.chat_msgs
        // Omnimessage shines when you're OK with server being source of truth
        |&gt; dict.merge(server_messages)

      #(Model(..model, chat_msgs:), effect.none())
    }
    // State handlers - use for initialization, debug, online/offline indicator
    TransportState(transports.TransportUp) -&gt; {
      #(
        model,
        effect.from(fn(dispatch) {
          dispatch(ClientMessage(shared.FetchChatMessages))
        }),
      )
    }
    TransportState(transports.TransportDown(_, _)) -&gt; {
      // Use this for debugging, online/offline indicator
      #(model, effect.none())
    }
    TransportState(transports.TransportError(_)) -&gt; {
      // Use this for debugging, online/offline indicator
      #(model, effect.none())
    }
  }
}

</code></pre><p>See? Same logic. The only differences are that some types are wrapped so we could share them with the server, and instead of handling network errors directly we now handle them through a <code>TransportState</code> variant.</p><p>Other than that, you dispatch messages and the server replies as if it&#8217;s the same app.</p><p>You decide how to encode the messages, and what transport to send them through. As long as the server can understand those, you can use whatever server, in any language. Currently we have transports for HTTP and websockets, but any transport is possible. For example, you could write one to communicate with an Electron or Tauri backend.</p><p>Here are some examples utilizing <code>omnimessage_server</code>, meant for Gleam servers. Say you have this simple handler:</p><pre><code>fn handle(ctx: Context, msg: Msg) -&gt; Msg {
  case msg {
    ClientMessage(shared.UserSendMessage(message)) -&gt; {
      ctx |&gt; context.add_message(message)

      context.get_chat_messages(ctx)
      |&gt; shared.ServerUpsertMessages
      |&gt; ServerMessage
    }
    ClientMessage(shared.UserDeleteMessage(message_id)) -&gt; {
      ctx |&gt; context.delete_message(message_id)

      context.get_chat_messages(ctx)
      |&gt; shared.ServerUpsertMessages
      |&gt; ServerMessage
    }
    ClientMessage(shared.FetchMessages) -&gt; {
      context.get_chat_messages(ctx)
      |&gt; shared.ServerUpsertMessages
      |&gt; ServerMessage
    }
    ServerMessage(_) | Noop -&gt; Noop
  }
}</code></pre><p>Here&#8217;s how you&#8217;d use it in a Gleam HTTP server:</p><pre><code>use &lt;- omniserver.wisp_http_middleware(
  req,
  "/omni-http",
  encoder_decoder(),
  handle(ctx, _),
)</code></pre><p>Just give it the request, the path it should handle, the messages encoder/decoder and the handler from above, and it will:</p><ol><li><p>Decode incoming messages</p></li><li><p>Run them through the handler</p></li><li><p>Encode the result</p></li><li><p>Generate an HTTP response with it</p></li></ol><p>Need to send messages without the client initiating a request? Use websockets:</p><pre><code>["omni-pipe-ws"], http.Get -&gt;
  omniserver.mist_websocket_pipe(
    req,
    encoder_decoder(), // this is the same encoder_decoer
    handle(ctx, _), // the same message handler
    logger, // error handler
  )</code></pre><p>Have a more complex app and you need structure? Use a Lustre server component!</p><pre><code>["omni-app-ws"], http.Get -&gt;
  omniserver.mist_websocket_application(
    req,
    chat.app(), // lustre server component
    ctx, // flags for its init
    logger // error handler
  )</code></pre><p>This is how the server component will look like:</p><pre><code>// MODEL ---------------------------------------------------------------
pub type Model {
  Model(messages: dict.Dict(String, shared.ChatMessage), ctx: Context)
}

fn init(ctx: Context) -&gt; #(Model, effect.Effect(Msg)) {
  #(Model(messages: ctx |&gt; context.get_chat_msgs, ctx:), effect.none())
}

// UPDATE --------------------------------------------------------------

pub type Msg {
  ClientMessage(ClientMessage)
  ServerMessage(ServerMessage)
}

pub fn update(model: Model, msg: Msg) {
  case msg {
    ClientMessage(shared.UserSendMessage(message)) -&gt; #(
      model,
      effect.from(fn(dispatch) {
        model.ctx |&gt; context.add_message(message)

        ctx
        |&gt; context.get_chat_msgs
        |&gt; shared.ServerUpsertMessages
        |&gt; ServerMessage
        |&gt; dispatch
      }),
    )
    ClientMessage(shared.UserDeleteMessage(message_id)) -&gt; #(
      model,
      effect.from(fn(dispatch) {
        model.ctx |&gt; context.delete_message(message_id)

        ctx
        |&gt; context.get_chat_msgs
        |&gt; shared.ServerUpsertMessages
        |&gt; ServerMessage
        |&gt; dispatch
      }),
    )
    ClientMessage(shared.FetchMessages) -&gt; #(
      model,
      effect.from(fn(dispatch) {
        get_messages(model.ctx)
        |&gt; shared.ServerUpsertMessages
        |&gt; ServerMessage
        |&gt; dispatch
      }),
    )
    ServerMessage(shared.ServerUpsertMessages(messages)) -&gt; #(
      Model(..model, messages:),
      effect.none(),
    )
  }
}</code></pre><p>It&#8217;s like having the same app spread across two different files.</p><p>OmniMessage is still very young and is missing some important features, but the vision is clear &#8212; it is the last piece in the trifecta that is zen state management:</p><ol><li><p>Client state &#8212; solved by MVU</p></li><li><p>Server state &#8212; solved by server components/LiveView approach</p></li><li><p>Hybrid state &#8212; solved by OmniState</p></li></ol><p>The hybrid state OmniMessage represents is a very sharp sword that can be tricky to wield without a clear separation of concerns. This is why OmniMessage shines when a single party is the source of truth, since we can simply override the other party&#8217;s state every time a message arrives. This gives us all the benefits of OmniMessage without the <s>beehive</s> hornets nest that is carefully merging state.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><p>This is why Lustre is the end game of frontend development. It collected all of the right solutions and wrapped them up in the nicest, gleamy package.</p><p>Full, working, code is available in the <a href="https://github.com/weedonandscott/omnimessage/tree/master/example">OmniMessage repository</a>.</p><p>Here is the OmniMessage documentation:</p><p><a href="https://hex.pm/packages/omnimessage_server">https://hex.pm/packages/omnimessage_server</a></p><p><a href="https://hex.pm/packages/omnimessage_lustre">https://hex.pm/packages/omnimessage_lustre</a></p><p></p><p>And don&#8217;t forget to check out <a href="https://nestful.app">Nestful</a>! Not only is Nestful written in Gleam (the new &#8220;written in Rust&#8221;), but it actually has novel ways to manage your time:</p><p><a href="https://nestful.app">https://nestful.app</a></p><div><hr></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/p/gleams-lustre-is-frontend-developments?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.nestful.app/p/gleams-lustre-is-frontend-developments?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Or you could use a CRDT (which we do, but that&#8217;s a story for another post)</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Why I Rewrote Nestful in Gleam]]></title><description><![CDATA[Going away from TypeScript was easy, the question was -- where to?]]></description><link>https://blog.nestful.app/p/why-i-rewrote-nestful-in-gleam</link><guid isPermaLink="false">https://blog.nestful.app/p/why-i-rewrote-nestful-in-gleam</guid><dc:creator><![CDATA[Nestful]]></dc:creator><pubDate>Wed, 09 Oct 2024 11:36:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!aZOI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13336315-f7a5-451b-ae0a-2cf7a1f81850_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Yesterday I completed a partial rewrite of <a href="https://nestful.app">Nestful</a> in <a href="https://gleam.run">Gleam</a>, a relatively young immutable functional language that compiles to both Erlang and JavaScript.</p><p>This post tells the story of why I wanted to leave TypeScript behind, and why I chose Gleam to replace it.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>A Need Rises</h2><p>Nestful is a list-taking app with the promise to consolidate all of the different things competing for its users&#8217; time. Those things (work, chores, musings, etc) would usually be stored in siloes &#8212;GitHub for personal projects, Trello for house chores, notes on life in a notes app, and so on. That&#8217;s great for prioritizing within a project, but how do you prioritize the projects themselves?</p><p>Nestful was born to solve this problem for me. The premise is simple: Nestful is a list of items, and items can be inside other items. That way I could have a list of my projects, prioritize them, and when it&#8217;s time to prioritize the project itself, I click the project&#8217;s item and inside there&#8217;s its own specific items, relating to its completion.</p><p>These were my main requirements from Nestful:</p><ol><li><p>Work completely offline, with composable sync (more on that later)</p></li><li><p>Fast iteration so I could dogfood it quickly</p></li><li><p>Work on all my devices</p></li></ol><p>Web it was.</p><p>I chose Vue + TypeScript because I was the most fluent with it, but it could easily have been any other client-side web technology and this post would have stayed mostly the same.</p><h2>TypeScript Frustrations</h2><p>As with every clean-slate project, the beginning was a breeze. I got started relatively quickly, but as I progressed and Nestful grew, problems started popping up.</p><h3>A Language Within a Language</h3><p>The types part of TypeScript is a language in and of itself. First you program the actual business logic, then you have to spend 10% of the time that took to wrangle types to be correct. The worst part of it?</p><p>When you&#8217;re done, you still don&#8217;t have confidence that it&#8217;s correct.</p><p>I did not delve deep enough into the typing system of TypeScript to know if it&#8217;s psychological. Maybe it is mathematically proven that once TypeScript compiles without any `any`s, it&#8217;s safe.</p><p>Maybe. I don&#8217;t care.</p><p>The way I see it, a language has to justify a learning curve. Learning to navigate Rust&#8217;s convoluted (at times) syntax is rewarded by very impressive compile time guarantees. </p><p>What does TypeScript give you that is worth the time it demands, in learning and maintenance?</p><h3>Inherited Difficulties</h3><p>Those problems with TypeScript are inherited and compounded when using a library. I used to spend 2-3 days when upgrading the client-side database I used at the time because for some reason, may it be TypeScript itself or the library&#8217;s author investment, the type system did not supply enough guarantees.</p><p>This is compounded when there&#8217;s a bug in a 3rd party library, or that I need to understand the inner-workings of one. I have become fairly adept in understanding other people&#8217;s code, especially in TypeScript, yet it is still more intensive of a task compared to a nicer, simple language like Gleam.</p><h3>Error Handling</h3><p>After Rust exposed me to the concept of errors as values, I could not go back. It was so much more ergonomic and resulted in such a better end product that I started sorely missing it. Although this is a Javascript problem first and foremost, its supersets can solve it, and some did try.</p><p>There are several attempts to make &#8220;Functional TypeScript&#8221;, but I did not like them. First, there was no nice solution to handle those returned errors, with the glaring lack of pattern matching. More than that, though &#8212; eventually, when the kitchen sink is full enough, the tools are too suffocated to be useful.</p><h3>TypeScript is No Fun</h3><p>Anything I do should be as enjoyable as possible, and when it comes down to it, TypeScript is just not fun.</p><p>Some things are just destined to suck, like the recent clearing up of a clogged sewer at the house. Programming Nestful is not one of them.</p><p>This is also important to its success as an application &#8212; as Nestful grew and will grow, refactors were and will be needed. Every TypeScript refactor resulted in more bugs. Gleam refactors resulted in less. More on that below.</p><h2>&#8220;Compiled to WASM&#8221;</h2><p>I am a relatively thorough person. As I got more frustrated with the stack I had, I began going one by one over the possible alternatives. There&#8217;s a <a href="https://github.com/appcypher/awesome-wasm-langs">lovely repository on GitHub</a> tracking languages that compile to WebAssembly, so I started looking at each, one by one.</p><p>They all had one problem in common, though. Although WASM runs in the browser, it will demand a practically complete rewrite of Nestful, due to interoperability issues that will inevitably arise.</p><p>The solution is then to strictly compile to JavaScript.</p><p>I can&#8217;t remember which list I looked at at the time (maybe the one on the <a href="https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS">CoffeScript wiki</a>?), but in the end I have narrowed it down to:</p><ul><li><p>Dart</p></li><li><p>F#</p></li><li><p>OCaml</p></li></ul><p>Dart was easy to pass on, even though it&#8217;s one I already knew, and on paper &#8212; the most fitting replacement &#8212; especially when adding a lot of the functional-style features I was looking for. However, it suffers from the crowded kitchen sink problem, and&#8230; Google makes it. I like using Flutter a fair bit, but I would rather split my eggs over multiple baskets, thank you very much.</p><p>F# and OCaml were pretty close. F# looked more approachable to me, but the ecosystem was fairly slow going, and I don&#8217;t paticularly enjoy working with Microsoft tools. OCaml seems to be back on track, and used in some major production codebases, but the syntax was not as familiar to me, and I got mixed signals about the rejuvenation they claim they have.</p><p>I have a soft-rule that says that in every new project I start, I must learn something new to the extent my bandwidth allows. Even though I decided not to go with either F# or OCaml this time, I&#8217;ll be happy if they turn out to be a good fit in a future project.</p><p>Then Gleam hit 1.0, which I discovered when YouTube recommended me a <a href="https://www.youtube.com/watch?v=9mfO821E7sE">Primeagen video</a>. Before, I probably skimmed over Gleam and passed for it being too young. Having it reach stability with the community momentum to compensate for being young relaxed my ecosystem worries (a little), and I took it for a spin.</p><h2>Star of the Show</h2><p>Gleam has quickly turned out to be an excellent choice made at the right time. It streamlined my development even though I had to write a lot of (FFI) boilerplate to get going.</p><h3>As Simple as it Gets</h3><p>Gleam has a simple-language philosophy. This means that the language itself is fairly small and contained (there&#8217;s no `if`!), which in turn makes it very easy to learn, and most importantly, extremely easy to understand 3rd party code.</p><p>Even with a language this young and some things that are not fully ready, I had a much better time than doing the same task with TypeScript.</p><h3>Easy Refactor</h3><p>As Nestful grows refactors are going to be needed. &#8220;Composable sync&#8221;, mentioned in the requirements, is an example of that.</p><p>If Nestful promises to be a place that hosts all the different things competing for my time, it better do that for things that need more than a list to be managed. Some projects do need the likes of Linear or GitHub, and I would like to be able to pull items from there and transparently display them to the user to be prioritized. They don&#8217;t need all of GitHub&#8217;s features to prioritize between fixing bug #233 and putting the car in the garage. When they actually reach fixing that bug they can click the item and Nestful will lead them right to GitHub.</p><p>The data layer is abstracted enough to achieve this, but you don&#8217;t really know what you need until you need it, especially if you avoid premature optimization like I do.</p><p>I am confident with Gleam having my back when it&#8217;s time to modify the data layer, helping me refactor correctly, even when it&#8217;s tricky. For example, Nestful has a feature that &#8220;bubbles up&#8221; items. It&#8217;s currently only used to show use deeply-nested due items in a single view (to prioritize between "bug #233 somewhere inside a project and &#8220;Put car in garage&#8221; somewhere inside house chores). Expanding this to more use cases is not trivial, and Gleam is here to help.</p><h3>Fluffy and Cozy</h3><p>Gleam is fun to write in. It&#8217;s a calming language if there ever was one. It&#8217;s soft to read and write (you&#8217;ll get it when you try it) and is overall a relaxing experience. That&#8217;s mostly thanks to its syntax choices.</p><p>Beyond that, it supplies the usual functional/immutable features you&#8217;d expect from a first release of such a language. Namely, to my liking, no nulls, errors as values, and pattern matching.</p><h3>Enter Vleam</h3><p>The cherry on the top is that I could adopt Gleam incrementally. I could add it piece by piece, replacing the TypeScript in my services, composables, and components. For that last one to be easy, I wrote <a href="https://github.com/vleam/vleam">Vleam</a>.</p><p>Vleam is a set of tools (Vite plugin, Vue FFI, and LSP) that allows the usage of Gleam in Vue single-file components with full LSP and hot-reload support. Give it a try if you&#8217;re looking for a change and running Vue yourself. Easily done one component at a time.</p><h2>Not Perfect Yet, But Overall Right</h2><p>Gleam doesn&#8217;t have everything nailed quite yet. Some LSP features are sorely lacking (it&#8217;s rename for me), and the language could use a bit of introspection and reflection so I could avoid a neverending unwrapping of types and the chore of serialization.</p><p>Even with those rough spots, though, Gleam is still a great choice. It&#8217;s a breath of fresh air over the chore that is TypeScript, and is making big strides with every release.</p><p>Next, I&#8217;ll give it a shot on the backend. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.nestful.app/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Nestful&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>