<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Albyte</title>
    <description>The latest articles on DEV Community by Albyte (@albifahriza).</description>
    <link>https://dev.to/albifahriza</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3968212%2F5f09350c-fd29-4df2-9768-71e1e50d55ee.png</url>
      <title>DEV Community: Albyte</title>
      <link>https://dev.to/albifahriza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/albifahriza"/>
    <language>en</language>
    <item>
      <title>I got tired of writing the same retry logic over and over, so I built Actly</title>
      <dc:creator>Albyte</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:44:45 +0000</pubDate>
      <link>https://dev.to/albifahriza/i-got-tired-of-writing-the-same-retry-logic-over-and-over-so-i-built-actly-23on</link>
      <guid>https://dev.to/albifahriza/i-got-tired-of-writing-the-same-retry-logic-over-and-over-so-i-built-actly-23on</guid>
      <description>&lt;p&gt;every async function i've ever written has the same problem.&lt;/p&gt;

&lt;p&gt;it can fail. the network blips. the service times out. three components mount at the same time and fire the same request in parallel. you know this, so you add retry. then you add a timeout. then you realize you're manually deduplicating requests. then product asks for caching.&lt;/p&gt;

&lt;p&gt;four separate concerns. written from scratch. every. single. project.&lt;/p&gt;

&lt;p&gt;i built &lt;a href="https://github.com/albytehq/actly" rel="noopener noreferrer"&gt;Actly&lt;/a&gt; because i was tired of copy-pasting the same patterns.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;the api is one function&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;act&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;actly&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user:42&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delayMs&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="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;totalTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dedupe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// your data&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'fresh' or 'cache'&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// how many tries it took&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// never throws — always resolves&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it never throws. failures come back as &lt;code&gt;result.ok === false&lt;/code&gt;. you check it, you move on.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;the policy ordering is intentional&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;this is the part i spent the most time on. internally the execution order is fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;totalTimeout → cache → dedupe → retry → timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;why this order specifically?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cache&lt;/code&gt; sits before &lt;code&gt;dedupe&lt;/code&gt; because a hit should skip &lt;em&gt;everything&lt;/em&gt; below it — no deduplication needed, no retry, nothing. &lt;code&gt;totalTimeout&lt;/code&gt; wraps the entire thing because it's a hard wall that doesn't reset — unlike &lt;code&gt;timeout&lt;/code&gt; which gives each retry attempt a fresh clock. &lt;code&gt;dedupe&lt;/code&gt; sits before &lt;code&gt;retry&lt;/code&gt; so concurrent callers collapse into one &lt;em&gt;before&lt;/em&gt; the retry loop starts. putting dedupe inside retry would mean each retry attempt spawns its own dedup group, which defeats the point.&lt;/p&gt;

&lt;p&gt;once the ordering clicks, the behavior becomes predictable. that was the goal.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;the thing that forced good design&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;when i added Redis/external store support, i had to define two interfaces: &lt;code&gt;SyncStateStore&lt;/code&gt; and &lt;code&gt;AsyncStateStore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;dedupe &lt;em&gt;requires&lt;/em&gt; synchronous store access. the reason is subtle — dedupe works by reading an in-flight promise from the store, and if it's missing, writing a new one. both operations have to happen in the same synchronous frame. if &lt;code&gt;get()&lt;/code&gt; were async, two concurrent callers could both observe a miss before either write lands, and you'd get duplicate requests anyway. the dedup would be broken silently.&lt;/p&gt;

&lt;p&gt;so async stores are only compatible with &lt;code&gt;cache&lt;/code&gt;. passing one to a chain that includes &lt;code&gt;dedupe&lt;/code&gt; is a TypeScript error and a runtime error — the executor throws immediately with a clear message instead of producing silent wrong behavior.&lt;/p&gt;

&lt;p&gt;small library. real constraint. forced a clean boundary i didn't plan for.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;install&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;actly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;zero dependencies. ESM + CJS. Node 18+.&lt;/p&gt;

&lt;p&gt;github: &lt;a href="https://github.com/albytehq/actly" rel="noopener noreferrer"&gt;github.com/albytehq/actly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;would love feedback — especially if you've hit edge cases with the policy ordering or have a use case the current api doesn't handle well.&lt;/p&gt;




</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
