<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jeremy Kahn</title>
    <description>The latest articles on Forem by Jeremy Kahn (@jeremyckahn).</description>
    <link>https://forem.com/jeremyckahn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F454951%2Fa9182287-de2e-49b5-980f-8404a8d0b5a4.jpeg</url>
      <title>Forem: Jeremy Kahn</title>
      <link>https://forem.com/jeremyckahn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jeremyckahn"/>
    <language>en</language>
    <item>
      <title>The Agony and Ecstasy of PWAs</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Sun, 05 Nov 2023 16:33:52 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/the-agony-and-ecstasy-of-pwas-2enn</link>
      <guid>https://forem.com/jeremyckahn/the-agony-and-ecstasy-of-pwas-2enn</guid>
      <description>&lt;p&gt;&lt;em&gt;This was &lt;a href="https://jeremyckahn.github.io/posts/pwa-agony-and-ecstasy/"&gt;originally posted on my principal blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I love &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps"&gt;PWA technology&lt;/a&gt;. I think it's the most powerful and empowering technology that has come along since the creation of the internet. And it's never going to succeed.&lt;/p&gt;

&lt;p&gt;Why is this? How could a great technology utterly fail to make meaningful market share inroads? Let's explore why PWAs are awesome, and why nobody cares.&lt;/p&gt;

&lt;h2&gt;
  
  
  User-centric design
&lt;/h2&gt;

&lt;p&gt;Progressive Web Apps (PWAs) are literally just web pages with some additional functionality to make them behave like a native application. The key components of this illusion are the ability to install the web page such that it appears alongside actually-native apps like Microsoft Paint or Apple Messages, and some level of offline functionality. When opened, PWAs render in a dedicated window as their own app, distinct from any browser. Under the hood they're just another browser tab, but they present as a fully-formed, installed application.&lt;/p&gt;

&lt;p&gt;Because PWAs are web pages at their core, they natively offer all of the benefits of a standard web page. That includes &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility"&gt;accessibility&lt;/a&gt;, &lt;a href="https://wiki.mozilla.org/Security/Sandbox#Overview"&gt;security sandboxing&lt;/a&gt;, the potential to be customized via browser extensions, and observability via browser devtools. Every mainstream web browser has devtools that allow the user to take a look at the code that's running on their device. Most people won't do this, but the important thing is that they &lt;em&gt;can&lt;/em&gt;. Observability is a de facto right of web users that cannot be taken away by web developers&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. And this is wonderful. I can't think of any other mainstream technology platform that puts the user's interests ahead of the developer's in this way. Because of this, PWAs are inherently user-centric in a manner that native apps uniformly choose not to be.&lt;/p&gt;

&lt;p&gt;In general the tech industry has sort of lost the plot of what we're trying to accomplish. Computers are magical and amazing things, and they could do so much great stuff for people. Unfortunately "tech" has been largely co-opted by VCs into being a wealth extraction mechanism, so user-centric design has taken a back seat to investor-centric design. This has led to widespread &lt;a href="https://en.wikipedia.org/wiki/Enshittification"&gt;enshittification&lt;/a&gt; and a general degradation of product quality. I don't see this changing in the near future, but the user-centric design built in at the platform level of PWAs at least gives us a hope that it someday could.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple deployment
&lt;/h2&gt;

&lt;p&gt;Not only do PWAs offer the most user-centric design, but they have the most developer-centric deployment mechanism available as well: The web! Depending on how a project is set up, deployment could be as simple as modifying a file on a server. By contrast: If you want to update a native app, you need to submit your change to the app store operator (Apple, Google, etc.), wait, and pray that they accept it. Native app developers have no real control over the software that is delivered to their users because app store operators could capriciously deny updates for any reason. &lt;em&gt;They&lt;/em&gt; own the deployment platform.&lt;/p&gt;

&lt;p&gt;With web technology, the server is the deployment platform. You could run one for free from home, if you'd like. The web also has no gatekeepers or censors. There is no inherent payment processor that skims 30% off of every transaction. The web lets you own and control the code, the infrastructure, and the content. And the best part? PWAs run on every device! There's only one build target. It is the most successful implementation of "&lt;a href="https://en.wikipedia.org/wiki/Write_once,_run_anywhere"&gt;write once, run everywhere&lt;/a&gt;" in history. I struggle to conceive of a better way to ship software.&lt;/p&gt;

&lt;p&gt;So far I've extolled the myriad virtues of PWAs and you're hopefully as excited about them as I am. If you are, prepare to be disappointed!&lt;/p&gt;

&lt;p&gt;PWAs have struggled to gain meaningful app market share, and I don't expect that to ever really change. There are two main reasons for this: Gatekeeping incumbents, and user expectations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corporate incentives stand in the way
&lt;/h2&gt;

&lt;p&gt;Apple effectively invented the concept of a modern app store, and it makes them billions of dollars a year. Many people seem to consider their practices unfair. I don't quite see it that way (nobody is requiring developers to publish with Apple), but I do see what such a market position means for us. iOS and its derivatives offer only one web browser&lt;sup id="fnref2"&gt;2&lt;/sup&gt;, Safari. The problem is that Safari has always lagged behind the other major browsers in its feature set. This holds iOS back, and it therefore holds the potential for PWAs back as well because it's such a major platform. After all, why would a developer invest resources into a project if it won't work properly on &lt;a href="https://www.statista.com/statistics/272698/global-market-share-held-by-mobile-operating-systems-since-2009/"&gt;over a quarter of mobile devices&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;Apple has an obvious incentive to limit its browser's capabilities: Apple can't monetize the web. Being the cunning profit-driven corporation that it is, Apple is going to seek to widen its various &lt;a href="https://www.investopedia.com/ask/answers/05/economicmoat.asp"&gt;moats&lt;/a&gt; as much as possible. Why invest in a technology that doesn't produce revenue when you could invest in one that does? It's hard to fault their logic, but the end result hurts users because it disincentivizes developers from creating PWAs and therefore diminishes user freedom.&lt;/p&gt;

&lt;h2&gt;
  
  
  User expectations
&lt;/h2&gt;

