<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: sjdonado</title>
    <description>The latest articles on DEV Community by sjdonado (@sjdonado).</description>
    <link>https://dev.to/sjdonado</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F360831%2F0b76b080-a2a2-40a0-b9ef-c83df1e2b728.jpeg</url>
      <title>DEV Community: sjdonado</title>
      <link>https://dev.to/sjdonado</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sjdonado"/>
    <language>en</language>
    <item>
      <title>Building a Fast and Compact SQLite Cache Store</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Wed, 24 Jul 2024 06:21:00 +0000</pubDate>
      <link>https://dev.to/sjdonado/building-a-fast-and-compact-sqlite-cache-store-2h9g</link>
      <guid>https://dev.to/sjdonado/building-a-fast-and-compact-sqlite-cache-store-2h9g</guid>
      <description>&lt;p&gt;When working on applications, caching is crucial for enhancing performance by reducing the need for repeated database fetches. Among the various SQLite libraries available, Bun's native integration is optimized for performance and takes advantage of its non-blocking I/O capabilities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;bun:sqlite&lt;/code&gt; module is roughly 3-6x faster than &lt;code&gt;better-sqlite3&lt;/code&gt; and 8-9x faster than &lt;code&gt;deno.land/x/sqlite&lt;/code&gt; for read queries. Each driver was benchmarked against the Northwind Traders dataset. View and run the benchmark source. &lt;a href="https://bun.sh/docs/api/sqlite" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In addition to efficient caching, serializing JavaScript objects can be slow when using JSON. This is why it makes sense to opt for efficient binary encoding alternatives like Msgpackr or CBOR. These formats are faster to parse, support complex data types, require less CPU usage, and store data more compactly, further enhancing overall application performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fux7b8g474xe68kv37yum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fux7b8g474xe68kv37yum.png" alt="Encoding/Decoding performance" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this in mind, let's explore how to build a cache manager using &lt;code&gt;bun:sqlite&lt;/code&gt; along with efficient binary encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  A &lt;a href="https://github.com/jaredwray/cache-manager" rel="noopener noreferrer"&gt;cache-manager&lt;/a&gt; Store
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cache-manager&lt;/code&gt; provides a straightforward and intuitive API for caching, abstracting away the complexity of managing different cache stores and their configurations. With support for multiple stores, built-in expiration and TTL management, and robust error handling and fallback mechanisms, it ensures data integrity and freshness.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;code&gt;cache-manager&lt;/code&gt; is highly customizable and extensible, allowing you to create custom cache stores tailored to your needs. This flexibility means you can set up and use caching with minimal code, allowing you to focus on your application's core logic.&lt;/p&gt;

&lt;p&gt;The required interface to fulfill as a cache-manager store is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Milliseconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;mset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;arguments_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Milliseconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;mget&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="na"&gt;arguments_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;mdel&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="na"&gt;arguments_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;See more &lt;a href="https://github.com/jaredwray/cache-manager/blob/main/packages/cache-manager/src/caching.ts#L21C1-L32C1" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Queries
&lt;/h2&gt;

&lt;p&gt;Configuring SQLite for Optimal Performance&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PRAGMA main.synchronous = NORMAL;&lt;/code&gt;: Ensures that SQLite writes are fast while still maintaining a reasonable level of data safety. It does not guarantee as much durability as FULL, but it is sufficient for many use cases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PRAGMA main.journal_mode = WAL2;&lt;/code&gt;: Improves concurrency by allowing readers to access the database while a write operation is ongoing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PRAGMA main.auto_vacuum = INCREMENTAL;&lt;/code&gt;: Allows SQLite to reclaim unused space incrementally, rather than all at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;1. Creating the Cache Table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nb"&gt;BLOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;expire_at&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;index_expire_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;expire_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;val&lt;/code&gt; column stores the cached value in a binary large object (BLOB) format, allowing it to handle various data types depending on the chosen serializer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Inserting or Updating Cache Entries&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expire_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;expire_at&lt;/code&gt; is pre-calculated based on the &lt;code&gt;ttl&lt;/code&gt; value in milliseconds, and &lt;code&gt;val&lt;/code&gt; is pre-checked by a function &lt;code&gt;isCacheable: (value: unknown) =&amp;gt; boolean;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Retrieving Cache Entries&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expire_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Returns one record that has not expired.&lt;/p&gt;
&lt;h2&gt;
  
  
  Auto Purge and Batch Operations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;purgeExpired&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;purgeExpired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastPurgeTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`DELETE FROM &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; WHERE expire_at &amp;lt; ?`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;lastPurgeTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Keeps the cache clean and efficient by regularly removing stale entries, ensuring that the cache does not grow indefinitely and affect performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Batch Set (mset)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;][],&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ttlValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultTtl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ttlValue&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expireAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ttlValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`INSERT OR REPLACE INTO &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (key, val, created_at, expire_at) VALUES &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(?, ?, ?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bindings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isCacheable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NoCacheableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" is not a cacheable value`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serializerAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;expireAt&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Improves efficiency by reducing the number of individual database operations. In the same way &lt;code&gt;mget&lt;/code&gt; executes one single query to returns valid records with the query:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expire_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bun's SQLite implementation combined with efficient binary encoding formats like Msgpackr provides a powerful solution for building fast and compact cache stores.&lt;/p&gt;

&lt;p&gt;For the complete source code and implementation details, visit:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sjdonado" rel="noopener noreferrer"&gt;
        sjdonado
      &lt;/a&gt; / &lt;a href="https://github.com/sjdonado/cache-manager-bun-sqlite3" rel="noopener noreferrer"&gt;
        cache-manager-bun-sqlite3
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Fast and compact sqlite3 cache store for Bun
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bun SQLite Store for &lt;a href="https://github.com/BryanDonovan/node-cache-manager" rel="noopener noreferrer"&gt;node-cache-manager&lt;/a&gt;
&lt;/h1&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Runs on top of &lt;a href="https://bun.sh/docs/api/sqlite" rel="nofollow noopener noreferrer"&gt;bun-sqlite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Optimized &lt;code&gt;mset&lt;/code&gt;/&lt;code&gt;mget&lt;/code&gt; support&lt;/li&gt;
&lt;li&gt;Multiple encoders support: &lt;code&gt;msgpackr&lt;/code&gt;, &lt;code&gt;cbor&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto purge (clean expired records every hour)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Installation&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;bun add cache-manager-bun-sqlite3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Single store&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;cacheManager&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'cache-manager'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;bunSqliteStore&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'cache-manager-bun-sqlite3'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-c"&gt;// SQLite :memory: cache store&lt;/span&gt;
&lt;span class="pl-s1"&gt;cache&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;cacheManager&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;caching&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;bunSqliteStore&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;serializer&lt;/span&gt;: &lt;span class="pl-s"&gt;'json'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c"&gt;// default is 'msgpackr'&lt;/span&gt;
  &lt;span class="pl-c1"&gt;ttl&lt;/span&gt;: &lt;span class="pl-c1"&gt;20&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c"&gt;// TTL in seconds&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// On-disk cache on employees table&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;cache&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;cacheManager&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;caching&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;bunSqliteStore&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;'employees'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;path&lt;/span&gt;: &lt;span class="pl-s"&gt;'/tmp/cache.db'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// TTL in seconds&lt;/span&gt;
