<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>Andi Duro</title>
 <link href="https://duro.me//atom.xml" rel="self"/>
 <link href="https://duro.me//"/>
 <updated>2023-07-27T05:32:42+00:00</updated>
 <id>https://duro.me</id>
 <author>
   <name></name>
   <email></email>
 </author>

 
 <entry>
   <title>How to Use Unreplied 2.0 CLI</title>
   <link href="https://duro.me//stories/2020/05/26/unreplied-cli"/>
   <updated>2020-05-26T07:00:00+00:00</updated>
   <id>https://duro.me/stories/2020/05/26/unreplied-cli</id>
   <content type="html">&lt;p&gt;&lt;a href=&quot;https://unreplied.app&quot;&gt;Unreplied&lt;/a&gt; just released with command line support! Here&amp;#39;s how to use it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://unreplied.app/assets/2.0/preferences.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;First, identify where Unreplied is installed. If you got it from the Mac App Store, it should be in /Applications/Unreplied.app.&lt;/p&gt;

&lt;p&gt;Next, verify that your version of Unreplied supports the CLI. To do that, run:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;andi@Andis-MacBook-Pro % /Applications/Unreplied.app/Contents/MacOS/Unreplied --cli --help

USAGE: unreplied [--cli][--csv] [--no-contacts][--ignore-non-contacts] [--ignore-short-codes][--cutoff &amp;lt;ts&amp;gt;]

OPTIONS:
--cli                   Launch Unreplied without a GUI.
--csv                   Print unreplied messages in CSV format.
--no-contacts           Don&amp;#39;t attempt to resolve message handles into contact names.
--ignore-non-contacts   Don&amp;#39;t include messages from people who are not in your Contacts.
--ignore-short-codes    Don&amp;#39;t include messages from short codes.
--cutoff &amp;lt;ts&amp;gt;           Ignore all messages received before this UNIX
                        timestamp, in milliseconds. (default: 0)
-h, --help              Show help information.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that you have to call the Unreplied executable itself from inside the .app - not the .app itself (which is actually just a folder).&lt;/p&gt;

&lt;p&gt;Now that you&amp;#39;ve gotten the help command working, you&amp;#39;re ready to get started! Simply call &lt;code&gt;/Applications/Unreplied.app/Contents/MacOS/Unreplied --cli&lt;/code&gt; without the &lt;code&gt;--help&lt;/code&gt; flag to get a list of &lt;em&gt;every&lt;/em&gt; iMessage and SMS you haven&amp;#39;t responded to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If you don&amp;#39;t want a list of every message, just recent ones, use &lt;code&gt;--cutoff&lt;/code&gt; to specify a UNIX timestamp that will make Unreplied ignore all messages sent before it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By default, Unreplied will use the Contacts API to attempt to resolve phone numbers and email addresses into contact names. If you don&amp;#39;t want this, use &lt;code&gt;--no--contacts&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you want to pipe Unreplied&amp;#39;s output into another application, like &lt;code&gt;awk&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt;, use &lt;code&gt;--csv&lt;/code&gt; to have Unreplied output in &lt;a href=&quot;https://en.wikipedia.org/wiki/Comma-separated_values&quot;&gt;CSV&lt;/a&gt;, which should be easier to parse.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;Behind The Scenes&lt;/h1&gt;

&lt;p&gt;I learned a bit trying to shove CLI functionality into a Swift app. There isn&amp;#39;t much documentation on this other than a few StackOverflow posts, so I&amp;#39;ll just document my findings here.&lt;/p&gt;

&lt;p&gt;First, when you create a Swift app through Xcode, you start off with an AppDelegate.swift file. At the top of the class declaration, you might see &lt;code&gt;@NSApplicationMain&lt;/code&gt;. &lt;a href=&quot;https://docs.swift.org/swift-book/ReferenceManual/Attributes.html&quot;&gt;Apple says&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Apply this attribute to a class to indicate that it is the application delegate. Using this attribute is equivalent to calling the NSApplicationMain function and passing this class’s name as the name of the delegate class.&lt;/p&gt;

&lt;p&gt;If you do not use this attribute, supply a main.swift file with a main function that calls the NSApplicationMain function. For example, if your app uses a custom subclass of NSApplication as its principal class, call the NSApplicationMain function instead of using this attribute.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;..So that&amp;#39;s exactly what I did. I took out &lt;code&gt;@NSApplicationMain&lt;/code&gt;, created a &lt;code&gt;main.swift&lt;/code&gt; file with the following line:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-swift&quot; data-lang=&quot;swift&quot;&gt;&lt;span&gt;&lt;/span&gt;    &lt;span class=&quot;kc&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NSApplicationMain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unsafeArgv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It runs, so now we have a way to intercept command line arguments as well as retain GUI functionality. But in CLI mode, we don&amp;#39;t want to spawn the GUI; we just want to &lt;code&gt;print&lt;/code&gt; to stdout. So we need a way to figure out when the app was called from the command line. My easy fix for this was to just require the &lt;code&gt;--cli&lt;/code&gt; flag:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-swift&quot; data-lang=&quot;swift&quot;&gt;&lt;span&gt;&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;--cli&amp;quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;Unreplied&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kc&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NSApplicationMain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CommandLine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unsafeArgv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What&amp;#39;s &lt;code&gt;Unreplied.main()&lt;/code&gt;? It&amp;#39;s a &lt;code&gt;ParseableCommand&lt;/code&gt; from the &lt;a href=&quot;https://github.com/apple/swift-argument-parser&quot;&gt;Swift Argument Parser&lt;/a&gt;, which, as its &lt;a href=&quot;https://swift.org/blog/argument-parser/&quot;&gt;introduction post&lt;/a&gt; says, &amp;quot;makes it straightforward — even enjoyable! — to parse command-line arguments in Swift.&amp;quot;&lt;/p&gt;

&lt;p&gt;I agree! It was very easy to add CLI functionality to Unreplied with the Swift Argument Parser. Here&amp;#39;s all the code I needed to add all of Unreplied&amp;#39;s flags:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-swift&quot; data-lang=&quot;swift&quot;&gt;&lt;span&gt;&lt;/span&gt;    &lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Unreplied&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ParsableCommand&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;Launch Unreplied without a GUI.&amp;quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cli&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Bool&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;Print unreplied messages in CSV format.&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Bool&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;Don&amp;#39;t attempt to resolve message handles into contact names.&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;noContacts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Bool&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;Don&amp;#39;t include messages from people who are not in your Contacts.&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ignoreNonContacts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Bool&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;Don&amp;#39;t include messages from short codes.&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ignoreShortCodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Bool&lt;/span&gt;

        &lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ArgumentHelp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;quot;Ignore all messages received before this UNIX timestamp, in milliseconds.&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;valueName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;quot;ts&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;cutoff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Int&lt;/span&gt;

        &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// do stuff with the arguments...&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That&amp;#39;s it! If you come up with some clever use for Unreplied on the command line, please let me know on Twitter &lt;a href=&quot;https://twitter.com/nexuist&quot;&gt;@Nexuist&lt;/a&gt; or my &lt;a href=&quot;/live&quot;&gt;Live Chat&lt;/a&gt;!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Announcing Unreplied 2.0</title>
   <link href="https://duro.me//stories/2020/05/26/announcing-unreplied"/>
   <updated>2020-05-26T07:00:00+00:00</updated>
   <id>https://duro.me/stories/2020/05/26/announcing-unreplied</id>
   <content type="html">&lt;p&gt;In the summer of 2017, I launched Unreplied 1.0 on the Mac App Store, making it the first Mac App I ever completed and delivered. It retailed for USD 4.99, and sold a few thousand copies. Not terrible, but definitely could have been better - unfortunately, at about the same time, I entered college! So I immediately became busy with trying to figure out how to, well, survive college, and couldn&amp;#39;t focus as much attention on Unreplied&amp;#39;s marketing as I would have liked.&lt;/p&gt;

&lt;p&gt;Now, almost three years later, I&amp;#39;m back again! I decided to use quarantine to rewrite Unreplied, add a few new features - like &lt;a href=&quot;/stories/2020/05/26/unreplied-cli&quot;&gt;CLI support&lt;/a&gt; - and overall get back into the Mac game.&lt;/p&gt;

&lt;p&gt;CLI support isn&amp;#39;t the only new feature: Unreplied 2.0 also supports reminder notifications, Dark Mode, a new date format, and more.&lt;/p&gt;

&lt;p&gt;So I&amp;#39;m back, and it&amp;#39;s ready! You can check out Unreplied on its website: &lt;a href=&quot;https://unreplied.app&quot;&gt;https://unreplied.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you already bought Unreplied through the Mac App Store, no worries - you&amp;#39;ll get the new update automatically, no extra purchase required.&lt;/p&gt;