&lt;p&gt;PWAs, as a concept, don't make sense to the average person. PWA installation is effectively a power user feature because typical users don't even consider the concept. Computer users have been implicitly trained for decades to think of web browsers as a means of basic content access and to rely on installed native apps for more advanced work and engagement. Can you honestly think of anyone in your life (who isn't a web developer or technology enthusiast) that would default to installing an app via the web rather than their app store?&lt;/p&gt;

&lt;p&gt;This isn't actually a technology or design problem. No, it's a &lt;em&gt;marketing&lt;/em&gt; problem. Marketing problems can only be solved with money, and there isn't a clear ROI in solving this particular one. So things will likely remain this way.&lt;/p&gt;

&lt;p&gt;"We just need an app store for PWAs!" I hear you say. Bad news: It's been tried and it hasn't succeeded. &lt;a href="https://appsco.pe/"&gt;Appscope&lt;/a&gt; has been abandoned and nobody is talking about &lt;a href="https://store.app/"&gt;store.app&lt;/a&gt;. I &lt;em&gt;want&lt;/em&gt; these stores to win. I just don't see the business model to support them, though.&lt;/p&gt;

&lt;p&gt;I really want to be wrong. I want to see PWAs flourish and render native apps irrelevant. But there needs to be a killer business model behind them for it to happen. Without that, the primary audience for PWAs is other web developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we can do
&lt;/h2&gt;

&lt;p&gt;The good news is that PWAs aren't going anywhere. It's a web standard that's in wide deployment. It's an effective technology choice for hobby projects and side hustles. I don't see Apple ceding its market share any time soon, but that doesn't mean that we can't enjoy PWAs today and into the future. I think &lt;strong&gt;we just have to accept that it will always be a less successful platform, especially for Apple users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I plan to keep making open source PWAs because it's such an effective solution for &lt;a href="https://github.com/jeremyckahn/"&gt;the hobby projects I like to build&lt;/a&gt;. I encourage you to do the same. If you value developer control and user freedom, PWAs are the obvious choice.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;It's worth mentioning that developers can (and generally should, for performance reasons) minify their code before deploying it. This makes it less than trivial to figure out what the code is doing, but the point is that it's possible to do so, rather than impossible (or &lt;a href="https://www.nowsecure.com/blog/2021/09/08/basics-of-reverse-engineering-ios-mobile-apps/"&gt;harder&lt;/a&gt;, anyways) like it is with native apps. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Yes, I know there's other browsers on the iOS App Store. But due to Apple's restrictions, they're implemented as reskins of Safari and therefore cannot implement the web platform features that Safari is missing. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>pwa</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Adding a chat room to any app with one line of code</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Sun, 08 Oct 2023 19:19:47 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/adding-free-anonymous-chat-to-any-web-with-one-line-of-code-44l7</link>
      <guid>https://forem.com/jeremyckahn/adding-free-anonymous-chat-to-any-web-with-one-line-of-code-44l7</guid>
      <description>&lt;p&gt;I just added a new feature to Chitchatter that I'm really excited about!&lt;/p&gt;

&lt;p&gt;If you haven't seen it before, &lt;a href="https://chitchatter.im/"&gt;Chitchatter&lt;/a&gt; is an &lt;a href="https://github.com/jeremyckahn/chitchatter"&gt;open source&lt;/a&gt; web app that instantly enables private, anonymous communication between people. Once connected you can easily chat, share audio and video, and transfer files. All communication is encrypted and peer-to-peer.&lt;/p&gt;

&lt;p&gt;The feature I just added is &lt;strong&gt;chat room embedding&lt;/strong&gt;. Much like a YouTube video, you can now embed a Chitchatter room into any web app. Here's a demo of an embedded Chitchatter room being loaded via CodePen:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/jeremyckahn/embed/BavvPvK?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;To create your own embedded chat room: Go to &lt;a href="https://chitchatter.im/"&gt;https://chitchatter.im/&lt;/a&gt;, enter a room name (or use the unique default), and click "Get embed code." Copy and paste the presented code snippet into your HTML code and you're done.&lt;/p&gt;

&lt;p&gt;That's it! Happy chatting. 🙂&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>news</category>
    </item>
    <item>
      <title>Taking the Power Back with Web Meshes</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Sat, 26 Nov 2022 20:38:02 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/taking-the-power-back-with-web-meshes-omg</link>
      <guid>https://forem.com/jeremyckahn/taking-the-power-back-with-web-meshes-omg</guid>
      <description>&lt;p&gt;We live in a strange time. As we trade independence for convenience, control of the web is steadily being concentrated in the hands of a few major tech companies. Platforms like AWS have become the defacto standard for architecting apps. When we cede control to for-profit megacorporations, we lose the ability to control our own destiny and do things in the collective interest.&lt;/p&gt;

&lt;p&gt;Web apps have historically been designed around a central service. The service is the sun, and clients orbit around it. This architecture is generally good. It enables highly-available systems and a good user experience. One tradeoff with this is that users have to hope that the service will be operated with the user's best interests in mind. This has become an increasingly bad bet, as you don't have to look far to find examples of it unfairly creating &lt;a href="https://www.businessinsider.com/google-reported-dad-police-photos-sick-sons-penis-child-abuse-2022-8" rel="noopener noreferrer"&gt;catastrophic problems&lt;/a&gt; for people. We don't have to settle for this state of affairs. We have to rethink the traditional relationship between web apps and servers, but there is a solution to this problem of service dependence.&lt;/p&gt;

&lt;p&gt;Don't worry, this is not a Web3 manifesto.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://indieweb.social/@jeremyckahn/109356983550735329" rel="noopener noreferrer"&gt;Web browsers are unbelievably powerful these days&lt;/a&gt;. They are featureful and performant enough to meet many users' needs entirely client-side. Central services will always be necessary for applications that require reliable access to massive data sources (think Google Search or Spotify), but there are a lot of applications that don't strictly require such data access. Many web apps' purpose is to connect users, and it is generally assumed that a central service is needed to enable the connection. But what if it wasn't? What if people could connect with each other without relying on a central service?&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing: Web meshes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Peer-to-peer" rel="noopener noreferrer"&gt;P2P&lt;/a&gt; is nothing new. It is a long-established means of connecting two or more people directly over a network. Web browsers are very capable of a wide range of P2P connections. Many apps use &lt;a href="https://webrtc.org/" rel="noopener noreferrer"&gt;WebRTC&lt;/a&gt; to enhance realtime apps, but it is still an underutilized technology. Even with WebRTC, many apps are designed around the dependence on a central app server with WebRTC performing a user experience enhancement. Web meshes turn this idea on its head: Instead of using P2P connections to &lt;em&gt;enhance&lt;/em&gt; the user experience, what if P2P connections were the &lt;em&gt;foundation&lt;/em&gt; of the user experience? In other words, &lt;strong&gt;what if there was no central server&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Decentralized architecture has been explored time and again to varying degrees of success. Typically it is discussed in terms of distributed server nodes. &lt;strong&gt;Web meshes take this a step further and treat each user's browser as a node by which the network is established&lt;/strong&gt;. In other words, users' browsers connect with each other rather than going through a server.&lt;/p&gt;

&lt;p&gt;The primary benefit to cutting out dependence on a server is that truly private data transmission becomes possible. Privacy is inherently valuable and it cannot be guaranteed when data flows through a third-party server. Many services make bold claims about how responsibly they handle user data, but as a user all you can really do is take the service operator at their word and hope for the best. This isn't good enough in a world where &lt;a href="https://www.npr.org/2022/08/12/1117092169/nebraska-cops-used-facebook-messages-to-investigate-an-alleged-illegal-abortion" rel="noopener noreferrer"&gt;service operators are known to act against users' best interests&lt;/a&gt;. When sensitive data never touches a service provider to begin with, there can be no risk of its misuse or mishandling.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Web meshes work
&lt;/h2&gt;

&lt;p&gt;First, I need to give credit to &lt;a href="https://oxism.com/" rel="noopener noreferrer"&gt;Dan Motzenbecker&lt;/a&gt; and his incredible work with the &lt;a href="https://github.com/dmotz/trystero" rel="noopener noreferrer"&gt;Trystero&lt;/a&gt; JavaScript library. Trystero serves as the direct inspiration for the web mesh concept. Trystero is one of the most powerful and fascinating libraries I've ever found and I don't understand how it doesn't have many thousands of stars on GitHub. Please give Trystero a look and consider how you might use it in your own projects.&lt;/p&gt;

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

&lt;p&gt;A web mesh starts by a browser connecting to one or more &lt;a href="https://webtorrent.io/" rel="noopener noreferrer"&gt;WebTorrent&lt;/a&gt; servers and offering its &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/SDP" rel="noopener noreferrer"&gt;SDP&lt;/a&gt; data. This SDP data contains information about how to connect to the offering browser via WebRTC. It also requests SDP data for any other browser that has already connected to the WebTorrent server similarly. These SDP data offers and requests are scoped to a specific, client-determined namespace (or "room") so that users only connect with others that have the same namespace/room name. Once SDP data is exchanged, browsers can then connect directly to each other. Each browser maintains a connection to the WebTorrent trackers to automatically connect to other browsers that try to connect to the room similarly later, but that is the only type of dynamic client/server interaction.&lt;/p&gt;

&lt;p&gt;This effectively turns the WebTorrent servers into &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#stun" rel="noopener noreferrer"&gt;STUN servers&lt;/a&gt;. There is significant value in additionally setting up a proper &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#turn" rel="noopener noreferrer"&gt;TURN server&lt;/a&gt; to ensure a reliable connection between browser peers, but it isn't strictly necessary unless any peers are on a local network that prevents a P2P connection (which is all but guaranteed to happen in practice).&lt;/p&gt;

&lt;p&gt;After the initial peer connections are established, the WebTorrent server's job is done. From this point, the web mesh has been created and connected browsers communicate directly with each other. Users can join and leave the mesh on an adhoc basis provided that they each know the room name beforehand. When all users leave the room, the mesh no longer exists until it is recreated via WebTorrent SDP exchange like it was initially.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web meshes in practice
&lt;/h2&gt;

&lt;p&gt;I discovered web mesh architecture by building &lt;a href="https://chitchatter.im/" rel="noopener noreferrer"&gt;Chitchatter&lt;/a&gt;, a privacy-focused anonymous chat app. Chitchatter connects peers to a chat room where they can communicate via text, video, and audio, as well as do screen and file sharing. My goal for this project was to enable people to communicate simply and safely, and I was able to achieve that by building it as a web mesh.&lt;/p&gt;

&lt;p&gt;When you first get to the Chitchatter home page, a client-side generated UUID is used as the default room name. When users enter the room, they join the web mesh as described above. If there were already people in the room, the new user will be connected to that room's web mesh. &lt;strong&gt;A Chitchatter "room" doesn't exist in terms of records in a database&lt;/strong&gt;. As a web mesh, a Chitchatter room is nothing more than a named conceptual construct for the connection between peers.&lt;/p&gt;

&lt;p&gt;There are many more potential use cases for this sort of design such as collaboration apps (like a decentralized Google Docs or Figma-like app) and multiplayer games.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why web meshes?
&lt;/h2&gt;

&lt;p&gt;P2P apps aren't novel, but there is a significant user benefit in restricting the connections to just web browsers. Modern browsers are &lt;a href="https://www.lambdatest.com/blog/browser-sandboxing/" rel="noopener noreferrer"&gt;sandboxed&lt;/a&gt; from the rest of the OS, so users are guarded against system compromises due to malicious code and bugs.&lt;/p&gt;

&lt;p&gt;Another benefit to designing around web browsers is the backbone of any web app: The URL. Users shouldn't need a service account to run code that's downloaded on their machine, but they will need to know how to find others to connect to. The room name can be embedded into the URL so that users just need to share their unique room link in order to connect.&lt;/p&gt;

&lt;p&gt;Web meshes change the relationship between browsers and servers. Aside from the generic WebTorrent and TURN servers that establish the peer connections, the app's server is reduced to being a static asset delivery mechanism that is cheap and easy to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web mesh advantages
&lt;/h2&gt;

&lt;p&gt;Web meshes are designed for maximum user benefit. Rather than creating reliance on service operators like more traditional web architectures, web meshes minimize the relationship between the client and server as much as possible. Sensitive information is never provided to service operators to misuse or mishandle. It is only ever passed back and forth between peers in &lt;a href="https://webrtc-security.github.io/" rel="noopener noreferrer"&gt;a secure manner&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Web meshes are resistant to censorship and surveillance. Any sort of web monitoring would require intervention at the server level, but web meshes have no dynamic server component to intervene with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web mesh disadvantages
&lt;/h2&gt;

&lt;p&gt;Web meshes are not a silver bullet, as they have a number of limitations that must be considered before adopting the design. They characteristically suffer from a lack of data availability and durability, as there is no cloud component to store data in. Users have come to expect reliable access to their data, so that would need to be considered and managed appropriately for any app designed around a web mesh. Because of this, &lt;strong&gt;web meshes would not be a good fit for many business needs&lt;/strong&gt;. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API" rel="noopener noreferrer"&gt;IndexedDB&lt;/a&gt; offers an excellent solution for persisting data to disk locally, but it's not geo-redundant.&lt;/p&gt;

&lt;p&gt;Web meshes only work so long as a P2P connection can be established. Well-configured TURN servers are the solution to this problem, but they can be costly to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Power to the people
&lt;/h2&gt;

&lt;p&gt;Web meshes are a great fit for projects that prioritize benefit for users over developers or service operators. With Trystero, they are an effective way to connect people in a safe and private way. As more and more software concentrates power in the hands of a few service operators, it's important to build ways for people to connect, share ideas, and organize. Web meshes put the user first, and that's the way that software should work.&lt;/p&gt;

&lt;p&gt;To channel the late &lt;a href="https://www.youtube.com/watch?v=zlQvMp5rB6g&amp;amp;t=201s" rel="noopener noreferrer"&gt;Steve Jobs&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It is now 2022. It appears Big Tech wants it all. The open web is perceived to be the only hope to offer Big Tech a run for their money. Users, initially welcoming Big Tech with open arms, now fear a Big Tech dominated and controlled future. They are increasingly and desperately turning back to the web as the only force that can ensure their future freedom. Big Tech wants it all. Will Big Tech dominate the entire computer industry? The entire information age? Was George Orwell right?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/qKSNABST4b0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Making LeetCode interviews more inclusive</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Sat, 22 Jan 2022 23:49:21 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/making-leetcode-interviews-more-inclusive-1nch</link>
      <guid>https://forem.com/jeremyckahn/making-leetcode-interviews-more-inclusive-1nch</guid>
      <description>&lt;p&gt;Something that has plagued me throughout my career is &lt;a href="https://leetcode.com/"&gt;LeetCode&lt;/a&gt;-style interview challenges. For whatever reason, most of the tech industry has converged on this format as the canonical litmus test for whether an individual can code, or whether they are an Imposter Programmer. This is a shame, because LeetCode-style interviews are deeply flawed: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An astonishing amount of false negatives. See: All the jobs you were qualified for but missed out on simply because you choked in the coding challenge.&lt;/li&gt;
&lt;li&gt;Even more false positives. See: All the engineer coworkers you've ever had that were difficult to work with.&lt;/li&gt;
&lt;li&gt;They are exclusionary to the point of being discriminatory against &lt;a href="https://www.understood.org/articles/en/neurodiversity-what-you-need-to-know"&gt;neurodivergent people&lt;/a&gt;. (This is the one that I'm personally most bothered by, because I am Autistic and therefore considered neurodivergent.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zooming out a bit, we all deserve better than this ineffective form of interviewing. This post will attempt to illustrate the many flaws with LeetCode interviews and offer some simple and more effective alternatives to use instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  WhatCode?
&lt;/h2&gt;

&lt;p&gt;When I talk about LeetCode-style challenges, I'm talking about live-coding challenges where you're given a prompt like this, on the spot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Given a string s, return the longest palindromic substring in s.&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @param {string} s
 * @return {string}
 */&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;longestPalindrome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This is a &lt;a href="https://leetcode.com/problems/longest-palindromic-substring/"&gt;verbatim example from LeetCode&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;It's not an unreasonably difficult challenge, and I estimate that nearly all professional programmers could solve it given the necessary resources and time. The issue is that interview candidates are given a specific time limit (typically 45-60 minutes), and are actively scrutinized while they work. When they complete the challenge, they are given another one repeatedly until time is up. The experience is every bit as nerve-wracking as it sounds, and the sheer anxiety causes many people to freeze up and &lt;em&gt;appear&lt;/em&gt; as though they genuinely can't code. However, that is typically not the case, as candidates probably wouldn't have gotten far enough in their career to be having the interview to begin with.&lt;/p&gt;

&lt;p&gt;The case for having these sorts of trials is typically "to see how the candidate thinks." This focus on the "how" obscures the "what" of the end result. &lt;strong&gt;Most companies are not in the business of selling how their employees think. They are in the business of selling what they produce.&lt;/strong&gt; So wouldn't that be the better thing to focus on in an interview?&lt;/p&gt;

&lt;h2&gt;
  
  
  Interview exclusion
&lt;/h2&gt;

&lt;p&gt;LeetCode-style interviews, by design, filter for a very specific type of mind: One that can performatively produce complete and working code on the spot. This would be completely acceptable if that's what an engineering job actually entailed, but this is not the case. I've met many, many tremendously talented engineers from many walks of life in my career, and &lt;em&gt;none&lt;/em&gt; of them work this way. The way actual, professional code is generally produced is by some mixture of quietly mulling over a problem, maybe going for a walk, talking it out with a coworker, sketching out some ideas, but most importantly: Iterating over the course of hours, days, or even weeks depending on the scale of the problem. No engineer regularly produces production-grade work in under an hour in their day job.&lt;/p&gt;

&lt;p&gt;Some among us can summon the energy and the strength to conjure working code into existence within a time limit and under active scrutiny, but that is a &lt;strong&gt;subset&lt;/strong&gt; of the engineers that are worth hiring. The &lt;strong&gt;whole&lt;/strong&gt; includes introverts, people with social anxiety, people who didn't sleep well the night before the interview because they were nervous about it, people with ADHD, Autistic people, and many, many more. The LeetCode filter prevents these qualified people from passing the interview and everybody loses out because of it.&lt;/p&gt;

&lt;p&gt;To expand a bit on the Autism point, since I can speak to that personally: The complexities of living with Autism and working with those who do are beyond the scope of this post. I also want to avoid speaking for all Autistic people, since it is an extremely varied and diverse group. However, the abbreviated version of what I want to say is that being an Autistic interviewee being actively judged by a neurotypical interviewer is inequitable to the point of being exclusionary. When I'm trying to navigate a LeetCode-style challenge, am also spending many mental cycles on distracting thoughts like the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Am I talking enough?&lt;/li&gt;
&lt;li&gt;Am I talking too much?&lt;/li&gt;
&lt;li&gt;Am I &lt;a href="https://theswaddle.com/for-people-with-autism-passing-as-neurotypical-takes-effort-strategy/"&gt;passing as normal&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;Am I doing this the way the interviewer expects it to be done, or the way that makes sense to me? Which one should I be doing?&lt;/li&gt;
&lt;li&gt;Should I be applying academic concepts for the sake of showing that I know them, or should I keep things simple?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the remainder of available mental cycles are actually going towards solving the problem, so you're only seeing maybe 30% of my actual coding ability. Many people might read this and say "just relax and focus on the challenge at hand." That is like telling a paraplegic to just believe in themself and stand up. This difficulty is the very essence of Autism and it cannot be willed away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better alternative
&lt;/h2&gt;

&lt;p&gt;Given the many flaws of LeetCode-style interviews, how can we do better? I've got good news: It's pretty easy. The first thing you need to do is take a step back and consider the sorts of characteristics you're actually hiring for. I'll make the bold assumption that the engineers on your team aren't actually solving contrived code puzzles in their day-to-day (and if they are, feel free to stick with LeetCode interviews and please never contact me for a job). Instead, list out the technical duties they &lt;em&gt;are&lt;/em&gt; actually performing to make the company successful. It hopefully looks something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding, improving, and extending business logic&lt;/li&gt;
&lt;li&gt;Participating constructively in code reviews&lt;/li&gt;
&lt;li&gt;Collaboratively problem-solving practical technical issues ("The API is experiencing a performance degradation, what do we do?")&lt;/li&gt;
&lt;li&gt;Finding and fixing bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are critical qualities that can be teased out of a much wider pool of candidates within an hour, simply by recreating relevant scenarios in the interview. Rather that handing interviewees an unimplemented function, show them your team's code and see how they might improve it. You'll learn what you need to know about them, and it will help them make a more informed decision about whether they and your company are a match. Let the candidate make a modest code change and see how they go about leading a Pull Request to get it merged. It's okay if you're working out of an alternate repo that's meant specifically for interviewing. In fact, that's the best way to create a consistent and fair challenge for each potential candidate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"This sounds hard to do."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It isn't, really. It requires a bit of upfront investment and experimentation to get a balanced and streamlined process, but hiring good people is inherently challenging and you need to put some work into it. Having a more thoughtful and effective interviewing process will save you money in the long run by setting up the candidates you actually want to hire for success.&lt;/p&gt;




&lt;p&gt;If you simply &lt;em&gt;must&lt;/em&gt; stick with LeetCode challenges, provide candidates the specific problems they will be given a week in advance instead of on the spot. Allow them to come to the interview with the solution already implemented. This will set candidates up for success by allowing them to solve the problem their way, on their schedule, and in whatever way works best for their unique situation. You can then collaboratively analyze the solution and extend it via a pairing session that they lead. This is not an unreasonable advantage, &lt;strong&gt;it is an accessibility accommodation&lt;/strong&gt;. The great thing about accessibility accommodations is that they make things better for &lt;em&gt;everyone&lt;/em&gt;. Having ample time to think about a solution ahead of time would be helpful in setting &lt;em&gt;all&lt;/em&gt; candidates up for success. And in this case, it changes the nature of the interview to be more of an exercise in collaboration rather than a gate designed to keep people out of the company.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"But what if the candidate cheats by getting help?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So what if they do? Getting outside assistance ahead of the interview won't help a candidate during an interactive review and pairing session on the code they provide. Also, what's the harm in getting some help? Is there a rule at your company that forbids engineers from getting assistance to develop a solution? As an interviewer, you should be looking for signals that indicate a candidate can produce results by any means necessary, be receptive to feedback, and iterate based on evolving requirements.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"It sounds like you're just bad at coding interviews and are taking it out on the world via a blog post."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You're right, I &lt;em&gt;am&lt;/em&gt; bad at coding interviews. I will never be good at them, because I have a disability that makes them disproportionately difficult for me. Funny thing about that, though: I am actually a very capable programmer. I'm not egotistical enough to believe I am exceptional, but speaking objectively: My coding ability has &lt;em&gt;never&lt;/em&gt; been called into question as lacking on any job I've ever had. I've never been too insufficient of a programmer to get my job done well. Minimally, I am a "good enough" programmer.&lt;/p&gt;

&lt;p&gt;If you're asking how I ever got a job given how bad I am at coding interviews, it's because I tend to get job offers from the exceptional companies that don't have LeetCode-style interviews as part of their hiring process.&lt;/p&gt;

&lt;h2&gt;
  
  
  We all deserve better
&lt;/h2&gt;

&lt;p&gt;There are many, many exceptional programmers who struggle tremendously with LeetCode-style interviews. Max Howell, the creator of the mission-critical package manager &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt; that millions of developers depend on, famously got rejected by Google (who uses Homebrew) because of such an interview:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-608682016205344768-610" src="https://platform.twitter.com/embed/Tweet.html?id=608682016205344768"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-608682016205344768-610');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=608682016205344768&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;There is something deeply wrong with this way of interviewing. It is depriving companies of excellent and diverse engineers, and it is making people miss out on good jobs that they actually deserve. It's time to go back to the drawing board (but &lt;em&gt;not&lt;/em&gt; the whiteboard) on these ineffective hiring practices.&lt;/p&gt;

</description>
      <category>diversity</category>
      <category>inclusion</category>
      <category>interviewing</category>
      <category>career</category>
    </item>
    <item>
      <title>Automating Godot game releases to itch.io</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Tue, 18 Jan 2022 00:21:13 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/automating-godot-game-releases-to-itchio-1a96</link>
      <guid>https://forem.com/jeremyckahn/automating-godot-game-releases-to-itchio-1a96</guid>
      <description>&lt;p&gt;If you're a game developer, you most likely want to spend your time actually designing and developing your game. Unfortunately, a lot of time goes into not-so-interesting manual administrative tasks. One of those tasks is publishing new builds. What if this work could be completely automated away and happen as you work on your game, in the background? With workflow automation tools like &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;, this is not only possible, but free to use and easy to achieve!&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;Me and my friend &lt;a href="https://github.com/lstebner" rel="noopener noreferrer"&gt;Luke&lt;/a&gt; are building &lt;a href="https://rainbowcow-studio.itch.io/farmhand-go" rel="noopener noreferrer"&gt;Farmhand Go!&lt;/a&gt;, a Godot game that's based around making in-game money by growing and harvesting crops with timed actions. What we wanted was for every merge to the &lt;code&gt;main&lt;/code&gt; Git branch to initiate a release to &lt;a href="https://rainbowcow-studio.itch.io/farmhand-go" rel="noopener noreferrer"&gt;https://rainbowcow-studio.itch.io/farmhand-go&lt;/a&gt;, where the game lives and can be played. We also wanted our Discord channel to be automatically notified of the new release as soon as it's available.&lt;/p&gt;

&lt;p&gt;With a little elbow grease, we came up with this GitHub Action that does all of this for us:&lt;/p&gt;


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


&lt;p&gt;The rest of this article will discuss how we got this set up, and how you can have something similar for your Godot games!&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;In order to set up this automation, you will need your GitHub repo and itch.io page already published. If you want the optional Discord notification functionality, you will need to have your server and relevant channel set up as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;In the Git repo, your Godot project needs to have an &lt;code&gt;export_presets.cfg&lt;/code&gt; file at its root. In Farmhand Go!, the actual Godot project is at &lt;code&gt;./project&lt;/code&gt; rather than the repo root, but your setup may vary. Be sure that &lt;code&gt;export_presets.cfg&lt;/code&gt; is not in your &lt;code&gt;.gitignore&lt;/code&gt; (&lt;a href="https://github.com/godotengine/godot-demo-projects/issues/329" rel="noopener noreferrer"&gt;it is by default&lt;/a&gt;), or that it is &lt;a href="https://github.com/godotengine/godot-demo-projects/issues/329#issuecomment-763625011" rel="noopener noreferrer"&gt;at least present for when the actual Godot export occurs&lt;/a&gt;. This is what our committed &lt;code&gt;export_presets.cfg&lt;/code&gt; looks like:&lt;/p&gt;


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


&lt;p&gt;Once that's set up, you'll need your modified version of the YAML file from above. Here's a more generic version you can use: &lt;a href="https://gist.github.com/jeremyckahn/ff4f0e409f089ec36bdecb5a5adb6819" rel="noopener noreferrer"&gt;https://gist.github.com/jeremyckahn/ff4f0e409f089ec36bdecb5a5adb6819&lt;/a&gt;. You'll just need to replace the following stubbed values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NAME-OF-YOUR-GAME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NAME-OF-YOUR-USER&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These occur in a few places in this file, so I recommend using your editor's Find and Replace feature and carefully reviewing the changes. Move this file to your repo's &lt;code&gt;.github/workflows/&lt;/code&gt; directory, and name it whatever you'd like. For Farmhand Go!, it's &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As written, this automation will run whenever a commit is pushed, either directly or via Pull Request merge, to the &lt;code&gt;main&lt;/code&gt; branch. If you want to use a different branch for this automation, change the &lt;code&gt;branches&lt;/code&gt; value towards the top of the file.&lt;/p&gt;

&lt;p&gt;Once that's in place, you will need an API key for itch.io and optionally a webhook URL for Discord. &lt;/p&gt;

&lt;h3&gt;
  
  
  The itch.io API key
&lt;/h3&gt;

&lt;p&gt;To create your itch.io API key, go to your settings in itch.io:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh92k0lqgxc9l2nk8mih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh92k0lqgxc9l2nk8mih.png" alt="Itch.io API key creation step 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, in the left-hand nav, go to "API keys"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxcgrcu3jdvqlelc28x5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxcgrcu3jdvqlelc28x5.png" alt="Itch.io API key creation step 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Input your password, and then click "Generate new API key." You should now have a screen that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx68qg21274s814t5mam8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx68qg21274s814t5mam8.png" alt="Itch.io API key creation step 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And you're done here! Copy the new key and head over to the Settings page for your game's GitHub repo and click on "Secrets" in the left-hand nav:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa65fd6ia1l9ruipt2zqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa65fd6ia1l9ruipt2zqz.png" alt="GitHub secret creation step 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, click "New Repository Secret." For "Name," put &lt;code&gt;BUTLER_API_KEY&lt;/code&gt;. For the value, paste in the key that you copied from itch.io. Click "Add secret," and you should now have a screen that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8kvxaoswm8i6c5b0fra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8kvxaoswm8i6c5b0fra.png" alt="GitHub secret creation step 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Discord webhook URL (optional)
&lt;/h3&gt;

&lt;p&gt;In your Discord server, go to "Server Settings":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyd47uh1rwa5or451lb1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdyd47uh1rwa5or451lb1.png" alt="Discord webhook creation step 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the left-hand nav, click "Integrations":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpsytypaho9m7ygy5dfb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmpsytypaho9m7ygy5dfb.png" alt="Discord webhook creation step 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Webhooks:"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3ciue0a0pkuq7a5j4oh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3ciue0a0pkuq7a5j4oh.png" alt="Discord webhook creation step 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "New Webhook" and fill out the form appropriately. Here's how ours looks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hix1lhvd4y3gdlwxefm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hix1lhvd4y3gdlwxefm.png" alt="Discord webhook creation step 4"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Copy Webhook" to copy the URL to your clipboard, and then "Save Changes" at the bottom. We're done here!&lt;/p&gt;

&lt;p&gt;Flip back to your GitHub repo's Secrets page, and create a new secret named &lt;code&gt;DISCORD_WEBHOOK&lt;/code&gt; with the webhook URL from Discord as the value. Your Secrets page should now look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfqx36cf7udxizjods9t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfqx36cf7udxizjods9t.png" alt="GitHub secret creation step 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that all the necessary GitHub secrets are in place, all that's left to do is commit &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; to your &lt;code&gt;main&lt;/code&gt; branch via PR. Starting with this commit and going forward on the &lt;code&gt;main&lt;/code&gt; branch, all commits will trigger a GitHub Action that deploys your web game to itch.io and notifies your community of the new release:&lt;/p&gt;

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

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

&lt;p&gt;And that's it! You can customize this workflow to your liking, perhaps by changing the Discord messaging or adding additional build targets. For the latter, I recommend looking at the Action that I learned from in order to see how to support Linux, Mac, and Windows builds: &lt;a href="https://github.com/RudyMis/Bubbles/blob/master/.github/workflows/godot-ci.yml" rel="noopener noreferrer"&gt;https://github.com/RudyMis/Bubbles/blob/master/.github/workflows/godot-ci.yml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What do you think? How do you think this automation could be improved? Please share your thoughts in the comments. Thanks for reading, and have fun with developing your Godot games!&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>github</category>
      <category>automation</category>
    </item>
    <item>
      <title>How I designed an abuse-resistant, fault-tolerant, zero cost, multiplayer online game</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Tue, 28 Dec 2021 23:47:58 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/how-i-designed-an-abuse-resistant-fault-tolerant-zero-cost-multiplayer-online-game-140g</link>
      <guid>https://forem.com/jeremyckahn/how-i-designed-an-abuse-resistant-fault-tolerant-zero-cost-multiplayer-online-game-140g</guid>
      <description>&lt;p&gt;Nearly a year ago I deployed a multiplayer feature for &lt;a href="https://www.farmhand.life/" rel="noopener noreferrer"&gt;Farmhand&lt;/a&gt;, an &lt;a href="https://github.com/jeremyckahn/farmhand" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and web-based farming game that I created. Since that initial deployment, the multiplayer system has experienced &lt;strong&gt;no downtime or service degradation&lt;/strong&gt;. And best of all, &lt;strong&gt;I've paid nothing&lt;/strong&gt; to host the service and therefore I am able to allow others to play for free. This article is an overview of how I designed this system from the ground up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The game
&lt;/h2&gt;

&lt;p&gt;In short, Farmhand is a game that mixes farming and market mechanics. The goal is to buy seeds for a low price, plant and harvest them, and then sell the crops at a high price. Prices fluctuate from day-to-day, so you'll have to be smart about your buy/sell decisions.&lt;/p&gt;

&lt;p&gt;Farmhand was initially designed as a single-player game and seed/crop values were randomly generated at the start of each game day. One day I thought it would be cool to create a shared, online market that players around the world could participate in together. My vision was for one player's buy/sell decisions to affect the global market that determines the seed/crop values for all connected players.&lt;/p&gt;

&lt;p&gt;In order for this market system to be fun, it needed to be simple and reliable. I gave myself the following constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero hosting costs&lt;/strong&gt;. I'm not making money from Farmhand, so I don't want to spend money to host it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal devops involvement&lt;/strong&gt;. Farmhand is just a hobby for me, and I have a day job. I don't want to be dealing with managing service outages during the work day (or the middle of the night).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault-tolerance and abuse-resistant&lt;/strong&gt;. If you're putting a service online, expect people to abuse it. I wanted this system to not only be highly-available, but resistant to &lt;a href="https://en.wikipedia.org/wiki/Griefer" rel="noopener noreferrer"&gt;griefing&lt;/a&gt; as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end I was able to ship a fun and functional multiplayer system that adhered to all of these constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tech
&lt;/h2&gt;

&lt;p&gt;There are a few pieces to this system:&lt;/p&gt;

&lt;h3&gt;
  
  
  The client
&lt;/h3&gt;

&lt;p&gt;Farmhand is implemented as a &lt;a href="https://web.dev/progressive-web-apps/" rel="noopener noreferrer"&gt;PWA&lt;/a&gt; that runs in a web browser. The client's overall architecture is outside the scope of this article, but for the purposes of online multiplayer it uses &lt;a href="https://github.com/dmotz/trystero" rel="noopener noreferrer"&gt;Trystero&lt;/a&gt; with the &lt;a href="https://github.com/dmotz/trystero#strategy-comparison" rel="noopener noreferrer"&gt;WebTorrent matchmaking strategy&lt;/a&gt; to connect peers to each other. It interacts with the central market server via a REST API.&lt;/p&gt;

&lt;h3&gt;
  
  
  The server
&lt;/h3&gt;

&lt;p&gt;Farmhand's API is hosted on &lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel's Hobby tier&lt;/a&gt;. Vercel provides an excellent &lt;a href="https://en.wikipedia.org/wiki/Serverless_computing" rel="noopener noreferrer"&gt;Serverless&lt;/a&gt; platform that offers scalable runtime performance, as well as static file hosting, automatic preview builds (great for testing out PRs), and more.&lt;/p&gt;

&lt;p&gt;The Vercel-based API is backed by a Redis instance for data "persistence." "Persistence" is in quotes because the data only ever lives in memory, so a system failure would result in complete data loss. However, the application logic is designed such that this kind of failure would be a &lt;strong&gt;feature&lt;/strong&gt; and not a bug. The Redis instance is hosted on &lt;a href="https://redis.com/redis-enterprise-cloud/pricing/" rel="noopener noreferrer"&gt;Redis Labs' free tier&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Farmhand, both Vercel and Redis Labs are configured to run in AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  System architecture
&lt;/h2&gt;

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

&lt;p&gt;At any given time, a player can go to &lt;a href="https://www.farmhand.life/" rel="noopener noreferrer"&gt;https://www.farmhand.life/&lt;/a&gt;, switch the "Play online" toggle and join a room of their choosing (&lt;code&gt;global&lt;/code&gt; by default). When this happens, two things occur:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A request is made to &lt;code&gt;GET https://farmhand.vercel.app/api/get-market-data?room=global&lt;/code&gt;, which is a Serverless function. This retrieves the latest market data and also informs the server of the player's presence. This request is repeated to serve as a heartbeat until the player leaves the room to maintain an "active" session with the API.&lt;/li&gt;
&lt;li&gt;A WebSocket connection to a WebTorrent tracker is made. The tracker then connects the client to any other clients in the requested room. The peer-to-peer connection is persistent until the player leaves the room. This complexity is abstracted away via Trystero.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The API manages room data that is stored in Redis. When a &lt;code&gt;GET https://farmhand.vercel.app/api/get-market-data?room=global&lt;/code&gt; request (&lt;a href="https://github.com/jeremyckahn/farmhand/blob/d2953b195d4d1470eb3dbce84ea04d057e524614/api/get-market-data.js#L25-L69" rel="noopener noreferrer"&gt;source&lt;/a&gt;) is made, the API checks to see if a value associated with the key &lt;code&gt;room-global&lt;/code&gt; exists. &lt;a href="https://github.com/jeremyckahn/farmhand/blob/d2953b195d4d1470eb3dbce84ea04d057e524614/api-etc/utils.js#L29-L40" rel="noopener noreferrer"&gt;If not, it is initialized&lt;/a&gt;. Here's an example of a room object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activePlayers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"4a793fe2-9eb1-4041-935b-5caf55177dde"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1640727668293&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"58f90cc1-1089-4394-a7e7-2f079f87ed4d"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1640727669934&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"b26b2d59-79f5-40f3-bc91-cfc0554bb994"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1640727674791&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"d1e34686-925e-4344-b7cb-e15ce6d7dad3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1640727667860&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valueAdjustments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"asparagus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.6798235686529905&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"asparagus-seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9797840434970977&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"carrot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5382522777963925&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"carrot-seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.1233740954422615&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"corn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.1524067154896047&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"corn-seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.2309158460921086&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;activePlayers&lt;/code&gt; is a map of unique player IDs (determined by clients via &lt;a href="https://github.com/uuidjs/uuid" rel="noopener noreferrer"&gt;uuid&lt;/a&gt; to timestamps of when they last made a &lt;code&gt;GET https://farmhand.vercel.app/api/get-market-data?room=global&lt;/code&gt; request. Each time the function is invoked, it examines the map to see which timestamps are older than the &lt;a href="https://github.com/jeremyckahn/farmhand/blob/d2953b195d4d1470eb3dbce84ea04d057e524614/src/common/constants.js#L3" rel="noopener noreferrer"&gt;&lt;code&gt;HEARTBEAT_INTERVAL_PERIOD&lt;/code&gt;&lt;/a&gt; (currently 10 seconds) and deletes any that are expired. This data is returned to the client and also written back to Redis to be persisted across function invocations. This is how the active room participants are tracked.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;valueAdjustments&lt;/code&gt; is the current state of the room's market. The map's keys refer to an item ID in the game and the values represent their respective in-game market value. Market values are bound between &lt;code&gt;0.5&lt;/code&gt; and &lt;code&gt;1.5&lt;/code&gt; and go up or down based on individual player activity. When a player ends their in-game day, an API request to &lt;code&gt;POST https://farmhand.vercel.app/api/post-day-results&lt;/code&gt; is made with a payload that looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"positions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"carrot-seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"room"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;positions&lt;/code&gt; represents all the items that the player either increased or decreased inventory of in their most recent in-game day. &lt;code&gt;1&lt;/code&gt; means that they increased their inventory of the associated item ID (either by buying seeds or harvesting crops), which increases the item's market value. &lt;code&gt;-1&lt;/code&gt; means they decreased their inventory (typically by selling the item), which decreases the item's market value. &lt;a href="https://github.com/jeremyckahn/farmhand/blob/ea40a1e364dbbd87a46494ccb87e6a1f750c17aa/api/post-day-results.js#L28-L56" rel="noopener noreferrer"&gt;Here's the source&lt;/a&gt; for that logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applyPositionsToMarket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valueAdjustments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valueAdjustments&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;itemPositionChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemPositionChange&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemPositionChange&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cm"&gt;/* itemPositionChange == 0 */&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// If item value is at a range boundary but was not changed in this&lt;/span&gt;
        &lt;span class="c1"&gt;// operation, randomize it to introduce some variability to the market.&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;itemName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;MIN&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;valueAdjustments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The updated market data is again persisted back to Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abuse mitigation
&lt;/h2&gt;

&lt;p&gt;One of the nice things about this server-side logic is that it naturally mitigates abuse. There is nothing stopping people from crafting custom &lt;code&gt;POST https://farmhand.vercel.app/api/post-day-results&lt;/code&gt; requests to manipulate the market however they want. However, once the adjusted value for any given item reaches the upper or lower bound (&lt;code&gt;1.5&lt;/code&gt; or &lt;code&gt;0.5&lt;/code&gt;, respectively), the item's value is randomized within those bounds. So while nefarious people can manipulate the market, it will reset itself and balance out before long. Even in those cases, it only presents as normal (if somewhat volatile) market dynamics to players.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fault tolerance
&lt;/h2&gt;

&lt;p&gt;Farmhand room data is only stored in memory via Redis and never written to a disk. Because of this, it is inherently ephemeral. The worst case scenario with this design is that room data gets lost either due to the Redis server shutting down or something like a &lt;a href="https://redis.io/commands/flushall" rel="noopener noreferrer"&gt;&lt;code&gt;FLUSHALL&lt;/code&gt;&lt;/a&gt; command being issued. However, since &lt;a href="https://github.com/jeremyckahn/farmhand/blob/d2953b195d4d1470eb3dbce84ea04d057e524614/api-etc/utils.js#L29-L40" rel="noopener noreferrer"&gt;the API initializes requested room data that doesn't already exist&lt;/a&gt;, this would only present to the user as a bit of market volatility that would likely go unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Peer-to-peer interaction
&lt;/h2&gt;

&lt;p&gt;The Vercel-based API effectively manages the shared market data, but I wanted players to have a sense of who else is playing with them and how they are affecting the experience for everyone else. This is where the peer-to-peer communication comes into play.&lt;/p&gt;

&lt;p&gt;Instead of a server and logic to act as a broker between clients, they connect to each other directly via Trystero and WebTorrent as explained previously. As players perform various actions such as buying or selling items, messages are broadcast to all connected peers to display in the "Active Players" modal:&lt;/p&gt;

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

&lt;p&gt;In an effort to mitigate abuse, player names are the result of &lt;a href="https://github.com/jeremyckahn/farmhand/blob/ea40a1e364dbbd87a46494ccb87e6a1f750c17aa/src/utils.js#L1007-L1021" rel="noopener noreferrer"&gt;a simple hashing algorithm based on player IDs&lt;/a&gt; (which are stable).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPlayerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memoize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playerIdNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;playerId&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adjective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adjectives&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;playerIdNumber&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;adjectives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adjectiveNumberValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adjective&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;animalNames&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="nx"&gt;playerIdNumber&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;adjectiveNumberValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;animalNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;adjective&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;animal&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retrospective analysis
&lt;/h2&gt;

&lt;p&gt;This system has been effective so far. Vercel and Redis Labs have provided excellent performance and availability since this system launched nearly a year ago, which is impressive given that I'm using the free tier of both services. The fault-tolerance and abuse mitigation measures have resulted in the minimal maintenance burden I was hoping to achieve. The only manual intervention that's been required from me so far to keep things running is having to log into Redis Labs every couple of months to indicate that my account is still active.&lt;/p&gt;

&lt;p&gt;I'm quite pleased with how this multiplayer system has turned out so far. I'd like to expand on Farmhand's multiplayer features and further develop its online market mechanics. I'd like to know what others think as well, as I've never designed a full stack system before this and I would like to learn how it can be improved. Let me know what you think via the comments below! And if you're up for some easygoing farming fun, &lt;a href="https://www.farmhand.life/" rel="noopener noreferrer"&gt;give Farmhand a try&lt;/a&gt; sometime. 🙂&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>architecture</category>
      <category>serverless</category>
    </item>
    <item>
      <title>TIL: Reading stdin to a BASH variable</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Mon, 14 Dec 2020 02:44:39 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/til-reading-stdin-to-a-bash-variable-1kln</link>
      <guid>https://forem.com/jeremyckahn/til-reading-stdin-to-a-bash-variable-1kln</guid>
      <description>&lt;p&gt;Today I wanted to read stdin to a BASH script variable for additional processing. It's not completely straightforward, but it's pretty easy once you know the syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;YOUR_VARIABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&amp;lt;/dev/stdin&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A full example:&lt;/p&gt;


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



&lt;p&gt;With this, you can easily make pipe-able BASH script files. For instance, with these two files using this technique:&lt;/p&gt;


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



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


&lt;p&gt;You can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;some-text | ./to-uppercase.sh | ./to-snake-case.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SOME_TEXT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shoutout to &lt;a href="https://stackoverflow.com/a/15269128"&gt;StackOverflow user Ingo Karkat for explaining this&lt;/a&gt;. It's a very handy little trick for making BASH scripts more flexible and reusable!&lt;/p&gt;

</description>
      <category>bash</category>
      <category>todayilearned</category>
      <category>stdin</category>
      <category>scripting</category>
    </item>
    <item>
      <title>The case for async/await-based JavaScript animations</title>
      <dc:creator>Jeremy Kahn</dc:creator>
      <pubDate>Tue, 18 Aug 2020 22:19:37 +0000</pubDate>
      <link>https://forem.com/jeremyckahn/the-case-for-async-await-based-javascript-animations-pkl</link>
      <guid>https://forem.com/jeremyckahn/the-case-for-async-await-based-javascript-animations-pkl</guid>
      <description>&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await"&gt;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; is one of my favorite features&lt;/a&gt; of modern JavaScript. While it's just syntactic sugar around &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"&gt;&lt;code&gt;Promise&lt;/code&gt;s&lt;/a&gt;, I've found that it enables much more readable and declarative asynchronous code. Recently I've started to experiment with &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;-based animations, and I've found it to be an effective and standards-based pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;There is no shortage of great JavaScript animation libraries available. For most use cases, &lt;a href="https://greensock.com"&gt;GreenSock&lt;/a&gt; is the gold standard and the library you should default to (and I am saying this as an author of a "competing" library). GreenSock, like most animation libraries such as &lt;a href="https://createjs.com/tweenjs"&gt;Tween.js&lt;/a&gt;, &lt;a href="https://animejs.com"&gt;anime.js&lt;/a&gt;, or &lt;a href="https://mojs.github.io"&gt;mo.js&lt;/a&gt;, has a robust and comprehensive animation-oriented API. This API works well, but like any domain-specific solution, it's an additional layer of programming semantics on top of the language itself. It raises the barrier of entry for newer programmers, and you can't assume that one bespoke API will integrate gracefully with another. What if we could simplify our animation scripting to be more standards-based to avoid these issues?&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: Enter &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; enables us to write asynchronous code as though it was synchronous, thereby letting us avoid unnecessarily nested callbacks and letting code execute more linearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bias warning&lt;/strong&gt;: For the examples in this post I am going to use &lt;a href="https://jeremyckahn.github.io/shifty/doc/"&gt;Shifty&lt;/a&gt;, an animation library I am the developer of. It is by no means the only library you could use to build &lt;code&gt;Promise&lt;/code&gt;-based animations, but it does provide it as a first-class feature whereas it's a bit more of an &lt;a href="https://greensock.com/docs/v3/GSAP/Timeline/then()"&gt;opt-in feature&lt;/a&gt; for GreenSock and other animation libraries. Use the tool that's right for you!&lt;/p&gt;

&lt;p&gt;Here's an animation that uses &lt;code&gt;Promise&lt;/code&gt;s directly:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#tweenable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;tween&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translateX(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easeInOutQuad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;tweenable&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;tweenable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tween&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is straightforward enough, but it could be simpler. Here's the same animation, but with &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#tweenable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tweenable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tween&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translateX(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easeInOutQuad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;tweenable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tween&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an example this basic, the difference isn't significant. However, we can see that the &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; version is free from the &lt;code&gt;.then()&lt;/code&gt; chaining, which keeps things a bit terser but also allows for a flatter overall code structure (at least once it's inside the &lt;code&gt;async&lt;/code&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE"&gt;IIFE wrapper&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Because the code is visually synchronous, it becomes easier to mix side effects into the "beats" of the animation:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/jeremyckahn/embed/poyEvog?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It gets more interesting when we look at using standard JavaScript loops with our animations. It's still weird to me that you can use a &lt;code&gt;for&lt;/code&gt; or a &lt;code&gt;while&lt;/code&gt; loop with asynchronous code and not have it block the thread, but &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; allows us to do it! Here's a metronome that uses a standard &lt;code&gt;while&lt;/code&gt; loop that repeats infinitely, but doesn't block the thread:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/jeremyckahn/embed/YzqGPqJ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Did you notice the &lt;code&gt;while (true)&lt;/code&gt; in there? In a non-&lt;code&gt;async&lt;/code&gt; function, this would result in an infinite loop and crash the page. But here, it does exactly what we want!&lt;/p&gt;

&lt;p&gt;This pattern enables straightforward animation scripting with minimal semantic overhead from third-party library code. &lt;code&gt;await&lt;/code&gt; is a fundamentally declarative programming construct, and it helps to wrangle the complexity of necessarily asynchronous and time-based animation programming. I hope that more animation libraries provide first-class &lt;code&gt;Promise&lt;/code&gt; support to enable more developers to easily write &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; animations!&lt;/p&gt;

&lt;h2&gt;
  
  
  Addendum: Handling interruptions with &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;After initially publishing this post, I iterated towards another powerful pattern that I wanted to share: Graceful animation interruption handling with &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; blocks.&lt;/p&gt;

&lt;p&gt;Imagine you have an animation running that is tied to a particular state of your app, but then that state changes and the animation either needs to respond to the change or cancel completely. With &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;-based animations, this becomes easy to do in a way that leverages the fundamentals of language.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/jeremyckahn/embed/abNmGwV?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In this example, the ball pulsates indefinitely. In the &lt;code&gt;async&lt;/code&gt; IIFE, notice that the &lt;code&gt;tween&lt;/code&gt;s are wrapped in a &lt;code&gt;try&lt;/code&gt; which is wrapped in a &lt;code&gt;while (true)&lt;/code&gt; to cause the animation to repeat. As soon as you click anywhere in the demo, the animation is &lt;code&gt;reject&lt;/code&gt;ed, thus causing the &lt;code&gt;await&lt;/code&gt;ed animation's &lt;code&gt;Promise&lt;/code&gt; to be treated as a caught exception which diverts the control flow into the &lt;code&gt;catch&lt;/code&gt; block. Here the &lt;code&gt;catch&lt;/code&gt; block &lt;code&gt;await&lt;/code&gt;s &lt;code&gt;reposition&lt;/code&gt;, another &lt;code&gt;async&lt;/code&gt; function that leverages a similar pattern to move the ball to where you clicked. Once &lt;code&gt;reposition&lt;/code&gt; &lt;code&gt;break&lt;/code&gt;s and exits its &lt;code&gt;while&lt;/code&gt; loop, the &lt;code&gt;async&lt;/code&gt; IIFE proceeds to repeat.&lt;/p&gt;

&lt;p&gt;This demo isn't terribly sophisticated, but it shows how &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;-based animations can enable rich interactivity with just a bit of plain vanilla JavaScript!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>animation</category>
    </item>
  </channel>
</rss>