&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;cache&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;set&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'foo'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;test&lt;/span&gt;: &lt;span class="pl-s"&gt;'bar'&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;30&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;cache&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'foo'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// TTL in seconds&lt;/span&gt;
&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;cache&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;set&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'foo'&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sjdonado/cache-manager-bun-sqlite3" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;You can also find the npm package &lt;a href="https://www.npmjs.com/package/cache-manager-bun-sqlite3" rel="noopener noreferrer"&gt;cache-manager-bun-sqlite3&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Happy hacking!&lt;/p&gt;

</description>
      <category>bunjs</category>
      <category>sqlite3</category>
      <category>msgpackr</category>
      <category>cbor</category>
    </item>
    <item>
      <title>Validated forms with useFetcher in Remix</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Wed, 10 Jul 2024 21:05:15 +0000</pubDate>
      <link>https://dev.to/sjdonado/validated-forms-with-usefetcher-in-remix-1ghb</link>
      <guid>https://dev.to/sjdonado/validated-forms-with-usefetcher-in-remix-1ghb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Build a custom "useFetcherForm" hook to easily handle fetcher requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Interacting with the server without using &lt;code&gt;window.navigation&lt;/code&gt; significantly improves the user experience. E.g: Login forms within a dialog box or modal, optimistic UI forms or submitting multiple forms within a complex view.&lt;/p&gt;

&lt;p&gt;If you are not familiar with Remix or the useFetcher hook, please refer to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://remix.run/docs/en/main/hooks/use-fetcher" rel="noopener noreferrer"&gt;https://remix.run/docs/en/main/hooks/use-fetcher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://remix.run/docs/en/main/discussion/form-vs-fetcher" rel="noopener noreferrer"&gt;https://remix.run/docs/en/main/discussion/form-vs-fetcher&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The FetcherForm Provider
&lt;/h2&gt;

&lt;p&gt;The Remix &lt;code&gt;fetcher&lt;/code&gt; object contains three primary attributes: &lt;code&gt;fetcher.state&lt;/code&gt;, &lt;code&gt;fetcher.data&lt;/code&gt; and the method &lt;code&gt;fetcher.submit&lt;/code&gt;. To interact with them we will use &lt;code&gt;React.useEffect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This post will show how to create a provider that manages the state of a fetcher submitted form, along with a minimal custom hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSubmitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetcherForm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the FetcherFormProvider props one by one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) onChange&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;External inputs will notify through this method to change the internal state of the provider, it receives a &lt;code&gt;FormData&lt;/code&gt; argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FetcherFormProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitOptions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFormData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FetcherFormContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setFormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;[...]&lt;/span&gt;
        &lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FetcherFormContext.Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2) submitForm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since the &lt;code&gt;formData&lt;/code&gt; was already captured by the &lt;code&gt;onChange&lt;/code&gt; method, the request can be send by calling &lt;code&gt;fetcher.submit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FetcherFormProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitOptions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetcher&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFormData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FetcherFormContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setFormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;[...]&lt;/span&gt;
      &lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FetcherFormContext.Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3) isSubmitted&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Submitting the form is an asynchronous operation. There is a separate variable to listen to the form submitted event: &lt;code&gt;isSubmitted&lt;/code&gt;. This is helpful in the following cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To close the Dialog or Modal when the form is successfully submitted.&lt;/li&gt;
&lt;li&gt;To check from outside that the form was submitted and/or the request returned an OK status.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export default function FetcherFormProvider({
&lt;/span&gt;  action,
  method,
  children,
}: {
  action: string;
  method: SubmitOptions['method'];
  children: React.ReactNode;
}) {
  const fetcher = useFetcher();
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ const [isSubmitted, setIsSubmitted] = useState(false);
&lt;/span&gt;  [...]
  const [formData, setFormData] = useState&amp;lt;FormData&amp;gt;();
&lt;span class="err"&gt;
&lt;/span&gt;  useEffect(() =&amp;gt; {
    const response = fetcher.data as { error: string } | undefined;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+   if (isSubmitted || error) return;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    if (fetcher.state === 'loading' &amp;amp;&amp;amp; response) {
      [...]
&lt;span class="gi"&gt;+     setIsSubmitted(true);
&lt;/span&gt;    }
  }, [fetcher, action, formData, isSubmitted, error]);
&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;FetcherFormContext.Provider
      value={[
        (formData: FormData) =&amp;gt; {
          setFormData(formData);
        },
        () =&amp;gt; {
          if (formData) {
            fetcher.submit(formData, {
              method,
              action,
            });
          }
        },
&lt;span class="gi"&gt;+       isSubmitted,
&lt;/span&gt;        [...]
      ]}
    &amp;gt;
      {children}
    &amp;lt;/FetcherFormContext.Provider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4) error&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One drawback of using Remix &lt;code&gt;useFetcher&lt;/code&gt; is the lack of a straightforward error handling method. There are proposals in progress to provide a more streamlined error handling approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/remix-run/remix/discussions/4645" rel="noopener noreferrer"&gt;https://github.com/remix-run/remix/discussions/4645&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/remix-run/react-router/discussions/10013" rel="noopener noreferrer"&gt;https://github.com/remix-run/react-router/discussions/10013&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a workaround we can rely on &lt;code&gt;fetcher.state&lt;/code&gt; to check if the request is complete and get the message with &lt;code&gt;fetcher.data&lt;/code&gt; by defining a common structure between the client and server actions.&lt;/p&gt;