&lt;p&gt;Fun fact: I randomly got the idea for Unreplied one day in January 2017 and pitched it to my friend Wilson (who now runs &lt;a href=&quot;https://getzbra.com/&quot;&gt;Zebra - an open source package manager for iOS devices&lt;/a&gt;). He was skeptical it could be done; as a result, Unreplied&amp;#39;s fate was sealed, and it would come out a few months later.&lt;/p&gt;

&lt;p&gt;&lt;img alt=&quot;Wilson&apos;s conversation with me&quot; src=&quot;https://s3.amazonaws.com/duro.me/wilson.png&quot; style=&quot;width:50% !important&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Saturday, January 14, 2017.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Announcing This Website (Again)</title>
   <link href="https://duro.me//stories/2020/05/17/new-site"/>
   <updated>2020-05-17T07:00:00+00:00</updated>
   <id>https://duro.me/stories/2020/05/17/new-site</id>
   <content type="html">&lt;p&gt;New year, new site, right? Although, it is May. A few things happened between January, when I wanted to do this, and now...&lt;/p&gt;

&lt;p&gt;Anyways.&lt;/p&gt;

&lt;p&gt;This is just a quick post mainly to highlight the new theme I&amp;#39;m using. It&amp;#39;s called &lt;a href=&quot;https://github.com/ronv/colorie/&quot;&gt;Colorie&lt;/a&gt;, and I really quite like it. The old site&amp;#39;s CSS was written by hand, and while I think it still looks neat, there were definitely areas that could use improvement. I like Colorie because it&amp;#39;s minimal and each page looks like...an actual page, made of paper. Perfect for long and short posts alike.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s the old site for reference:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/newer.png&quot; alt=&quot;Old duro.me&quot;&gt;&lt;/p&gt;

&lt;p&gt;And the site before that:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/old.png&quot; alt=&quot;Old duro.me&quot;&gt;&lt;/p&gt;

&lt;p&gt;And the old blog post about this: &lt;a href=&quot;/stories/2019/01/09/new-site&quot;&gt;Announcing This Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In that post I talked about dark mode and why I wanted the redesign to be dark. I&amp;#39;ve since reconsidered, mainly due to Colorie&amp;#39;s color pallete. It&amp;#39;s definitely nicer on the eyes, and I think it just looks &lt;em&gt;fun&lt;/em&gt;. So, this redesign is light. Maybe dark mode returns next year? ;P&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Finding That One Song You Forgot Through Spotify Streaming History</title>
   <link href="https://duro.me//stories/2020/04/27/spotify-history"/>
   <updated>2020-04-27T05:00:00+00:00</updated>
   <id>https://duro.me/stories/2020/04/27/spotify-history</id>
   <content type="html">&lt;p&gt;One night I was listening to Spotify Radio while doing homework and, through sheer luck and algorithmic grace, I was handed a set of tracks I absolutely loved. I saved almost every one.&lt;/p&gt;

&lt;p&gt;The next day, I was playing through the list, only to realize I had forgotten one! To make matters worse, the tune was stuck in my head, and so while I knew exactly what it sounded like, I had no idea what the title or artist was. The only other relevant information I had was a rough idea of what time I was listening to it (the early morning of February 9th, around 2-3 AM).&lt;/p&gt;

&lt;p&gt;I went back through my Spotify history, but I couldn&amp;#39;t find it. Sadly, Spotify only stores &lt;a href=&quot;https://support.spotify.com/us/using_spotify/features/play-queue/&quot;&gt;the last 50 tracks&lt;/a&gt; in the History tab.&lt;/p&gt;

&lt;p&gt;At this point I thought this song would be lost forever. However...&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/spotify-history/spotify-download.png&quot; alt=&quot;Spotify Download Your Data&quot;&gt;&lt;/p&gt;

&lt;p&gt;Could &lt;a href=&quot;https://en.wikipedia.org/wiki/General_Data_Protection_Regulation&quot;&gt;GDPR&lt;/a&gt; save me?&lt;/p&gt;

&lt;h2&gt;Spotify Download Your Data&lt;/h2&gt;

&lt;p&gt;To comply with GPDR, Spotify lets you download all the data they keep on you, which happens to include a full year of streaming history (streaming means any song, podcast, or video played through Spotify - not just radio history). The song I want should be in here somewhere!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sidenote: I still find it incredibly strange that you can watch full, 1080p quality videos on Spotify. Not just music videos but also like...video game let&amp;#39;s plays and mini documentaries. Who uses this?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I applied and waited for my link email. It only took a few days. Here&amp;#39;s the files it came with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FamilyPlan.json&lt;/li&gt;
&lt;li&gt;Follow.json&lt;/li&gt;
&lt;li&gt;Identity.json&lt;/li&gt;
&lt;li&gt;Payments.json&lt;/li&gt;
&lt;li&gt;Playlist1.json&lt;/li&gt;
&lt;li&gt;SearchQueries.json&lt;/li&gt;
&lt;li&gt;Userdata.json&lt;/li&gt;
&lt;li&gt;YourLibrary.json&lt;/li&gt;
&lt;li&gt;StreamingHistory[0-5].json&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For info on each, check out &lt;a href=&quot;https://support.spotify.com/us/account_payment_help/privacy/understanding-my-data/&quot;&gt;Understanding My Data&lt;/a&gt; from Spotify. There&amp;#39;s a lot of useful data here that isn&amp;#39;t just about streaming history; you can also see every song you&amp;#39;ve saved, every playlist you&amp;#39;ve made and all the tracks they hold, who you follow, and your search query history.&lt;/p&gt;

&lt;p&gt;For today, though, I&amp;#39;m only using streaming history. There&amp;#39;s 6 of these files, and it turns out they go in chronological order: StreamingHistory0&amp;#39;s first track was played in February 2019, while StreamingHistory5 begins in January 2020. So, we finally have a file to work with.&lt;/p&gt;

&lt;p&gt;Unfortunately, it&amp;#39;s 31,352 lines long!&lt;/p&gt;

&lt;h2&gt;Filtering with jq&lt;/h2&gt;

&lt;p&gt;Each piece of play history is represented in JSON:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;{
    &amp;quot;endTime&amp;quot; : &amp;quot;2020-01-09 15:15&amp;quot;,
    &amp;quot;artistName&amp;quot; : &amp;quot;Noisestorm&amp;quot;,
    &amp;quot;trackName&amp;quot; : &amp;quot;Crab Rave&amp;quot;,
    &amp;quot;msPlayed&amp;quot; : 161280
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we consider that each track takes up 6 lines of the file, then 31352/6 ~= 6,270 tracks over a period from January 9 (first entry) to February 12 (last entry). 6,270 tracks over a period of 43 days is roughly 146 tracks a day or 6 songs an hour, which is pretty accurate (I use Spotify a lot).&lt;/p&gt;

&lt;p&gt;6,270 tracks is still a lot, though, and we can quickly cut this down using our good friend &lt;a href=&quot;https://stedolan.github.io/jq/&quot;&gt;jq&lt;/a&gt;. jq lets you easily splice and dice through JSON files without needing to write custom scripts. I never used it before, and I knew this would be the perfect opportunity to try, so I &lt;a href=&quot;https://stedolan.github.io/jq/manual/&quot;&gt;read the manual&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; cat StreamingHistory5.json | jq &amp;#39;map(select(.endTime | test(&amp;quot;2020-02-09&amp;quot;)))&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A few things are going on at once here (and it took me a lot more than one try to get this to work properly). First, some definitions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For any filter x, map(x) will run that filter for each element of the input array, and return the outputs in a new array.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The function select(foo) produces its input unchanged if foo returns true for that input, and produces no output otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;test(val), test(regex; flags)
Like match, but does not return match objects, only true or false for whether or not the regex matches the input.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Effectively what we are doing is filtering out any track object that doesn&amp;#39;t match our criteria (endTime contains &amp;quot;2020-02-09&amp;quot;). This gives us a new array of tracks. We can count how many using &lt;code&gt;length&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; cat StreamingHistory5.json | jq &amp;#39;map(select(.endTime | test(&amp;quot;2020-02-09&amp;quot;))) | length&amp;#39;
&amp;gt; 297
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;297! A 95% decrease, which is great, but still way too many songs to deal with. I did mention that I heard this song during the night, and so we can try to see if we can filter by hours:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; cat StreamingHistory5.json | jq &amp;#39;map(select(.endTime | test(&amp;quot;2020-02-09 02&amp;quot;))) | length&amp;#39;
&amp;gt; 59
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we&amp;#39;re looking for end times that contain &amp;quot;2020-02-09 02&amp;quot;, which limits us only to songs that were listened to at 2 AM.&lt;/p&gt;

&lt;p&gt;I can listen to 59 songs, but right now I only have an artist name and a track name. It would take a long time to type all of that into Spotify, find a track, play it, and determine if it&amp;#39;s the song I want or not.&lt;/p&gt;

&lt;p&gt;It would be better if I could just click on the song to play it.&lt;/p&gt;

&lt;h2&gt;Markdown with jq&lt;/h2&gt;

&lt;p&gt;I&amp;#39;m not sure if anyone has actually done this before, but I figured the next step would be to convert my JSON object into a Markdown text file. I could have a list of songs, and each song could be wrapped in a hyperlink that opens Spotify.&lt;/p&gt;

&lt;p&gt;Unfortunately, this JSON file doesn&amp;#39;t give us any track ID or URL - only the artist and track name. We&amp;#39;re going to have to use Spotify search to find the song before we can play it back.&lt;/p&gt;

&lt;p&gt;From &lt;a href=&quot;https://github.com/nexuist/scratchify&quot;&gt;previous endeavors&lt;/a&gt; I knew that Spotify had a URI handler for &lt;code&gt;spotify:search:&lt;/code&gt;, so you could, for example, click on &lt;a href=&quot;spotify://search:%22Another%20One%20Bites%20the%20Dust%22&quot;&gt;spotify://search:Another One Bites the Dust&lt;/a&gt; to bring up the Spotify window with search results.&lt;/p&gt;

&lt;p&gt;Using what we know, we can massage the original objects into the format that we want (still using only jq):&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; cat StreamingHistory5.json | jq &amp;#39;map(select((.endTime | test(&amp;quot;2020-02-09 02:59&amp;quot;)))) | map({time: .endTime, song: (.trackName + &amp;quot; - &amp;quot; + .artistName), link: (@uri &amp;quot;spotify://search:\\(.trackName + &amp;quot; &amp;quot; + .artistName)&amp;quot;)})&amp;#39;

&amp;gt; [
    {
        &amp;quot;time&amp;quot;: &amp;quot;2020-02-09 02:59&amp;quot;,
        &amp;quot;song&amp;quot;: &amp;quot;Jerry Was A Race Car Driver - Primus&amp;quot;,
        &amp;quot;link&amp;quot;: &amp;quot;spotify://search:Jerry%20Was%20A%20Race%20Car%20Driver%20Primus&amp;quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The first map acts as a filter, getting rid of songs that don&amp;#39;t match the criteria, while the second map acts as a...map, transforming keys from the old object to the new one.&lt;/p&gt;

&lt;p&gt;(I&amp;#39;ve narrowed the time criteria to 2:59 just for this demo).&lt;/p&gt;

&lt;p&gt;One more map later:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;map(&amp;quot;- \\(.time) [ \\(.song) ]( \\(.link) \)&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And we get:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;[
    &amp;quot;- 2020-02-09 02:59 [Jerry Was A Race Car Driver - Primus](spotify://search:Jerry%20Was%20A%20Race%20Car%20Driver%20Primus)&amp;quot;,
    &amp;quot;- 2020-02-09 02:59 [Blitzkrieg Bop - 2016 Remaster - Ramones](spotify://search:Blitzkrieg%20Bop%20-%202016%20Remaster%20Ramones)&amp;quot;,
    &amp;quot;- 2020-02-09 02:59 [Giant - Nebula](spotify://search:Giant%20Nebula)&amp;quot;,
    &amp;quot;- 2020-02-09 02:59 [Lies (feat. Luciana) - KSHMR](spotify://search:Lies%20(feat.%20Luciana)%20KSHMR)&amp;quot;
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that jq&amp;#39;s output still contains opening and closing brackets for the array, and puts each line in quotes. If we want to pipe this to a file, we need to get rid of those by using jq&amp;#39;s -r argument (raw output) and &lt;code&gt;.[]&lt;/code&gt; (which tells jq to separate objects with newlines):&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; jq -r &amp;#39;map(select((.endTime | test(&amp;quot;2020-02-09 02:59&amp;quot;)))) | map({time: .endTime, song: (.trackName + &amp;quot; - &amp;quot; + .artistName), link: (@uri &amp;quot;spotify://search:\(.trackName + &amp;quot; &amp;quot; + .artistName)&amp;quot;)}) | map(&amp;quot;- \\(.time) [ \\(.song) ]( \\ (.link) \)&amp;quot;) | .[]&amp;#39;

- 2020-02-09 02:59 [Jerry Was A Race Car Driver - Primus](spotify://search:Jerry%20Was%20A%20Race%20Car%20Driver%20Primus)
- 2020-02-09 02:59 [Blitzkrieg Bop - 2016 Remaster - Ramones](spotify://search:Blitzkrieg%20Bop%20-%202016%20Remaster%20Ramones)
- 2020-02-09 02:59 [Giant - Nebula](spotify://search:Giant%20Nebula)
- 2020-02-09 02:59 [Lies (feat. Luciana) - KSHMR](&amp;lt;spotify://search:Lies%20(feat.%20Luciana)%20KSHMR)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, let&amp;#39;s give it a header, by simply adding an array containing [&amp;quot;# Songs&amp;quot;, &amp;quot; &amp;quot;] to the front of the results:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; jq -r &amp;#39;map(select((.endTime | test(&amp;quot;2020-02-09 02:59&amp;quot;)))) | map({time: .endTime, song: (.trackName + &amp;quot; - &amp;quot; + .artistName), link: (@uri &amp;quot;spotify://search:\\(.trackName + &amp;quot; &amp;quot; + .artistName)&amp;quot;)}) | [&amp;quot;# Songs&amp;quot;, &amp;quot; &amp;quot;] + map(&amp;quot;- \\(.time) [ \\(.song) ]( \\ (.link) \)&amp;quot;) | .[]&amp;#39;

# Songs

- 2020-02-09 02:59 [Jerry Was A Race Car Driver - Primus](spotify://search:Jerry%20Was%20A%20Race%20Car%20Driver%20Primus)
- 2020-02-09 02:59 [Blitzkrieg Bop - 2016 Remaster - Ramones](spotify://search:Blitzkrieg%20Bop%20-%202016%20Remaster%20Ramones)
- 2020-02-09 02:59 [Giant - Nebula](spotify://search:Giant%20Nebula)
- 2020-02-09 02:59 [Lies (feat. Luciana) - KSHMR](&amp;lt;spotify://search:Lies%20(feat.%20Luciana)%20KSHMR)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And now we pipe the end command to a file:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;&amp;gt; jq -r &amp;#39;map(select((.endTime | test(&amp;quot;2020-02-09 02:59&amp;quot;)))) | map({time: .endTime, song: (.trackName + &amp;quot; - &amp;quot; + .artistName), link: (@uri &amp;quot;spotify://search:\\(.trackName + &amp;quot; &amp;quot; + .artistName)&amp;quot;)}) | [&amp;quot;# Songs&amp;quot;, &amp;quot; &amp;quot;] + map(&amp;quot;- \\(.time) [ \\(.song) ]( \\ (.link) \)&amp;quot;) | .[]&amp;#39; &amp;gt; test.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...presto!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/spotify-history/spotify-result.png&quot; alt=&quot;Side by side Markdown and Spotify&quot;&gt;&lt;/p&gt;

&lt;p&gt;(I found the song, by the way. It was &lt;a href=&quot;https://open.spotify.com/track/2d9iP38QXgV7xzhmsY2VHB?si=XS4SpT0FTM2A-OPV2GK-TQ&quot;&gt;Shift by ANG&lt;/a&gt;).&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>The Ballad of TurboChair</title>
   <link href="https://duro.me//stories/2019/10/23/ballad-of-turbochair"/>
   <updated>2019-10-23T05:00:00+00:00</updated>
   <id>https://duro.me/stories/2019/10/23/ballad-of-turbochair</id>
   <content type="html">&lt;!-- markdownlint-disable no-inline-html --&gt;

&lt;p&gt;On December 20th, 2018, I saw &lt;a href=&quot;https://i.imgur.com/IsYlqC0.gifv&quot;&gt;this&lt;/a&gt; GIF and I immediately knew that I had to build my own motorized chair.&lt;/p&gt;

&lt;p&gt;On October 20th, 2019, we finally presented the TurboChair at &lt;a href=&quot;https://hackumass.com&quot;&gt;HackUMass VII&lt;/a&gt; and won Best Hardware Hack. The following article is an attempt to explain everything that happened in between that 10 month span.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Andi - Me!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amrit - WPI CS &amp;#39;21 - One of my best friends from high school, we did FRC robotics together. Great at putting together hardware and building software.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Matt - WPI Aeronautical Engineering &amp;#39;21 - Another one of my high school best friends, also did robotics. Can build and wire anything!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Norlinda - UConn BME &amp;#39;22 - One of the first friends I made at UConn, who kept up team moral and put in major contributions to the design of the final product.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;December, 2018&lt;/h3&gt;

&lt;p&gt;I found out about HackWPI, which is how this whole chair-ade started. I asked Amrit, Matt, and Dan (previous hackathon team members) if they&amp;#39;d like to do something together, and we started brainstorming ideas.&lt;/p&gt;

&lt;p&gt;Originally we wanted to do a self driving Barbie jeep (the kind for kids), but we decided to save that project for next time once I saw the &lt;a href=&quot;https://i.imgur.com/IsYlqC0.gifv&quot;&gt;GIF&lt;/a&gt; which made me want to build the chair. After a few hours of discussion, everyone was on board and we started writing up a parts list.&lt;/p&gt;

&lt;h3&gt;January 8 - Sofa Hunt&lt;/h3&gt;

&lt;p&gt;Arguably the most important part was, well, the chair. We started checking Craigslist for any cheap offers that would be small enough to transport via car yet sturdy enough to allow us to bolt on motors and other electronics. Furthermore, we wanted an ergonomic seat for comfortable rides.&lt;/p&gt;

&lt;h3&gt;January 12 - Chair Secured&lt;/h3&gt;

&lt;p&gt;We found two listings that matched our requirements, but unfortunately both were pretty far and the cheapest one was \$25. However, the scales tipped once one of the owners sent back:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;    You do not need to pay cash. At this point, if you would like to come and get it, it is yours at no cost.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I would like to say thank you to Rik from Connecticut for giving us the chair for free. Without him, TurboChair would not exist.&lt;/p&gt;

&lt;p&gt;Matt came over with his dad&amp;#39;s pickup truck and we both took a trip to Rik&amp;#39;s house to collect the chair. Rik was visiting his granddaughter in Massachusetts at the time and he helpfully left the chair right outside the door to make it easier for us to load it into the truck.&lt;/p&gt;

&lt;p&gt;After sending my thanks to him, I received the following reply:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;    You’re very welcome. We had a lovely visit with our granddaughter. I hope you enjoy the chair and get as much good use out of it as we did.

    Rik
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If only you knew, Rik!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/chair-secured.jpg&quot; width=&quot;50%&quot; height=&quot;70%&quot; alt=&quot;The chair in Matt&apos;s pickup&quot;&gt;&lt;/p&gt;

&lt;p&gt;Once we claimed the chair, we took it to Matt&amp;#39;s house and got to work on the base assembly. To make a good day even better, it reclined! Matt happened to have some motors laying around from a mobility scooter he and a friend had found at the town dump - these would end up being part of the final product as well.&lt;/p&gt;

&lt;p&gt;The motors appear to be the &amp;quot;M4-7UM&amp;quot; model from the PIHSIANG company, and they went for around \$300 the last time we checked eBay. It certainly looked like we got a deal.&lt;/p&gt;

&lt;div style=&quot;padding: 10px&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/chair-flipped.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;The chair upside down&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/motor-testbed.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;Trying out the motors&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/blowtorch.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;Matt welding a piece for the assembly&quot;&gt;
&lt;/div&gt;

&lt;p&gt;Eventually I went home while Matt and Clay (another one of our high school friends) worked on the assembly.&lt;/p&gt;

&lt;div style=&quot;padding: 10px&quot;&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://www.youtube.com/embed/b_sMK3G3ZjA&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://www.youtube.com/embed/FLMYUD_j9Hg&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h3&gt;January 13 - Max Power&lt;/h3&gt;

&lt;p&gt;Up till this point we had been using two power drill batteries in series to power the motors. While this was fine for testing, we&amp;#39;d need something stronger for the actual chair.&lt;/p&gt;

&lt;p&gt;Matt found a battery:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/battery.jpg&quot; width=&quot;40%&quot; height=&quot;50%&quot; alt=&quot;Beast Battery&quot;&gt;&lt;/p&gt;

&lt;p&gt;This thing is absolutely overkill. It holds more juice than a typical car battery and outputs nearly twice the voltage. Regardless, it was exactly the type of over the top absurdity TurboChair demanded.&lt;/p&gt;

&lt;p&gt;The battery ended up being the most expensive part of the chair, totaling well over \$200 and requiring its own special charger. Thankfully Matt happened to have it lying around from past projects so we didn&amp;#39;t have to wait for it to shipping. Also, if we weren&amp;#39;t shooting for absurdity, we definitely could have found cheaper options to fit the bill - but how fun would that have been?&lt;/p&gt;

&lt;p&gt;By the end of this day, Matt and Clay finished the drive assembly and had it securely attached to the bottom of the chair. They added shopping cart wheels to the front (also from the dump) so the chair now officially qualified as a motor vehicle. Everything was starting come together.&lt;/p&gt;

&lt;h3&gt;January 14 - Move In Day&lt;/h3&gt;

&lt;p&gt;Matt and Amrit drove the chair to Worcester and stored it in the basement of Amrit&amp;#39;s frat house. Things were looking good for Dan and I to come up on Friday for HackWPI.&lt;/p&gt;

&lt;p&gt;At this point I also ordered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 5V voltage regulator&lt;/li&gt;
&lt;li&gt;A Raspberry Pi touchscreen display&lt;/li&gt;
&lt;li&gt;Turn signal lamps, like the kind you find on tractor trailers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;January 15 - Unexpected Turbulence&lt;/h3&gt;

&lt;p&gt;Unfortunately, this is where things went downhill. Right before the hackathon, a really nasty winter storm moved in that kept us from going up to WPI. Amrit and Matt ended up not competing, and the chair stayed in the basement.&lt;/p&gt;

&lt;p&gt;With the school year starting for all of us again, the chair became an afterthought.&lt;/p&gt;

&lt;h3&gt;Five Months Later...&lt;/h3&gt;

&lt;h3&gt;May 9 - Shipment Delivered&lt;/h3&gt;

&lt;p&gt;Remember how I ordered some stuff four months ago? It finally came in on this day. I had totally forgotten about it while trying to figure out how to deal with the storm.&lt;/p&gt;

&lt;p&gt;At least we felt better about missing HackWPI now, because even if we had gone these parts would definitely have not been there.&lt;/p&gt;

&lt;h3&gt;Another Five Months Later...&lt;/h3&gt;

&lt;h3&gt;September 10 - Revival&lt;/h3&gt;

&lt;p&gt;About this time HackUMass started sending out emails again asking people to sign up to compete. I figured, why not give the chair a second try? It had been over half a year, but we already bought the parts, and we might as well use them to finish the project. I didn&amp;#39;t care about winning - the completed chair was the prize.&lt;/p&gt;

&lt;p&gt;So I asked Amrit and Matt to join me once again, and I wanted to bring Lin along so she could help out and see how big hardware projects like these came to fruition. Up until this point she only had experience with Arduino kits. This was going to be her second hackathon, and we needed to go big!&lt;/p&gt;

&lt;h3&gt;October 18 - Point A to B to C&lt;/h3&gt;

&lt;p&gt;Today was the big day: HackUMass begins.&lt;/p&gt;

&lt;p&gt;This day was a logistics nightmare. Because the chair was still in Amrit&amp;#39;s frat house basement and it couldn&amp;#39;t fit in his car, we needed to go from UConn to WPI to UMass in about 5 hours. Once Lin got out of class, Matt got dropped off at my place and we began the journey to Matt&amp;#39;s apartment in Worcester. We grabbed some tools and equipment (including the battery) from there and drove over to Amrit&amp;#39;s frat house, where we acquired the chair.&lt;/p&gt;

&lt;p&gt;At this point it was about 7:30PM, and we only had an hour to get to HackUMass before check-in ended. Amrit and Matt drove in Amrit&amp;#39;s car while Lin and I drove with the chair to Amherst.&lt;/p&gt;

&lt;p&gt;By the time opening ceremonies were over, we were all way too tired from driving to get much hacking done. We decided to get some rest for a few hours before working on the chair.&lt;/p&gt;

&lt;h3&gt;October 19 - Hacking Day&lt;/h3&gt;

&lt;p&gt;All of the ground work for the chair happened on this day. This is also where we encountered all of our major obstacles.&lt;/p&gt;

&lt;h4&gt;Motor Reversing&lt;/h4&gt;

&lt;p&gt;The first goal was to integrate MOSFETs with the motor controllers so that they could go backwards. Up until this point both motors could only go forward, making it hard to turn and impossible to reverse. Unfortunately it turns out one of the MOSFETs we brought was totally abnormal and didn&amp;#39;t respond correctly to the right commands.&lt;/p&gt;

&lt;p&gt;Amrit and Matt lost at least a few hours to debugging this stupid MOSFET. One of the most annoying parts of hardware development is that there are a hundred different places where things can go wrong - if it&amp;#39;s not your wiring, it could be your code, and if not any of that, it could be the hardware itself or any one of the components connected to that hardware. There&amp;#39;s not really any way to know. Software is much easier in this regard - at least you can use error messages to figure out what&amp;#39;s going on.&lt;/p&gt;

&lt;p&gt;Eventually they decided to retire the MOSFETs in favor of two simple on/off relays courtesy of the HackUMass hardware table. The relays were a joy to work with and soon TurboChair was capable of individually reversing motors.&lt;/p&gt;

&lt;h4&gt;GoPro Woes&lt;/h4&gt;

&lt;p&gt;While this was going on, I put together the Raspberry Pi that would make up our &amp;quot;infotainment system.&amp;quot; A few days ago I had ordered a Raspberry Pi 4 and a Pi camera which would give us a backup view like most modern cars have. Unfortunately, I had misjudged how everything would get wired together. I thought that the touchscreen I got from May (still in its box untouched) would use HDMI, allowing me to put the Pi on the back of the chair and thus making the short ribbon cable for the Pi camera not too big of a deal.&lt;/p&gt;

&lt;p&gt;In reality the Pi was meant to be attached directly onto the back of the touchscreen display. While this wasn&amp;#39;t hard to assemble, it did mean that the Pi camera ribbon cable became a major issue - it simply wasn&amp;#39;t long enough to reach the back of the chair.&lt;/p&gt;

&lt;p&gt;In my great wisdom, I had brought my GoPro with me, which solved everything, right? Just duct tape the GoPro wherever I want and then live stream its viewfinder to the Pi over WiFi (which is built in to the Pi 4). Problem solved, right?&lt;/p&gt;

&lt;p&gt;Wrong. GoPro apparently discontinued .m3u8 livestreaming from its API for the newer models (I have a Hero 7 Silver), meaning that the only way to do it now is through some wild amalgamation of &lt;code&gt;ffmpeg&lt;/code&gt; commands. I&amp;#39;m sure there&amp;#39;s a way, but I&amp;#39;m simply not a video streaming expert so I was way out of my league trying to solve that problem. At the same time, hours were ticking by and we still had nothing to show.&lt;/p&gt;

&lt;p&gt;Around 8PM I gave up and took Lin to Walmart with me to buy a USB camera. It was $30 (I was hoping it&amp;#39;d be like $5...) but it got the job done.&lt;/p&gt;

&lt;p&gt;The USB camera worked flawlessly and gave us a consistent 30+ FPS stream. With both goals complete, the next big step was to get the chair moving.&lt;/p&gt;

&lt;h4&gt;Interior Redesign&lt;/h4&gt;

&lt;p&gt;While Matt and Amrit worked on MOSFETs and I wrestled with the GoPro, Lin was hard at work ripping up the side of the chair to give us the &amp;quot;engine hood&amp;quot; - a giant rectangle of fabric that covered all of the electronics including the battery. When we needed to work on chair electronics, we could simply lift the hood up. When driving, the hood was held in place by strategically applied Velcro strips.&lt;/p&gt;

&lt;p&gt;Lin also worked with me to come up with preliminary UI designs for our infotainment system, which would run on the Raspberry Pi.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/prelim-ui.jpg&quot; width=&quot;50%&quot; height=&quot;25%&quot; alt=&quot;Preliminary UI design&quot;&gt;&lt;/p&gt;

&lt;h4&gt;First Drive&lt;/h4&gt;

&lt;p&gt;With the motor controlling apparatus installed under the hood and an Arduino thrown in to interface with the relays, we were ready to drive. Amrit wrote some simple control code that let us use a joystick module to command the motors.&lt;/p&gt;

&lt;p&gt;The joystick ended up being completely awful to work with, but it was enough to give us the moral boost to keep working on the chair throughout the night.&lt;/p&gt;

&lt;p&gt;The first static run of TurboChair occured at 10:14PM, with the full hallway run at 11:12PM.&lt;/p&gt;

&lt;div style=&quot;padding: 10px&quot;&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/lu8hmJDn80Y&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/PIxQue-6M_Y&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;It wasn&amp;#39;t very long until bystanders saw what we were up to and wanted a shot at driving!&lt;/p&gt;

&lt;div style=&quot;padding: 10px&quot;&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/x9Z7Ff4Ml0w&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
    &lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/ifli5gUdo5w&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Eventually though, it was time to get back to work.&lt;/p&gt;

&lt;h3&gt;October 20 - After Hours&lt;/h3&gt;

&lt;p&gt;We had a driving chair. But we wanted more.&lt;/p&gt;

&lt;p&gt;The two big goals for this sprint were to get the Nyko Kama nunchuck I brought to interface with the chair, and to also mount the Raspberry Pi w/ touchscreen display onto the side of the chair. As a bonus goal, we wanted to add turn signals and brake lights using this purpose-built LED strip we got from our previous Walmart trip:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/led-strip.jpg&quot; width=&quot;50%&quot; height=&quot;40%&quot; alt=&quot;The brake light LED strip&quot;&gt;&lt;/p&gt;

&lt;h4&gt;Nyko Kama Wii Nunchuck&lt;/h4&gt;

&lt;p&gt;The nunchuck was the most important part as the tiny joystick we were using before was doing a really poor job at being useful. The nunchuck would give us a (hopefully) more precise 2-axis joystick as well as 2 buttons and force readings from its built-in accelerometer. While we didn&amp;#39;t end up using accelerometer readings, it would of course be helpful nonetheless in case we ever wanted to in the future.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/kama.jpg&quot; width=&quot;50%&quot; height=&quot;50%&quot; alt=&quot;The Nyko Kama nunchuck&quot;&gt;&lt;/p&gt;

&lt;p&gt;I had actually &lt;a href=&quot;https://github.com/Nexuist/Wii-Kama-API&quot;&gt;gotten this model to work four years ago&lt;/a&gt; in 2015. I open sourced the code I used then but afterwards I had no idea what I would use it for (originally I bought it for an electric skateboard project that fizzled out). I figured it shouldn&amp;#39;t be too hard to port that code to the Raspberry Pi. However, the breakout adapter used I2C, and I&amp;#39;m not an I2C expert. To complicate matters, the Raspberry Pi had various libraries available for I2C that didn&amp;#39;t provide a 1:1 match with Arduino&amp;#39;s &lt;code&gt;Wire&lt;/code&gt; library. In the end, I decided to hook up the nunchuck to the Arduino instead, and just connect the Arduino with the Pi together to transfer data over (more on this later). This ended up being a better choice as it made the Pi non-essential to chair operation - if my app crashed or the Pi restarted, it wouldn&amp;#39;t stop the nunchuck from controlling the chair.&lt;/p&gt;

&lt;p&gt;The nunchuck control code ended up getting finished &lt;a href=&quot;https://github.com/Nexuist/HackUMass2019/commit/47413156cda311b4d3ec53a7688118a3eb16010b&quot;&gt;around 4AM&lt;/a&gt;. At the same time I had been working on the front end web design for the infotainment system. I was able to use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices&quot;&gt;MediaDevices&lt;/a&gt; web API to get video access to the USB camera, and wrote some basic CSS to place a sidebar next to the camera feed.&lt;/p&gt;

&lt;p&gt;After finishing the UI design, I converted the whole page into a Vue web app. The idea was to set up a render loop that would read data from the Arduino, parse it into values that I could plug into Vue, and from there Vue would render the page.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/ui.png&quot; width=&quot;100%&quot; height=&quot;50%&quot; alt=&quot;The infotainment web app&quot;&gt;&lt;/p&gt;

&lt;p&gt;I ended up writing a &lt;a href=&quot;https://github.com/Nexuist/HackUMass2019/commit/dd0f42ab3283d756b25bf43783005a53493f9f27&quot;&gt;WebSocket bridge server&lt;/a&gt; in Python that read data values over serial USB (connected to the Arduino) and sent them through a connected client. In this way I could read any values from the Arduino from the web page - as long as the bridge server was running.&lt;/p&gt;

&lt;p&gt;An example data packet from the Arduino looked like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DATA:1|1|128|130|0.55559|0.83244&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Each field, in order, represents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Armed: Whether the Z button on the nunchuck was being held. This would show an &amp;quot;ARMED&amp;quot; bubble on the screen and prevented the chair from moving unless the user was holding down that button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Going Forward: The infotainment system had this concept of &amp;quot;gears&amp;quot; (D/N/R) even though the chair itself didn&amp;#39;t really have a transmission. To support the illusion, we calculated whether the chair was going forward based off of if either motor was in reverse. If both motors were moving forward, the gear was &amp;quot;D&amp;quot;, if they were both going backwards it was &amp;quot;R&amp;quot;, if neither of them were moving the gear would show &amp;quot;N&amp;quot;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joystick X: Exactly what it sounds like, the nunchuck&amp;#39;s joystick X value from 0 to 255 (in the center, it was 128).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joystick Y: Same as the last value but for vertical direction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Left motor output: The voltage going to the left motor scaled from 0 to 1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Right motor output: Same as before but for the right motor.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all, the infotainment system consisted of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Raspberry Pi connected to the USB webcam and the Arduino via USB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The bridge server running in the background&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Vue web app running in Chrome full screen&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hackathon Pro Tip&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of people seem to struggle with getting code onto the Raspberry Pis. Generally you&amp;#39;ll have a better dev environment (like VSCode) set up on your laptop, so trying to code directly on the Pi is inefficient. Usually it wouldn&amp;#39;t matter, but in a hackathon seconds count.&lt;/p&gt;

&lt;p&gt;What I ended up doing is setting up the Git repo first, and then &lt;code&gt;git clone&lt;/code&gt;-ing it onto the Pi. This way I could push changes from my laptop and &lt;code&gt;git pull&lt;/code&gt; them onto the Pi&amp;#39;s file system.&lt;/p&gt;

&lt;p&gt;What you definitely want to avoid is editing code in multiple places (i.e. on your laptop and on the Pi). Unless you set up version control, this is a great way to get confused and use the wrong versions of things and then wonder why things that used to work start breaking...and why things that were broken started working.&lt;/p&gt;

&lt;p&gt;Although you generally shouldn&amp;#39;t have to worry about other teams stealing your code (in our case, they&amp;#39;d have to steal a chair too...), if you are that concerned you can just make a private repo on GitHub and use that between your laptop(s) and Pi(s). Fun for the whole electronic family!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;Pi Mount&lt;/h4&gt;

&lt;p&gt;Also around 4AM, Matt and Amrit started building the mount for the Pi. We realized that running the Pi, Arduino, and LEDs on the same circuit was drawing too much power, so we switched the Pi&amp;#39;s power supply to a handheld USB battery bank. I was worried it wouldn&amp;#39;t last nearly long enough to survive demo day, but it did!&lt;/p&gt;

&lt;div style=&quot;padding:10px&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/mount-installed.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;Installing the Pi mount&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/aerial.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;A view of the chair from above&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/camera-mounted.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;The USB webcam&quot;&gt;
&lt;/div&gt;

&lt;p&gt;At this point it was 6AM, and I was starting to crash. Matt, Amrit, and Lin had already gone to bed and woken up. Since the LED strip needed 24V to power the lights, it would have been too much work to set up 3 new relays to support interfacing with it from the Pi. No worries - we had another LED strip around, which Matt ended up installing under the chair!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/undercarriage.jpg&quot; width=&quot;40%&quot; height=&quot;30%&quot; alt=&quot;The bottom of the chair&quot;&gt;&lt;/p&gt;

&lt;p&gt;After a few trial runs, we were ready to call it. TurboChair was officially finished at 8:08AM, October 20th 2019. Almost exactly 10 months to the hour. When we went to submit the project, it asked us what we wanted to name it. Up until this point we hadn&amp;#39;t put any thought into it - so in our sleep deprived state we came up with TurboChair and called it a day.&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/Dp4oMUsDALQ&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;h3&gt;October 20 - Demo Day&lt;/h3&gt;

&lt;p&gt;The Big Day!&lt;/p&gt;

&lt;p&gt;Hacking officially ended at 10:30AM but I had submitted our project a few hours ago before I went to sleep. By the time I woke up we had already been assigned a table and were busy packing things up to get the table ready. Lin helpfully drafted a presentation in Google Slides to catch people&amp;#39;s attention in case the giant motorized chair didn&amp;#39;t.&lt;/p&gt;

&lt;div style=&quot;padding:10px&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/pres-1.png&quot; width=&quot;40%&quot; height=&quot;35%&quot; alt=&quot;Presentation slide #1&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/pres-2.png&quot; width=&quot;40%&quot; height=&quot;35%&quot; alt=&quot;Presentation slide #2&quot;&gt;
&lt;/div&gt;

&lt;p&gt;Our chair was super popular, with other hackathon participants (and judges!) all taking rides around the lobby. Shockingly, at the end of it all, our battery had only gone down to 58% throughout the ~3 days we had been driving this chair around!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/demo.jpg&quot; width=&quot;30%&quot; height=&quot;50%&quot; alt=&quot;Chair at demo table&quot;&gt;&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s a video showing off the infotainment system:&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/n9UK1WEMzYk&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;And here are some more pictures under the hood.&lt;/p&gt;

&lt;div style=&quot;padding:10px&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/hood-covered.jpg&quot; width=&quot;40%&quot; height=&quot;35%&quot; alt=&quot;The side hood closed&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/hood-up.jpg&quot; width=&quot;40%&quot; height=&quot;35%&quot; alt=&quot;The side hood opened&quot;&gt;
&lt;/div&gt;

&lt;p&gt;From left to right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &amp;quot;emergency cutoff&amp;quot; switch, which killed power from the battery in case anything went wrong (thankfully, it didn&amp;#39;t!) It might look far away but it&amp;#39;s actually pretty easy to grab on to in the driver&amp;#39;s seat&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The power box for the undercarriage LED strip, which Matt carefully spliced open to allow it to be powered by our circuit instead of a USB port&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The two relays that allow the motors to go backwards and forwards&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From top to bottom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Arduino Uno which interfaced with the motor controllers and talked to the Pi!&lt;/li&gt;
&lt;li&gt;Our 5VDC regulator which powered the Uno and the relays&lt;/li&gt;
&lt;li&gt;The two huge (but cheap) motor controllers running our scooter motors&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Nyko Kama receiver and breakout board which allowed the Arduino to read commands from the nunchuck over Bluetooth&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://s3.amazonaws.com/duro.me/turbochair/hood-zoomed.jpg&quot;&gt;Here&amp;#39;s a closeup of the electronics panel.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally, here&amp;#39;s the chair in typical operation:&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/FErP-NAGuoA&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;h4&gt;Turbo Mode&lt;/h4&gt;

&lt;p&gt;A few hours into demoing, Amrit decided to add a tiny little addition to the control loop: pressing the B button (above Z) would enable &amp;quot;Turbo Mode.&amp;quot; See, up until this point, the chair had been operating at half power, because we were afraid of crashing it into things. With people dispersing to view other projects, we had a clear view at an empty hallway to try going Turbo.&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/dpygAqnjYFQ&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;It might be hard to tell how fast the chair is going from the video alone - try turning up your volume :)&lt;/p&gt;

&lt;h4&gt;Aftermath&lt;/h4&gt;

&lt;p&gt;Demo time came and went, and now was the closing ceremony (around 4PM). We were all anxious to see if we had won. We had been told beforehand that we were finalists for the &amp;quot;Best Hardware Hack&amp;quot; prize category, but there had been other impressive hardware projects...&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/prize.jpg&quot; width=&quot;25%&quot; height=&quot;50%&quot; alt=&quot;Drone prize&quot;&gt;&lt;/p&gt;

&lt;p&gt;We won!&lt;/p&gt;

&lt;p&gt;Not only did we each win a free drone, but each drone came with a little slip letting us know that we were randomly selected to win &lt;em&gt;another&lt;/em&gt; drone model from the same manufacturer!&lt;/p&gt;

&lt;p&gt;Clearly it was all worth it. But the final challenge loomed ahead of us: how do we get everything home?&lt;/p&gt;

&lt;p&gt;Well....&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/WLFebwpiiWU&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;TurboChair doesn&amp;#39;t need anyone in the driver&amp;#39;s seat to move, so obviously...&lt;/p&gt;

&lt;iframe width=&quot;40%&quot; height=&quot;400px&quot; src=&quot;https://youtube.com/embed/T52NKibL3xk&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;We put all of our stuff onto the chair and drove it outside. I don&amp;#39;t think anyone else could say they had their project drive their stuff to their cars :)&lt;/p&gt;

&lt;h3&gt;Fin.&lt;/h3&gt;

&lt;p&gt;So that was TurboChair. It may have taken nearly a year, but it was oh-so-worth it.&lt;/p&gt;

&lt;p&gt;I&amp;#39;d like to thank Amrit, Matt, and Lin for completing this fever dream of a project. What a ride it was - metaphorically, and now physically too.&lt;/p&gt;

&lt;div style=&quot;padding:10px&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/credits/amrit.jpg&quot; width=&quot;33%&quot; height=&quot;50%&quot; alt=&quot;Amrit on the chair&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/credits/msj.jpg&quot; width=&quot;33%&quot; height=&quot;50%&quot; alt=&quot;Matt on the chair&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/credits/andi.jpg&quot; width=&quot;33%&quot; height=&quot;50%&quot; alt=&quot;Me on the chair&quot;&gt;
    &lt;img src=&quot;https://s3.amazonaws.com/duro.me/turbochair/credits/lin.jpg&quot; width=&quot;100%&quot; height=&quot;50%&quot; alt=&quot;Lin on the chair&quot;&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://dashboard.hackumass.com/projects/11&quot;&gt;Our HackUMass project listing.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/nexuist/hackumass2019&quot;&gt;Our open source GitHub repo.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Till next time.&lt;/p&gt;

&lt;!-- markdownlint-enable no-inline-html --&gt;
</content>
 </entry>
 
 <entry>
   <title>I Used Serverless to Build Something Real: Here&apos;s What I Learned</title>
   <link href="https://duro.me//stories/2019/05/26/updated-live-chat"/>
   <updated>2019-05-26T05:00:00+00:00</updated>
   <id>https://duro.me/stories/2019/05/26/updated-live-chat</id>
   <content type="html">&lt;p&gt;In January of this year I thought it would be an interesting exercise to &lt;a href=&quot;/stories/2019/01/10/live-chat&quot;&gt;try to build an anonymous live chat webapp for my site using AWS serverless technologies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It worked, up until the point where real people starting using it. Then everything broke.&lt;/p&gt;

&lt;p&gt;Let me back up a bit, and explain what exactly this thing was.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to jump straight to the code, here&amp;#39;s the &lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;Serverless Crash Course&lt;/h2&gt;

&lt;p&gt;In the early 2000s, in order to host a web app you&amp;#39;d typically need to put it on a server running special software (like Apache, PHP, and MySQL). Some clever people quickly realized that there was money to be made in hosting servers for other people, so the concept of the VPS (Virtual Private Server) was born.&lt;/p&gt;

&lt;p&gt;With a VPS, you&amp;#39;d pay a monthly fee to a VPS provider (like DigitalOcean or AWS EC2 today) and in turn they&amp;#39;d give you access to one of their virtual servers. It was virtual because it wasn&amp;#39;t a physical machine in a data center - instead it ran on a real, powerful computer with dozens or hundreds of other virtual servers. As long as you paid the monthly rent, you could have as many VPS&amp;#39;s as you want with whatever specs you wanted. Plus, you&amp;#39;d never have to deal with a tangled wires, electric companies, or ISPs to make sure your servers stayed up.&lt;/p&gt;

&lt;p&gt;At some point down the line someone realized that, hey, like 90% of these VPS&amp;#39;s are just web servers, so why don&amp;#39;t we just offer a web server service instead? This way, instead of individually managing a fleet of virtual servers, you could just focus on your website/webapp. You could use something like &lt;a href=&quot;https://cpanel.net/&quot;&gt;cPanel&lt;/a&gt; to manage the files, databases, and domains connected to the server. You could even install pre-built server software packages like WordPress with a few clicks. You&amp;#39;d still have to pay a monthly fee, but it would save you from the headache of dealing with low level sysadmin details like configuring database software or controlling firewalls. If you&amp;#39;ve ever paid a website to run a Minecraft server for you, this was basically the same deal.&lt;/p&gt;

&lt;p&gt;Soon enough, there were Infrastracture-as-a-Service and Platform-as-a-Service startups popping up everywhere. Need a database but don&amp;#39;t know how to manage it? Need to send email or text messages? User authentication? Storage? Analytics? All of these services offered to replace the software you&amp;#39;d need to build yourself with something that already existed and that was managed by professionals.&lt;/p&gt;

&lt;p&gt;None other than Amazon exemplified the idea of &amp;quot;let someone else deal with it&amp;quot; with its Web Services division. AWS offered services for practically everything - at the time of this blog post there are 130+ services. It&amp;#39;s the Build-a-Bear of web software. This is what the &amp;quot;cloud&amp;quot; is. As long as you&amp;#39;ve got the money, someone else will be willing to deal with it for dirt cheap thanks to economies of scale.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It should be noted that Microsoft and Google are also heavy competitors in the cloud space at this point in time, but AWS was the one to kick it off in 2006 with S3. I&amp;#39;ll keep saying AWS because that&amp;#39;s what I&amp;#39;m most familiar with, but keep in mind that its competitors have very similar services to those I&amp;#39;ll mention by name.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 2014, Amazon released AWS Lambda which was meant to be the &amp;quot;glue&amp;quot; in the AWS Build-a-Bear. See, a database or API by itself isn&amp;#39;t useful unless it&amp;#39;s used by something else. Lambda was that missing piece that would go on to link all of these services together.&lt;/p&gt;

&lt;p&gt;With Lambda, you could make Lambda &lt;em&gt;functions&lt;/em&gt; which would get called in response to events that originated on other AWS services. For example, when you talk to a custom skill on Amazon Alexa, Amazon Lex (the AWS service that powers Alexa) calls that custom skill&amp;#39;s Lambda function with an &lt;em&gt;event&lt;/em&gt; object containing information related to that event (like a transcription of what you said, what time you said it, and which Alexa you said it from). From there your Lambda function could use the regular AWS APIs and any other libraries/frameworks you uploaded to process the event.&lt;/p&gt;

&lt;p&gt;Most AWS services charge against usage, and Lambda was no exception. As long as you weren&amp;#39;t dealing with millions of requests a month, you could operate a Lambda function for pennies! Contrast this to the \$5-\$45 / month you&amp;#39;d need to pay for a single VPS.&lt;/p&gt;

&lt;p&gt;Again, at some point someone realized it was possible to run a full-blown web app on AWS Lambda taking advantage of other AWS services, without ever having to &lt;em&gt;pay for a server&lt;/em&gt;. Hence, serverless.&lt;/p&gt;

&lt;p&gt;Spoiler alert: there&amp;#39;s still a server involved. You just don&amp;#39;t have to pay for it as much anymore. That monthly flat fee would turn into a scalable usage fee, so if your service didn&amp;#39;t get used much it wouldn&amp;#39;t cost more than a few dollars a month to operate.&lt;/p&gt;

&lt;h2&gt;Live Chat Crash Course&lt;/h2&gt;

&lt;p&gt;The live chat used the following AWS services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway, which let me proxy requests from the Internet to my Lambda function and back to the requester

&lt;ul&gt;
&lt;li&gt;In particular I took advantage of the new WebSockets feature, which is what allowed me to make this chat real-time. API Gateway endpoints can also be REST APIs.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;DynamoDB, which was the database I chose to use to save mesage history between users. It&amp;#39;s NoSQL, which I didn&amp;#39;t really need for this type of project, but it was also dead simple to use with Lambda, so I chose mainly it for that purpose.&lt;/li&gt;
&lt;li&gt;Lambda, which let me write a few dozen lines of Node.js to process each incoming request, insert it into DynamoDB, and attempt to send every message to its recipient through API Gateway if it could.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting up API Gateway and DynamoDB were easy enough through AWS&amp;#39;s console. But that was to be expected; the Lambda was where the magic would happen.&lt;/p&gt;

&lt;p&gt;And so, slowly through bruteforce and sheer will, I built out the Lambda function over a few days. There were quite a lot of pitfalls that I&amp;#39;d like to talk about, but before I do that I want to describe the live chat &amp;quot;protocol&amp;quot; I ended up building so you can get a sense of what the function actually did.&lt;/p&gt;

&lt;p&gt;Every Lambda instance needs a handler, which is the function AWS Lambda executes whenever a request comes in. Here&amp;#39;s the abbreviated handler for my chat:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ow&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AWS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ApiGatewayManagementApi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ow&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ne&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;missing required keys&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connectionId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;identity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sourceIp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestId&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;andi&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;auth&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PASSWORD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ow&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ne&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;incorrect auth&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;adminHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;userHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So, in this protocol, every request has to be in the form of valid JSON and contain a &lt;code&gt;uuid&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; key. Depending on what the &lt;code&gt;uuid&lt;/code&gt; is and if the &lt;code&gt;auth&lt;/code&gt; key matches the authentication password, the request is either sent to the &lt;code&gt;userHandler&lt;/code&gt; or the &lt;code&gt;adminHandler&lt;/code&gt; (which are both ultimately just other functions).&lt;/p&gt;

&lt;p&gt;When you use the live chat, your request is sent to the &lt;code&gt;userHandler&lt;/code&gt; for processing. When I use it, mine goes through &lt;code&gt;adminHandler&lt;/code&gt;. There are very heavy similarities between the two so I&amp;#39;ll only cover the user part.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s an abbreviated version of &lt;code&gt;userHandler&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;userHandler&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;hello&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;lastConnected&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastConnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;register&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;welcome&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;ping&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;pong&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;list&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;history&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;send&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sent&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;sent&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;sendError&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;adminHandler&lt;/code&gt; is not much different, it just has different cases to support different actions. Speaking of which, let&amp;#39;s talk about what they do.&lt;/p&gt;

&lt;h3&gt;DynamoDB Item Structure&lt;/h3&gt;

&lt;p&gt;Here&amp;#39;s a quote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show me your code and conceal your data structures, and I shall continue to be mystified. Show me your data structures, and I won’t usually need your code; it’ll be obvious.
- Eric Raymond&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which itself is an updated version of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.
- Fred Brooks&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have no idea if either of these are true. I just needed something to introduce this section.&lt;/p&gt;

&lt;p&gt;For your reference, here&amp;#39;s what a fake account looks like in my DynamoDB table:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;uuid&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;b445629b-4061-419a-b3a1-caf486ab71a1&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;nickname&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;booey@email.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;ip&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;connection&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testConnection&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;lastConnected&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;lastRequestsServed&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;wrapperName&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;Set&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;values&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;null&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;String&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;updates&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that for this table, the HASH key is &lt;code&gt;uuid&lt;/code&gt; and the RANGE is &lt;code&gt;timestamp&lt;/code&gt;. Having a timestamp of 0 means that this is an account type of item, whereas having a timestamp above 0 makes it a message type (in that case, the timestamp would correspond to what time that message was sent).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I expect that last paragraph to only make sense to you if you&amp;#39;ve ever used DynamoDB. If not, don&amp;#39;t worry. It always sounds like gibberish until it doesn&amp;#39;t.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here&amp;#39;s what a message looks like:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testing message&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;to&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;uuid&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;b445629b-4061-419a-b3a1-caf486ab71a1&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1559200025231&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, here&amp;#39;s the special admin account:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;uuid&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;andi&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;ip&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;127.0.0.2&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;lastConnected&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1559200023104&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;unread&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;wrapperName&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;Set&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;values&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;e1b3b0b4-1cd2-4a77-a74a-6df7e0bd820c&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;null&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;String&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;connection&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testConnection2&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;conversations&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;e1b3b0b4-1cd2-4a77-a74a-6df7e0bd820c&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba-1&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;919e2811-e502-466c-ac47-966a62a4deeb&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba-2&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;c781c0c3-ac19-4550-96fe-c8a00268a006&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba-3&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two unique keys here, &lt;code&gt;conversations&lt;/code&gt; and &lt;code&gt;unread&lt;/code&gt;. &lt;code&gt;unread&lt;/code&gt; is a String Set that contains the UUIDs of any accounts I have unread messages from. Whenever a user sends me a message, their UUID gets added to that set. &lt;code&gt;conversations&lt;/code&gt; offers a way to &amp;quot;match the name to the face&amp;quot;, where the face is the UUID. This way I know the nickname of every account in the unread list without having to query each account individually. This saves time and prevents unnecessary queries to DynamoDB (\$\$\$).&lt;/p&gt;

&lt;p&gt;I don&amp;#39;t expect most of these items or keys to make sense to you right now, but hopefully they inform you of what the different actions are doing to the table.&lt;/p&gt;

&lt;h3&gt;userHandler&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hello&lt;/code&gt; - returns the Unix timestamp of the last time I (the admin) connected to the service&lt;/li&gt;
&lt;li&gt;&lt;code&gt;register&lt;/code&gt; - creates an account for a new visitor whenever they click the &amp;quot;Start Chat!&amp;quot; button. The account consists of their nickname, email, and a randomly generated UUID. The UUID serves as their password to keep others from accessing the conversation and gets saved in their browser on the client side. Obviously not the most secure method, but this isn&amp;#39;t a situation where security is paramount. Additionally, it adds a new entry to the &lt;code&gt;conversations&lt;/code&gt; key in the admin account that pair the UUID to the username.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ping&lt;/code&gt; - this is actually a v2 development in response to the Connections Die When They Are Killed Pitfall (read on to hear about that). All this does is updates the user&amp;#39;s account with the latest connection ID. It gets called periodically every few seconds by the live chat client.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list&lt;/code&gt; - returns a list of every message sent and received by the user&amp;#39;s UUID. This is called every time the chat page loads.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send&lt;/code&gt; - this one is the most complicated but also the most important. I&amp;#39;ll include the whole thing:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;send&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isUniqueRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;duplicate&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addMessageToConversation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;andi&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;markUUIDUnread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sent&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;andi&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ws&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sent&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;sent&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;JSONReply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;sendError&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And here are the steps it takes, in English:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Throw out the request if there&amp;#39;s no &lt;code&gt;msg&lt;/code&gt; to send.&lt;/li&gt;
&lt;li&gt;Check if the request is unique to solve the Idempotency Pitfall.&lt;/li&gt;
&lt;li&gt;Insert the message as an item in the DynamoDB table.&lt;/li&gt;
&lt;li&gt;Mark the UUID unread - add it to the admin account&amp;#39;s unread list so that the next time I log in I&amp;#39;ll see that I have an unread message from this UUID.&lt;/li&gt;
&lt;li&gt;Try to send the message through the connection of the recipient with &lt;code&gt;utils.sendResponse&lt;/code&gt;. In this case, this method looks up my account, picks out my connection ID, and attempts to send the new message through my WebSocket connection using the API Gateway Management API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all works out right, the new message gets added to the database as an item, updates my admin account so I know about it, and sends a reply to my live connection.&lt;/p&gt;

&lt;p&gt;Boy, did it take a while to get it to all work out right.&lt;/p&gt;

&lt;h2&gt;Take Two&lt;/h2&gt;

&lt;p&gt;The first version of the live chat (I&amp;#39;ll call it the legacy version from now on) was built using test-driven development. That is, I wrote code in the AWS inline code editor, saved it, and tried sending random events to it to see if it would work. I hit the Test Events Suck With JSON Pitfall here which was incredibly annoying to figure out, but at least I only had to fix it once.&lt;/p&gt;

&lt;p&gt;As you can imagine, this move-fast-and-break-things attitude is probably why things broke.&lt;/p&gt;

&lt;p&gt;To be fair, I wouldn&amp;#39;t have been able to succeed quite as well as I did the second time around if I didn&amp;#39;t have the legacy attempt to build on. I learned a &lt;em&gt;ton&lt;/em&gt; from the first time, and that experience allowed me to deliver a more professional solution that avoids all the pitfalls I discovered since then.&lt;/p&gt;

&lt;p&gt;I decided to do things properly the second time around. I built unit tests &amp;amp; integration tests, for the first time on a serious project that would be put in production.&lt;/p&gt;

&lt;p&gt;It took so much longer! But as a reward, I&amp;#39;m much more confident in the codebase now. And for any new feature I need to support, I only need to add a few new tests to verify everything. In fact, as I solved the last few pitfalls I ran into, the test suite proved itself useful time and time again as every once in a while a minor change I would make would break everything. Being able to catch this before deploying the code to AWS was such a godsend.&lt;/p&gt;

&lt;h3&gt;Unit Testing&lt;/h3&gt;

&lt;p&gt;I had been trying to get into unit testing for a long time, but it only started clicking when I started writing tests for this existing codebase. I had previously read a bunch of tutorials and checked out a few demos, and they all sucked for one simple reason:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;it adds five together correctly&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...is NOT how real, production, web app code looks like. It&amp;#39;s the same problem with &amp;quot;Hello World&amp;quot; examples for new programming languages. Like, what is &lt;code&gt;print(&amp;quot;Hello world!&amp;quot;)&lt;/code&gt; supposed to tell me? How do I talk to a database with this? How do I send a HTTP request? How do I save something to disk or read a configuration file?&lt;/p&gt;

&lt;p&gt;Actually, more importantly, how do I NOT do all these things? Because when I&amp;#39;m unit testing my code, I don&amp;#39;t actually want it to hit a database or website. That&amp;#39;s the whole point of unit testing, I gather. You just fake everything and check to make sure the code is calling all the fake stuff correctly, so that in the real world it&amp;#39;ll do the same thing with all the real stuff. But now you have to figure out how to turn your &amp;quot;real&amp;quot; code into &amp;quot;fake&amp;quot; code without ever actually changing the code.&lt;/p&gt;

&lt;p&gt;But I continued in my testing journey, armed with &lt;a href=&quot;https://jestjs.io/&quot;&gt;Jest&lt;/a&gt; and dozens of Medium tutorials. Soon enough, my questions were answered with the almighty &lt;code&gt;jest.fn()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I&amp;#39;m not going to go too in depth with Jest unit testing, but the idea is something like this:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;accountExists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;it registers a new account correctly&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;accountExists&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;jest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;password&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toBeTruthy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;it fails to register an existing account&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;accountExists&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;jest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registerAccount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;password&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toBeFalsy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This way you could fake external functions so they would return the values you want to test your code against. This is also great for handling errors your code could run up against that wouldn&amp;#39;t usually occur in normal practice. You can just fake the error to make sure your code deals with it correctly.&lt;/p&gt;

&lt;p&gt;Of course, it&amp;#39;s important that those external functions do their job correctly too. For me, those external functions were the &lt;code&gt;utils&lt;/code&gt; suite which comprised of the following functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSONReply&lt;/li&gt;
&lt;li&gt;JSONError&lt;/li&gt;
&lt;li&gt;DynamoDocumentClient&lt;/li&gt;
&lt;li&gt;dynamo&lt;/li&gt;
&lt;li&gt;andiItem&lt;/li&gt;
&lt;li&gt;createConversation&lt;/li&gt;
&lt;li&gt;updateConversation&lt;/li&gt;
&lt;li&gt;addMessageToConversation&lt;/li&gt;
&lt;li&gt;getAllMessagesWith&lt;/li&gt;
&lt;li&gt;markUUIDUnread&lt;/li&gt;
&lt;li&gt;isUniqueRequest&lt;/li&gt;
&lt;li&gt;sendResponse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;JSONReply&lt;/code&gt; and &lt;code&gt;JSONError&lt;/code&gt; were just helper methods for constructing responses, so I didn&amp;#39;t bother testing them (I&amp;#39;m sure this will come back to bite me eventually).&lt;/p&gt;

&lt;p&gt;The next two were critical to solving the Fake Database Pitfall.&lt;/p&gt;

&lt;p&gt;The remaining functions all manipulated the database in some way, so I built integration tests for them.&lt;/p&gt;

&lt;h3&gt;Integration Tests&lt;/h3&gt;

&lt;p&gt;The difference between a unit test and an integration test is that a unit test tests a unit of code (like a function), while an integration test tests a system (compromised of units).&lt;/p&gt;

&lt;p&gt;To build off the previous example, a unit test would check if the &lt;code&gt;registerAccount()&lt;/code&gt; method returns the right value, while an integration test would run the method and then check the database to see if the new entry was created correctly. This way you&amp;#39;re not just confident that &amp;quot;the method works&amp;quot;, but you&amp;#39;re also confident that &amp;quot;this part of the service works.&amp;quot; Which is a fantastic thing to be confident about when delivering a service.&lt;/p&gt;

&lt;p&gt;In my case, I was interested in testing all the &lt;code&gt;utils&lt;/code&gt; functions to make sure they would create, read and update the correct items in DynamoDB when they were called. I would do this by having them send their queries to a local DynamoDB server, and then issuing read queries to the server from my tests to check to see if the resulting changes were the ones I intended.&lt;/p&gt;

&lt;p&gt;After the initial setup, writing the tests wasn&amp;#39;t all that bad. Here&amp;#39;s one for &lt;code&gt;markUUIDUnread&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;it&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;adds the uuid to the unread list when unread is true&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testUUID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;bababooey-markUUIDUnread&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;oldAndiItem&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;andiItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;markUUIDUnread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newAndiItem&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;andiItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newAndiItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;not&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;oldAndiItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newAndiItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unread&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toEqual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arrayContaining&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;testUUID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Reads more or less like English. Simple, concise, and reliable. Just like it should be.&lt;/p&gt;

&lt;h2&gt;Pitfalls&lt;/h2&gt;

&lt;p&gt;Okay, I&amp;#39;ve been mentioning these the whole article. Let&amp;#39;s open this can of worms. Here&amp;#39;s every pitfall I ran into building the live chat, in the order I ran into it.&lt;/p&gt;

&lt;h3&gt;Normal Databases Suck With Lambda Pitfall&lt;/h3&gt;

&lt;p&gt;This one I knew about already from previous experience, but figured it would be worth mentioning to further explain why I chose DynamoDB.&lt;/p&gt;

&lt;p&gt;Okay, remember VPS&amp;#39;s from the serverless crash course? Turns out Lambdas aren&amp;#39;t that different from them. In fact, whenever you call a Lambda for the first time, Amazon essentially spawns a VPS (they call it a container) for that Lambda to run on. Once it finishes running, Amazon keeps that container around for the next few minutes in case another request comes in.&lt;/p&gt;

&lt;p&gt;When a container exists for the Lambda, it&amp;#39;s said to be &amp;quot;hot&amp;quot; because it can immediately resolve incoming requests. When there are no containers left (because there were no incoming requests for a long time), it&amp;#39;s &amp;quot;cold.&amp;quot;&lt;/p&gt;

&lt;p&gt;You might be thinking that it takes a lot of time for Amazon to spin up a container when a function is cold. And indeed, it does take a lot more time to go from cold to hot, but that &amp;quot;startup cost&amp;quot; is only incurred when the function is cold. Depending on how hefty the function is the startup cost might range between a few milliseconds to a few seconds. To get around this and keep a function perpetually hot, some people set up timers (like with AWS CloudWatch) that send a sample request to the Lambda every 5 minutes to ensure it&amp;#39;s hot and ready to serve any real requests. This way, even if nobody has used the Lambda for months, it&amp;#39;ll still be ready the millisecond someone tries. Of course, you still have to pay the usage cost for all those fake requests, so it may always be a good idea.&lt;/p&gt;

&lt;p&gt;So, to get to the point, say you don&amp;#39;t want to use DynamoDB. Maybe you want to use AWS RDS (their other database service), which lets you use RDBMS systems you&amp;#39;re used to like MySQL or PostgreSQL. In order to do this, you have to put your Lambda function in a VPC (Virtual Public Cloud) with your RDS server. There&amp;#39;s no getting around this; you need your Lambda to be in a VPC to access RDS. And that wouldn&amp;#39;t be a problem if it didn&amp;#39;t mean three (three!) very bad things...&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In order to join the VPC, your Lambda container needs to be assigned an IP address through an ENI (Elastic Network Interface). So now, not only does it need to boot up, it also needs to resolve an IP address and connect to a private network &lt;em&gt;before&lt;/em&gt; it can go hot and serve your request. Considering that Lambda bills by every hundred milliseconds, this not only costs far more than regular functions but also takes up an incredible amount of time. For a real world example I&amp;#39;ve sometimes had Lambdas in VPCs take up to 15 seconds to respond to my requests (to play fair, sometimes it finished in 2-3 seconds too. Lambda is unpredictable). I&amp;#39;m sure this can get better, but I mean...how much better could it get? The fact is there&amp;#39;s a lot of hefty setup that needs to take place to join a VPC.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once you&amp;#39;ve joined the VPC, you lose access to the Internet. Jeff really screwed you on this one. If you&amp;#39;re lucky, you only need access to your database and have no 3rd party APIs you need to talk to. But, if you really need to, you can restore Internet access to your VPC by buying an AWS NAT Gateway for....\$0.045 / hour. Yes, per hour. Napkin math: &lt;code&gt;$0.045 * 24 hours * 30 days = $32.4 / month&lt;/code&gt;. You&amp;#39;re actually better off just paying for a server if you&amp;#39;re not getting a ton of traffic already.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connection pooling isn&amp;#39;t Lambda compatible. This one actually isn&amp;#39;t that bad &lt;em&gt;unless&lt;/em&gt; you intend for your service to get popular. The general premise of connection pooling is to set up only one connection to the database, and then reuse that connection for any queries that need executing. This way, instead of having to connect to the database everytime you want to run a query, you can use the pooled connection instead. Well, because of how Lambda scales horizontally, you instead get a thousand Lambda containers with a thousand individual connections to your singular RDS server. This would not be a problem if each database didn&amp;#39;t also have a limit to the maximum amount of connections. I think the limit for Postgres by default is 100, and although you can change it, there&amp;#39;s only so many connections one server can support before performance suffers. You can get around this, ironically, with another server - you can use something like &lt;a href=&quot;https://wiki.postgresql.org/wiki/PgBouncer&quot;&gt;pgBouncer&lt;/a&gt; to do the connection pooling for you, and have all your Lambdas hit your pgBouncer server instead. But at this point you&amp;#39;ve introduced a server into your serverless environment and everything stopped making sense a few hours ago anyways.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So yeah, normal databases suck with Lambda. That&amp;#39;s why I prefer to stick with Dynamo for ultra low cost projects. For something that I know will require SQL in the future? Probably best to stick with a server, and then break that down into smaller Lambdas once you have an existing codebase to inspect.&lt;/p&gt;

&lt;p&gt;I should mention that there&amp;#39;s a new RDS addition called &lt;a href=&quot;https://aws.amazon.com/rds/aurora/pricing/&quot;&gt;Aurora Serverless&lt;/a&gt; which was billed as the solution to this entire pitfall. It&amp;#39;s Postgres and MySQL compatible, and it only costs...\$0.041 an hour for the cheapest MySQL option. Now, \$29.52 / month isn&amp;#39;t an insane amount of money to pay for something like this, but again, think about whether just using a server would be a better idea at this point. Serverless requires a lot of special consideration and practical experience before it really clicks, and if you think it&amp;#39;s not any better than paying for a server, go for the server instead. Don&amp;#39;t feel bad! Amazon&amp;#39;s EC2 and Elastic Beanstalk teams will happily take your money if serverless isn&amp;#39;t your cup of tea.&lt;/p&gt;

&lt;h3&gt;Test Events Suck With JSON Pitfall&lt;/h3&gt;

&lt;p&gt;Few things are more annoying than having some weird error that keeps popping up regardless of what you do the code. In this case, I got to the point where the majority of my code was gone and I &lt;em&gt;still&lt;/em&gt; kept getting a dumb JSON error!&lt;/p&gt;

&lt;p&gt;One of the Lambda console&amp;#39;s features is something called &amp;quot;Test Events&amp;quot; where you can build your own JSON event and have Lambda invoke your function with that as its event. For example, here&amp;#39;s a test event to make sure the &lt;code&gt;hello&lt;/code&gt; action works right:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;requestContext&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;domainName&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;58yojgvxyi.execute-api.us-east-1.amazonaws.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;stage&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;dev&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;connectionId&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testConnection&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;identity&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;sourceIp&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;test.ip&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;body&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;{ &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;: &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;: &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bababooey&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;}&amp;quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Maybe you&amp;#39;ve spotted the error already, but if not, here&amp;#39;s the problem:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;body&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;{ &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;: &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;: &amp;quot;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bababooey&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;}&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Turns out you need backslashes:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;body&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;{ \&amp;quot;action\&amp;quot;: \&amp;quot;hello\&amp;quot;, \&amp;quot;uuid\&amp;quot;: \&amp;quot;bababooey\&amp;quot;}&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or else Lambda will think you meant to set &lt;code&gt;body&lt;/code&gt; to &amp;quot;{ \&amp;quot;.&lt;/p&gt;

&lt;p&gt;To be fair, this isn&amp;#39;t a Lambda centric problem, but it was something I struggled to fix for hours. So I&amp;#39;m putting it in here in the hopes that it saves someone else.&lt;/p&gt;

&lt;h3&gt;Jest Has Weird Setup Files Pitfall&lt;/h3&gt;

&lt;p&gt;When you configure jest in your &lt;code&gt;package.json&lt;/code&gt;, there&amp;#39;s a neat key called &lt;code&gt;setupFiles&lt;/code&gt; you can set to a group of files that get run once before any of your tests run. I set it to &lt;code&gt;setup.js&lt;/code&gt;. Inside &lt;code&gt;setup.js&lt;/code&gt;, you get access to the &lt;code&gt;global&lt;/code&gt; object which all of your tests subsequently have access to. So I defined a global &lt;code&gt;call&lt;/code&gt; method:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;domainName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;58yojgvxyi.execute-api.us-east-1.amazonaws.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;dev&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connectionId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testConnection&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;identity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sourceIp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;testRequestID&amp;quot;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lambda&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;....&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Which could then be called in any test file:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lambda&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;bababooey&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;hello&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This allowed me to fake whatever Lambda event I wanted without having to duplicate code across multiple tests.&lt;/p&gt;

&lt;p&gt;Once I did this, I also wanted to manually set &lt;code&gt;process.env.PASSWORD&lt;/code&gt; to a fake password at the beginning of the jest test session, so that I could provide it to &lt;code&gt;call&lt;/code&gt; to verify that the base handler forwarded my actions to the admin handler when I gave the right password. Seemed easy, right?&lt;/p&gt;

&lt;p&gt;Well, it just did not work at all. I tried a few variations and eventually went to search for it on Google, and turns out that only the globalSetup key can be used to set &lt;code&gt;process.env&lt;/code&gt; across test files for some reason. So, I aded the globalSetup key pointing towards &lt;code&gt;envSetup.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/*&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;  - setup.js exists because global.* variables can&amp;#39;t be declared in this file but they can be in there&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;  - on the other hand, process.env can&amp;#39;t be changed in there but it can in here&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;  - jest is confusing sometimes&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;*/&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PASSWORD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;password&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Maybe there&amp;#39;s a better way to do this, but this was the best I could do from my limited experience with Jest.&lt;/p&gt;

&lt;h3&gt;Fake Database Pitfall&lt;/h3&gt;

&lt;p&gt;In order for me to build integration tests, I needed an actual database to query against. I could have used a separate DynamoDB table as the test table in this situation, but I would rather run DynamoDB locally to avoid the risk of having any crazy situation where I end up getting charged \$900 because a function being tested inserted a hundred thousand items or something.&lt;/p&gt;

&lt;p&gt;First I needed a local DynamoDB server. For that, I installed &lt;a href=&quot;https://github.com/localstack/localstack&quot;&gt;LocalStack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, I needed a way to make the code use my DynamoDB server instead of AWS&amp;#39;s. I was able to do this with the &lt;code&gt;utils.DynamoDocumentClient&lt;/code&gt; object:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* utils.js */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dynamo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDocumentClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;TableName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;DuroLiveChat&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dynamo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDocumentClient&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;cm&quot;&gt;/* utils.test.js */&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoParams&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;2012-10-08&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;region&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;us-east-1&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;http://localhost:4569&amp;quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DDB&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ow&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AWS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DDC&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ow&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AWS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDB&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DocumentClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;beforeAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDocumentClient&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DDC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The important part here is that &lt;code&gt;utils.dynamo&lt;/code&gt; calls &lt;code&gt;module.exports.DynamoDocumentClient&lt;/code&gt; &lt;em&gt;instead of&lt;/em&gt; &lt;code&gt;DynamoDocumentClient&lt;/code&gt;. That makes it susceptible to replacement in the integration testing file. If &lt;code&gt;utils.dynamo&lt;/code&gt; called &lt;code&gt;DynamoDocumentClient&lt;/code&gt; instead, Node would have used the one declared at the beginning of &lt;code&gt;utils&lt;/code&gt; (pointing to the actual AWS DynamoDB) rather than the one with the custom endpoint.&lt;/p&gt;

&lt;p&gt;Also, shoutout to &lt;a href=&quot;https://github.com/aaronshaf/dynamodb-admin&quot;&gt;dynamodb-admin&lt;/a&gt; for providing a perfectly simple yet cable web interface to interact with the DynamoDB tables running on LocalStack. The only change I had to make was to define an environment variable before running it so it used the right port to talk to Dynamo:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;andi$ DYNAMO_ENDPOINT=http://localhost:4569 dynamodb-admin&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;Just Runs Tests Concurrently Pitfall&lt;/h3&gt;

&lt;p&gt;Also kind of knew about this one from past experience, but I think it&amp;#39;s important to mention because it really should influence the way you design tests, especially integration tests.&lt;/p&gt;

&lt;p&gt;DO NOT assume your test runs in order! That means if you insert an item into the database in &lt;code&gt;test1&lt;/code&gt; and try to retrieve it in &lt;code&gt;test2&lt;/code&gt;, you&amp;#39;re doing it wrong. Yes, even if the tests are in the same file.&lt;/p&gt;

&lt;p&gt;Try to keep every integration test self-contained. If possible, it should create, read, update, and delete every item within itself.&lt;/p&gt;

&lt;p&gt;The problem with concurrency is that you never know which test will get run first, so sometimes your test suite passes and sometimes it might fail even without changing a line of code. I got bit by this and had to rewrite a bunch of tests as a result.&lt;/p&gt;

&lt;h3&gt;Lambda Uses Old AWS SDK Versions Pitfall&lt;/h3&gt;

&lt;p&gt;Oh, this one sucks. What&amp;#39;s the latest AWS SDK version?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;2.437.0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So when I run &lt;code&gt;require(&amp;quot;AWS-SDK&amp;quot;)&lt;/code&gt; inside my Lambda running Node.js 8.10, I should get version &lt;code&gt;2.437.0&lt;/code&gt;, right?&lt;/p&gt;

&lt;p&gt;From &lt;a href=&quot;https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html&quot;&gt;AWS Lambda Runtimes&lt;/a&gt;:&lt;/p&gt;

&lt;h4&gt;Node.js Runtimes&lt;/h4&gt;

&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Node.js Version&lt;/th&gt;
&lt;th&gt;AWS SDK For JavaScript&lt;/th&gt;
&lt;th&gt;Operating System&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 10&lt;/td&gt;
&lt;td&gt;nodejs10.x&lt;/td&gt;
&lt;td&gt;10.15&lt;/td&gt;
&lt;td&gt;2.437.0&lt;/td&gt;
&lt;td&gt;Amazon Linux 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 8.10&lt;/td&gt;
&lt;td&gt;nodejs8.10&lt;/td&gt;
&lt;td&gt;10.15&lt;/td&gt;
&lt;td&gt;2.290.0&lt;/td&gt;
&lt;td&gt;Amazon Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;Sigh. This wouldn&amp;#39;t be a problem if AWS API Gateway WebSockets wasn&amp;#39;t a new feature when I first started building this, but it was, and so the &lt;code&gt;ApiGatewayManagementApi&lt;/code&gt; module which is used to send messages to WebSocket connections didn&amp;#39;t exist in the &lt;code&gt;2.290.0&lt;/code&gt; version of the AWS SDK. In the legacy live chat, I included the files needed to make this work manually in a custom &lt;code&gt;node_modules/aws-sdk&lt;/code&gt; folder. Knowing that the AWS SDK had updated since January, I figured the management API would come with it at this point. And well, it did, but not if you ran Node 8.10.&lt;/p&gt;

&lt;p&gt;This turned out to be the easiest pitfall to solve. I just switched my runtime to Node.js 10.x. However, I was really lucky that my code was able to handle this major version upgrade without needing to change a single line.&lt;/p&gt;

&lt;h3&gt;Lambda Hates .Zip Files Pitfall&lt;/h3&gt;

&lt;p&gt;Lambda has three ways to upload code to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;via the inline editor (which I used for the legacy live chat version)&lt;/li&gt;
&lt;li&gt;via a .zip file&lt;/li&gt;
&lt;li&gt;via a S3 bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I opted to use the zip file this time around because I could make one easily. It took a few tries to finally get it in a format Lambda accepted.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s what ended up working for me:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;zip duro.zip base.js utils.js;
aws lambda update-function-code --function-name duro --zip-file fileb://duro.zip;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Try unzipping your zip file before uploading it to Lambda if it doesn&amp;#39;t work the first time. Make sure the source code files are inside the zip file and not inside a &lt;em&gt;folder&lt;/em&gt; inside the zip file. Lambda won&amp;#39;t be able to find your handler through a folder.&lt;/p&gt;

&lt;p&gt;That means you want:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span&gt;&lt;/span&gt;duro.zip
&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;-&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;base.js
&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;-&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;utils.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And not:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-sh&quot; data-lang=&quot;sh&quot;&gt;&lt;span&gt;&lt;/span&gt;duro.zip
&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;-&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;duro
&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;-&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;base.js
&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;|&lt;/span&gt;-&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;utils.js
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At this point I was really excited because I had finally solved all the pitfalls in front of me, deployed a working version of the live chat to AWS Lambda, and had over 40 individual tests covering the entire code base.&lt;/p&gt;

&lt;p&gt;But then I had to learn a new word.&lt;/p&gt;

&lt;h3&gt;Idempotency Pitfall&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Q: What happens if my function fails while processing an event?&lt;/p&gt;

&lt;p&gt;For Amazon S3 bucket notifications and custom events, AWS Lambda will attempt execution of your function three times in the event of an error condition in your code or if you exceed a service or resource limit.
- &lt;a href=&quot;https://aws.amazon.com/lambda/faqs/&quot;&gt;AWS Lambda FAQ&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here&amp;#39;s the tricky part about this answer. You know that it&amp;#39;s possible for your function to be executed up to 3 times, but &lt;em&gt;only if it fails while processing an event.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was operating off the premise that if I had enough tests to cover every reasonable event I would throw at the function, it would &lt;em&gt;never&lt;/em&gt; fail while processing an event. And yes, I know that no amount of test suites can truly cover every scenario. But, still. It turned out, &lt;em&gt;my&lt;/em&gt; code wasn&amp;#39;t failing. Amazon&amp;#39;s was.&lt;/p&gt;

&lt;p&gt;I caught this particular issue because every once in a while while testing the live chat frontends (user and admin), I would notice a message would get sent two or three times. Sometimes I&amp;#39;d refresh the page and the same message would&amp;#39;ve been saved to the database twice. This was really odd, and also random. I couldn&amp;#39;t reliably trigger it. So instead I added logging to my function and spammed messages until I could get the error to happen again. And although it was random, it &lt;em&gt;did&lt;/em&gt; happen often enough to be a serious problem. It could definitely happen to a user while they were using the application.&lt;/p&gt;

&lt;p&gt;The problem turned out to be the &lt;code&gt;ApiGatewayManagementApi&lt;/code&gt;. I would get the console logs from before its &lt;code&gt;postToConnection&lt;/code&gt; method was called, but none after. The method would hang, and the Lambda function would get axed once it reached its timeout of 3 seconds. And once that happened, it would get called...again. And sometimes another time too. The problem is that before calling &lt;code&gt;postToConnection&lt;/code&gt;, the function also added the message to the database. This would explain why there were both duplicate sends as well as duplicate inserts.&lt;/p&gt;

&lt;p&gt;Now, to be fair, this may not be an actual issue with the &lt;code&gt;ApiGatewayManagementApi&lt;/code&gt;. It might be designed to fail for whatever reason, and then end up working when Lambda calls it again post-retry. But the problem is my code wasn&amp;#39;t idempotent, so I would end up with duplicates.&lt;/p&gt;

&lt;p&gt;Idempotency in this context means that you should only change things the first time you process an event. If you are given the same exact event again, you shouldn&amp;#39;t change anything.&lt;/p&gt;

&lt;p&gt;Most of the &lt;code&gt;utils&lt;/code&gt; methods were idempotent already, because they only read and returned items. They didn&amp;#39;t add or update existing items. But for the ones that did, I needed a way to tell if the request was a duplicate so I could avoid executing them a second time.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;utiis.isUniqueRequest()&lt;/code&gt; comes in. Every Lambda event also comes with a &lt;code&gt;requestId&lt;/code&gt; which is a unique identifier that identifies that particular event. If we save the &lt;code&gt;requestId&lt;/code&gt; into the database the first time the event is processed, we can check the database the second time the event is processed to see if we already processed it, and ignore it if that&amp;#39;s the case.&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// Lambda sometimes retries the same event if it didn&amp;#39;t work the first time, so this is here to prevent duplicate messages from being sent&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isUniqueRequest&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Key&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;convo&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dynamo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;get&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ProjectionExpression&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;lastRequestsServed, updates&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;convo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// unknown uuid&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;convo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updates&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;convo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// add new request&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shift&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// drop the oldest saved id if there are too many&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateParams&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;UpdateExpression&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;SET lastRequestsServed = :newSet, updates = updates + :one&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ConditionExpression&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;updates = :updates&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;:newSet&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;DynamoDocumentClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastRequestsServed&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;:updates&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;:one&amp;quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dynamo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;update&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateParams&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;ConditionalCheckFailedException&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;throw&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// the conversation was updated by the time we tried to update it, so just invalidate everything&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see we save the last 5 &lt;code&gt;requestIds&lt;/code&gt; processed to a set in the account item, and return true/false depending on if our &lt;code&gt;requestId&lt;/code&gt; is in that set.&lt;/p&gt;

&lt;p&gt;Technically we can have an event get retried after 5 others have already been processed, but the likelihood of that happening is so low it&amp;#39;s not worth accounting for. Especially because Lambda immediately retries the function once it fails the first time; there&amp;#39;s very little time delay between initial execution and retry.&lt;/p&gt;

&lt;p&gt;What&amp;#39;s &lt;code&gt;updates&lt;/code&gt; for? Well, turns out, you can have 2+ Lambda functions access the same Dynamo item at the same time. If the &lt;code&gt;lastRequestsServed&lt;/code&gt; set doesn&amp;#39;t include their &lt;code&gt;requestId&lt;/code&gt;, and they both have the same &lt;code&gt;requestId&lt;/code&gt;, both functions will conclude that the request is unique, even though it&amp;#39;s actually not. To prevent against this, we increment the &lt;code&gt;updates&lt;/code&gt; key and use a &lt;code&gt;ConditionExpression&lt;/code&gt; to tell DynamoDB to reject this update if the current value of &lt;code&gt;updates&lt;/code&gt; doesn&amp;#39;t match the one we have. This way, only one Lambda will get to claim the request ID is unique (as it should). All others running concurrently will report that it is a duplicate. This technique is not unique - it&amp;#39;s actually a duplicate of a common DynamoDB tactic called &lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.OptimisticLocking.html&quot;&gt;Optimistic Locking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I&amp;#39;d also like to thank the following two articles for holding my hand through figuring this out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://cloudonaut.io/your-lambda-function-might-execute-twice-deal-with-it/&quot;&gt;https://cloudonaut.io/your-lambda-function-might-execute-twice-deal-with-it/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://hackernoon.com/why-your-lambda-functions-may-be-doomed-to-fail-4ede5885824d&quot;&gt;https://hackernoon.com/why-your-lambda-functions-may-be-doomed-to-fail-4ede5885824d&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Too Many Arguments Pitfall&lt;/h3&gt;

&lt;p&gt;This one is not Lambda related at all, just a JavaScript optimization.&lt;/p&gt;

&lt;p&gt;I used to have methods that looked like this:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createConversation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;sendReponse&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The problem is, when you actually call them, it&amp;#39;s really obtuse to figure out which index is for which argument:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createConversation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;e1b3b0b4-1cd2-4a77-a74a-6df7e0bd820c&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;bababooey&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba@email.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connectionId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requestContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ipAddress&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Instead, accept an object instead. It&amp;#39;s much simpler to parse and also easier to add arguments in the future when you want to extend what the function does.&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createConversation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ip&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is some nice ES6+ syntax sugar that deconstructs the object for you. The following is another way to do the same thing:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createConversation&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And you can call it like so:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createConversation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;e1b3b0b4-1cd2-4a77-a74a-6df7e0bd820c&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;nickname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;bababooey&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;baba@email.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Much easier to follow. Named parameters are always welcome.&lt;/p&gt;

&lt;p&gt;Anyways, I wish I had followed this advice before I needed to rewrote most of my utils functions, which ended up breaking all my tests, which I also had to rewrite somewhat.&lt;/p&gt;

&lt;h3&gt;Connections Die When They Are Killed Pitfall&lt;/h3&gt;

&lt;p&gt;Last pitfall before the end! I&amp;#39;ll keep it short.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s how &lt;code&gt;ApiGatewayManagementApi.postToConnection()&lt;/code&gt; works:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-js&quot; data-lang=&quot;js&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ws&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;postToConnection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ConnectionId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;connectionString&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;ConnectionId&lt;/code&gt; is the most important part here. It refers to which WebSocket to send this data to, and if that WebSocket doesn&amp;#39;t exist or isn&amp;#39;t connected anymore, &lt;code&gt;postToConnection()&lt;/code&gt; throws an error.&lt;/p&gt;

&lt;p&gt;The legacy version of this sending function handled two scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The user was sending a message to the admin, so it would get the &lt;code&gt;ConnectionId&lt;/code&gt; from the admin account&amp;#39;s &lt;code&gt;connection&lt;/code&gt; key&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The admin was sending a message to the user, in which case they provided a &lt;code&gt;connectionTo&lt;/code&gt; key which would be passed to &lt;code&gt;postToConnection()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As you can imagine, this brittle system didn&amp;#39;t really work when the user switched WiFi networks or refreshed the page.&lt;/p&gt;

&lt;p&gt;The new system instead always takes the &lt;code&gt;ConnectionId&lt;/code&gt; from the user&amp;#39;s account, and makes sure it is updated frequently with the &lt;code&gt;ping&lt;/code&gt; action I talked about before. This way, as long as the live chat is open and running code locally, the system will continue being aware of what its connection ID is. Of course, the above code is wrapped in a try/catch in the event that sending still fails, but at least then we&amp;#39;ll know it&amp;#39;s because the user isn&amp;#39;t connected anymore and not because we passed the wrong &lt;code&gt;connectionId&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Congratulations on making it this far! I hope this article helped you figure out how you can use Serverless on a real, not-built-for-a-tutorial real-time service.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s the source code for everything: &lt;a href=&quot;https://github.com/nexuist/duro-chat&quot;&gt;https://github.com/nexuist/duro-chat&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside, you&amp;#39;ll find a &lt;code&gt;__tests__&lt;/code&gt; folder and a &lt;code&gt;legacy&lt;/code&gt; folder containing all my unit/integration tests and the legacy version of the live chat, respectively.&lt;/p&gt;

&lt;p&gt;If you want to try out Jest on your own machine, make sure you install LocalStack and have it running beforehand or all the integration tests will fail.&lt;/p&gt;

&lt;p&gt;If you find any better workarounds for the pitfalls, please don&amp;#39;t hesitate to let me know using the live chat tab in the sidebar ;)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Fixing Netlify SSL Certificates</title>
   <link href="https://duro.me//stories/2019/03/18/fixing-netlify-ssl-certificates"/>
   <updated>2019-03-18T09:00:00+00:00</updated>
   <id>https://duro.me/stories/2019/03/18/fixing-netlify-ssl-certificates</id>
   <content type="html">&lt;p&gt;I like &lt;a href=&quot;https://netlify.com&quot;&gt;Netlify&lt;/a&gt; a lot. It&amp;#39;s what I use to host this site and a bunch of other domains.&lt;/p&gt;

&lt;p&gt;One of the neat things they offer is SSL certificates through Let&amp;#39;s Encrypt. All you have to do is click a button.&lt;/p&gt;

&lt;p&gt;So a while back I &lt;em&gt;did&lt;/em&gt; click that button. Unfortunately, things didn&amp;#39;t go as planned. Both domains were marked as untrusted by Chrome and Safari. While you could still click proceed, that wouldn&amp;#39;t be a great user experience by any stretch of the imagination.&lt;/p&gt;

&lt;p&gt;After browsing Netlify&amp;#39;s help docs, I wasn&amp;#39;t able to find anything relating to my issue, so I wrote to their support email instead:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;Hello,

I have Let&amp;#39;s Encrypt set up for two domains: unreplied.app and duro.me. For both, it is saying that the certificates are invalid in Chrome and Safari.

Is there a way to regenerate my certificates, or am I doing something wrong with my DNS records?

Thanks,
Andi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I sent this email at 1:30AM and got a response promptly at 6AM (way to go Netlify!)&lt;/p&gt;

&lt;p&gt;While I was expecting a reply pointing out what I did wrong, I actually got quite a fulfilling explanation which is why I thought I&amp;#39;d share it in this post. Here&amp;#39;s the email in its entirety:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt; Hello Andi,

I got your certificates fixed.

Generally, the reason we are unable to provision a complete SSL certificate for your custom domain is that the DNS cache timeout value (TTL) for a record has not had time to expire (from your old settings) before you tried to use it with Netlify. Our SSL provider (https://letsencrypt.org) is unable to create certificates for names that have old cached values still in effect.

Depending on how you configure your domain, we may only attempt to fetch the certificate once - when you update your settings with the new domain name. Usually, if it is your first hostname on a site, we&amp;#39;ll try several times until we succeed.

If that process generates a partial certificate, that&amp;#39;s what you&amp;#39;re stuck with until you write in to support. I realize that this experience isn&amp;#39;t the greatest - but letting you repeatedly press a &amp;quot;refresh certificate&amp;quot; button would be worse as you&amp;#39;d likely get locked out of getting a certificate for a week, due to our SSL provider&amp;#39;s strict rate limits for re-requests.

The additional delay seems to have been enough to allow things to work right when I attempted to re-issue the certificate.

Please let me know if things are not working as expected now.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Basically, the process for procuring a Let&amp;#39;s Encrypt certificate for my domains failed and left me with a &lt;em&gt;partial certificate&lt;/em&gt; (I had no idea that was a possibility). This is why Chrome was throwing those mean errors.&lt;/p&gt;

&lt;p&gt;My awesome Netlify rep (Dennis) was able to restart the certificate process on his side and, since enough time had passed for my old DNS record values to be thrown out of cache (that&amp;#39;s the TTL he was talking about), it went through flawlessly and built the full certificates I was looking for.&lt;/p&gt;

&lt;p&gt;After that, I had no more SSL issues.&lt;/p&gt;

&lt;p&gt;The lesson learned here is to wait a while after pointing a domain&amp;#39;s DNS records to Netlify, before setting up that domain within Netlify. In these particular instances, I changed the DNS records for the domains with my registrar (Namecheap) and then immediately set them up in the site portal. Hence, Let&amp;#39;s Encrypt left me with partial certificates because each domains&amp;#39; records were in the process of changing.&lt;/p&gt;

&lt;p&gt;A big thanks goes out to Netlify&amp;#39;s fantastic support (especially Dennis) for remedying this situation quickly. I&amp;#39;m writing this post in the hopes that someone else who experiences this will now know what to do - just contact support.&lt;/p&gt;

&lt;p&gt;Have a great day!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>An Update on NowPlaying</title>
   <link href="https://duro.me//stories/2019/01/21/now-playing"/>
   <updated>2019-01-21T21:22:00+00:00</updated>
   <id>https://duro.me/stories/2019/01/21/now-playing</id>
   <content type="html">&lt;p&gt;Hi everyone! I hope all of you enjoyed the holiday season and had fun during your break.&lt;/p&gt;

&lt;p&gt;I wanted to write a small essay to describe the research I did since we last saw each other and the next steps I wanted to take with the project.&lt;/p&gt;

&lt;h2&gt;The Goal&lt;/h2&gt;

&lt;p&gt;We want to build a user-based service that allows any one user to share their music, live, with everyone else who wants to listen in.&lt;/p&gt;

&lt;p&gt;We plan to do this by using the Spotify API and various other Web technologies to provide a cohesive, cross platform solution. We would eventually monetize this service and use it as an income generator for the club.&lt;/p&gt;

&lt;p&gt;Variations of such an idea have already been suggested in the Spotify community forums:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://community.spotify.com/t5/Live-Ideas/Social-Real-Time-quot-Listen-with-Friend-s-quot/idi-p/100613&quot;&gt;https://community.spotify.com/t5/Live-Ideas/Social-Real-Time-quot-Listen-with-Friend-s-quot/idi-p/100613&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://community.spotify.com/t5/Closed-Ideas/All-Platforms-Social-Spotify-Live-Livestream-music-playlists/idi-p/1606512&quot;&gt;https://community.spotify.com/t5/Closed-Ideas/All-Platforms-Social-Spotify-Live-Livestream-music-playlists/idi-p/1606512&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With thousands of people upvoting these suggestions, it seems that the market wants something like NowPlaying.&lt;/p&gt;

&lt;h3&gt;The Name&lt;/h3&gt;

&lt;p&gt;We will call this service NowPlaying. Personally I still don&amp;#39;t think it&amp;#39;s a good fit for us, but while it&amp;#39;s in development the name isn&amp;#39;t that important. What &lt;em&gt;is&lt;/em&gt; important is to have a name that adequately reflects the primary adjectives we want to associate with our service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live&lt;/li&gt;
&lt;li&gt;Sharing&lt;/li&gt;
&lt;li&gt;Music&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anyone has any better suggestions for a name, I&amp;#39;m open to hearing them.&lt;/p&gt;

&lt;h2&gt;The Spotify APIs&lt;/h2&gt;

&lt;p&gt;There&amp;#39;s actually a bunch of them. I&amp;#39;ve compiled a list of the ones we would care about the most. Some are in beta, meaning they&amp;#39;ll be changing around and it may not be a great idea to base a product on top of them until they&amp;#39;ve left beta stage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://developer.spotify.com/documentation/web-api/reference/&quot;&gt;Web API&lt;/a&gt; - Provides access to the majority of the data you can access on Spotify, such as playlists, artists, tracks, and account specific data (who you&amp;#39;re following, your email, your library, etc.)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The most relevant portion of this to us is the &lt;a href=&quot;https://developer.spotify.com/documentation/web-api/reference/player/&quot;&gt;Player API (Beta)&lt;/a&gt;, also referred to as the &lt;a href=&quot;https://developer.spotify.com/documentation/web-api/guides/using-connect-web-api/&quot;&gt;Spotify Connect Web API&lt;/a&gt;. If you&amp;#39;re not aware, Spotify Connect is the technology that allows you to switch your playback source from your phone to your laptop/desktop/Alexa/etc. Using the web API, we can send commands to each of your Spotify devices, allowing us to remotely control what the Spotify app on every device is doing.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://developer.spotify.com/documentation/web-playback-sdk/quick-start/&quot;&gt;Web Playback API (Beta)&lt;/a&gt; - Lets us embed a Spotify player into our web pages. Spotify songs are encrypted through some unknown proprietary format (they&amp;#39;re not just streamed as MP3s), and this API is an official way to play Spotify songs in web apps without requiring the native Spotify app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unfortunately, this only works on desktops, as mobile devices are missing the cryptographical components that allow the Web Playback API to decrypt song streams. As far as I can tell, there are no plans to make this work with mobile devices. In addition, it will not work in Safari for Mac. See the &lt;em&gt;Supported Browsers&lt;/em&gt; section for more information.&lt;/li&gt;
&lt;li&gt;&amp;quot;This SDK must not be used in commercial projects without Spotify&amp;#39;s prior written approval&amp;quot;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/spotify/ios-streaming-sdk/&quot;&gt;iOS/Android Streaming SDK&lt;/a&gt; - No longer actively developed, but was the primary way to integrate Spotify into mobile apps. Offered the same functionality as the Web API and the Web Playback API combined.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...And that brings us to the latest addition to the lineup: App Remote SDK.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.spotify.com/documentation/ios/&quot;&gt;App Remote SDK&lt;/a&gt; - Released in September 2018, so relatively recent. Its release was accompanied by this &lt;a href=&quot;https://developer.spotify.com/community/news/2018/09/14/updates-mobile-platform/&quot;&gt;blog post&lt;/a&gt; on the Spotify blog. Some important paragraphs:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Earlier this year, we released beta versions of the App Remote SDK for iOS and Android. Unlike our previous playback SDKs, the Android and iOS SDK now allow you to “remote control” playback in the Spotify app. As a result, you can control playback for all users (not just Premium) and provide offline playback.&lt;/p&gt;

&lt;p&gt;Also as of this release, the iOS SDK is out of beta. This means that we consider it to be stable, and able to be used confidently in your integrations.&lt;/p&gt;

&lt;p&gt;The Android SDK remains in beta, and we will provide updates on this blog.&lt;/p&gt;

&lt;p&gt;Note that these [Android and iOS] streaming SDKs are not actively maintained and no additional work on these are planned. We therefore urge you to work with the new SDKs going forward.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a big deal. With the old mobile SDKs gone, App Remote is what we&amp;#39;ll have to use if we ever want to deliver native apps.&lt;/p&gt;

&lt;h2&gt;Which API For Us?&lt;/h2&gt;

&lt;p&gt;We wanted NowPlaying to be cross platform - to work on both desktop and mobile devices. We wanted to do this using their Web Playback API. But there are some big problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It doesn&amp;#39;t work on mobile at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It can&amp;#39;t be used for commercial projects (hopefully, us) without talking to Spotify&amp;#39;s legal team.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, Web Playback is out. What about Web/Spotify Connect?&lt;/p&gt;

&lt;p&gt;It allows us to remote control the native app on desktop and mobile. And indeed, we could implement all of NowPlaying using a web app and the Connect API. But.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API is in beta, so we have no idea if it will stick around or change heavily in later Spotify versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The whole API is rate limited. This, in itself, invalidates the Connect API as a choice for us.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Why not rate limiting?&lt;/h3&gt;

&lt;p&gt;Just a quick primer for those who are unfamiliar. As you are probably aware, computing is a finite resource, and companies like Spotify have to build or rent massive data centers to process all the requests they get every second. To prevent their services from failing due to overuse, Spotify implements per-second request rate limiting. This means, as a user of the Spotify Web API, you can only send X requests per second before Spotify will block your request and refuse to execute it. When this happens, you&amp;#39;re given a &lt;code&gt;Retry-After&lt;/code&gt; header, which contains the amount of seconds you need to wait before Spotify will accept requests from your application again. During this duration, you cannot use the Spotify Web API &lt;em&gt;at all&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I tried finding more concrete details on the limits, but it seems like they&amp;#39;re &lt;a href=&quot;https://stackoverflow.com/questions/30548073/spotify-web-api-rate-limits&quot;&gt;prone to changing and undocumented&lt;/a&gt;. The guy in that post said to expect something like 10-20 requests / second, which at first glance seems like a lot, but when you do the math, it wouldn&amp;#39;t work for us.&lt;/p&gt;

&lt;p&gt;Let&amp;#39;s assume the rate limit is set to 20 requests a second. That&amp;#39;s 1,200 requests a minute, or 72,000 an hour, which is &lt;em&gt;huge.&lt;/em&gt; But it&amp;#39;s not enough for NowPlaying.&lt;/p&gt;

&lt;p&gt;This is because NowPlaying implements live streaming. That is, for every little change you make in your Spotify app, &lt;em&gt;every single one of your followers&lt;/em&gt; also need to have that change performed to &lt;em&gt;their&lt;/em&gt; Spotify app. This requires at minimum one request per user.&lt;/p&gt;

&lt;p&gt;We only get 20 requests a second. If any single person on our platform gains more than 20 followers at a time (or if more than 20 people stream at the same time that have at least one follower), we can no longer keep their followers in sync with the streamer. We would get rate limited every time they changed anything. Obviously, that&amp;#39;s not acceptable.&lt;/p&gt;

&lt;p&gt;The limit can apparently be lifted but I have never heard of anyone who has successfully done so, or if doing so would require entering a contract with Spotify or paying them royalties. I found &lt;a href=&quot;https://github.com/spotify/web-api/issues/804&quot;&gt;this post&lt;/a&gt; where a Spotify employee tells the poster to contact Spotify legal, but no information on if that would cost anything.&lt;/p&gt;

&lt;p&gt;We&amp;#39;re not the only ones who have to deal with this problem. One of the &lt;a href=&quot;https://github.com/spotify/web-api/issues/492&quot;&gt;most popular requests in Spotify&amp;#39;s issue tracker&lt;/a&gt; is a way to receive real time player updates without having to poll the &lt;code&gt;Player&lt;/code&gt; endpoint (each poll attempt would use up a request, and we&amp;#39;d have to poll it at a frequency that seems akin to real time). Discord seems to have a private, custom solution built for them, but I doubt that deal was done for free.&lt;/p&gt;

&lt;p&gt;Regardless, even if we got realtime updates, it still wouldn&amp;#39;t solve the problem of us having to update every follower&amp;#39;s player in real time.&lt;/p&gt;

&lt;p&gt;So, with Web Playback and Web/Spotify Connect out, that leaves us with the mobile SDKs. And with the old iOS and Android SDKs now being unsupported, that leaves us with...App Remote.&lt;/p&gt;

&lt;h2&gt;We&amp;#39;re Using App Remote&lt;/h2&gt;

&lt;p&gt;And with that, abandoning our plans of building a web app. App Remote is a native-only solution, so we can only use it on iOS and Android.&lt;/p&gt;

&lt;p&gt;I wish there was an easy way to make this work on Web, but until the Web Playback SDK comes through or the rate limiting situation is drastically changed (both of which I highly doubt), this is what we have to deal with.&lt;/p&gt;

&lt;p&gt;So now we have to build mobile apps. This is obviously a huge change in the direction of the project, which is why I wanted to write up this letter and distribute it to you all before our first meeting.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s how I see it working.&lt;/p&gt;

&lt;h3&gt;WebSockets&lt;/h3&gt;

&lt;p&gt;This part is necessary just so you understand the next section.&lt;/p&gt;

&lt;p&gt;Typically the way computers communicate on the Web is through a protocol called HTTP. It&amp;#39;s based on a request/response architecture, meaning that you send a request, and the server sends back a response. You can think of this as talking to Siri. When you ask Siri a question, she&amp;#39;ll give you an answer, but Siri would never pop up on your phone unprompted and ask &lt;em&gt;you&lt;/em&gt; a question or let &lt;em&gt;you&lt;/em&gt; know of a new event that happened. This architecture is great for requesting &lt;em&gt;documents&lt;/em&gt; like the Web was originally meant for, but terrible for building interactive apps since there&amp;#39;s no way to allow for bidirectional communcation.&lt;/p&gt;

&lt;p&gt;WebSockets is a Web standard that allows real time communication between various devices. These devices can be Web browsers, mobile phones, laptops, desktops, etc.
You can think of WebSockets as texting, for computers. It&amp;#39;s how they can have conversations with each other.&lt;/p&gt;

&lt;h3&gt;App Remote &amp;amp; Us&lt;/h3&gt;

&lt;p&gt;NowPlaying would be its own app that uses the App Remote SDK to talk to the Spotify app on the user&amp;#39;s phone. When NowPlaying is opened, it establishes a WebSocket connection to our NowPlaying service and also a separate connection to the Spotify app. From here, updating player states between multiple devices is easy: just send a message to the WebSocket connection each device maintains with our service, and have the NowPlaying app forward that message to the App Remote SDK.&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s a visualization of what a typical NowPlaying session could look like between 10 devices, where one device is streaming and the others are following.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NowPlaying App (Device 1) -&amp;gt; NowPlaying Service: Hello. I represent Spotify user &amp;lt;whoever is logged into Spotify on that device&amp;gt;.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Device 1) -&amp;gt; App Remote SDK -&amp;gt; Spotify App: What song are we playing right now?&lt;/p&gt;

&lt;p&gt;Spotify App -&amp;gt; App Remote SDK -&amp;gt; NowPlaying App (Device 1): We&amp;#39;re playing &amp;quot;Thank You For The Music&amp;quot; by ABBA, currently at 0:38.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Device 1) -&amp;gt; NowPlaying Service: I want to stream now. I&amp;#39;m streaming &amp;quot;Thank You For The Music&amp;quot; by ABBA, currently at 0:38.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Devices 2...10) -&amp;gt; NowPlaying Service: I want to follow Device 1!&lt;/p&gt;

&lt;p&gt;NowPlaying Service -&amp;gt; NowPlaying App (Devices 2..10): Okay! Device 1 is currently streaming &amp;quot;Thank You For The Music&amp;quot; by ABBA, currently at 0:38.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Devices 2..10) -&amp;gt; App Remote SDK -&amp;gt; Spotify App: Play &amp;quot;Thank You For The Music&amp;quot; by ABBA, starting from 0:38.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...And if the user on Device 1 changes something in the Spotify app, say they skip ahead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Spotify App -&amp;gt; App Remote SDK -&amp;gt; NowPlaying App (Device 1): Heads up. The user changed the player state. We&amp;#39;re now playing &amp;quot;Thank You For The Music&amp;quot; by ABBA at 1:12, not 0:38 anymore.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Device 1) -&amp;gt; NowPlaying Service: Hey. I&amp;#39;m changing my stream. I&amp;#39;m streaming &amp;quot;Thank You For The Music&amp;quot; by ABBA at 1:12 now.&lt;/p&gt;

&lt;p&gt;NowPlaying Service -&amp;gt; NowPlaying App (Devices 2..10): Device 1 is changing their stream. They&amp;#39;re going to play &amp;quot;Thank You For The Music&amp;quot; by ABBA at 1:12 now.&lt;/p&gt;

&lt;p&gt;NowPlaying App (Devices 2..10) -&amp;gt; App Remote SDK -&amp;gt; Spotify App: Change of plans. Play &amp;quot;Thank You For The Music&amp;quot; by ABBA at 1:12 now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because we&amp;#39;re not directly touching Spotify&amp;#39;s services, there is no rate limit. This is infinitely scalable, and we don&amp;#39;t need to pay Spotify anything to use the App Remote SDK or WebSockets between our own private apps.&lt;/p&gt;

&lt;h2&gt;Hosting The WebSocket Server&lt;/h2&gt;

&lt;p&gt;There are a variety of ways to do this but I am leaning towards &lt;a href=&quot;https://aws.amazon.com/api-gateway/&quot;&gt;AWS API Gateway&lt;/a&gt; which just recently came out with WS support. API Gateway has no servers involved, which reduces the complexity of running our service by a huge magnitude. We pay for connection minutes (number of minutes each client is connected) and total number of requests. You can look at the pricing tab for more information, but the basic gist is that we pay $1 per million requests and $0.25 per million connection minutes. I just used API Gateway to implement live chat on my own personal website, so I now have enough experience to figure out how to make it work for NowPlaying as well.&lt;/p&gt;

&lt;h2&gt;Building The Mobile App&lt;/h2&gt;

&lt;p&gt;Again, various ways we could do this, but I am leaning towards &lt;a href=&quot;https://facebook.github.io/react-native/&quot;&gt;React Native&lt;/a&gt;. RN is a cross platform mobile development framework for iOS and Android that allows you to write apps in JavaScript. It is supported by Facebook and used in their FB Market and Messenger apps as well as the Facebook app itself.&lt;/p&gt;

&lt;p&gt;It&amp;#39;s important to know that the App Remote SDK itself only works fully on iOS. The Android counterpart is in beta, meaning it&amp;#39;s being actively developed, but will take a bit to reach a stable stage.&lt;/p&gt;

&lt;p&gt;There is one more barrier stopping us: the App Remote SDK is written in Objective-C, while RN uses JavaScript. To get these to play together, we have to use a &lt;em&gt;native module bridge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I set out to build NowPlaying over break, I realized I would have to build that bridge myself. And while I broke some ground to try to get the two frameworks co-operating, I soon found &lt;a href=&quot;https://github.com/lufinkey/react-native-spotify&quot;&gt;react-native-spotify&lt;/a&gt; which appears to be a more complete, community driven Spotify API implementation. Right now it conforms to the old streaming SDK so it&amp;#39;s not immediately useful to us but &lt;a href=&quot;https://github.com/lufinkey/react-native-spotify/issues/78&quot;&gt;work is underway&lt;/a&gt; to get it up to date with the new App Remote SDK.&lt;/p&gt;

&lt;p&gt;Once this project is complete within the next few weeks, we will have everything required to implement NowPlaying in a simple, straightforward, scalable, and long lasting manner.&lt;/p&gt;

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

&lt;p&gt;NowPlaying is switching from a Web app to a mobile app. This not only allows us to leverage the App Remote SDK, avoid rate limiting, and provide a real, truly live experience, but it opens up the possibility of using other technologies in the future, such as push notifications and geolocation tracking.&lt;/p&gt;

&lt;p&gt;We are going to use AWS API Gateway to offer a WebSocket service to connect our various clients together and build the business logic that makes NowPlaying possible.&lt;/p&gt;

&lt;p&gt;Our clients will be React Native applications that use the Spotify App Remote SDK to talk to the Spotify app on each user&amp;#39;s device. They&amp;#39;ll connect to the WebSocket service to receive orders and allow us to remote control Spotify.&lt;/p&gt;

&lt;p&gt;We are waiting on the &lt;code&gt;react-native-spotify&lt;/code&gt; package to finish adding the App Remote SDK before starting development on the mobile app. Once that package implements App Remote, we can finally break ground and bring the idea to reality.&lt;/p&gt;

&lt;p&gt;This is the plan as I see it. Of course I&amp;#39;d love to gather feedback from you all to decide how to proceed. Once again, I hope you had a great break and I look forward to discussing this in person at our next meeting.&lt;/p&gt;

&lt;p&gt;Andi&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Announcing Live Chat</title>
   <link href="https://duro.me//stories/2019/01/10/live-chat"/>
   <updated>2019-01-10T09:00:00+00:00</updated>
   <id>https://duro.me/stories/2019/01/10/live-chat</id>
   <content type="html">&lt;blockquote&gt;
&lt;p&gt;UPDATE: Live Chat has been rebuilt from the ground up! Check out the &lt;a href=&quot;/stories/2019/05/26/updated-live-chat&quot;&gt;new article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Live Chat is a duro.me feature that lets you talk to me pretty much instantly. Before you read the rest of this article, &lt;a href=&quot;/live&quot;&gt;go ahead, try it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because sometimes I want to ask a question, but I don&amp;#39;t want to go through the whole process of drafting a good-looking email, logging into Twitter, finding my old Facebook login, etc... it&amp;#39;s too much of a hassle. And that hassle is a lost opportunity! As a developer and a person, talking to anyone - literally anyone - can help you see things in a different way. And whether it&amp;#39;s a spelling misake in a blog post or a critical bug in a product I&amp;#39;ve shipped, I want to know about it as soon as I can.&lt;/p&gt;

&lt;p&gt;What can you talk about? Well, anything, for starters. But I&amp;#39;m particularly interested in what you&amp;#39;re working on and what you&amp;#39;re struggling with - maybe I could help.&lt;/p&gt;

&lt;h2&gt;Development&lt;/h2&gt;

&lt;p&gt;This apparently ended up being really long. So, if you want, you can &lt;a href=&quot;#conclusion&quot;&gt;skip&lt;/a&gt;. No worries, I won&amp;#39;t judge.&lt;/p&gt;

&lt;h3&gt;1/10/19&lt;/h3&gt;

&lt;p&gt;This is &lt;em&gt;live&lt;/em&gt;. This is &lt;em&gt;raw&lt;/em&gt;. And so is this post! I&amp;#39;m typing it as I develop the actual feature, so this should be interesting to look back on. I&amp;#39;ve never tried anything like this before.&lt;/p&gt;

&lt;p&gt;4 AM - Obviously chat has been done before, a million times over, by pretty much every web dev at some point during their career. But this is different; namely, I have a budget of about five dollars, &lt;em&gt;total&lt;/em&gt;, a total dev time limit of about a day, and you can only talk to one person: me. Let&amp;#39;s get started.&lt;/p&gt;

&lt;p&gt;4:45 AM - Starting with frontend. I&amp;#39;ve built the CSS/HTML for the header containing the online indicator, last online time, and email/Twitter links. I might end up using Vue or something to make the frontend dynamic.&lt;/p&gt;

&lt;p&gt;5 AM - I&amp;#39;ve come to the realization that I&amp;#39;ll also need an admin portal for this. Somewhat of an afterthought, but we go on!&lt;/p&gt;

&lt;p&gt;5:30 AM - Redoing header a bit to make it look nicer on mobile. I&amp;#39;ve also got the chat body and footer in the right places.&lt;/p&gt;

&lt;p&gt;6 AM - I&amp;#39;ve found &lt;a href=&quot;https://leaverou.github.io/bubbly/&quot;&gt;Bubbly&lt;/a&gt; which makes is super easy to make chat bubbles. Thanks, leaverou!&lt;/p&gt;

&lt;p&gt;6:13 AM - Taking a break for breakfast.&lt;/p&gt;

&lt;p&gt;9:17 AM - Starting up again.&lt;/p&gt;

&lt;p&gt;9:36 AM - I&amp;#39;ve figured out to and from message bubbles. Onto the footer.&lt;/p&gt;

&lt;p&gt;10:23 AM - Refactored CSS so I could have different font sizes for things on mobile.&lt;/p&gt;

&lt;p&gt;10:28 AM - Tested the site on my iPhone for the first time. Noticed a rendering bug with the sidebar, so dealing with that now.&lt;/p&gt;

&lt;p&gt;10:32 AM - Thankfully was able to reproduce the issue on desktop Safari and found an easy fix. Safari was oddly respecting the 0 font size for sidebar elements when in mobile mode - the text is supposed to disappear. Well, it wasn&amp;#39;t there, but its placement was still rendered, so the icons were pushed out of the center for seemingly no reason. I fixed this by wrapping the text in a span and just using &lt;code&gt;display: none&lt;/code&gt; on it. Back to chat.&lt;/p&gt;

&lt;p&gt;10:34 AM - The frontend styling is done, I think! Works well on mobile too. Next step: make it dynamic. I&amp;#39;m thinking of using React or Vue for this, and I&amp;#39;m leaning towards Vue because I can just drop it into the page without any custom tooling.&lt;/p&gt;

&lt;p&gt;10:50 AM - Interesting predicament. Both Liquid (preprocessed by Jekyll) and Vue (processed by the user&amp;#39;s browser) use  &lt;code&gt;{{ double brackets }}&lt;/code&gt;  to specify expressions that should be rendered. If I put that in my HTML, Liquid will pick it up and fail to process it since it&amp;#39;s meant for Vue and just leave the result empty. Thankfully, there&amp;#39;s an easy fix: &lt;a href=&quot;https://stackoverflow.com/a/43506751&quot;&gt;the delimiter property&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;10:55 AM - I accidentally solved the problem in a better way. In an attempt to show you the  &lt;code&gt;{{ double brackets }}&lt;/code&gt;  thing in the last update, I realized that Liquid ate that, too, and any attempts to escape the expression didn&amp;#39;t work. Googling how to fix that so I could show you Liquid rendering expressions in a Liquid-rendered page led to this: &lt;a href=&quot;https://stackoverflow.com/a/7585479&quot;&gt;Outputting Literal Curly Braces in Liquid Templates
&lt;/a&gt;, and thus the &lt;code&gt;{% raw %}&lt;/code&gt; tag (yes, I had to escape that too). Ultimately what this means is I can just put my whole page inside a raw tag and not have to worry about Liquid accidentally rendering it and breaking my Vue app.&lt;/p&gt;

&lt;p&gt;11:20 AM - I wanted the ability to drop links in messages and have them get rendered as clickable links. I found a Vue library named &lt;code&gt;vue-linkify&lt;/code&gt;, but was having trouble getting it working, until I found &lt;a href=&quot;https://github.com/phanan/vue-linkify/issues/2#issuecomment-396179918&quot;&gt;this guy&amp;#39;s tiny solution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;11:24 AM - Finished the Vue app, minus the part where it talks to the backend. That&amp;#39;s what I&amp;#39;m working on when I come back to this.&lt;/p&gt;

&lt;h3&gt;1/12/19&lt;/h3&gt;

&lt;!-- DEPLOY: that separate project was MOMPDF. Write abt it and link it --&gt;

&lt;p&gt;10:23 AM - Had to work on a separate project for a bit. Back to chat.&lt;/p&gt;

&lt;p&gt;11:30 AM - Fighting against API Gateway to get a WebSocket API off the ground. I&amp;#39;ve managed to get it to execute a Lambda and return the result, so I should be able to implement the rest of what I need with this one function.&lt;/p&gt;

&lt;h3&gt;1/13/19&lt;/h3&gt;

&lt;p&gt;9 AM - I&amp;#39;ve made a DynamoDB table to hold conversations. I&amp;#39;m planning on using a UUID as the primary key, and an incrementing number for the sort key. The idea is that you can send a message containing a UUID and my Lambda function will append it to the bottom of the list. Then, when you need to load the conversation list at the beginning of the session, I can return the list of all messages with one Query call. I&amp;#39;m going to try playing with the AWS CLI to mock up the Dynamo actions I want, and then it should be pretty easy to translate those to Node parameters.&lt;/p&gt;

&lt;p&gt;9:04 AM - After doing more research into how I could auto increment sort key, I learned that apparently Dynamo doesn&amp;#39;t like auto incrementing numbers and that there are better solutions, like a UUID based off of timestamps. That&amp;#39;s fine; after thinking it over for a bit I realized I can just use the timestamp itself as my sort key (since time is auto incrementing) and achieve the sa,e result. I don&amp;#39;t even need to include a timestamp field now, since the sort key will function as that. I&amp;#39;ll remake the table with UUID/timestamp keys instead (I wish there was a way to rename them without tearing down the whole table, but oh well, I didn&amp;#39;t have anything in it anyways).&lt;/p&gt;

&lt;p&gt;9:48 AM - Currently playing with API Gateway and Lambda to see if I can get connection IDs into Dynamo like the &lt;a href=&quot;https://aws.amazon.com/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway/&quot;&gt;example&lt;/a&gt;. Because I want to fit everything into one function, I&amp;#39;ll be doing all my work in the inline Lambda editor.&lt;/p&gt;

&lt;p&gt;10:07 AM - Want a way to link a connectionID with a UUID in onConnect, but there&amp;#39;s no way to specify a message before first connecting to the WebSocket.&lt;/p&gt;

&lt;p&gt;11:22 AM - Bought some pizza dough and now back to this. Rather than dealing with connect/disconnect events, I&amp;#39;ve decided to just have everything get processed through message events. I can include the connectionID with every message stored, and then I&amp;#39;ll always have the most recent connectionID available to use (for my admin backend). This means that connectionIDs in previous messages will become stale as soon as the connection is dead, resulting in a lot of useless values stored, but hey, storage is cheap.&lt;/p&gt;

&lt;p&gt;11:35 AM - I figure people might want an option to get notified whenever I respond if they navigate away from the page. I looked into web push notifications, but iOS Safari doesn&amp;#39;t support them, so that&amp;#39;s a dead end. I&amp;#39;m signing my domain up for AWS SES (Simple Email Service) so I can send emails from my Lambda whenever I reply to someone who has their email configured and their WebSocket connection is closed. I guess I&amp;#39;ll need to modify the frontend to support this too.&lt;/p&gt;

&lt;p&gt;1:33PM - Now exploring AWS SES for the first time. Verified my domain and filed a support ticket to get my account out of sandbox mode so I can actually send emails to real people.&lt;/p&gt;

&lt;p&gt;2:30 PM - Getting into deep specifics with Dynamo now, I&amp;#39;ve come to the conclusion that a frontend web GUI (think phpMyAdmin) to execute queries in would be a huge boon to development times. Oh well, maybe for another time.&lt;/p&gt;

&lt;p&gt;2:53 PM - I&amp;#39;ve cracked the code and figured out how to store user emails and nicknames (what I&amp;#39;m calling metadata). This was a bit confusing at first because I wanted it all to go into one message with a timestamp of -1, but that message may not exist yet if this was the first time the user was changing their metadata. The query parameters I&amp;#39;m using now will allow that key to be automatically made and then have values inside it set if that&amp;#39;s the case.&lt;/p&gt;

&lt;p&gt;6:37 PM - Took some breaks in between, but I spent the better part of 3 hours painfully learning that &lt;a href=&quot;https://stackoverflow.com/a/44860804/5084415&quot;&gt;single quotes are not valid JSON&lt;/a&gt;. This kept breaking &lt;code&gt;JSON.parse()&lt;/code&gt;, leading to me trying to figure out WTF happened to Lambda that it suddenly couldn&amp;#39;t parse incoming event bodies. Oh well, that&amp;#39;s software dev.&lt;/p&gt;

&lt;p&gt;6:40 PM - I&amp;#39;ve got message sending &amp;amp; email/nickname changing down. I&amp;#39;m going out to get sushi; when I come back I&amp;#39;ll figure out retrieving message history and my admin portal (and also link the frontend to all of this somewhere between there). I really want to get this done today because it&amp;#39;s blocking my progress on the rest of the site. Given that I&amp;#39;ve paved enough ground to figure out this much, the rest of it should be a breeze (famous last words).&lt;/p&gt;

&lt;h3&gt;1/14/19&lt;/h3&gt;

&lt;p&gt;11:40 AM - I&amp;#39;ve successfully gotten the first messages sent through the frontend. At some point yesterday I also realized I won&amp;#39;t need a &lt;code&gt;list-messages&lt;/code&gt; action because I can store them locally along with the UUID. If you clear local storage and lose the UUID, it makes sense to lose the conversation history as well.&lt;/p&gt;

&lt;p&gt;12:45 PM - I found a bug (well, not really, but it&amp;#39;s an aesthetic thing) where if you get too many messages they scroll off your screen. I fixed this already with &lt;code&gt;overflow: scroll&lt;/code&gt;, but as it stands you have to manually scroll down when this happens. I need to automate that.&lt;/p&gt;

&lt;p&gt;12:51 PM - From the client, you can now send and receive messages, change your nickname, and change your email. The client is effectively finished (barring one or two features), now I&amp;#39;m going to figure out how the admin portal works. The features remaining are the &amp;quot;last online&amp;quot; info bar at the top and testing if receiving messages works (I haven&amp;#39;t yet because I have no admin backend to send messages from).&lt;/p&gt;

&lt;h3&gt;1/15/19&lt;/h3&gt;

&lt;p&gt;11:18 AM - Decided to build the admin portal under &lt;a href=&quot;/andi&quot;&gt;/andi&lt;/a&gt;. You can&amp;#39;t interact with it, of course, but you can check out the source code. It&amp;#39;s secured with a local, client side password.&lt;/p&gt;

&lt;p&gt;2:08 PM - After coding for a fewi hours, I took some time to draw out what I wanted the backend to look like. I&amp;#39;ve also decided to scrap the &lt;code&gt;andi&lt;/code&gt; specific page because there wasn&amp;#39;t enough difference to justify two seaprate codebases, and just do everything on the one frontend file.&lt;/p&gt;

&lt;p&gt;5:36 PM - I&amp;#39;ve finally gotten a reply to show up in the admin portal from a user session. A bunch of other stuff happened in between then; hastily trying to wrap up loose ends and get everything into a shippable state.&lt;/p&gt;

&lt;p&gt;6:45 PM - Abandoning the &lt;code&gt;timestamp: 0&lt;/code&gt; metadata storage method. Opting to just send all of it with every message - storage is cheap, retrieval/processing is expensive.&lt;/p&gt;

&lt;p&gt;7:47 PM - Finally locking down the backend. Only doing frontend work now, as I think I&amp;#39;ve got everything needed for both client side and admin side portals sans email sending functionality.&lt;/p&gt;

&lt;p&gt;10:42 PM - Calling it a night. Admin portal needs work.&lt;/p&gt;

&lt;h3&gt;1/16/19&lt;/h3&gt;

&lt;p&gt;10:46 AM - Rewriting frontend and backend.&lt;/p&gt;

&lt;p&gt;2:25 PM - Done.&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;A wild ride, right? Sometimes it ends up like this. What was supposed to take a few hours ended up taking 6 days. But hey, it got done in the end. And that is all that matters.&lt;/p&gt;

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

&lt;p&gt;So yeah, I built my own chat portal using AWS. If there&amp;#39;s interest I can also do a technical writeup on how everything works together to make Live Chat, since it was all built more or less from scratch. Let me know using the live chat! (see how handy this is now?)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Announcing This Website</title>
   <link href="https://duro.me//stories/2019/01/09/new-site"/>
   <updated>2019-01-09T07:00:00+00:00</updated>
   <id>https://duro.me/stories/2019/01/09/new-site</id>
   <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;

&lt;p&gt;This article is being written on January 9th, 2019, but I officially started the new redesign for this site on November 22nd, at 2 o&amp;#39; clock in the morning, which is obviously the prime time for great ideas.&lt;/p&gt;

&lt;p&gt;Nonetheless, if you&amp;#39;re reading this post, it&amp;#39;s here!&lt;/p&gt;

&lt;p&gt;Here&amp;#39;s the old site for reference:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://s3.amazonaws.com/duro.me/old.png&quot; alt=&quot;Old duro.me&quot;&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Coming soon, copyright 2017 (that tweet ended up being the only one on there).&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Why wasn&amp;#39;t it good enough?&lt;/p&gt;

&lt;h3&gt;The Tabs&lt;/h3&gt;

&lt;p&gt;There was no obvious way to throw new projects into it. Where would I put, well, anything? A tab at the top? But anything more than a couple tabs quickly becomes a nightmare to navigate. Here&amp;#39;s AWS&amp;#39;s old product menu for reference:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://cdn-images-1.medium.com/max/1600/1*GbC9pSSCiL7HB7gHepYyUQ.png&quot; alt=&quot;Old AWS Products Page&quot;&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;This alone kept me from using AWS for, like, two years. How can you possibly get started with that?&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;So the tab-per-project thing is out. I could have made a Projects tab, but then, isn&amp;#39;t Unreplied technically a project? And, of course, it is, but I wanted it to have its own page: duro.me/unreplied looked better than duro.me/projects/unreplied.&lt;/p&gt;

&lt;p&gt;The obvious solution to avoid this was to just throw Unreplied onto its own domain: &lt;a href=&quot;https://unreplied.app&quot;&gt;https://unreplied.app&lt;/a&gt;. If you&amp;#39;re reading this, I might have already done that. I opted not to do this in the beginning because domains require hosting and hosting costs money. I was taking advantage of GitHub pages to host duro.me for free, but that only worked for one subdomain. I couldn&amp;#39;t have nexuist.github.io &lt;em&gt;and&lt;/em&gt; unreplied.github.io, and I couldn&amp;#39;t have &lt;a href=&quot;https://unreplied.app&quot;&gt;https://unreplied.app&lt;/a&gt; redirect to nexuist.github.io/projects/unreplied seemlessly (i.e. having it say unreplied.app in the address bar). Today, I know about &lt;a href=&quot;https://netlify.com&quot;&gt;Netlify&lt;/a&gt;, and that&amp;#39;s probably what I&amp;#39;ll use to address this.&lt;/p&gt;

&lt;h3&gt;The Colors&lt;/h3&gt;

&lt;p&gt;...Aren&amp;#39;t mine. They&amp;#39;re actually the default for &lt;a href=&quot;https://bulma.io&quot;&gt;Bulma&lt;/a&gt;, the CSS framework I used to build the old site. Or, at least, they were, Bulma received a pretty significant overhaul last year that now makes my site look even more outdated.&lt;/p&gt;

&lt;p&gt;I thought it looked fine at the time, and to me it still kind of does, but having used dark mode in VSCode and now system wide in macOS Mojave, it&amp;#39;s just nicer on the eyes. So this redesign is dark.&lt;/p&gt;

&lt;h3&gt;The Concept&lt;/h3&gt;

&lt;p&gt;The old site wasn&amp;#39;t really &lt;em&gt;meant&lt;/em&gt; to be anything. It was just a humble page on the Internet about me. But it did a terrible job of conveying what cool things I&amp;#39;d done in the past, who I was outside of software dev, or any stories I wanted to share. You&amp;#39;d take a glance at it and figure out that I was a geek but not much more.&lt;/p&gt;

&lt;p&gt;So I built this blog instead. It&amp;#39;s powered by &lt;a href=&quot;https://jekyllrb.com&quot;&gt;Jekyll&lt;/a&gt; which should make it really easy to add new content to the site, and boy, do I have content to add.&lt;/p&gt;

&lt;p&gt;A few words about Jekyll. It&amp;#39;s a static microsite generator written in Ruby, focused on blog platforms. What does that mean? Well, I have a &lt;code&gt;_posts&lt;/code&gt; directory, and to make a new post I simply create a new Markdown file (this post is also in Markdown).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don&amp;#39;t know about Markdown: check out the &lt;a href=&quot;https://daringfireball.net/projects/markdown/&quot;&gt;original inception post on Daring Fireball&lt;/a&gt;. It&amp;#39;s stupid easy to understand, super powerful, and available on basically anything you can type into.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Jekyll takes over the rest and applies themes, preprocesses &lt;a href=&quot;https://shopify.github.io/liquid/&quot;&gt;Liquid&lt;/a&gt;, generates HTML, compiles SASS, builds a prod directory, etc. Very easy and takes away most of the stress of administrating a blog since, well, it&amp;#39;s really just a folder of files you&amp;#39;re throwing onto a server somewhere. Not much that can go wrong! (famous last words)&lt;/p&gt;

&lt;p&gt;But it&amp;#39;s not meant to be &lt;em&gt;just&lt;/em&gt; a blog. Unlike previous iterations of my Web presence, I wanted this to be interactive in a way that most personal sites aren&amp;#39;t. I wanted someone who just found out about me to be able to spark up a conversation about anything they wanted, and get a reply as soon as I was able to respond, straight from the site. &lt;a href=&quot;/stories/announcement/announcing-live-chat&quot;&gt;Announcing Live Chat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also wanted a way to stay in touch with readers even after they&amp;#39;ve left the site. So, I did what a lot of blog owners do, and started a email newsletter. The catch?&lt;/p&gt;

&lt;p&gt;Well, I don&amp;#39;t know if it counts as a catch, but you only get emailed whenever I make a post. No daily, weekly, monthly schedule, because I&amp;#39;m sure you&amp;#39;re a busy person too, and you don&amp;#39;t need me jumping into your inbox every day with whatever I could scrape up from Reddit. Only the important stuff! &lt;a href=&quot;/newsletter&quot;&gt;Learn more and subscribe&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;Fin&lt;/h3&gt;

&lt;p&gt;I remember the good old days when I was just starting out with web dev and trying to make my first version of a portfolio showcase - only to realize that, oh hey, I have nothing to show yet. Now I have the opposite problem: too much to show, not enough time to show it! I hope this redesign is the first step in the right direction.&lt;/p&gt;
</content>
 </entry>
 

</feed>