&lt;p&gt;The highlights&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;const response = fetcher.data as { error: string } | undefined;&lt;/code&gt; -&amp;gt; defines the JSON response structure to be received form the server &lt;code&gt;return json({ error: new Error() }, { status: 400 } );&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;if (fetcher.state === 'loading' &amp;amp;&amp;amp; response) {&lt;/code&gt; -&amp;gt; checks if the request is completed and a response is available.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export default function FetcherFormProvider({
&lt;/span&gt;  action,
  method,
  children,
}: {
  action: string;
  method: SubmitOptions['method'];
  children: React.ReactNode;
}) {
  const fetcher = useFetcher();
&lt;span class="err"&gt;
&lt;/span&gt;  const [isSubmitted, setIsSubmitted] = useState(false);
&lt;span class="gi"&gt;+ const [error, setError] = useState&amp;lt;string&amp;gt;();
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  const [formData, setFormData] = useState&amp;lt;FormData&amp;gt;();
&lt;span class="err"&gt;
&lt;/span&gt;  useEffect(() =&amp;gt; {
    const response = fetcher.data as { error: string } | undefined;
&lt;span class="err"&gt;
&lt;/span&gt;    if (isSubmitted || error) return;
&lt;span class="err"&gt;
&lt;/span&gt;    if (fetcher.state === 'loading' &amp;amp;&amp;amp; response) {
&lt;span class="gi"&gt;+     if (response.error) {
+       setError(response.error);
+       return;
+     }
&lt;/span&gt;      setIsSubmitted(true);
    }
  }, [fetcher, action, formData, isSubmitted, error]);
&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;FetcherFormContext.Provider
      value={[
        (formData: FormData) =&amp;gt; {
          setFormData(formData);
&lt;span class="gi"&gt;+         setError(undefined);
&lt;/span&gt;        },
        () =&amp;gt; {
          if (formData) {
            fetcher.submit(formData, {
              method,
              action,
            });
          }
        },
        isSubmitted,
&lt;span class="gi"&gt;+       error,
&lt;/span&gt;      ]}
    &amp;gt;
      {children}
    &amp;lt;/FetcherFormContext.Provider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optionally, the error state management can be sent as a callback. This can be received in the &lt;code&gt;onSubmit&lt;/code&gt; function and registered as state. For example, using a &lt;code&gt;registeredCallback&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- if (response.error) {
-  setError(response.error);
-  return;
- }
&lt;/span&gt;&lt;span class="gi"&gt;+ registeredCallback?.(response.error);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The useFetcherForm hook
&lt;/h2&gt;

&lt;p&gt;The values sent to &lt;code&gt;FetcherFormContext.Provider&lt;/code&gt; are defined in the &lt;code&gt;FetcherFormContext&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FetcherFormContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?]&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the hook exposes them in an object structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useFetcherForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSubmitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FetcherFormContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isSubmitted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example of how it can be defined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AssignmentUpdateStatusDialogButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;assignmentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;assignmentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AssignmentStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isAttached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsAttached&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDialogElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAttached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;showModal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isAttached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ClientOnly&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isAttached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="nf"&gt;createPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FetcherFormProvider&lt;/span&gt;
                &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/assignments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assignmentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/status`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
              &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AssignmentUpdateStatusDialog&lt;/span&gt;
                  &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="p"&gt;[...]&lt;/span&gt;
                  &lt;span class="nx"&gt;setIsAttached&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setIsAttached&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;              &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/FetcherFormProvider&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;              &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
            &lt;span class="p"&gt;)}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
            &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cursor-pointer leading-none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsAttached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AssignmentStatusBadge&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ClientOnly&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Real-world example available on Github: &lt;a href="https://github.com/sjdonado/remix-dashboard/blob/da9445646392626cea065442f7758230b3d8d1fa/app/components/dialog/AssignmentUpdateStatusDialog.tsx#L32C56-L32C70" rel="noopener noreferrer"&gt;https://github.com/sjdonado/remix-dashboard/blob/da9445646392626cea065442f7758230b3d8d1fa/app/components/dialog/AssignmentUpdateStatusDialog.tsx#L32C56-L32C70&lt;/a&gt;&lt;/p&gt;

</description>
      <category>remix</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Fast, Intuitive, Smart: Restaurant search engine with Cloudflare AI</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Sat, 13 Apr 2024 16:50:29 +0000</pubDate>
      <link>https://dev.to/sjdonado/fast-intuitive-smart-restaurant-search-engine-with-cloudflare-ai-3m64</link>
      <guid>https://dev.to/sjdonado/fast-intuitive-smart-restaurant-search-engine-with-cloudflare-ai-3m64</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/devteam/join-us-for-the-cloudflare-ai-challenge-3000-in-prizes-5f99"&gt;Cloudflare AI Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;Craving your favorite meal? Our AI-powered search engine makes it easy to find the top 3 restaurants serving it near you. Just enter the dish you're looking for and your location, and let our intelligent algorithm do the rest. Whether it's a local specialty or a global delicacy, our free app will guide you to the perfect spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjp6ltwap8ithxp8b97w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjp6ltwap8ithxp8b97w.gif" alt="Searching on culefilo" width="600" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Source Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sjdonado" rel="noopener noreferrer"&gt;
        sjdonado
      &lt;/a&gt; / &lt;a href="https://github.com/sjdonado/culefilo" rel="noopener noreferrer"&gt;
        culefilo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Discover the top 3 restaurants serving your favorite food near you. Just enter your craving and location in our free AI-powered app, and start your culinary adventure today!
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Cule Filo - AI-powered restaurant search engine&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Discover the top 3 restaurants serving your favorite food near you. Just enter your craving and location in our free AI-powered app, and start your culinary adventure today!. Our submittion to the &lt;a href="https://dev.to/devteam/join-us-for-the-cloudflare-ai-challenge-3000-in-prizes-5f99" rel="nofollow"&gt;Cloudflare AI Challenge.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://cule-filo.pages.dev" rel="nofollow noopener noreferrer"&gt;https://cule-filo.pages.dev&lt;/a&gt;&lt;/p&gt;

  
    

    &lt;span class="m-1"&gt;Screen.Recording.2024-04-13.at.18.09.12.mov&lt;/span&gt;
  

  

  


&lt;p&gt;&lt;strong&gt;Team:&lt;/strong&gt; &lt;a href="http://github.com/sjdonado" rel="noopener noreferrer"&gt;@sjdonado&lt;/a&gt; &lt;a href="http://github.com/gjhernandez" rel="noopener noreferrer"&gt;@gjhernandez&lt;/a&gt; &lt;a href="http://github.com/krthr" rel="noopener noreferrer"&gt;@krthr&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Search your favorite meal
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/27580836/322215057-d4d31e2e-ece1-424f-bcb1-bec90ab14553.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTUwNTctZDRkMzFlMmUtZWNlMS00MjRmLWJjYjEtYmVjOTBhYjE0NTUzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE0NzMzOTI0NjM2MmY5Y2YwNzExZGIzMzU5ZTg4YWEwODVkODk4MTNmODRhNGQxM2I0ZGJjNjU2ZjBlMDliOTQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.FUnGvt9pg3T_G14JTEbQ3_pt-bJ4Qdm5z1NICaZnAPA"&gt;&lt;img width="1840" alt="Screenshot 2024-04-13 at 17 23 33" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F27580836%2F322215057-d4d31e2e-ece1-424f-bcb1-bec90ab14553.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTUwNTctZDRkMzFlMmUtZWNlMS00MjRmLWJjYjEtYmVjOTBhYjE0NTUzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE0NzMzOTI0NjM2MmY5Y2YwNzExZGIzMzU5ZTg4YWEwODVkODk4MTNmODRhNGQxM2I0ZGJjNjU2ZjBlMDliOTQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.FUnGvt9pg3T_G14JTEbQ3_pt-bJ4Qdm5z1NICaZnAPA"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Real time search logs
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/27580836/322214398-5849dcaf-81e7-48d0-909a-d413078d1e78.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQzOTgtNTg0OWRjYWYtODFlNy00OGQwLTkwOWEtZDQxMzA3OGQxZTc4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTU2ODdmMjU5YWNkOWEwOWU3Y2FmMTU5N2E4MDRlYWE4NDY1MDJkNjY3YmYyYzM4ZTM0YzE1NTZmNzcyNTk5Y2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.zqF26e02J6F3kY0W0SpYt8Kpfqb4VJCK5KL09jZFOPU"&gt;&lt;img width="1840" alt="Screenshot 2024-04-13 at 17 11 58" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F27580836%2F322214398-5849dcaf-81e7-48d0-909a-d413078d1e78.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQzOTgtNTg0OWRjYWYtODFlNy00OGQwLTkwOWEtZDQxMzA3OGQxZTc4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTU2ODdmMjU5YWNkOWEwOWU3Y2FmMTU5N2E4MDRlYWE4NDY1MDJkNjY3YmYyYzM4ZTM0YzE1NTZmNzcyNTk5Y2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.zqF26e02J6F3kY0W0SpYt8Kpfqb4VJCK5KL09jZFOPU"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/27580836/322214443-8e9f22b2-fe96-4061-9dfb-b13631a18add.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQ0NDMtOGU5ZjIyYjItZmU5Ni00MDYxLTlkZmItYjEzNjMxYTE4YWRkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI5ZWM5MzYxY2I0NTU5Y2M2NDY5ZTc5ZGE2NGFiNGQ4MDJhYWRmZWY4ZWI3ZTY2MWU4M2ZkYTg4OGIxNTA0M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0pDJJR-_X9TWMMfW-BuF1HpaqwJwrgFyGajAaStMzGA"&gt;&lt;img width="1840" alt="Screenshot 2024-04-13 at 17 12 59" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F27580836%2F322214443-8e9f22b2-fe96-4061-9dfb-b13631a18add.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQ0NDMtOGU5ZjIyYjItZmU5Ni00MDYxLTlkZmItYjEzNjMxYTE4YWRkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI5ZWM5MzYxY2I0NTU5Y2M2NDY5ZTc5ZGE2NGFiNGQ4MDJhYWRmZWY4ZWI3ZTY2MWU4M2ZkYTg4OGIxNTA0M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.0pDJJR-_X9TWMMfW-BuF1HpaqwJwrgFyGajAaStMzGA"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;See and share your results&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/27580836/322214489-ebf2fba3-0151-4c1c-bda0-039f3b678df5.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQ0ODktZWJmMmZiYTMtMDE1MS00YzFjLWJkYTAtMDM5ZjNiNjc4ZGY1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTZiOWNjNWNiZDg5NzNkOWFmNzI2MDM4YTRjNzE3NjAyYmU0MTI3YTRjOGRkYzJiYWNlNmI1YjNjZDc0NmExMjMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.97f6GVD2qTwhv0JSUQoQR4_rFNKQWswHadaL372W8EI"&gt;&lt;img width="1840" alt="Screenshot 2024-04-13 at 17 13 41" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F27580836%2F322214489-ebf2fba3-0151-4c1c-bda0-039f3b678df5.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjA4Njk2ODQsIm5iZiI6MTc2MDg2OTM4NCwicGF0aCI6Ii8yNzU4MDgzNi8zMjIyMTQ0ODktZWJmMmZiYTMtMDE1MS00YzFjLWJkYTAtMDM5ZjNiNjc4ZGY1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTEwMTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUxMDE5VDEwMjMwNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTZiOWNjNWNiZDg5NzNkOWFmNzI2MDM4YTRjNzE3NjAyYmU0MTI3YTRjOGRkYzJiYWNlNmI1YjNjZDc0NmExMjMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.97f6GVD2qTwhv0JSUQoQR4_rFNKQWswHadaL372W8EI"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Design&lt;/h2&gt;

&lt;/div&gt;

  &lt;div class="js-render-enrichment-target"&gt;
    &lt;div class="render-plaintext-hidden"&gt;
      &lt;pre&gt;graph TD
    A[Start] --&amp;gt; B{Job state?}
    B --&amp;gt;|Created| C[Update job state to Running]
    B --&amp;gt;|Running or Finished| D[Return encoded message]
    C --&amp;gt; E[Search for places with original query]
    E --&amp;gt; F{Number of places found?}
    F --&amp;gt;|Less than 3| G[Generate suggestions list - llama-2-13b-chat-awq]
    F --&amp;gt;|3 or more| H[Enhancing results]
    G --&amp;gt; I{Number of suggestions?}
    I --&amp;gt;|Greater than 0| J[Search for places with suggestions]
    I --&amp;gt;|0| K[Log error]
    J --&amp;gt; L{Number of places found?}
    L --&amp;gt;|Less than 3| G
    L --&amp;gt;|3 or more| H
    H --&amp;gt; M[Fetch&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sjdonado/culefilo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;Our journey in building the "Cule filo" AI-powered restaurant search engine has been an exciting and challenging one. We started by brainstorming ideas on how to leverage Cloudflare's AI capabilities to create a unique and valuable application. After exploring various domains, we settled on the idea of helping users find the best restaurants serving their favorite meals nearby.&lt;/p&gt;

&lt;p&gt;To achieve this, we designed a system that combines multiple AI models and services. We used the &lt;code&gt;llama-2-13b-chat-awq&lt;/code&gt; model for generating suggestions when the initial search yielded less than 3 results, ensuring that users always receive relevant recommendations. The same model was also employed for selecting appropriate thumbnails for the search results, enhancing the visual appeal and contextual relevance of the displayed images.&lt;/p&gt;

&lt;p&gt;For generating informative and concise descriptions of restaurants based on user reviews, we utilized the &lt;code&gt;bart-large-cnn&lt;/code&gt; model. This allowed us to provide users with a quick overview of each restaurant's characteristics and customer experiences. To further enrich the search results, we used the &lt;code&gt;uform-gen2-qwen-500m&lt;/code&gt; model to generate textual descriptions of restaurant photos, giving users a better sense of the ambiance and offerings of each place.&lt;/p&gt;

&lt;p&gt;Throughout the development process, we focused on creating an intuitive and responsive user interface that prioritizes simplicity and ease of use. We implemented real-time search logs to provide transparency and keep users engaged during the search process. Additionally, we integrated search history functionality, allowing users to easily access and revisit their previous searches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Models and/or Triple Task Types
&lt;/h2&gt;

&lt;p&gt;Our project, "Cule filo", utilized multiple models per task and incorporated three distinct task types, qualifying it for the additional prize categories.&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Multiple Models per Task:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the task of generating suggestions when the initial search yields less than 3 results, we employed the llama-2-13b-chat-awq model. This model was also used for selecting appropriate thumbnails for the search results.&lt;/li&gt;
&lt;li&gt;To generate informative and concise descriptions of restaurants based on user reviews, we utilized the bart-large-cnn model.&lt;/li&gt;
&lt;li&gt;For generating textual descriptions of restaurant photos, we used the uform-gen2-qwen-500m model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2) &lt;strong&gt;Triple Task Types:&lt;/strong&gt; Our project incorporated the following three task types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text Generation: We used the llama-2-13b-chat-awq model to generate suggestions for alternative search queries when the initial search yielded insufficient results. This model was also used to generate captions for the selected thumbnails.&lt;/li&gt;
&lt;li&gt;Text Summarization: The bart-large-cnn model was employed to summarize user reviews and generate concise descriptions of each restaurant, providing users with a quick overview of the place's characteristics and customer experiences.&lt;/li&gt;
&lt;li&gt;Image-to-Text: We utilized the uform-gen2-qwen-500m model to convert restaurant photos into textual descriptions, enhancing the richness and contextual relevance of the search results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we are proud of
&lt;/h2&gt;

&lt;p&gt;We are particularly proud of the seamless integration of multiple AI models and the overall user experience we have created. Seeing the application come to life and witnessing its ability to help users discover new and exciting dining options has been incredibly rewarding.&lt;/p&gt;

&lt;p&gt;Looking ahead, we hope to expand the capabilities of "Cule filo" by incorporating additional features such as personalized recommendations based on user preferences, integration with reservation systems, and support for multiple languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Team
&lt;/h2&gt;


&lt;div class="ltag__user ltag__user__id__360831"&gt;
    &lt;a href="/sjdonado" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F360831%2F0b76b080-a2a2-40a0-b9ef-c83df1e2b728.jpeg" alt="sjdonado image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/sjdonado"&gt;sjdonado&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/sjdonado"&gt;M.Sc CS - Software Engineer&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__user ltag__user__id__1416412"&gt;
    &lt;a href="/gjhernandez" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1416412%2F98e6f886-c027-4332-813c-d5869efe9d5f.jpeg" alt="gjhernandez image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/gjhernandez"&gt;José Gregorio Hernández González&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/gjhernandez"&gt;/gjhernandez&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="ltag__user ltag__user__id__27836"&gt;
    &lt;a href="/krthr" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F27836%2F10b093ab-f3e2-4064-986d-baac6390b99b.jpg" alt="krthr image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/krthr"&gt;Wilson Tovar&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/krthr"&gt;Software Engineer @ Amazon. Applying MLOps to power state of the art ML model building.&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>cloudflarechallenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>remix</category>
    </item>
    <item>
      <title>HTMX with Bun: A Real World App</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Tue, 05 Mar 2024 05:47:49 +0000</pubDate>
      <link>https://dev.to/sjdonado/htmx-bun-a-real-world-app-2c3a</link>
      <guid>https://dev.to/sjdonado/htmx-bun-a-real-world-app-2c3a</guid>
      <description>&lt;p&gt;Let's build a real-world app with HTMX + Tailwind CSS + Bun. If you are already familiar with these tools, feel free to skip the context part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;HTMX, HTMZ, HTMY...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All we need to achieve decent smooth reactivity is already out there, built-in by the major browsers. As an example, here is one interesting front-end framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;htmz&lt;/span&gt; &lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setTimeout(()=&amp;gt;document.querySelector(contentWindow.location.hash||null)?.replaceWith(...contentDocument.body.childNodes))&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/iframe&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it, just an iframe, try it out yourself on &lt;a href="https://leanrada.com/htmz/" rel="noopener noreferrer"&gt;https://leanrada.com/htmz/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The philosophy behind this is the same as HTMX and the same one that drives this post: Don't reinvent rendering, just use HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddj62wppkbdxcb9o3ose.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fddj62wppkbdxcb9o3ose.png" alt="HTMX meme" width="800" height="847"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The blazingly fast JS server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With HTMX in mind, the next step is to get up and running a a simple web server.&lt;/p&gt;

&lt;p&gt;Bun has gained a lot of popularity in the last few months. It is fast, and there is a framework called &lt;a href="https://elysiajs.com/" rel="noopener noreferrer"&gt;ElysiaJS&lt;/a&gt; that resembles Express.js and promises to be 22 times faster than Express.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8kqqyohtrk1p14x1pnsx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8kqqyohtrk1p14x1pnsx.png" alt="ElysiaJS Benchmark" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, Bun understands TypeScript and JSX out of the box, so we can structure our app with components, server-side render them, and rely on HTMX for client-side actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt;&lt;br&gt;
Unlike traditional CSS frameworks that come with predefined components, Tailwind CSS emphasizes atomic classes that represent individual CSS properties. The bundle size could be lighter in comparison to other tools, but the strength here, IMHO, is rapid prototyping.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Real World App
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyyrd88dd2m71xlehd4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftyyrd88dd2m71xlehd4e.png" alt="I Don't Have Spotify Web App" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem: I'm an Apple Music user. Someone shares a Spotify link with me, and I don't have a way to know what's inside or listen to it.&lt;/p&gt;

&lt;p&gt;The solution: I Don't Have Spotify scrapes the links (songs/albums/artists etc) from all your favorite streaming services, and it even provides a listening preview if you just want a quick look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approach&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Given a Spotify link, we need to extract its metadata: Fetch request + HTML parsing.&lt;/li&gt;
&lt;li&gt;Given the metadata, we need to search for the resource on each supported streaming service: Multiple API/HTML requests + parsing (adapters).&lt;/li&gt;
&lt;li&gt;Given the results of (1) and (2), we need to render and return the data to the client.&lt;/li&gt;
&lt;li&gt;Additionally, we list a JSON endpoint for a cool Raycast extension.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are two major design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The folder structure / separation of concerns&lt;/li&gt;
&lt;li&gt;Tooling + build process, how to live reload the JSX components?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first one, since we have multiple streaming services, we'll inevitably encounter shared logic between them. Drawing inspiration from the adapter pattern but without the boilerplate, I organized the adapters folder with pure functions. These functions all receive the same arguments: &lt;code&gt;query: string&lt;/code&gt; and &lt;code&gt;metadata: SpotifyMetadata&lt;/code&gt;, and they return a &lt;code&gt;SpotifyContentLink&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The adapters are called by the &lt;code&gt;spotifySearch&lt;/code&gt; function in the search service, which is also responsible for caching and updating the statistics. Why caching? In short: these calls are quite time-consuming, and we can easily hit a rate limit if we send many of them at the same time.&lt;/p&gt;

&lt;p&gt;For the second aspect, running Bun is very straightforward: &lt;code&gt;bun run --watch www/bin.ts&lt;/code&gt;. However, as a real-world app, we need some JavaScript on the client side, apart from sending AJAX requests. There is an audio player that has to be rendered on the client side (in order to append listeners to the DOM). Additionally, we would like to access the Clipboard API to improve the user experience when searching.&lt;/p&gt;

&lt;p&gt;To bundle and minify the required JavaScript, I used Vite with the &lt;code&gt;rollup-plugin-copy&lt;/code&gt; plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rollup-plugin-copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;copyOnce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;writeBundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;~/config/constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/config/constants.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esnext&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/js/audio-preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/views/js/audio-preview.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/js/search-bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/views/js/search-bar.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/css/index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/views/css/index.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;entryFileNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name].min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;chunkFileNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[name].min.js`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;assetFileNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`[name].min.[ext]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Showcase&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The search bar component (where HTMX shines)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"search-form"&lt;/span&gt;
        &lt;span class="na"&gt;hx-post&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/search"&lt;/span&gt;
        &lt;span class="na"&gt;hx-target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#search-results"&lt;/span&gt;
        &lt;span class="na"&gt;hx-swap&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"innerHTML"&lt;/span&gt;
        &lt;span class="na"&gt;hx-indicator&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#loading-indicator"&lt;/span&gt;
        &lt;span class="na"&gt;hx-request&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'\"timeout\":24000'&lt;/span&gt;
        &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex w-full max-w-3xl items-center justify-center"&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"song-link"&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Search
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"song-link"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"spotifyLink"&lt;/span&gt;
          &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 rounded-lg border bg-white p-2.5 text-sm font-normal text-black placeholder:text-gray-400 lg:text-base"&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://open.spotify.com/track/7A8MwSsu9efJXP6xvZfRN3?si=d4f1e2eb324c43df"&lt;/span&gt;
          &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;SPOTIFY_LINK_REGEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;
          &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ml-2 rounded-lg border border-green-500 bg-green-500 p-2.5 text-sm font-medium text-white focus:outline-none focus:ring-1 focus:ring-white"&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;i&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"fas fa-search p-1 text-black"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sr-only"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"search-results"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The web server
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Elysia&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elysia&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@elysiajs/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;staticPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@elysiajs/static&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils/logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;apiRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./routes/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pageRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./routes/page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Elysia&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;staticPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public, max-age=86400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeHandle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiRouter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our case, testing involves mocks and more mocks. They run on push changes to master, thanks to GitHub Actions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx50fehumi35q0kcomxdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx50fehumi35q0kcomxdy.png" alt="Testing output from Github Actions" width="800" height="1061"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The setup relies on &lt;code&gt;bun:test&lt;/code&gt; and &lt;code&gt;AxiosMockAdapter&lt;/code&gt;. It is separated into integration and unit tests, and the requests are injected using two helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JSONRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formDataRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incorporating e2e shouldn't be complex. An example of running the dev server with Playwright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;webServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bun run dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3333&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;reuseExistingServer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The source code is available on GitHub: &lt;a href="https://github.com/sjdonado/idonthavespotify" rel="noopener noreferrer"&gt;https://github.com/sjdonado/idonthavespotify&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The app is up and running thanks to Dokku: &lt;a href="https://idonthavespotify.donado.co" rel="noopener noreferrer"&gt;https://idonthavespotify.donado.co&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The Raycast extension will be addressed in a future post, stay tuned: &lt;a href="https://www.raycast.com/sjdonado/idonthavespotify" rel="noopener noreferrer"&gt;https://www.raycast.com/sjdonado/idonthavespotify&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>htmx</category>
      <category>bunjs</category>
      <category>tailwindcss</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tower of Hanoi in P5.js + WASM</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Mon, 08 May 2023 06:08:39 +0000</pubDate>
      <link>https://dev.to/sjdonado/tower-of-hanoi-in-p5js-wasm-4kn0</link>
      <guid>https://dev.to/sjdonado/tower-of-hanoi-in-p5js-wasm-4kn0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Implementation of the Tower of Hanoi problem using P5.js for animation and Rust compiled to WebAssembly (WASM).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 2018, a professor at the Uni asked me to build a series of computer science games; he was looking for a very visual way to show students how the solution algorithms of &lt;a href="https://en.wikipedia.org/wiki/NP_(complexity)" rel="noopener noreferrer"&gt;NP problems&lt;/a&gt; (like the Knapsack Problem, Tower of Hanoi, Tic-Tac-Toe, etc.) behave and grow with their inputs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60ltbfz5s9nm9ejj07bf.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60ltbfz5s9nm9ejj07bf.gif" alt="Tower of Hanoi Animation" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Tower_of_Hanoi" rel="noopener noreferrer"&gt;Tower of Hanoi&lt;/a&gt; is a good exercise when students are getting started with recursion and vectors. The pegs are usually fixed at 3, so it is easy to define the base case, and the only variable is the number of disks. It is worth noting that there are other more efficient approaches, such as The Binary solution or the Gray-code solution, but this was a Complexity course and we wanted to show how exponential time can break your computer while keeping it as minimal as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vanilla JS Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jkliqf9hskgr3dcvuzd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jkliqf9hskgr3dcvuzd.png" alt="Vanilla JS Implementation" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was only one design requirement: to show the animated solutions of the problems in a web browser. At that time I was familiar with HTML5 but not with any modern UI frameworks. Frankly, I couldn't imagine myself rendering objects from scratch in a plain HTML canvas. There had to be something out there that had already invented that wheel, and there was.&lt;/p&gt;

&lt;p&gt;I found &lt;a href="https://p5js.org/" rel="noopener noreferrer"&gt;P5.js&lt;/a&gt;, it is sort of the JS version of &lt;a href="https://processing.org/" rel="noopener noreferrer"&gt;Processing&lt;/a&gt; and it has everything I was looking for, a complete API for rendering, painting images and text, calling DOM elements and much more.&lt;/p&gt;

&lt;p&gt;The main core of the animation system was the &lt;code&gt;draw&lt;/code&gt; function provided by P5. It works as an infinite loop, so if we want to show the disks moving from &lt;code&gt;x1, y1&lt;/code&gt; to &lt;code&gt;x2, y2&lt;/code&gt;, we have to update the current position &lt;code&gt;x += speed y += speed&lt;/code&gt; where &lt;code&gt;0 &amp;lt; speed &amp;lt; 120&lt;/code&gt;. There are two types of motion, vertical and horizontal. An example of the former is:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Note that the &lt;code&gt;refreshCanvas&lt;/code&gt; function refreshes the canvas and redraws the towers filled by the disks, but not the one being animated.&lt;/p&gt;

&lt;p&gt;You may wonder how the disks know where to move, do they move at the same time as the Hanoi calculations? That would be cool, but unfortunately they don't. We are limited by the recursive algorithm, so we have to wait until all calculations are done (until the call stack is empty).&lt;/p&gt;

&lt;p&gt;Therefore, the &lt;code&gt;draw&lt;/code&gt; function constantly checks if the array of moves &lt;code&gt;[towerFrom:towerTo, ...]&lt;/code&gt; generated by &lt;code&gt;recursiveHanoi&lt;/code&gt; is not empty. If it is, the animation starts.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;recursiveHanoi&lt;/code&gt; did the job, but it was very inefficient: it couldn't handle more than 14 disks (16K steps). The root cause was not in the algorithm itself, but in its execution in the V8 interpreter. Anyway, the same recursive function in C++ could run perfectly for more than 20 disks (aprox 16M of steps). So, running compiled bytecode in the browser was clearly the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  wasm-pack
&lt;/h2&gt;

&lt;p&gt;After four years, I found some time to pay that deb-tech (yes, quite a long time, eh). To make it fun I rewrote everything from scratch in SolidJS, which went smooth thanks to this amazing library &lt;a href="https://www.npmjs.com/package/p5js-wrapper" rel="noopener noreferrer"&gt;p5js-wrapper&lt;/a&gt;. For WASM, C++ is still a good choice, but what about Rust? I did some research and found &lt;a href="https://github.com/rustwasm/wasm-pack" rel="noopener noreferrer"&gt;wasm-pack&lt;/a&gt;. A few lines in the &lt;code&gt;cargo.toml&lt;/code&gt; file and we were ready to generate compiled + ready to import bytecode!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5sg6n8rehm3s1pegu66.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5sg6n8rehm3s1pegu66.gif" alt="wasm-pack output" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's dive into the configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;crate-type&lt;/code&gt; is set to indicate that the crate will be compiled as a dynamic library: &lt;code&gt;wasm-bindgen&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gloo-utils&lt;/code&gt; and &lt;code&gt;serde&lt;/code&gt; are included separately to avoid circular dependencies (&lt;a href="https://rustwasm.github.io/wasm-bindgen/reference/arbitrary-data-with-serde.html?highlight=serde#history" rel="noopener noreferrer"&gt;history&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Once the configuration is ready, we run &lt;code&gt;wasm-pack build -d wasm --no-typescript&lt;/code&gt; to compile. There is no advantage in generating ts files because we won't use those definitions since the &lt;code&gt;get_moves&lt;/code&gt; function is gonna be called using a Web Worker.&lt;/p&gt;

&lt;p&gt;Then, to make the &lt;code&gt;wasm&lt;/code&gt; folder accessible to the solid app, we need to update the &lt;code&gt;vite.config.js&lt;/code&gt; file following the recommended setup from &lt;a href="https://github.com/Menci/vite-plugin-wasm" rel="noopener noreferrer"&gt;vite-plugin-wasm&lt;/a&gt;:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The compiled output is now accessible as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;But wait a second, we haven't talked about this function yet, and why do we need a Web Worker? Let's move on to the final implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hanoi Algorithm in Rust
&lt;/h2&gt;

&lt;p&gt;The tricky part of writing WASM code is how to interact with the outside world (I/O operations). Fortunately, our function is quite small, its conditions are mapped as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The function takes a &lt;code&gt;Number -&amp;gt; i32&lt;/code&gt; argument: The number of disks.&lt;/li&gt;
&lt;li&gt;The function returns an array of strings serialised by the &lt;code&gt;serde_json&lt;/code&gt; crate (which allows you to convert a Rust data structure that implements the &lt;code&gt;serde::Serialize&lt;/code&gt; trait to a &lt;code&gt;serde_json::Value&lt;/code&gt; type, in this case a &lt;code&gt;JsValue&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that's it, inside the public fn, we can use regular Rust types like &lt;code&gt;Vect&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Web Worker
&lt;/h2&gt;

&lt;p&gt;As mentioned above, the number of disks is exponentially related to the number of steps required in the solution. 20 disks are about 1M, but 24 are more than 16M (which is quite a lot 😰). In fact, the execution time of the recursive function exceeds the Doherty threshold (300ms). So, it becomes mandatory to execute it inside a non-blocking thread, for which we can use the Web Workers API.&lt;/p&gt;

&lt;p&gt;A new &lt;code&gt;.js&lt;/code&gt; file must be created for the worker anywhere in the &lt;code&gt;src&lt;/code&gt; folder.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Then, since the worker instance has nothing to do with the &lt;code&gt;solid&lt;/code&gt; component lifecycle, we can instance it outside.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Finally, when the user clicks on the play button, the async function is called:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  In Summary
&lt;/h2&gt;

&lt;p&gt;NP problems are highly CPU-intensive, and &lt;code&gt;wasm-pack&lt;/code&gt; makes it easier to reduce the execution time. In addition, wrapping these operations in a Web Worker improves the user experience.&lt;/p&gt;

&lt;p&gt;A picture is worth a thousand words: for 20 disks the Vanilla JS implementation takes more than &lt;code&gt;312000ms&lt;/code&gt;, while Rust does it in under &lt;code&gt;200ms&lt;/code&gt; (tested on a Macbook Air M2).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vanilla JS&lt;/th&gt;
&lt;th&gt;WASM (Rust)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc70vx9b6ulzndq9w2aro.png" alt="20 disks execution time - JS implementation" width="800" height="474"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6gic9o198fj17aakii8q.png" alt="20 disks execution time - WASM" width="800" height="474"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source code available on Github:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sjdonado" rel="noopener noreferrer"&gt;
        sjdonado
      &lt;/a&gt; / &lt;a href="https://github.com/sjdonado/cs-games" rel="noopener noreferrer"&gt;
        cs-games
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Computer Science Games powered by P5.js, Rust + wasm-pack and SolidJS
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Article&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/sjdonado/tower-of-hanoi-in-p5js-wasm-4kn0" rel="nofollow"&gt;https://dev.to/sjdonado/tower-of-hanoi-in-p5js-wasm-4kn0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tic Tac Toe&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/27580836/225850771-377dd850-cdc4-4a65-860a-b9ef025c0fb9.png"&gt;&lt;img width="1097" alt="image" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F27580836%2F225850771-377dd850-cdc4-4a65-860a-b9ef025c0fb9.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tower of Hanoi&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/27580836/225851282-aa59f717-503c-489e-82e5-df303cdccbf6.png"&gt;&lt;img width="1429" alt="image" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F27580836%2F225851282-aa59f717-503c-489e-82e5-df303cdccbf6.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sjdonado/cs-games" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;That's all I have for you today. Check out my website &lt;a href="https://sjdonado.de" rel="noopener noreferrer"&gt;https://sjdonado.de&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feedback is more than welcome, drop me a line in the comments section 🙂.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs02425liwooa994kha96.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs02425liwooa994kha96.gif" alt="gif" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>p5</category>
      <category>webassembly</category>
      <category>solidjs</category>
      <category>rust</category>
    </item>
    <item>
      <title>Self-Hosted Password Manager with Dokku</title>
      <dc:creator>sjdonado</dc:creator>
      <pubDate>Wed, 19 Apr 2023 05:46:07 +0000</pubDate>
      <link>https://dev.to/sjdonado/self-hosted-password-manager-with-dokku-2fjg</link>
      <guid>https://dev.to/sjdonado/self-hosted-password-manager-with-dokku-2fjg</guid>
      <description>&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;I relied on 1Password for the last few years, it is robust, and very user-friendly. However, these types of services are prime targets for attacks, one popular example is the &lt;a href="https://www.forbes.com/sites/daveywinder/2023/03/03/why-you-should-stop-using-lastpass-after-new-hack-method-update/?sh=70d57e3d28fc" rel="noopener noreferrer"&gt;Lastpass&lt;/a&gt; case. It is definitely not cool to have your sensitive info spread in another data breach. Also, privacy is precious nowadays, whoever has the data, has the power.&lt;/p&gt;

&lt;p&gt;While exploring Open Source alternatives, I found &lt;a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer"&gt;vaultwarden&lt;/a&gt;, an unofficial Bitwarden-compatible server written in Rust.&lt;/p&gt;

&lt;p&gt;Migrating data manually is a tedious process, so I first checked how it would be like with Bitwarden. Surprise, surprise, they have it all! You don't have to do it by hand, it already supports the most popular password managers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Web - Import Data Page&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7tn5l91lm52b0qkg8fh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7tn5l91lm52b0qkg8fh.png" alt="Bitwarden Import Data Page" width="800" height="779"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PS: It was a relief to discover a well-done user interface, here's a spoiler:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;macOS app&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgjgb2boxexkqrwxhfwr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgjgb2boxexkqrwxhfwr.png" alt="macos App" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make it cost effective, I decided to use my current Dokku setup, hosted on Google Cloud and powered by Ubuntu 20.04. If you haven't heard of Dokku, this is your chance to try it out &lt;a href="https://dokku.com/docs/getting-started/installation/" rel="noopener noreferrer"&gt;https://dokku.com/docs/getting-started/installation/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, having in mind the advantages and the infra required, here's how I did it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we get started, check that you already have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A server with Dokku installed.&lt;/li&gt;
&lt;li&gt;A domain name (letsencrypt will check it).&lt;/li&gt;
&lt;li&gt;Local machine with Docker, Git and any SSH client.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step by Step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create Dokku App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set your app name and link to the domain (the Dokku Proxy plugin is built in since version 0.5.0)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dokku apps:create bitwarden

&lt;span class="c"&gt;# example: dokku domains:set bitwarden mypasswords.sjdonado.de&lt;/span&gt;
dokku domains:set bitwarden yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Set up TLS certificates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Let's Encrypt plugin automates the generation and renewal of our certificates&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="c"&gt;# plugin installation requires root, hence the user change&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

dokku letsencrypt:enable bitwarden
&lt;span class="c"&gt;# enable the automatic renewal of certificates &lt;/span&gt;
dokku letsencrypt:cron-job &lt;span class="nt"&gt;--add&lt;/span&gt;

&lt;span class="c"&gt;# replace proxy port, default: 5000&lt;/span&gt;
dokku config:set bitwarden &lt;span class="nv"&gt;DOKKU_PROXY_PORT_MAP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http:80:80 https:443:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Set up database&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'll use the PostgreSQL plugin&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# setup plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dokku plugin:install https://github.com/dokku/dokku-postgres.git

&lt;span class="c"&gt;# create a new database for our app&lt;/span&gt;
dokku postgres:create bitwarden

&lt;span class="c"&gt;# link the postgres service to the app, DATABASE_URL will be attached to the ENV variables automatically. &lt;/span&gt;
dokku postgres:link bitwarden bitwarden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Set up persistent storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can use the built-in Dokku storage plugin&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dokku storage:ensure-directory bitwarden

dokku storage:mount bitwarden /var/lib/dokku/data/storage/bitwarden:/data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Set up ENV variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, Bitwarden is open to public signups, although we can restrict it to invitation-only by setting &lt;code&gt;SIGNUPS_ALLOWED&lt;/code&gt; to false and providing a valid &lt;code&gt;ADMIN_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We're gonna use &lt;code&gt;argon2&lt;/code&gt; to generate a PHC string for the &lt;code&gt;ADMIN_TOKEN&lt;/code&gt; ENV variable&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# skip this line if you are a linux user&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ubuntu

&lt;span class="nv"&gt;$ &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;openssl argon2
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"MySecretPassword"&lt;/span&gt; | argon2 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nt"&gt;-id&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; 65540 &lt;span class="nt"&gt;-t&lt;/span&gt; 3 &lt;span class="nt"&gt;-p&lt;/span&gt; 4
&lt;span class="c"&gt;# output: $argon2id$v=19$m=65540,t=3,p=4$bXBGMENBZUVzT3VUSFErTzQzK25Jck1BN2Z0amFuWjdSdVlIQVZqYzAzYz0$T9m73OdD2mz9+aJKLuOAdbvoARdaKxtOZ+jZcSL9/N0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the &lt;code&gt;SMTP_&lt;/code&gt; variables, feel free to use your own SMTP server or comercial solutions with free-tier such as &lt;a href="https://sendgrid.com/" rel="noopener noreferrer"&gt;sendgrid&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dokku config:set bitwarden &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://mypasswords.sjdonado.de &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SIGNUPS_ALLOWED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;ADMIN_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$argon2id...'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smtp.sjdonado.de &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vaultwarden@sjdonado.de &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;587 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_SECURITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;starttls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SMTP_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Create the Dockerfile&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's create a new folder for the project, the Dockerfile and initialise git&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;yourfolder &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;yourfolder
git init
&lt;span class="nb"&gt;touch &lt;/span&gt;Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;yourfolder/Dockerfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; vaultwarden/server:latest&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DB=postgresql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Deploy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We are almost there! Before to push the changes, we have to set the dokku remote url&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add dokku-bitwarden dokku@yourdomain.com:bitwarden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add -A &amp;amp;&amp;amp; git commit -m "My bitwarden Dockerfile"
git push dokku master

# output:
git push using:  dokku master
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), 551 bytes | 551.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
-----&amp;gt; Cleaning up...
-----&amp;gt; Building bitwarden from Dockerfile
remote: #1 [internal] load build definition from Dockerfile
remote: #1 transferring dockerfile: 65B done
remote: #1 DONE 0.0s
remote: 
remote: #2 [internal] load .dockerignore
remote: #2 transferring context: 2B done
remote: #2 DONE 0.1s
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. That's it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now you can access the admin panel via &lt;a href="https://yourdomain.com/admin" rel="noopener noreferrer"&gt;https://yourdomain.com/admin&lt;/a&gt; with your &lt;code&gt;MySecretPassword&lt;/code&gt; (from step 5) and send invitations by email 😎.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh421y9wu4ap1kgd8m8ta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh421y9wu4ap1kgd8m8ta.png" alt="Admin Portal" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Be sure to add your domain when installing the official Bitwarden apps/extensions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chrome Extension&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3txh5xhfjsk82f8ocs8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3txh5xhfjsk82f8ocs8f.png" alt="Chrome Extension example" width="800" height="1293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;We've explored the process of setting up a self-hosted password manager using Dokku. This can be easily replicated on any IaaS provider.&lt;/p&gt;

&lt;p&gt;Now, you can have peace of mind knowing that your passwords are securely stored and easily accessible whenever and wherever you need them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5yjxpeamufpzqidm13d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq5yjxpeamufpzqidm13d.gif" alt="gif" width="250" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code available on &lt;a href="https://github.com/sjdonado/dokku-self-hosted-services" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Go check my website &lt;a href="https://sjdonado.de" rel="noopener noreferrer"&gt;https://sjdonado.de&lt;/a&gt;, drop me a line. Suggestions are more than welcome in the comments box.&lt;/p&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>tutorial</category>
      <category>dokku</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
