<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://kieran.casa/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kieran.casa/" rel="alternate" type="text/html" /><updated>2026-04-08T15:19:58+00:00</updated><id>https://kieran.casa/feed.xml</id><title type="html">Kieran Hunt</title><subtitle>Kieran Hunt&apos;s blog</subtitle><entry><title type="html">Introducing crul</title><link href="https://kieran.casa/introducing-crul/" rel="alternate" type="text/html" title="Introducing crul" /><published>2026-03-16T08:55:32+00:00</published><updated>2026-03-16T08:55:32+00:00</updated><id>https://kieran.casa/introducing-crul</id><content type="html" xml:base="https://kieran.casa/introducing-crul/"><![CDATA[<p>Peter Steinberger (of OpenClaw fame) maintains the <a href="https://github.com/steipete/sweet-cookie">sweet-cookie</a> (<a href="https://archive.ph/wip/9vGnA">archive</a>) library.
It vends an npm artefact for fishing cookies out of various web browsers and making them available in the various Javascript runtimes.
I <em>think</em> he uses sweet-cookie to bridge the gap between websites and APIs that require cookie-based authentication and OpenClaw.</p>

<p>I thought it’d be super useful if we could do the same thing with cURL.
That is, authenticate cURL requests directly from the browser’s cookie store. 
So I’ve just published the first release of <a href="https://github.com/KieranHunt/crul"><code class="language-plaintext highlighter-rouge">crul</code></a> (pronounced <em>“cruel”</em>).
Let me show you how it works:</p>

<p>Say you’re logged into a website in your web browser, and you want to access that site through cURL.
<code class="language-plaintext highlighter-rouge">crul</code> makes that super easy:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx @kieranhunt/crul <span class="nt">--browsers</span> firefox <span class="nt">--url</span> <span class="s2">"https://news.ycombinator.com"</span>
<span class="c"># Netscape HTTP Cookie File</span>
<span class="c"># https://curl.se/docs/http-cookies.html</span>
<span class="c"># This file was generated by crul.</span>
<span class="c"># Edit at your own risk.</span>


<span class="c">#HttpOnly_news.ycombinator.com	FALSE	/	TRUE	0	user	kieranhunt&amp;SECRET</span>
</code></pre></div></div>

<p>That spits out a Netscape HTTP Cookie File. Perfectly compatible with cURL.
Avoid the intermediate step of saving the file to disk by piping the output directly into cURL:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="se">\</span>
  <span class="nt">-b</span> &lt;<span class="o">(</span>npx @kieranhunt/crul <span class="nt">--browsers</span> firefox <span class="nt">--url</span> <span class="s2">"https://news.ycombinator.com"</span><span class="o">)</span> <span class="se">\</span>
  <span class="nt">--silent</span> <span class="se">\</span>
  <span class="s2">"https://news.ycombinator.com/upvoted?id=kieranhunt"</span> <span class="se">\</span>
  | htmlq <span class="s1">'.titleline &gt; a'</span> <span class="nt">--text</span>
  
<span class="c"># Show HN: C++ AWS MSK IAM Auth Implementation – Goodbye Kafka Passwords</span>
<span class="c"># Two Starkly Similar Novels and the Puzzle of Plagiarism</span>
<span class="c"># Browning Fever: A story of fandom, literary societies, and impenetrable verse</span>
<span class="c"># Alphabet’s Sidewalk Labs seeks share of property taxes for Toronto smart city</span>
<span class="c"># Cipherli.st – Strong Ciphers for Apache, Nginx and Lighttpd</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">crul</code>’s CLI parameters map one-to-one to sweet-cookie’s <code class="language-plaintext highlighter-rouge">GetCookiesOptions</code> type.
I’d like to make it so that anything you can do with sweet-cookie you can also do with <code class="language-plaintext highlighter-rouge">crul</code>.</p>

<p>Find it @ <a href="https://github.com/KieranHunt/crul">KieranHunt\/crul</a> on GitHub or <a href="https://www.npmjs.com/package/@kieranhunt/crul">@kieranhunt/crul</a> on npm.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Peter Steinberger (of OpenClaw fame) maintains the sweet-cookie (archive) library. It vends an npm artefact for fishing cookies out of various web browsers and making them available in the various Javascript runtimes. I think he uses sweet-cookie to bridge the gap between websites and APIs that require cookie-based authentication and OpenClaw.]]></summary></entry><entry><title type="html">Introducing CDK Search</title><link href="https://kieran.casa/introducing-cdk-search/" rel="alternate" type="text/html" title="Introducing CDK Search" /><published>2026-03-11T20:30:58+00:00</published><updated>2026-03-11T20:30:58+00:00</updated><id>https://kieran.casa/introducing-cdk-search</id><content type="html" xml:base="https://kieran.casa/introducing-cdk-search/"><![CDATA[<p>I’ve gotten pretty good at navigating the <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html">CDK API Reference documentation</a> (<a href="https://archive.ph/izYDt">archive</a>). 
It’s usually a two step process:</p>
<ol>
  <li>First, remember the name of the service that you’re looking for. <kbd><kbd>⌘</kbd> + <kbd>f</kbd></kbd> for it on the docs page. I  usually remember to remove spaces and that its all lowercase. Then find it in the sidebar.</li>
  <li>After that I expand that section of the sidebar (with my mouse 😱) and then <kbd><kbd>⌘</kbd> + <kbd>f</kbd></kbd> again for the Construct I’m looking for. Then I click on the link to go to the Construct’s documentation.</li>
</ol>

<p>To speed up this process, I’ve created <a href="https://cdk-search.kieran.casa">CDK Search</a>:</p>

<p><a href="https://cdk-search.kieran.casa"><img src="/assets/2026-03-11-cdk-search.png" alt="Screenshot of the CDK Search website" /></a></p>

<p>It’s a continously updated search index of all L1 and L2 CDK constructs found in <a href="https://www.npmjs.com/package/aws-cdk-lib">aws-cdk-lib</a> (<a href="https://archive.ph/wip/ZdGFF">archive</a>).
It’s super fast, offline friendly* and supports keyboard navigation.</p>

<p>My workflow is now:</p>
<ol>
  <li>Go to <a href="https://cdk-search.kieran.casa">CDK Search</a>, type some portion of the construct’s name. Press <kbd><kbd>⏎</kbd></kbd> to open the docs.</li>
</ol>

<p>That’s it.
It’s a super focused experience.
I’d love to hear if you find it useful. 
Or even better, what you’d like to see added next.</p>

<p><br /></p>

<p>*<kbd><kbd>⌘</kbd> + <kbd>s</kbd></kbd> the page as HTML and you can view a (stale) offline copy any time!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve gotten pretty good at navigating the CDK API Reference documentation (archive). It’s usually a two step process: First, remember the name of the service that you’re looking for. ⌘ + f for it on the docs page. I usually remember to remove spaces and that its all lowercase. Then find it in the sidebar. After that I expand that section of the sidebar (with my mouse 😱) and then ⌘ + f again for the Construct I’m looking for. Then I click on the link to go to the Construct’s documentation.]]></summary></entry><entry><title type="html">Every AWS SDK Paginator</title><link href="https://kieran.casa/every-paginator/" rel="alternate" type="text/html" title="Every AWS SDK Paginator" /><published>2026-03-04T19:55:00+00:00</published><updated>2026-03-04T19:55:00+00:00</updated><id>https://kieran.casa/every-paginator</id><content type="html" xml:base="https://kieran.casa/every-paginator/"><![CDATA[<p>Now available is an auto-updating list of all AWS SDK paginators. 
Find it @ <a href="https://github.com/KieranHunt/aws-sdk-paginators">KieranHunt/aws-sdk-paginators</a> on GitHub.</p>

<p><a href="https://github.com/KieranHunt/aws-sdk-paginators"><img src="/assets/2026-03-04-paginators.png" alt="" /></a></p>

<p>It uses the same technique I described in <a href="/every-waiter/">Every AWS SDK Waiter</a> so do give that a read if you’re interested.
For more on paginators, read <a href="/aws-sdk-paginators/">Stop writing AWS SDK pagination loops that break</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Now available is an auto-updating list of all AWS SDK paginators. Find it @ KieranHunt/aws-sdk-paginators on GitHub.]]></summary></entry><entry><title type="html">JVM snapshot tests with selfie</title><link href="https://kieran.casa/selfie/" rel="alternate" type="text/html" title="JVM snapshot tests with selfie" /><published>2025-11-23T21:16:00+00:00</published><updated>2025-11-23T21:16:00+00:00</updated><id>https://kieran.casa/selfie</id><content type="html" xml:base="https://kieran.casa/selfie/"><![CDATA[<p>I’m a big fan of snapshot testing.
Snapshots are great to avoid manually writing a bunch of assertions.
You just pass in the <code class="language-plaintext highlighter-rouge">actual</code> object and let the snapshot library produce the <code class="language-plaintext highlighter-rouge">expected</code> result.
With traditional assertions, your tests are only as good as the fields you assert against.
But snapshot tests tend to capture _everything_or at least a lot more than a human writing assertions would.
So you often catch issues in fields that you wouldn’t ordinarily.</p>

<p>I’ve used Jest’s snapshot tests for years.
And now there’s a way to run snapshot tests on the JVM 🙌.
All thanks to <a href="https://selfie.dev/">selfie</a>.</p>

<p><a href="https://selfie.dev/"><img src="/assets/2025-11-23-selfie.png" alt="" /></a></p>

<p>🗒️ Tests performed using JDK21 (Corretto), Kotlin v2.2.20, and selfie-runner-junit5 v2.5.0.</p>

<h2 id="inline-snapshots">Inline snapshots</h2>

<p>My favourite kind of snapshot tests are inline ones.
I love grouping the snapshots with the code making the assertion. 
Selfie supports these just fine.</p>

<div class="mb-8 flex flex-wrap border-b border-white/10">
  
  
    <input type="radio" name="inline-snapshot" id="inline-snapshot-1" class="peer/tab1 hidden" checked="" />
    <label for="inline-snapshot-1" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab1:border-slate-400 peer-checked/tab1:text-slate-200">
      Before
    </label>
  
  
  
  
    <input type="radio" name="inline-snapshot" id="inline-snapshot-2" class="peer/tab2 hidden" />
    <label for="inline-snapshot-2" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab2:border-slate-400 peer-checked/tab2:text-slate-200">
      After
    </label>
  
  
  
  
  
  
  
  
  
  

  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab1:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="k">inline</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toBe_TODO</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

    </div>
  
  
  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab2:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="k">inline</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toBe</span><span class="p">(</span><span class="s">"[1, 2, 3]"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

    </div>
  
  
  
  
  
  
  
  
  
  
</div>

<h2 id="disk-snapshots">Disk snapshots</h2>

<p>Disk snapshots work exactly as you’d expect.</p>

<div class="mb-8 flex flex-wrap border-b border-white/10">
  
  
    <input type="radio" name="file-snapshot" id="file-snapshot-1" class="peer/tab1 hidden" checked="" />
    <label for="file-snapshot-1" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab1:border-slate-400 peer-checked/tab1:text-slate-200">
      Before
    </label>
  
  
  
  
    <input type="radio" name="file-snapshot" id="file-snapshot-2" class="peer/tab2 hidden" />
    <label for="file-snapshot-2" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab2:border-slate-400 peer-checked/tab2:text-slate-200">
      After
    </label>
  
  
  
  
  
  
  
  
  
  

  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab1:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="n">disk</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toMatchDisk_TODO</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

    </div>
  
  
  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab2:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="n">disk</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toMatchDisk</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The file is placed in the same directory as the test, and is named after the test class.
So in <code class="language-plaintext highlighter-rouge">SnapshotTest.ss</code> we find:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╔═ testDiskSnapshot ═╗
[a, b, c]
╔═ [end of file] ═╗
</code></pre></div></div>

    </div>
  
  
  
  
  
  
  
  
  
  
</div>

<h2 id="updating-snapshots">Updating snapshots</h2>

<p>As you iterate on your code, you’ll need to update your existing snapshots to match new expectations.
Selfie provides <em>a bunch</em> of different ways to do this.</p>

<h3 id="delete-the-snapshot-and-add-_todo-back">Delete the snapshot and add <code class="language-plaintext highlighter-rouge">_TODO</code> back</h3>

<p>The is the most basic way of updating snapshots.
You just delete the existing snapshot and add <code class="language-plaintext highlighter-rouge">_TODO</code> back to the tests.
But this will quickly become laborious and time consuming.</p>

<p>So you turn this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="k">inline</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toBe</span><span class="p">(</span><span class="s">"[1, 2, 3]"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>back in to this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="k">inline</span> <span class="nf">snapshot`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">Selfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">).</span><span class="nf">toString</span><span class="p">()).</span><span class="nf">toBe_TODO</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then re-run the tests.</p>

<h3 id="via-the-selfieselfie-environment-variable--system-propety">Via the <code class="language-plaintext highlighter-rouge">selfie</code>/<code class="language-plaintext highlighter-rouge">SELFIE</code> environment variable / system propety.</h3>

<p>Probably my favourite method is to just re-run the test suite and tell selfie to update all snapshots.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./gradlew <span class="nb">test</span> <span class="nt">-Pselfie</span><span class="o">=</span>overwrite
<span class="c"># or</span>
<span class="nv">SELFIE</span><span class="o">=</span>overwrite ./gradlew <span class="nb">test</span>
</code></pre></div></div>

<p>I like this method because it matches how I update Jest snapshots (<code class="language-plaintext highlighter-rouge">npm run test -- --updateSnapshot</code>).
You can use your version control to see which snapshots have changed and revert the snapshots back if they don’t look right.</p>

<h3 id="with-the-selfieonce-magic-comment">With the <code class="language-plaintext highlighter-rouge">//selfieonce</code> magic comment</h3>

<p>Adding this comment into the test file will cause selfie to overwrite the snapshots in the file and then remove the comment.</p>

<h3 id="with-the-selfiewrite-magic-comment">With the <code class="language-plaintext highlighter-rouge">//SELFIEWRITE</code> magic comment</h3>

<p>The same as above but the comment will stay there. Subsequent test runs will overwrite the snapshots.</p>

<h2 id="use-selfiesettingsapi-to-write-custom-formatters">Use <code class="language-plaintext highlighter-rouge">SelfieSettingsAPI</code> to write custom formatters</h2>

<p>In much the same way that AssertJ lets your write custom assertions, selfie lets you write custom snapshot serializers.
In the following example, I’ve written a serializer that makes string lists look like markdown.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">object</span> <span class="nc">CustomSelfie</span> <span class="p">:</span> <span class="nc">SelfieSettingsAPI</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">expectSelfie</span><span class="p">(</span><span class="n">response</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;):</span> <span class="nc">StringSelfie</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">expectSelfie</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="p">{</span>
            <span class="nc">Snapshot</span><span class="p">.</span><span class="nf">of</span><span class="p">(</span><span class="n">it</span><span class="p">.</span><span class="nf">joinToString</span><span class="p">(</span>
                <span class="n">prefix</span> <span class="p">=</span> <span class="s">"\n"</span><span class="p">,</span> 
                <span class="n">separator</span> <span class="p">=</span> <span class="s">"\n"</span><span class="p">,</span> 
                <span class="n">postfix</span> <span class="p">=</span> <span class="s">"\n"</span>
            <span class="p">)</span> <span class="p">{</span> <span class="n">item</span> <span class="p">-&gt;</span> <span class="s">"- $item"</span> <span class="p">})</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="mb-8 flex flex-wrap border-b border-white/10">
  
  
    <input type="radio" name="custom-serializer-snapshot" id="custom-serializer-snapshot-1" class="peer/tab1 hidden" checked="" />
    <label for="custom-serializer-snapshot-1" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab1:border-slate-400 peer-checked/tab1:text-slate-200">
      Before
    </label>
  
  
  
  
    <input type="radio" name="custom-serializer-snapshot" id="custom-serializer-snapshot-2" class="peer/tab2 hidden" />
    <label for="custom-serializer-snapshot-2" class="order-1 px-4 py-2 cursor-pointer text-slate-300 border-b-2 border-transparent transition-colors duration-200 peer-checked/tab2:border-slate-400 peer-checked/tab2:text-slate-200">
      After
    </label>
  
  
  
  
  
  
  
  
  
  

  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab1:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="n">custom</span> <span class="nf">serializer`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">CustomSelfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">)).</span><span class="nf">toBe_TODO</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

    </div>
  
  
  
  
    <div class="order-2 w-full mt-6 hidden peer-checked/tab2:block">
      
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">`test</span> <span class="n">custom</span> <span class="nf">serializer`</span><span class="p">()</span> <span class="p">{</span>
    <span class="nc">CustomSelfie</span><span class="p">.</span><span class="nf">expectSelfie</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="s">"a"</span><span class="p">,</span> <span class="s">"b"</span><span class="p">,</span> <span class="s">"c"</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="s">"""
- a
- b
- c
"""</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Unfortunately, selfie doesn’t give you pretty indentation using Kotlin’s <code class="language-plaintext highlighter-rouge">trimIndent</code> (yet!).</p>

    </div>
  
  
  
  
  
  
  
  
  
  
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m a big fan of snapshot testing. Snapshots are great to avoid manually writing a bunch of assertions. You just pass in the actual object and let the snapshot library produce the expected result. With traditional assertions, your tests are only as good as the fields you assert against. But snapshot tests tend to capture _everything_or at least a lot more than a human writing assertions would. So you often catch issues in fields that you wouldn’t ordinarily.]]></summary></entry><entry><title type="html">Every AWS SDK Waiter</title><link href="https://kieran.casa/every-waiter/" rel="alternate" type="text/html" title="Every AWS SDK Waiter" /><published>2025-11-19T19:00:45+00:00</published><updated>2025-11-19T19:00:45+00:00</updated><id>https://kieran.casa/every-waiter</id><content type="html" xml:base="https://kieran.casa/every-waiter/"><![CDATA[<p>I’ve just published a new repo containing every AWS SDK waiter, and its configuration. 
Find it @ <a href="https://github.com/KieranHunt/aws-sdk-waiters">KieranHunt/aws-sdk-waiters</a> on GitHub.</p>

<p><a href="https://github.com/KieranHunt/aws-sdk-waiters"><img src="/assets/2025-11-19-waiters.png" alt="" /></a></p>

<p>To build that table, I’m relying on a technique I learned from <a href="https://til.simonwillison.net/github-actions/conditionally-run-a-second-job">Simon Willison</a>. 
It works as follows:</p>
<ol>
  <li>Using GitHub actions, run a workflow daily.</li>
  <li>In the workflow, <code class="language-plaintext highlighter-rouge">git clone</code> the AWS SDK for Java repository.</li>
  <li>Use a script to find all waiter files and compile a markdown table with their configuration. Make the script update the repo’s README.</li>
  <li>Back in the GitHub Action’s worklow, commit the changes (if there are any) before pushing.</li>
</ol>

<p>Read the entire workflow in <a href="https://github.com/KieranHunt/aws-sdk-waiters/blob/main/.github/workflows/daily-aws-sdk-check.yml">aws-sdk-waiters/.github/workflows/daily-aws-sdk-check.yml at main · KieranHunt/aws-sdk-waiters</a> on GitHub.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve just published a new repo containing every AWS SDK waiter, and its configuration. Find it @ KieranHunt/aws-sdk-waiters on GitHub.]]></summary></entry><entry><title type="html">A deep dive into WaiterOverrideConfiguration</title><link href="https://kieran.casa/waiter-override-configuration/" rel="alternate" type="text/html" title="A deep dive into WaiterOverrideConfiguration" /><published>2025-10-18T10:10:14+00:00</published><updated>2025-10-18T10:10:14+00:00</updated><id>https://kieran.casa/waiter-override-config</id><content type="html" xml:base="https://kieran.casa/waiter-override-configuration/"><![CDATA[<p>This post is 4ᵗʰ in a series about AWS SDK Waiters.
You definitely don’t need to have read all the others before reading this one.
Though perhaps “Use AWS SDK Waiters” deserves a little skim to get up to speed:</p>

<ol>
  <li><a href="/aws-sdk-waiters/">Use AWS SDK Waiters</a></li>
  <li><a href="/custom-waiters/">Write custom waiters</a></li>
  <li><a href="/aws-sdk-waiters-ts/">Typescript waiters are a bit weird</a></li>
</ol>

<p>💃 <strong>This post is dynamic</strong> 💃<br />
Each configuration value uses a simulator and timeline for visualizations.
Many of the retry behaviours depend on random values for things like jitter.
Reload this page to see how randomisation affects them.</p>

<p>🎮 <strong>This post includes a simulator</strong> 🎮<br />
<a href="#simulator">Scroll to the bottom</a> of this page to use the complete simulator.
It lets you configure every parameter in <code class="language-plaintext highlighter-rouge">WaiterOverrideConfiguration</code> and see how it affects the retry behaviour.</p>

<h2 id="introduction">Introduction</h2>

<p><code class="language-plaintext highlighter-rouge">WaiterOverrideConfiguration</code> (<a href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/waiters/WaiterOverrideConfiguration.html">docs</a>, <a href="https://archive.ph/wip/f6Pfm">archive</a>) is how you configure the timeout and retry behaviour for AWS SDK Waiters on the JVM.</p>

<p>Each AWS service defines their own <code class="language-plaintext highlighter-rouge">WaiterOverrideConfiguration</code>.
These are configured per-waiter method.
That is, the waiter for a CloudFront’s <code class="language-plaintext highlighter-rouge">DistributionDeployed</code> is going to look quite different to the waiter for ECS’s <code class="language-plaintext highlighter-rouge">TasksRunning</code>.
For example, I dug into the EC2 <code class="language-plaintext highlighter-rouge">waitUntilInstanceRunning</code> method and pulled out the following <code class="language-plaintext highlighter-rouge">WaiterOverrideConfiguration</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WaiterOverrideConfiguration</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
  <span class="p">.</span><span class="nf">maxAttempts</span><span class="p">(</span><span class="mi">40</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">waitTimeout</span><span class="p">(</span><span class="k">null</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">backoffStrategyV2</span><span class="p">(</span>
    <span class="nc">BackoffStrategy</span><span class="p">.</span><span class="nf">fixedDelayWithoutJitter</span><span class="p">(</span>
      <span class="cm">/* delay */</span> <span class="mi">15</span><span class="p">.</span><span class="n">seconds</span><span class="p">.</span><span class="nf">toJavaDuration</span><span class="p">()</span>
    <span class="p">)</span>
  <span class="p">)</span>
  <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</code></pre></div></div>

<p>In the rest of this post, I dig in to what each of those configuration values means and how best to pick between them.</p>

<p>Just before I start, I’m going to show the behaviour of waiters using different configuration values.
It helps to have a certain resource in mind while going through these examples.
I won’t use any resource in particular, but maybe you could think of:</p>
<ul>
  <li>Waiting for an SSM Command Invocation to complete</li>
  <li>Waiting for a CloudWatch Insights Query to complete, or</li>
  <li>Waiting for a Kinesis Stream to be created.</li>
</ul>

<p>In the following examples the resource is, by default, set to reach its desired state after 10 seconds.
If the waiter hasn’t timed out or reached its maximum attempt threshold, it will transition to success</p>

<h2 id="maxattempts"><code class="language-plaintext highlighter-rouge">maxAttempts</code></h2>

<p>The <code class="language-plaintext highlighter-rouge">maxAttempts</code> value sets an upper bound on the number of times the waiter will check the state of the resource.
In the below example, I’ve set <code class="language-plaintext highlighter-rouge">maxAttempts</code> to low enough that the waiter will never finish.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-max-attempts" value="2" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-max-attempts" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-max-attempts" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter" selected="">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-max-attempts" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-max-attempts" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-max-attempts" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-max-attempts" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-max-attempts" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-max-attempts" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-max-attempts" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-max-attempts" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-max-attempts" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-max-attempts" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-max-attempts" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-max-attempts">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-max-attempts">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-max-attempts">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-max-attempts'));
const output = document.getElementById('output-max-attempts');
const delayInput = document.getElementById('delayInput-max-attempts');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-max-attempts');
const backoffSelect = document.getElementById('backoffStrategyV2-max-attempts');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-max-attempts');
  const outcomeDisplay = document.getElementById('outcomeDisplay-max-attempts');
  const ganttTemplate = document.getElementById('ganttChartTemplate-max-attempts');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-max-attempts');
  const outcomeTemplate = document.getElementById('outcomeTemplate-max-attempts');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<p>After two checks of the resource, the waiter gives up.</p>

<p>With the rest of the configuration parameters available for a waiter, I don’t actually think its necessary to constrain an upper bound on attempts.
Doing so means you have to do some complicated maths (or use my simulator below) to figure out the right upper value to pick.
Instead, trusting <code class="language-plaintext highlighter-rouge">waitTimeout</code> and picking a sensible delay between API calls should be enough.</p>

<h2 id="waittimeout"><code class="language-plaintext highlighter-rouge">waitTimeout</code></h2>

<p>The <code class="language-plaintext highlighter-rouge">waitTimeout</code> value configures how long you’re willing to wait for the resource to transition into the desired state.
This is probably the most important value to configure in your waiter.</p>

<p>Some resources, like the SSM Commands Invocations in <a href="/aws-sdk-waiters/">my previous post</a>, should have a timeout value that depends on what the command is doing.
If your command includes a 5-minute sleep then your timeout should probably be configured for something a little bit more than 5 minutes.
SSM has some delay in delivering the command to the target instances, so you’ll need to factor that in too.</p>

<p>Other resources, like an EC2 instance going into running, mostly depend on the AWS service and so can be generically defined.
It’s probably fine to leave it up to whatever default AWS picked.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-wait-timeout" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-wait-timeout" value="8" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-wait-timeout" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter" selected="">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-wait-timeout" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-wait-timeout" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-wait-timeout" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-wait-timeout" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-wait-timeout" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-wait-timeout" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-wait-timeout" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-wait-timeout" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-wait-timeout" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-wait-timeout" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-wait-timeout" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-wait-timeout">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-wait-timeout">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-wait-timeout">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-wait-timeout'));
const output = document.getElementById('output-wait-timeout');
const delayInput = document.getElementById('delayInput-wait-timeout');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-wait-timeout');
const backoffSelect = document.getElementById('backoffStrategyV2-wait-timeout');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-wait-timeout');
  const outcomeDisplay = document.getElementById('outcomeDisplay-wait-timeout');
  const ganttTemplate = document.getElementById('ganttChartTemplate-wait-timeout');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-wait-timeout');
  const outcomeTemplate = document.getElementById('outcomeTemplate-wait-timeout');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h2 id="backoffstrategyv2"><code class="language-plaintext highlighter-rouge">backoffStrategyV2</code></h2>

<h3 id="retryimmediately"><code class="language-plaintext highlighter-rouge">retryImmediately</code></h3>

<p>The simplest backoff strategy is <code class="language-plaintext highlighter-rouge">retryImmediately</code>.
It’s the <code class="language-plaintext highlighter-rouge">for</code> loop of backoff strategies.</p>

<p>The waiter iterates, checking the state of the resource, over and over again, until, at some point, the resource reaches the desired state.
The next check completes the waiter.</p>

<p>In the following example, I’ve shrunk the resource state change delay to 1 second.
This keeps it within bounds for the simulator.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-retry-immediately" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-retry-immediately" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-retry-immediately" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately" selected="">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-retry-immediately" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-retry-immediately" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-retry-immediately" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-retry-immediately" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-retry-immediately" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-retry-immediately" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-retry-immediately" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-retry-immediately" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-retry-immediately" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-retry-immediately" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-retry-immediately" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-retry-immediately">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-retry-immediately">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-retry-immediately">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-retry-immediately'));
const output = document.getElementById('output-retry-immediately');
const delayInput = document.getElementById('delayInput-retry-immediately');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-retry-immediately');
const backoffSelect = document.getElementById('backoffStrategyV2-retry-immediately');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-retry-immediately');
  const outcomeDisplay = document.getElementById('outcomeDisplay-retry-immediately');
  const ganttTemplate = document.getElementById('ganttChartTemplate-retry-immediately');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-retry-immediately');
  const outcomeTemplate = document.getElementById('outcomeTemplate-retry-immediately');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h3 id="fixeddelaywithoutjitter"><code class="language-plaintext highlighter-rouge">fixedDelayWithoutJitter</code></h3>

<p>The first real “strategy” is <code class="language-plaintext highlighter-rouge">fixedDelayWithoutJitter</code>.
This does exactly what you’d think.
It waits for the specified <code class="language-plaintext highlighter-rouge">delay</code> between checks of the resource.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-fixed-delay-without-jitter" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-fixed-delay-without-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-fixed-delay-without-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter" selected="">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-fixed-delay-without-jitter" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-fixed-delay-without-jitter" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-fixed-delay-without-jitter" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-fixed-delay-without-jitter" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-fixed-delay-without-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-fixed-delay-without-jitter" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-fixed-delay-without-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-fixed-delay-without-jitter" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-fixed-delay-without-jitter" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-fixed-delay-without-jitter" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-fixed-delay-without-jitter" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-fixed-delay-without-jitter">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-fixed-delay-without-jitter">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-fixed-delay-without-jitter">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-fixed-delay-without-jitter'));
const output = document.getElementById('output-fixed-delay-without-jitter');
const delayInput = document.getElementById('delayInput-fixed-delay-without-jitter');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-fixed-delay-without-jitter');
const backoffSelect = document.getElementById('backoffStrategyV2-fixed-delay-without-jitter');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-fixed-delay-without-jitter');
  const outcomeDisplay = document.getElementById('outcomeDisplay-fixed-delay-without-jitter');
  const ganttTemplate = document.getElementById('ganttChartTemplate-fixed-delay-without-jitter');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-fixed-delay-without-jitter');
  const outcomeTemplate = document.getElementById('outcomeTemplate-fixed-delay-without-jitter');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<p>This seems to be the default backoff strategy in the SDKs.
At least, I’ve yet to see one using something else.</p>

<h3 id="fixeddelay"><code class="language-plaintext highlighter-rouge">fixedDelay</code></h3>

<p>Fixed delay has a little secret. 
It includes jitter!</p>

<p>With jitter, fixed delay waits between <code class="language-plaintext highlighter-rouge">0</code> seconds and the <code class="language-plaintext highlighter-rouge">delay</code> amount before re-checking the resource.
This means that you always get at least the same number of checks of the resource as you would with <a href="#fixeddelaywithoutjitter"><code class="language-plaintext highlighter-rouge">fixedDelayWithoutJitter</code></a>. 
Often you get more.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-fixed-delay" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-fixed-delay" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-fixed-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay" selected="">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-fixed-delay" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-fixed-delay" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-fixed-delay" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-fixed-delay" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-fixed-delay" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-fixed-delay" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-fixed-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-fixed-delay" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-fixed-delay" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-fixed-delay" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-fixed-delay" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-fixed-delay">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-fixed-delay">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-fixed-delay">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-fixed-delay'));
const output = document.getElementById('output-fixed-delay');
const delayInput = document.getElementById('delayInput-fixed-delay');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-fixed-delay');
const backoffSelect = document.getElementById('backoffStrategyV2-fixed-delay');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-fixed-delay');
  const outcomeDisplay = document.getElementById('outcomeDisplay-fixed-delay');
  const ganttTemplate = document.getElementById('ganttChartTemplate-fixed-delay');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-fixed-delay');
  const outcomeTemplate = document.getElementById('outcomeTemplate-fixed-delay');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h3 id="exponentialdelaywithoutjitter"><code class="language-plaintext highlighter-rouge">exponentialDelayWithoutJitter</code></h3>

<p>Exponential delay, unlike fixed delay, introduces more and more wait time between checks of the resource.
The algorithm looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>delay = min(baseDelay × 2 ^ (attempt - 1), maxDelay)
</code></pre></div></div>

<p>With a large enough <code class="language-plaintext highlighter-rouge">maxDelay</code>, and a resource that transitions state towards the second half of the <code class="language-plaintext highlighter-rouge">waitTimeout</code>, you can expect substantial periods of waiting. 
In the following example, the last wait took 8 seconds which is more than half of the entire waiter period.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-exponential-delay-without-jitter" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-exponential-delay-without-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-exponential-delay-without-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter" selected="">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-exponential-delay-without-jitter" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-exponential-delay-without-jitter" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-exponential-delay-without-jitter" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-exponential-delay-without-jitter" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-exponential-delay-without-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-exponential-delay-without-jitter" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-exponential-delay-without-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-exponential-delay-without-jitter" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-exponential-delay-without-jitter" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-exponential-delay-without-jitter" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-exponential-delay-without-jitter" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-exponential-delay-without-jitter">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-exponential-delay-without-jitter">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-exponential-delay-without-jitter">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-exponential-delay-without-jitter'));
const output = document.getElementById('output-exponential-delay-without-jitter');
const delayInput = document.getElementById('delayInput-exponential-delay-without-jitter');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-exponential-delay-without-jitter');
const backoffSelect = document.getElementById('backoffStrategyV2-exponential-delay-without-jitter');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-exponential-delay-without-jitter');
  const outcomeDisplay = document.getElementById('outcomeDisplay-exponential-delay-without-jitter');
  const ganttTemplate = document.getElementById('ganttChartTemplate-exponential-delay-without-jitter');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-exponential-delay-without-jitter');
  const outcomeTemplate = document.getElementById('outcomeTemplate-exponential-delay-without-jitter');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<p>Since the algorithm picks the minimum between the <code class="language-plaintext highlighter-rouge">baseDelay</code> and <code class="language-plaintext highlighter-rouge">maxDelay</code>, you can actually reproduce the <code class="language-plaintext highlighter-rouge">fixedDelay</code> output by matching the two delays:</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-fake-fixed-delay" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-fake-fixed-delay" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-fake-fixed-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter" selected="">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-fake-fixed-delay" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-fake-fixed-delay" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-fake-fixed-delay" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-fake-fixed-delay" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-fake-fixed-delay" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-fake-fixed-delay" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-fake-fixed-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-fake-fixed-delay" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-fake-fixed-delay" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-fake-fixed-delay" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-fake-fixed-delay" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-fake-fixed-delay">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-fake-fixed-delay">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-fake-fixed-delay">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-fake-fixed-delay'));
const output = document.getElementById('output-fake-fixed-delay');
const delayInput = document.getElementById('delayInput-fake-fixed-delay');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-fake-fixed-delay');
const backoffSelect = document.getElementById('backoffStrategyV2-fake-fixed-delay');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-fake-fixed-delay');
  const outcomeDisplay = document.getElementById('outcomeDisplay-fake-fixed-delay');
  const ganttTemplate = document.getElementById('ganttChartTemplate-fake-fixed-delay');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-fake-fixed-delay');
  const outcomeTemplate = document.getElementById('outcomeTemplate-fake-fixed-delay');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h3 id="exponentialdelay"><code class="language-plaintext highlighter-rouge">exponentialDelay</code></h3>

<p>Full jitter combines the jitter implementation as seen in <a href="#fixeddelay"><code class="language-plaintext highlighter-rouge">fixedDelay</code></a> with an exponential delay.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-exponential-delay" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-exponential-delay" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-exponential-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay" selected="">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-exponential-delay" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-exponential-delay" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-exponential-delay" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-exponential-delay" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-exponential-delay" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-exponential-delay" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-exponential-delay" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-exponential-delay" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-exponential-delay" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-exponential-delay" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-exponential-delay" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-exponential-delay">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-exponential-delay">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-exponential-delay">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-exponential-delay'));
const output = document.getElementById('output-exponential-delay');
const delayInput = document.getElementById('delayInput-exponential-delay');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-exponential-delay');
const backoffSelect = document.getElementById('backoffStrategyV2-exponential-delay');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-exponential-delay');
  const outcomeDisplay = document.getElementById('outcomeDisplay-exponential-delay');
  const ganttTemplate = document.getElementById('ganttChartTemplate-exponential-delay');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-exponential-delay');
  const outcomeTemplate = document.getElementById('outcomeTemplate-exponential-delay');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h3 id="exponentialdelayhalfjitter"><code class="language-plaintext highlighter-rouge">exponentialDelayHalfJitter</code></h3>

<p>And finally, half jitter gives you jitter somewhere between half the full value and the full value.
This gives you something closer to the behaviour of not having jitter while still keeping the jitter safety.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Waiter Override Configuration</h3>
  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-exponential-delay-half-jitter" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-exponential-delay-half-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class="hidden pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-exponential-delay-half-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter" selected="">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-exponential-delay-half-jitter" class="hidden">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-exponential-delay-half-jitter" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-exponential-delay-half-jitter" class="hidden space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-exponential-delay-half-jitter" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-exponential-delay-half-jitter" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-exponential-delay-half-jitter" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 hidden">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-exponential-delay-half-jitter" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="hidden">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-exponential-delay-half-jitter" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 hidden">Timeline</h3>
    <div id="legend-exponential-delay-half-jitter" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-exponential-delay-half-jitter" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-exponential-delay-half-jitter" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-exponential-delay-half-jitter">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-exponential-delay-half-jitter">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-exponential-delay-half-jitter">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-exponential-delay-half-jitter'));
const output = document.getElementById('output-exponential-delay-half-jitter');
const delayInput = document.getElementById('delayInput-exponential-delay-half-jitter');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-exponential-delay-half-jitter');
const backoffSelect = document.getElementById('backoffStrategyV2-exponential-delay-half-jitter');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-exponential-delay-half-jitter');
  const outcomeDisplay = document.getElementById('outcomeDisplay-exponential-delay-half-jitter');
  const ganttTemplate = document.getElementById('ganttChartTemplate-exponential-delay-half-jitter');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-exponential-delay-half-jitter');
  const outcomeTemplate = document.getElementById('outcomeTemplate-exponential-delay-half-jitter');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>

<h2 id="simulator">Simulator</h2>

<p>Play around with the simulator by tweaking any of its values below.</p>

<div class="bg-slate-800 border border-slate-500/20 rounded-lg p-4 pt-0 my-4">
  <h3 class="text-lg font-semibold text-slate-100 mb-4 ">Waiter Override Configuration</h3>
  <div class="">
    <label class="block text-sm text-slate-200 mb-1">Max Attempts</label>
    <input type="number" id="maxAttempts-expanded" value="10" max="10" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Limited to 10 attempts for this visualization</p>
  </div>

  <div class="">
    <label class="block text-sm text-slate-200 mb-1">Wait Timeout (seconds)</label>
    <input type="number" id="waitTimeout-expanded" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">Must be a positive integer</p>
  </div>

  <div class=" pb-4">
    <label class="block text-sm text-slate-200 mb-1">Backoff Strategy</label>
    <select id="backoffStrategyV2-expanded" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="retryImmediately">Retry Immediately</option>
      <option value="fixedDelay" selected="">Fixed Delay</option>
      <option value="fixedDelayWithoutJitter">Fixed Delay Without Jitter</option>
      <option value="exponentialDelay">Exponential Delay</option>
      <option value="exponentialDelayWithoutJitter">Exponential Delay Without Jitter</option>
      <option value="exponentialDelayHalfJitter">Exponential Delay Half Jitter</option>
    </select>
  </div>

  <div id="delayInput-expanded" class="">
    <label class="block text-sm text-slate-200 mb-1">Delay (seconds)</label>
    <input type="number" id="delaySeconds-expanded" value="5" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
  </div>

  <div id="exponentialDelayInputs-expanded" class=" space-y-3">
    <div>
      <label class="block text-sm text-slate-200 mb-1">Base Delay (seconds)</label>
      <input type="number" id="baseDelaySeconds-expanded" value="1" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
    <div>
      <label class="block text-sm text-slate-200 mb-1">Max Delay (seconds)</label>
      <input type="number" id="maxDelaySeconds-expanded" value="60" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    </div>
  </div>

  <div class="pt-4">
    <pre id="output-expanded" class="text-sm text-slate-300 bg-slate-900 p-3 rounded whitespace-pre-wrap break-all overflow-x-auto"></pre>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4">Simulation Configuration</h3>
    <label class="block text-sm text-slate-200 mb-1">Resource State</label>
    <select id="resourceState-expanded" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100">
      <option value="success" selected="">Success</option>
      <option value="error">Error</option>
    </select>
    <p class="text-xs text-slate-400 mt-1">Once the resource changes state, which state it transitions to</p>
  </div>

  <div class="">
    <label class="block text-sm text-slate-200 mb-1">State Delay (seconds)</label>
    <input type="number" id="stateDelay-expanded" value="10" step="1" min="1" class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-slate-100" />
    <p class="text-xs text-slate-400 mt-1">How long the resource takes to reach the selected state</p>
  </div>

  <div class="pt-4 ">
    <h3 class="text-lg font-semibold text-slate-100 mb-4 ">Timeline</h3>
    <div id="legend-expanded" class="flex gap-4 mb-3 text-xs">
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-cyan-400"></div>
        <span class="text-slate-300">API Call (100ms)</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-purple-400"></div>
        <span class="text-slate-300">Waiting</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-green-400"></div>
        <span class="text-slate-300">→ Success</span>
      </div>
      <div class="flex items-center gap-1">
        <div class="w-3 h-3 bg-red-400"></div>
        <span class="text-slate-300">→ Error</span>
      </div>
    </div>
    <div id="ganttChart-expanded" class="bg-slate-900 rounded p-3 overflow-x-auto">
      <!-- Gantt chart will be generated here -->
    </div>
    <div id="outcomeDisplay-expanded" class="mt-3 p-3 rounded text-sm font-medium">
      <!-- Outcome will be displayed here -->
    </div>
  </div>

  <!-- Templates -->
  <template id="ganttChartTemplate-expanded">
    <div class="grid grid-cols-21 gap-px p-2 rounded overflow-x-auto min-h-32" data-gantt-grid="">
    </div>
  </template>

  <template id="ganttItemTemplate-expanded">
    <div data-gantt-item=""></div>
  </template>

  <template id="outcomeTemplate-expanded">
    <div class="flex items-center gap-2">
      <span class="px-1.5 py-0.5 rounded text-xs font-medium" data-outcome-badge=""></span>
      <span class="text-slate-400 text-xs" data-outcome-text=""></span>
    </div>
  </template>
</div>

<script type="module">
const inputs = ['maxAttempts', 'waitTimeout', 'backoffStrategyV2', 'delaySeconds', 'baseDelaySeconds', 'maxDelaySeconds', 'resourceState', 'stateDelay'].map(id => document.getElementById(id + '-expanded'));
const output = document.getElementById('output-expanded');
const delayInput = document.getElementById('delayInput-expanded');
const exponentialDelayInputs = document.getElementById('exponentialDelayInputs-expanded');
const backoffSelect = document.getElementById('backoffStrategyV2-expanded');


const toggleDelayInput = () => {
  const strategy = backoffSelect.value;
  const isFixedDelay = strategy === 'fixedDelay' || strategy === 'fixedDelayWithoutJitter';
  const isExponentialWithParams = ['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(strategy);
  const isRetryImmediately = strategy === 'retryImmediately';
  
  // Check if configuration is visible by looking at the backoff select visibility
  const isConfigurationVisible = !backoffSelect.closest('div').classList.contains('hidden');
  
  if (isConfigurationVisible) {
    if (isFixedDelay) {
      delayInput.classList.remove('hidden');
      exponentialDelayInputs.classList.add('hidden');
    } else if (isExponentialWithParams) {
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.remove('hidden');
    } else {
      // Hide both delay inputs for retryImmediately and any other strategies
      delayInput.classList.add('hidden');
      exponentialDelayInputs.classList.add('hidden');
    }
  }
};

const calculateDelay = ({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds }) => {
  const baseMs = (parseFloat(baseDelaySeconds) || 1) * 1000; // ms
  const maxMs = (parseFloat(maxDelaySeconds) || 60) * 1000; // ms
  const fixedMs = (parseFloat(delaySeconds) || 5) * 1000; // ms
  
  switch (backoffStrategy) {
    case 'retryImmediately':
      return 0; // No delay - retry immediately
    case 'fixedDelay': {
      // Full Jitter: actualDelay = random(0, fixedDelay)
      return Math.random() * fixedMs;
    }
    case 'fixedDelayWithoutJitter':
      return fixedMs;
    case 'exponentialDelay': {
      // Full Jitter: actualDelay = random(0, exponentialDelay)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return Math.random() * exponentialDelayMs;
    }
    case 'exponentialDelayWithoutJitter':
      return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
    case 'exponentialDelayHalfJitter': {
      // Half Jitter: actualDelay = exponentialDelay/2 + random(0, exponentialDelay/2)
      const exponentialDelayMs = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
      return (exponentialDelayMs / 2) + (Math.random() * (exponentialDelayMs / 2));
    }
    default:
      throw `unknown backoffStrategy=${backoffStrategy}`
  }
};

const generateGanttChart = ({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay }) => {
  const ganttChart = document.getElementById('ganttChart-expanded');
  const outcomeDisplay = document.getElementById('outcomeDisplay-expanded');
  const ganttTemplate = document.getElementById('ganttChartTemplate-expanded');
  const ganttItemTemplate = document.getElementById('ganttItemTemplate-expanded');
  const outcomeTemplate = document.getElementById('outcomeTemplate-expanded');
  
  const maxAttemptsNum = Math.min(parseInt(maxAttempts), 10); // Limit to 10 attempts
  const waitTimeoutMs = parseInt(waitTimeout) * 1000; // ms
  const stateDelayMs = parseFloat(stateDelay) * 1000; // ms
  const responseTimeMs = 100; // 100ms response time
  
  const { timeline, totalTime } = Array.from({ length: maxAttemptsNum }, (_, i) => i + 1)
    .reduce((acc, attempt) => {
      if (acc.finished) return acc;
      
      const apiCall = {
        type: 'api-call',
        attempt: attempt,
        start: acc.currentTime,
        duration: responseTimeMs,
        label: `API Call ${attempt}`
      };
      
      const timeAfterApiCall = acc.currentTime + responseTimeMs;
      const newTimeline = [...acc.timeline, apiCall];
      
      // Check if resource state is reached
      if (timeAfterApiCall >= stateDelayMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      // If this is the last attempt or we've hit timeout, don't add delay
      if (attempt >= maxAttemptsNum || timeAfterApiCall >= waitTimeoutMs) {
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: timeAfterApiCall,
          finished: true
        };
      }
      
      const waitDurationMs = calculateDelay({ attempt, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds });
      
      if (waitDurationMs === 0) {
        // No wait period for retryImmediately - continue directly to next attempt
        return {
          timeline: newTimeline,
          currentTime: timeAfterApiCall,
          totalTime: 0,
          finished: false
        };
      }
      
      const waitPeriod = {
        type: 'wait',
        attempt: attempt,
        start: timeAfterApiCall,
        duration: waitDurationMs,
        label: `Wait ${(waitDurationMs / 1000).toFixed(1)}s`
      };
      
      const timeAfterWait = timeAfterApiCall + waitDurationMs;
      
      // Check timeout after wait
      if (timeAfterWait >= waitTimeoutMs) {
        return {
          timeline: [...newTimeline, waitPeriod],
          currentTime: timeAfterWait,
          totalTime: waitTimeoutMs,
          finished: true
        };
      }
      
      return {
        timeline: [...newTimeline, waitPeriod],
        currentTime: timeAfterWait,
        totalTime: 0,
        finished: false
      };
    }, { timeline: [], currentTime: 0, totalTime: 0, finished: false });
  
  const finalTotalTimeMs = totalTime || timeline.reduce((max, item) => Math.max(max, item.start + item.duration), 0);

  const stateTransitionMs = 100;

  const stateDelayStart = stateDelayMs - stateTransitionMs;
  
  // Insert state marker into timeline at correct chronological position
  if (stateDelayMs > 0 && stateDelayMs <= finalTotalTimeMs) {
    const stateMarker = {
      type: 'state-change',
      start: stateDelayStart,
      duration: stateTransitionMs,
      label: `${resourceState === 'success' ? 'Success' : 'Error'} State`,
      resourceState: resourceState
    };
    
    const insertIndex = timeline.findIndex(item => item.start > stateDelayMs) ?? timeline.length;

    timeline.splice(insertIndex, 0, stateMarker);
  }
  
  // Generate CSS Grid-based timeline using template
  const maxTimeMs = Math.max(finalTotalTimeMs, 1000); // At least 1 second
  const timeSlots = Math.ceil(maxTimeMs / 100); // 100ms slots
  
  // Clone the gantt chart template
  const ganttClone = ganttTemplate.content.cloneNode(true);
  const ganttGrid = ganttClone.querySelector('[data-gantt-grid]');
  
  // Set grid rows
  ganttGrid.style.gridTemplateRows = `repeat(${timeSlots}, 4px)`;
  
  // Create gantt items
  timeline.forEach((item, colIndex) => {
    const startRow = Math.floor(item.start / 100) + 1; // Convert ms to 100ms slots
    const endRow = Math.floor((item.start + item.duration) / 100) + 1;
    const rowSpan = Math.max(endRow - startRow, 1);
    
    let bgColorClass;
    if (item.type === 'api-call') {
      bgColorClass = 'bg-cyan-400';
    } else if (item.type === 'wait') {
      bgColorClass = 'bg-purple-400';
    } else if (item.type === 'state-change') {
      bgColorClass = item.resourceState === 'success' ? 'bg-green-400' : 'bg-red-400';
    }
    
    const itemClone = ganttItemTemplate.content.cloneNode(true);
    const itemElement = itemClone.querySelector('[data-gantt-item]');
    itemElement.className = bgColorClass;
    itemElement.style.gridColumn = colIndex + 1;
    itemElement.style.gridRow = `${startRow} / span ${rowSpan}`;
    itemElement.title = item.label;
    
    ganttGrid.appendChild(itemClone);
  });
  
  // Replace gantt chart content
  ganttChart.innerHTML = '';
  ganttChart.appendChild(ganttClone);
  
  // Update outcome display using template
  let outcome, outcomeColorClass, outcomeText;
  const finalTotalTimeSeconds = finalTotalTimeMs / 1000; // Convert back to seconds for display
  const waitTimeoutSeconds = waitTimeoutMs / 1000;
  
  if (finalTotalTimeMs >= stateDelayMs && resourceState === 'success') {
    outcome = 'SUCCESS';
    outcomeColorClass = 'text-green-400';
    outcomeText = `Resource reached success state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= stateDelayMs && resourceState === 'error') {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `Resource reached error state after ${finalTotalTimeSeconds.toFixed(1)}s`;
  } else if (finalTotalTimeMs >= waitTimeoutMs) {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-yellow-400';
    outcomeText = `The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT${waitTimeoutSeconds}S`;
  } else {
    outcome = 'SdkClientException';
    outcomeColorClass = 'text-red-400';
    outcomeText = `The waiter has exceeded the max retry attempts: ${maxAttemptsNum}`;
  }
  
  const outcomeClone = outcomeTemplate.content.cloneNode(true);
  const outcomeBadge = outcomeClone.querySelector('[data-outcome-badge]');
  const outcomeTextElement = outcomeClone.querySelector('[data-outcome-text]');
  
  outcomeBadge.className = `px-1.5 py-0.5 rounded text-xs font-medium ${outcomeColorClass}`;
  outcomeBadge.textContent = outcome;
  outcomeTextElement.textContent = outcomeText;
  
  outcomeDisplay.innerHTML = '';
  outcomeDisplay.appendChild(outcomeClone);
};

const update = () => {
  const [maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay] = inputs.map(i => i.value);
  
  let backoffLine;
  if (backoffStrategy === 'fixedDelay') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelay(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (backoffStrategy === 'fixedDelayWithoutJitter') {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.fixedDelayWithoutJitter(
      /* delay */ ${delaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else if (['exponentialDelay', 'exponentialDelayWithoutJitter', 'exponentialDelayHalfJitter'].includes(backoffStrategy)) {
    backoffLine = `.backoffStrategyV2(
    BackoffStrategy.${backoffStrategy}(
      /* baseDelay */ ${baseDelaySeconds}.seconds.toJavaDuration(),
      /* maxDelay */ ${maxDelaySeconds}.seconds.toJavaDuration()
    )
  )`;
  } else {
    backoffLine = `.backoffStrategyV2(BackoffStrategy.${backoffStrategy}())`;
  }
  
  output.textContent = `WaiterOverrideConfiguration.builder()
  .maxAttempts(${maxAttempts})
  .waitTimeout(${waitTimeout}.seconds.toJavaDuration())
  ${backoffLine}
  .build()`;
  
  generateGanttChart({ maxAttempts, waitTimeout, backoffStrategy, delaySeconds, baseDelaySeconds, maxDelaySeconds, resourceState, stateDelay });
};

inputs.forEach(input => input.addEventListener('input', update));
inputs.forEach(input => input.addEventListener('change', update));
backoffSelect.addEventListener('change', toggleDelayInput);
toggleDelayInput();
update();
</script>]]></content><author><name></name></author><summary type="html"><![CDATA[This post is 4ᵗʰ in a series about AWS SDK Waiters. You definitely don’t need to have read all the others before reading this one. Though perhaps “Use AWS SDK Waiters” deserves a little skim to get up to speed:]]></summary></entry><entry><title type="html">Typescript waiters are a bit weird</title><link href="https://kieran.casa/aws-sdk-waiters-ts/" rel="alternate" type="text/html" title="Typescript waiters are a bit weird" /><published>2025-10-05T05:05:38+00:00</published><updated>2025-10-05T05:05:38+00:00</updated><id>https://kieran.casa/aws-sdk-waiters-ts</id><content type="html" xml:base="https://kieran.casa/aws-sdk-waiters-ts/"><![CDATA[<p>Typescript (and Javascript) waiters for the AWS SDKs are weird enough that they deserve a post of their own.</p>

<p>I’ll start here with an example using SSM and waiting on a Command Invocation. 
That’s the resource you get back when you call <code class="language-plaintext highlighter-rouge">SendCommand</code>.
It’s just like the examples in the <a href="/aws-sdk-waiters/">Use AWS SDK Waiters</a> post which focuses on the JVM.</p>

<h2 id="a-successful-waiter">A successful waiter</h2>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
  <span class="nx">SendCommandCommand</span><span class="p">,</span>
  <span class="nx">SSMClient</span><span class="p">,</span>
  <span class="nx">waitUntilCommandExecuted</span><span class="p">,</span>
<span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@aws-sdk/client-ssm</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">ssmClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SSMClient</span><span class="p">();</span>

<span class="kd">const</span> <span class="nx">sendCommandOutput</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="k">new</span> <span class="nc">SendCommandCommand</span><span class="p">({</span>
  <span class="na">InstanceIds</span><span class="p">:</span> <span class="p">[</span><span class="nx">instanceId</span><span class="p">],</span>
  <span class="na">DocumentName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AWS-RunShellScript</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">Parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">commands</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
      <span class="cm">/* https://kieran.casa/starting-bash-scripts/ */</span>
      <span class="dl">"</span><span class="s2">#!/usr/bin/env bash</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">set -o xtrace</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">set -o errexit</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">set -o nounset</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">set -o pipefail</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">echo 'Hello, World!'</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">exit 0</span><span class="dl">"</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}));</span>

<span class="kd">const</span> <span class="nx">waiterResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">waitUntilCommandExecuted</span><span class="p">({</span> <span class="na">client</span><span class="p">:</span> <span class="nx">ssmClient</span><span class="p">,</span> <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">30</span> <span class="p">},</span> <span class="p">{</span>
  <span class="na">CommandId</span><span class="p">:</span> <span class="nx">sendCommandOutput</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
  <span class="na">InstanceId</span><span class="p">:</span> <span class="nx">instanceId</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">getCommandInvocationCommandOutput</span> <span class="o">=</span> <span class="nx">waiterResult</span><span class="p">.</span><span class="nx">reason</span> <span class="kd">as </span><span class="nx">GetCommandInvocationCommandOutput</span><span class="p">;</span>
  
<span class="nx">assert</span><span class="p">.</span><span class="nf">equal</span><span class="p">(</span><span class="nx">getCommandInvocationCommandOutput</span><span class="p">.</span><span class="nx">Status</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Success</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>Here’s where the javascript waiters start to drift from the Java ones.
First, you <em>must</em> import the wait function directly. 
It’s not a function on the ssmClient.</p>

<p>You pass in the client and <code class="language-plaintext highlighter-rouge">maxWaitTime</code> as fields in a object for the first parameter. 
<code class="language-plaintext highlighter-rouge">maxWaitTime</code> is how long you’re willing to wait (in seconds) for the operation to complete.</p>

<p>Just to linger on that <code class="language-plaintext highlighter-rouge">maxWaitTime</code> for a little bit.
The Java-based waiters <em>always</em> have a default wait time.
AWS decides for them how long waiters should wait.
But there are loads of occasions where it makes sense for you to override that value.
Like for resources where you control how long that take to run—and so you know best.
Or is situations where you have a finite time budget.
SSM Command Invocations is a great example of when you know better than AWS how long you expect them to take.</p>

<p><code class="language-plaintext highlighter-rouge">minDelay</code> and <code class="language-plaintext highlighter-rouge">maxDelay</code> are also accepted along side <code class="language-plaintext highlighter-rouge">maxWaitTime</code> but are optional.
The waiter doesn’t let you configure a retry strategy.
Instead it always uses exponential backoff.
If you want to get back to a fixed wait time back-off, just set <code class="language-plaintext highlighter-rouge">minDelay</code> and <code class="language-plaintext highlighter-rouge">maxDelay</code> to the same values.</p>

<p>You get back a <code class="language-plaintext highlighter-rouge">WaiterResult</code> and in the <code class="language-plaintext highlighter-rouge">reason</code> property is the result of the last polling operation.
In our case its a <code class="language-plaintext highlighter-rouge">GetCommandInvocationCommandOutput</code>.
But it’s not strongly typed… Instead its <code class="language-plaintext highlighter-rouge">any</code>. 
So you need to cast it into <code class="language-plaintext highlighter-rouge">GetCommandInvocationCommandOutput</code> if you want to reach in to any of its fields.</p>

<h2 id="a-failed-waiter">A failed waiter</h2>

<p>Now here’s what a waiter looks like when it fails.
That is, when the resource enters a state where it could never possibly reach the desired state.</p>

<p>You’ll see that I’ve used Node’s assertion library in the rest of this post.
Waiters are particularly useful during integration-style tests and so you’ll often see assertions paired with waiters this way.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">sendCommandOutput</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="k">new</span> <span class="nc">SendCommandCommand</span><span class="p">({</span>
  <span class="na">InstanceIds</span><span class="p">:</span> <span class="p">[</span><span class="nx">instanceId</span><span class="p">],</span>
  <span class="na">DocumentName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AWS-RunShellScript</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">Parameters</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">commands</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
      <span class="dl">"</span><span class="s2">#!/usr/bin/env bash</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">exit 1</span><span class="dl">"</span> <span class="cm">/* ← bound to fail */</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}));</span>

<span class="k">await</span> <span class="nx">assert</span><span class="p">.</span><span class="nf">rejects</span><span class="p">(</span>
  <span class="nf">waitUntilCommandExecuted</span><span class="p">({</span> <span class="na">client</span><span class="p">:</span> <span class="nx">ssmClient</span><span class="p">,</span> <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">30</span> <span class="p">},</span> <span class="p">{</span>
    <span class="na">CommandId</span><span class="p">:</span> <span class="nx">sendCommandOutput</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
    <span class="na">InstanceId</span><span class="p">:</span> <span class="nx">instanceId</span>
  <span class="p">}),</span>
  <span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="nb">Error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nf">ok</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">Error</span><span class="dl">"</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">parsedMessage</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>

    <span class="nx">assert</span><span class="p">.</span><span class="nf">equal</span><span class="p">(</span><span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span> <span class="dl">"</span><span class="s2">FAILURE</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nf">ok</span><span class="p">(</span><span class="dl">"</span><span class="s2">200: OK</span><span class="dl">"</span> <span class="k">in</span> <span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">observedResponses</span><span class="p">);</span>

    <span class="nx">assert</span><span class="p">.</span><span class="nf">partialDeepStrictEqual</span><span class="p">(</span><span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">reason</span> <span class="kd">as </span><span class="nx">GetCommandInvocationCommandOutput</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">Status</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Failed</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">StandardErrorContent</span><span class="p">:</span> <span class="dl">"</span><span class="s2">failed to run commands: exit status 1</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">StatusDetails</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Failed</span><span class="dl">'</span><span class="p">,</span>
      <span class="c1">// And all of the rest of the output fields</span>
    <span class="p">});</span>

    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<p>So here’s the next bit of weirdness.
The AWS SDK hides a bunch of information in a JSON string inside the error’s message property.
Hmm…</p>

<p>Inside a failed waiter message we find:</p>
<ul>
  <li>A state field with the string <code class="language-plaintext highlighter-rouge">FAILURE</code>.</li>
  <li>An <code class="language-plaintext highlighter-rouge">observedResponses</code> object where the keys are the response <a href="https://httpgoats.com/">HTTP status codes</a> and the values are the count.</li>
  <li>A <code class="language-plaintext highlighter-rouge">reason</code> object which is the response from the last call. Again you’ll want to cast into the actual output type.</li>
</ul>

<p>When using a waiter like this, that <code class="language-plaintext highlighter-rouge">reason</code> information will probably be very helpful when debugging.</p>

<h2 id="a-waiter-that-times-out">A waiter that times out</h2>

<p>To make a waiter time out, we can just make the SSM Command sleep for longer than the waiter is willing to wait.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">sendCommandOutput</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">SendCommandCommand</span><span class="p">({</span>
    <span class="na">InstanceIds</span><span class="p">:</span> <span class="p">[</span><span class="nx">instanceId</span><span class="p">],</span>
    <span class="na">DocumentName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AWS-RunShellScript</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Parameters</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">commands</span><span class="p">:</span> <span class="p">[</span>
        <span class="dl">"</span><span class="s2">#!/usr/bin/env bash</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">sleep 60</span><span class="dl">"</span> <span class="cm">/* ← that'll sleep for 1 minute */</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">exit 0</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">],</span>
    <span class="p">},</span>
  <span class="p">}),</span>
<span class="p">);</span>

<span class="k">await</span> <span class="nx">assert</span><span class="p">.</span><span class="nf">rejects</span><span class="p">(</span>
  <span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
    <span class="p">{</span>
      <span class="na">client</span><span class="p">:</span> <span class="nx">ssmClient</span><span class="p">,</span>
      <span class="cm">/**
       * `maxWaitTime` is set to something well below the 60 second sleep.
       * This is guaranteed to fail.
       */</span>
      <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="na">CommandId</span><span class="p">:</span> <span class="nx">sendCommandOutput</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
      <span class="na">InstanceId</span><span class="p">:</span> <span class="nx">instanceId</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">),</span>
  <span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="nb">Error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nf">ok</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">TimeoutError</span><span class="dl">"</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">parsedMessage</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>

    <span class="nx">assert</span><span class="p">.</span><span class="nf">equal</span><span class="p">(</span><span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span> <span class="dl">"</span><span class="s2">TIMEOUT</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nf">equal</span><span class="p">(</span><span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">reason</span><span class="p">,</span> <span class="dl">"</span><span class="s2">Waiter has timed out</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">assert</span><span class="p">.</span><span class="nf">ok</span><span class="p">(</span><span class="dl">"</span><span class="s2">200: OK</span><span class="dl">"</span> <span class="k">in</span> <span class="nx">parsedMessage</span><span class="p">.</span><span class="nx">observedResponses</span><span class="p">);</span>

    <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
  <span class="p">},</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Again you’ll see that we need to parse information out of JSON in the error message.</p>

<p>Unfortunately that’s all the information that you’re given when a waiter times out.
It would’ve been great if it returned the value of the last response it got before timing out.</p>

<h2 id="racing-waiters">Racing waiters</h2>

<p>Finally, waiters can be aborted if you decide that you no longer want to wait for them.</p>

<p>Waiters accepts an (optional) abort signal parameter.
You can create the signal by first creating an <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController"><code class="language-plaintext highlighter-rouge">AbortController</code></a> and then passing along the signal.
An abort controller allows you to send a cancellation message to the waiter thread.</p>

<p>In this post I use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race"><code class="language-plaintext highlighter-rouge">Promise.race</code></a>.
It accepts an array of promises and will wait for the first promise to resolve (or reject).
My waiters are configured with the same timeout (<code class="language-plaintext highlighter-rouge">PT30S</code>) but I expected Command #2 to always finish first.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">sendCommandOutputOne</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">SendCommandCommand</span><span class="p">({</span>
    <span class="na">InstanceIds</span><span class="p">:</span> <span class="p">[</span><span class="nx">instanceId</span><span class="p">],</span>
    <span class="na">DocumentName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AWS-RunShellScript</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Parameters</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">commands</span><span class="p">:</span> <span class="p">[</span>
        <span class="dl">"</span><span class="s2">#!/usr/bin/env bash</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">sleep 60</span><span class="dl">"</span> <span class="cm">/* ← 🥈 always going to lose */</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">exit 0</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">],</span>
    <span class="p">},</span>
    <span class="na">Comment</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Command #1</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">}),</span>
<span class="p">);</span>

<span class="kd">const</span> <span class="nx">sendCommandOutputTwo</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">SendCommandCommand</span><span class="p">({</span>
    <span class="na">InstanceIds</span><span class="p">:</span> <span class="p">[</span><span class="nx">instanceId</span><span class="p">],</span>
    <span class="na">DocumentName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AWS-RunShellScript</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">Parameters</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">commands</span><span class="p">:</span> <span class="p">[</span>
        <span class="dl">"</span><span class="s2">#!/usr/bin/env bash</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">sleep 5</span><span class="dl">"</span> <span class="cm">/* ← 🥇 always going to win */</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">exit 0</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">],</span>
    <span class="p">},</span>
    <span class="na">Comment</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Command #2</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">}),</span>
<span class="p">);</span>

<span class="kd">const</span> <span class="nx">commandOneWaiterController</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AbortController</span><span class="p">();</span>
<span class="c1">// Note that we don't _await_ the waiter. We just want the Promise.</span>
<span class="kd">const</span> <span class="nx">commandOneWaiterPromise</span> <span class="o">=</span> <span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
  <span class="p">{</span>
    <span class="na">client</span><span class="p">:</span> <span class="nx">ssmClient</span><span class="p">,</span>
    <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
    <span class="na">abortSignal</span><span class="p">:</span> <span class="nx">commandOneWaiterController</span><span class="p">.</span><span class="nx">signal</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="na">CommandId</span><span class="p">:</span> <span class="nx">sendCommandOutputOne</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
    <span class="na">InstanceId</span><span class="p">:</span> <span class="nx">instanceId</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">);</span>

<span class="kd">const</span> <span class="nx">commandTwoWaiterController</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AbortController</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">commandTwoWaiterPromise</span> <span class="o">=</span> <span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
  <span class="p">{</span>
    <span class="na">client</span><span class="p">:</span> <span class="nx">ssmClient</span><span class="p">,</span>
    <span class="na">maxWaitTime</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
    <span class="na">abortSignal</span><span class="p">:</span> <span class="nx">commandTwoWaiterController</span><span class="p">.</span><span class="nx">signal</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="na">CommandId</span><span class="p">:</span> <span class="nx">sendCommandOutputTwo</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
    <span class="na">InstanceId</span><span class="p">:</span> <span class="nx">instanceId</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">);</span>

<span class="kd">const</span> <span class="nx">winningCommandWaiter</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nf">race</span><span class="p">([</span>
  <span class="nx">commandOneWaiterPromise</span><span class="p">,</span>
  <span class="nx">commandTwoWaiterPromise</span><span class="p">,</span>
<span class="p">]);</span>

<span class="kd">const</span> <span class="nx">winningGetCommandInvocationCommandOutput</span> <span class="o">=</span>
    <span class="nx">winningCommandWaiter</span><span class="p">.</span><span class="nx">reason</span> <span class="kd">as </span><span class="nx">GetCommandInvocationCommandOutput</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">winningCommandId</span> <span class="o">=</span> <span class="nx">winningGetCommandInvocationCommandOutput</span><span class="p">.</span><span class="nx">CommandId</span><span class="p">;</span>

<span class="nx">assert</span><span class="p">.</span><span class="nf">equal</span><span class="p">(</span>
  <span class="nx">winningCommandId</span><span class="p">,</span>
  <span class="nx">sendCommandOutputTwo</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">,</span>
  <span class="nx">dedent</span><span class="s2">`
    Expected command #2 to finish first. Instead command #1 won.
    Command #1 ID: </span><span class="p">${</span><span class="nx">sendCommandOutputTwo</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">}</span><span class="s2">
    Command #2 ID: </span><span class="p">${</span><span class="nx">sendCommandOutputOne</span><span class="p">.</span><span class="nx">Command</span><span class="p">?.</span><span class="nx">CommandId</span><span class="p">}</span><span class="s2">
  `</span><span class="p">,</span>
<span class="p">);</span>

<span class="nx">commandOneWaiterController</span><span class="p">.</span><span class="nf">abort</span><span class="p">();</span>

<span class="k">await</span> <span class="nx">ssmClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span>
  <span class="k">new</span> <span class="nc">CancelCommandCommand</span><span class="p">({</span>
    <span class="na">CommandId</span><span class="p">:</span> <span class="nx">winningCommandId</span><span class="p">,</span>
  <span class="p">}),</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Once command #2 is determined to be the winner, we have to do a bit of clean-up.
We abort command #1’s waiter thread, so it stops polling.
But aborting the waiter won’t stop the Command Invcation.
That’s done by calling <code class="language-plaintext highlighter-rouge">CancelCommand</code>.</p>

<p>Read more about waiters in:</p>
<ul>
  <li><a href="https://aws.amazon.com/blogs/developer/waiters-in-modular-aws-sdk-for-javascript/">Waiters in modular AWS SDK for JavaScript</a> on the AWS Developer Tools Blog.</li>
  <li><a href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrate-waiters-signers.html">Waiters and signers</a> on the AWS SDK for JavaScript documentation.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Typescript (and Javascript) waiters for the AWS SDKs are weird enough that they deserve a post of their own.]]></summary></entry><entry><title type="html">Write custom waiters</title><link href="https://kieran.casa/custom-waiters/" rel="alternate" type="text/html" title="Write custom waiters" /><published>2025-09-27T00:00:00+00:00</published><updated>2025-09-27T00:00:00+00:00</updated><id>https://kieran.casa/custom-waiters</id><content type="html" xml:base="https://kieran.casa/custom-waiters/"><![CDATA[<p>In <a href="/aws-sdk-waiters/">Use AWS SDK Waiters</a> I showed how useful AWS SDK waiters are when working with resources that are eventually consistent.
But not all resources are AWS resources.
And not all AWS resources even ship with waiters…</p>

<p>Well that shouldn’t stop us.
The AWS SDKs actually ship with a really easy way to build waiters ourselves.</p>

<p>In that previous post, I showed you how I used AWS SDK waiters to wait for an SSM Command Invocation to complete.
I’d like to do something similar for an SSM Automation.
But SSM doesn’t (currently) ship an official waiter for automations.
So I wrote my own.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * @constructor is private to prevent instantiation from outside the Builder
 * @property ssmClient the real SSM client. 
 *   This waiter implements the full SSM waiter interface through Kotlin delegation.
 */</span>
<span class="kd">class</span> <span class="nc">ExtendedSsmWaiter</span> <span class="k">private</span> <span class="k">constructor</span><span class="p">(</span>
  <span class="k">private</span> <span class="kd">val</span> <span class="py">ssmClient</span><span class="p">:</span> <span class="nc">SsmClient</span><span class="p">,</span>
  <span class="k">private</span> <span class="kd">val</span> <span class="py">waiterOverrideConfiguration</span><span class="p">:</span> <span class="nc">WaiterOverrideConfiguration</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">SsmWaiter</span> <span class="k">by</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">waiter</span><span class="p">()</span> <span class="p">{</span>
  <span class="cm">/**
   * The waiter function.
   * When implementing waiters, think of them encapsulating a desired state and a way to query that state.
   * The desired state is expressed through the function itself. 
   * The desired state of this function is that an automation is executed.
   * The way to query the state is through the getAutomationExecution call and the [ssmClient] property.
   *
   * @return a waiter response object.
   *   If successful this will contain the [GetAutomationExecutionResponse] object from the last successful call.
   */</span>
  <span class="k">fun</span> <span class="nf">waitUntilAutomationExecuted</span><span class="p">(</span><span class="n">getAutomationExecutionRequest</span><span class="p">:</span> <span class="nc">GetAutomationExecutionRequest</span><span class="p">):</span> <span class="nc">WaiterResponse</span><span class="p">&lt;</span><span class="nc">GetAutomationExecutionResponse</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">waiter</span> <span class="p">=</span> <span class="nc">Waiter</span><span class="p">.</span><span class="nf">builder</span><span class="p">(</span><span class="nc">GetAutomationExecutionResponse</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
      <span class="cm">/**
       * A waiter builder is looking for two fields: acceptors and override configuration.
       *
       * [WaiterAcceptor]s is a series of predicate functions looking to advance the waiter state.
       * While a waiter is running, it will poll the [getAutomationExecutionRequest] call and pass the response to the acceptors.
       *
       * There are three waiter states that the acceptors can advance to: success, retry, and error.
       * Each type of acceptor can act on a normal response or an exception.
       * Which means that there are (`3 × 2 =`) 6 types of acceptors in total.
       *
       * The AWS SDK makes these available as static methods on the [WaiterAcceptor] interface.
       *
       * Waiters work by making the [getAutomationExecutionRequest] call and then passing the response into the acceptors.
       * They then one-by-one through the acceptors in the order they are defined.
       * If one of the acceptors returns `true`, the waiter will advance to whatever the acceptor tells it to.
       * If the acceptors return `false`, the waiter will continue on to the next acceptor.
       * If the acceptors throw an exception, the waiter will stop and the exception is returned.
       */</span>
      <span class="p">.</span><span class="nf">acceptors</span><span class="p">(</span>
        <span class="n">listOf</span><span class="p">&lt;</span><span class="nc">WaiterAcceptor</span><span class="p">&lt;</span><span class="k">in</span> <span class="nc">GetAutomationExecutionResponse</span><span class="p">&gt;&gt;(</span>
          <span class="cm">/**
           * There is likely only one or two responses that you'd consider successful.
           * This is the one state that you really want your waiter to reach.
           */</span>
          <span class="nc">WaiterAcceptor</span><span class="p">.</span><span class="nf">successOnResponseAcceptor</span> <span class="p">{</span>
            <span class="n">it</span><span class="p">.</span><span class="nf">automationExecution</span><span class="p">().</span><span class="nf">automationExecutionStatus</span><span class="p">()</span> <span class="p">==</span> <span class="nc">AutomationExecutionStatus</span><span class="p">.</span><span class="nc">SUCCESS</span>
          <span class="p">},</span>
          <span class="cm">/**
           * There are likely quite a few responses that warrant retrying the call.
           * Certainly there are more than what I've listed here.
           * Looking through the [AutomationExecutionStatus] I see twenty unique states there.
           * Maintaining this list here is probably the best argument in favour of always trying to use the AWS-vended waiters when possible.
           */</span>
          <span class="nc">WaiterAcceptor</span><span class="p">.</span><span class="nf">retryOnResponseAcceptor</span> <span class="p">{</span>
            <span class="n">it</span><span class="p">.</span><span class="nf">automationExecution</span><span class="p">().</span><span class="nf">automationExecutionStatus</span><span class="p">()</span> <span class="p">==</span> <span class="nc">AutomationExecutionStatus</span><span class="p">.</span><span class="nc">IN_PROGRESS</span>
          <span class="p">},</span>
          <span class="nc">WaiterAcceptor</span><span class="p">.</span><span class="nf">retryOnResponseAcceptor</span> <span class="p">{</span>
            <span class="n">it</span><span class="p">.</span><span class="nf">automationExecution</span><span class="p">().</span><span class="nf">automationExecutionStatus</span><span class="p">()</span> <span class="p">==</span> <span class="nc">AutomationExecutionStatus</span><span class="p">.</span><span class="nc">PENDING</span>
          <span class="p">},</span>
          <span class="cm">/**
           * Never forget to check for failure states.
           * Failure are not successes (duh!) but also mean that there's no point retrying the waiter.
           * They're non-successful terminal states.
           * When you forget failure states, the waiter will just keep retrying until the timeout is reached.
           */</span>
          <span class="nc">WaiterAcceptor</span><span class="p">.</span><span class="nf">errorOnResponseAcceptor</span><span class="p">(</span>
            <span class="p">{</span>
              <span class="n">it</span><span class="p">.</span><span class="nf">automationExecution</span><span class="p">().</span><span class="nf">automationExecutionStatus</span><span class="p">()</span> <span class="p">==</span> <span class="nc">AutomationExecutionStatus</span><span class="p">.</span><span class="nc">FAILED</span>
            <span class="p">},</span>
            <span class="cm">/**
             * Supplying a message on failure is a good idea.
             * The message is used in the exception thrown by the waiter.
             * It's a great way to quickly surface information about the resource that you're waiting on.
             * The more you reveal here, the quicker you'll debug a failure.
             */</span>
            <span class="s">"""
              Automation execution has status FAILED. Waiter transitioned to a failure state. 
              Automation Execution: $getAutomationExecutionRequest
            """</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">()</span>
          <span class="p">),</span>
        <span class="p">)</span>
          <span class="cm">/**
           * [WaitersRuntime.DEFAULT_ACCEPTORS] just instruct the waiter to retry on a response.
           * It essentially looks like this:
           *
           * ```kotlin
           * WaiterAcceptor.retryOnResponseAcceptor { true }
           * ```
           *
           * Including it last here means that the waiter will always retry non-exception responses if no other waiter matched earlier.
           */</span>
          <span class="p">.</span><span class="nf">plus</span><span class="p">(</span><span class="nc">WaitersRuntime</span><span class="p">.</span><span class="nc">DEFAULT_ACCEPTORS</span><span class="p">),</span>
      <span class="p">)</span>
      <span class="cm">/**
       * Override configuration tells the waiter how long to poll for, which backoff strategy to use and how many attempts to make.
       */</span>
      <span class="p">.</span><span class="nf">overrideConfiguration</span><span class="p">(</span><span class="n">waiterOverrideConfiguration</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>

    <span class="k">return</span> <span class="n">waiter</span><span class="p">.</span><span class="nf">run</span> <span class="p">{</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">getAutomationExecution</span><span class="p">(</span><span class="n">getAutomationExecutionRequest</span><span class="p">)</span> <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
    <span class="k">fun</span> <span class="nf">builder</span><span class="p">():</span> <span class="nc">Builder</span> <span class="p">=</span> <span class="nc">Builder</span><span class="p">()</span>
  <span class="p">}</span>

  <span class="kd">class</span> <span class="nc">Builder</span> <span class="k">internal</span> <span class="k">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">private</span> <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">ssmClient</span><span class="p">:</span> <span class="nc">SsmClient</span>

    <span class="cm">/**
     * Default waiter configurations can be quite tricky to pick.
     * For something like EC2's `waitUntilInstanceRunning`, you can make an educated guess on how long to wait.
     * But other processes may depend on what work you're waiting to happen.
     * [waitUntilAutomationExecuted] is an example of the latter type where the duration depends on what the automation is doing.
     *
     * As an example, EC2's [software.amazon.awssdk.services.ec2.waiters.DefaultEc2Waiter.instanceRunningWaiterConfig] sets a config of:
     * 40 total attempts × 15-second fixed delay backoff = 600 seconds or 10 minutes.
     *
     * In this example I've not set an upper bound on `maxAttempts`.
     * You either need to set max attempts or a wait timeout. Not both.
     * And wait timeout is far easier to reason about.
     * Just ask yourself, "how long should I wait for this to happen?"
     *
     * I've set it to wait 5 seconds between each attempt for a tight feedback loop.
     * Generally longer, wait timeouts can come with longer delays between attempts.
     *
     * I've set a wait timeout of 2 minutes as that's pretty reasonable for my use-case.
     */</span>
    <span class="k">private</span> <span class="kd">var</span> <span class="py">waiterOverrideConfiguration</span><span class="p">:</span> <span class="nc">WaiterOverrideConfiguration</span> <span class="p">=</span> <span class="nc">WaiterOverrideConfiguration</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">maxAttempts</span><span class="p">(</span><span class="nc">Int</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">backoffStrategyV2</span><span class="p">(</span><span class="nc">BackoffStrategy</span><span class="p">.</span><span class="nf">fixedDelay</span><span class="p">(</span><span class="mi">5</span><span class="p">.</span><span class="n">seconds</span><span class="p">.</span><span class="nf">toJavaDuration</span><span class="p">()))</span>
      <span class="p">.</span><span class="nf">waitTimeout</span><span class="p">(</span><span class="mi">2</span><span class="p">.</span><span class="n">minutes</span><span class="p">.</span><span class="nf">toJavaDuration</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>

    <span class="k">fun</span> <span class="nf">withWaiterOverrideConfiguration</span><span class="p">(</span><span class="n">waiterOverrideConfiguration</span><span class="p">:</span> <span class="nc">WaiterOverrideConfiguration</span><span class="p">):</span> <span class="nc">Builder</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="n">waiterOverrideConfiguration</span> <span class="p">=</span> <span class="n">waiterOverrideConfiguration</span>
      <span class="k">return</span> <span class="k">this</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">withSsmClient</span><span class="p">(</span><span class="n">ssmClient</span><span class="p">:</span> <span class="nc">SsmClient</span><span class="p">):</span> <span class="nc">Builder</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="n">ssmClient</span> <span class="p">=</span> <span class="n">ssmClient</span>
      <span class="k">return</span> <span class="k">this</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">build</span><span class="p">():</span> <span class="nc">ExtendedSsmWaiter</span> <span class="p">=</span> <span class="nc">ExtendedSsmWaiter</span><span class="p">(</span><span class="n">ssmClient</span><span class="p">,</span> <span class="n">waiterOverrideConfiguration</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then you use it just like you would any other waiter:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">ssmClient</span> <span class="p">=</span> <span class="nc">SsmClient</span><span class="p">.</span><span class="nf">builder</span><span class="p">().</span><span class="nf">build</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">ssmWaiter</span> <span class="p">=</span> <span class="nc">ExtendedSsmWaiter</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
  <span class="p">.</span><span class="nf">withSsmClient</span><span class="p">(</span><span class="n">ssmClient</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">build</span><span class="p">()</span>

<span class="kd">val</span> <span class="py">startAutomationResponse</span> <span class="p">=</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">startAutomationExecution</span><span class="p">(</span>
  <span class="nc">StartAutomationExecutionRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">documentName</span><span class="p">(</span><span class="s">"AWS-ConfigureCloudWatchOnEC2Instance"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">parameters</span><span class="p">(</span><span class="nf">mapOf</span><span class="p">(</span>
      <span class="s">"InstanceId"</span> <span class="n">to</span> <span class="nf">listOf</span><span class="p">(</span><span class="n">instanceId</span><span class="p">)</span>
    <span class="p">))</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="kd">val</span> <span class="py">getAutomationExecutionResponseWaiterResponse</span> <span class="p">=</span> <span class="n">ssmWaiter</span><span class="p">.</span><span class="nf">waitUntilAutomationExecuted</span><span class="p">(</span>
  <span class="nc">GetAutomationExecutionRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">automationExecutionId</span><span class="p">(</span><span class="n">startAutomationResponse</span><span class="p">.</span><span class="nf">automationExecutionId</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="nf">assertThat</span><span class="p">(</span><span class="n">getAutomationExecutionResponseWaiterResponse</span><span class="p">.</span><span class="nf">attemptsExecuted</span><span class="p">())</span>
  <span class="p">.</span><span class="nf">isGreaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="nf">assertThat</span><span class="p">(</span><span class="n">getAutomationExecutionResponseWaiterResponse</span><span class="p">.</span><span class="nf">matched</span><span class="p">().</span><span class="nf">response</span><span class="p">())</span>
  <span class="p">.</span><span class="n">isPresent</span>
</code></pre></div></div>

<p>Hopefully you can also see that there’s nothing AWS-specific about implementing a custom waiter.
So write them for: AWS’s resources, other service’s resources, and even your own resources. ✨</p>]]></content><author><name></name></author><summary type="html"><![CDATA[In Use AWS SDK Waiters I showed how useful AWS SDK waiters are when working with resources that are eventually consistent. But not all resources are AWS resources. And not all AWS resources even ship with waiters…]]></summary></entry><entry><title type="html">Use AWS SDK Waiters</title><link href="https://kieran.casa/aws-sdk-waiters/" rel="alternate" type="text/html" title="Use AWS SDK Waiters" /><published>2025-09-22T00:00:00+00:00</published><updated>2025-09-22T00:00:00+00:00</updated><id>https://kieran.casa/aws-sdk-waiters</id><content type="html" xml:base="https://kieran.casa/aws-sdk-waiters/"><![CDATA[<p>2025-10-15: I’ve added an example of when a waiter exceeds its configured <code class="language-plaintext highlighter-rouge">maxAttempts</code> value.</p>

<p>2025-10-05: I’ve written about using waiters in Javascript/Typescript. Read about it on <a href="/aws-sdk-waiters-ts/">Typescript waiters are a bit weird</a>.</p>

<p>2025-09-27: Continuing on with waiters, I’ve written a new post about building your own waiters for an resource. Read more at <a href="/custom-waiters/">Write custom waiters</a>.</p>

<p>Most distributed systems are eventually consistent. Callers instruct a resource to change from one state to another (like going from running to stopped) and must then wait some amount of time before that new state is achieved.</p>

<p>Writing your own polling code to manage this adds complexity, maintenance cost and the opportunity for bugs.
It’ll either be too simple and not cover all of the edge cases (what if instead of going to stopped, it goes into failed) or too complex and force the user to consider exponential back-off and jitter.</p>

<p>When used correctly, AWS SDK waiters can be the perfect tool to help one system drive consistency in another.
 They’re a pattern that helps to make your code simpler, easier to reason about, and more resilient.</p>

<h2 id="an-example">An example</h2>

<p>Since waiters wait for a resource to reach a certain state, and I want to provide real examples in this post, I’ll need a resource to use. 
From here on I’ve used an <a href="https://docs.aws.amazon.com/cli/latest/reference/ssm/get-command-invocation.html">AWS SSM Command Invocation</a> (<a href="https://archive.ph/wip/lK5lU">archive</a>) as the resource. 
There’s nothing particularly special about SSM or a Command Invocation that makes it more suitable as a waiter, it’s just a resource that is eventually consistent.</p>

<p>If you’re not already familiar, SSM SendCommand executes an SSM document on a target EC2 instance. 
An SSM document is usually a series of shell scripts. 
It can take some time after you call SendCommand for the command to reach the instance and it can take some time after that for the document to run; depending on what the document does.</p>

<p>I’ve also used AssertJ assertions throughout the following examples. 
Waiters are particularly useful during integration and canary testing and AssertJ provides a nice way to show the reader the expected behaviour. 
Read more about <a href="/why-use-an-assertion-library/">why I use an assertion library</a> and <a href="/hamcrest-vs-assertj/">why I choose AssertJ over others</a> elsewhere in this blog.</p>

<p>Here how to a waiter to wait until an SSM Command Invocation completes. 
The command itself is super basic. 
It just echo’s <code class="language-plaintext highlighter-rouge">Hello, World!</code> before completing.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">ssmClient</span> <span class="p">=</span> <span class="nc">SsmClient</span><span class="p">.</span><span class="nf">builder</span><span class="p">().</span><span class="nf">build</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">ssmWaiter</span> <span class="p">=</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">waiter</span><span class="p">()</span>

<span class="kd">val</span> <span class="py">sendCommandResponse</span> <span class="p">=</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">sendCommand</span><span class="p">(</span>
  <span class="nc">SendCommandRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">instanceIds</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="n">instanceId</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">documentName</span><span class="p">(</span><span class="s">"AWS-RunShellScript"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">parameters</span><span class="p">(</span><span class="nf">mapOf</span><span class="p">(</span>
      <span class="s">"commands"</span> <span class="n">to</span> <span class="nf">listOf</span><span class="p">(</span>
        <span class="cm">/* https://kieran.casa/starting-bash-scripts/ */</span>
        <span class="s">"#!/usr/bin/env bash"</span><span class="p">,</span>
        <span class="s">"set -o xtrace"</span><span class="p">,</span>
        <span class="s">"set -o errexit"</span><span class="p">,</span>
        <span class="s">"set -o nounset"</span><span class="p">,</span>
        <span class="s">"set -o pipefail"</span><span class="p">,</span>
        <span class="s">"echo 'Hello, World!'"</span><span class="p">,</span>
        <span class="s">"exit 0"</span>
      <span class="p">)</span>
    <span class="p">))</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="kd">val</span> <span class="py">getCommandInvocationResponseWaiterResponse</span> <span class="p">=</span> <span class="n">ssmWaiter</span><span class="p">.</span><span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
  <span class="nc">GetCommandInvocationRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">commandId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">commandId</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">instanceId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">instanceIds</span><span class="p">().</span><span class="nf">first</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="nf">assertThat</span><span class="p">(</span><span class="n">getCommandInvocationResponseWaiterResponse</span><span class="p">.</span><span class="nf">attemptsExecuted</span><span class="p">())</span>
  <span class="p">.</span><span class="nf">isGreaterThan</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="nf">assertThat</span><span class="p">(</span><span class="n">getCommandInvocationResponseWaiterResponse</span><span class="p">.</span><span class="nf">matched</span><span class="p">().</span><span class="nf">response</span><span class="p">().</span><span class="k">get</span><span class="p">().</span><span class="nf">standardOutputContent</span><span class="p">())</span>
  <span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span>
</code></pre></div></div>

<p>Execution of your code will pause at <code class="language-plaintext highlighter-rouge">ssmWaiter.waitUntilCommandExecuted</code> until one of the following happens:</p>

<ul>
  <li>The command invocation successfully completes. As shown above.</li>
  <li>The command ends in a failure</li>
  <li>The waiter times out</li>
</ul>

<h2 id="failure-modes">Failure modes</h2>

<p>When a command ends in failure, expect the waiter to raise an exception:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">sendCommandResponse</span> <span class="p">=</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">sendCommand</span><span class="p">(</span>
  <span class="nc">SendCommandRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">instanceIds</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="n">instanceId</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">documentName</span><span class="p">(</span><span class="s">"AWS-RunShellScript"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">parameters</span><span class="p">(</span><span class="nf">mapOf</span><span class="p">(</span>
      <span class="s">"commands"</span> <span class="n">to</span> <span class="nf">listOf</span><span class="p">(</span>
        <span class="s">"#!/usr/bin/env bash"</span><span class="p">,</span>
        <span class="s">"exit 1"</span>
      <span class="p">)</span>
    <span class="p">))</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="nf">assertThatThrownBy</span> <span class="p">{</span>
  <span class="n">ssmWaiter</span><span class="p">.</span><span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
    <span class="nc">GetCommandInvocationRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">commandId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">commandId</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">instanceId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">instanceIds</span><span class="p">().</span><span class="nf">first</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
  <span class="p">)</span>
<span class="p">}</span>
  <span class="p">.</span><span class="nf">isInstanceOf</span><span class="p">(</span><span class="nc">SdkClientException</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">hasMessageContaining</span><span class="p">(</span><span class="s">"A waiter acceptor with the matcher (path) was matched on parameter (Status=Failed) and transitioned the waiter to failure state"</span><span class="p">)</span>
</code></pre></div></div>

<p>And the same goes for when a waiter times out:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">sendCommandResponse</span> <span class="p">=</span> <span class="n">ssmClient</span><span class="p">.</span><span class="nf">sendCommand</span><span class="p">(</span>
  <span class="nc">SendCommandRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">instanceIds</span><span class="p">(</span><span class="nf">listOf</span><span class="p">(</span><span class="n">instanceId</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">documentName</span><span class="p">(</span><span class="s">"AWS-RunShellScript"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">parameters</span><span class="p">(</span><span class="nf">mapOf</span><span class="p">(</span>
      <span class="s">"commands"</span> <span class="n">to</span> <span class="nf">listOf</span><span class="p">(</span>
        <span class="s">"#!/usr/bin/env bash"</span><span class="p">,</span>
        <span class="s">"sleep 20"</span><span class="p">,</span>
        <span class="s">"exit 0"</span>
      <span class="p">)</span>
    <span class="p">))</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
<span class="p">)</span>

<span class="nf">assertThatThrownBy</span> <span class="p">{</span>
  <span class="n">ssmWaiter</span><span class="p">.</span><span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
    <span class="nc">GetCommandInvocationRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">commandId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">commandId</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">instanceId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">instanceIds</span><span class="p">().</span><span class="nf">first</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
  <span class="p">)</span>
<span class="p">}</span>
  <span class="p">.</span><span class="nf">isInstanceOf</span><span class="p">(</span><span class="nc">SdkClientException</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">hasMessageContaining</span><span class="p">(</span><span class="s">"The waiter has exceeded the max wait time or the next retry will exceed the max wait time + PT5S"</span><span class="p">)</span>
</code></pre></div></div>

<p>And when a waiter exceeds the maximum number of attempts:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">assertThatThrownBy</span> <span class="p">{</span>
  <span class="n">ssmWaiter</span><span class="p">.</span><span class="nf">waitUntilCommandExecuted</span><span class="p">(</span>
    <span class="nc">GetCommandInvocationRequest</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">commandId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">commandId</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">instanceId</span><span class="p">(</span><span class="n">sendCommandResponse</span><span class="p">.</span><span class="nf">command</span><span class="p">().</span><span class="nf">instanceIds</span><span class="p">().</span><span class="nf">first</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
  <span class="p">)</span>
<span class="p">}</span>
  <span class="p">.</span><span class="nf">isInstanceOf</span><span class="p">(</span><span class="nc">SdkClientException</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">hasMessageContaining</span><span class="p">(</span><span class="s">"The waiter has exceeded the max retry attempts: 1"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="tuning-timeouts">Tuning timeouts</h2>

<p>AWS SDK waiters come with default wait times, max attempts and backoff strategies. 
Much the same as what you’d expect to see when configuring retries. 
However, depending on the resource, different wait times may be more acceptable than the default.</p>

<p>In the SSM waiter example, you really want to set your wait time to whatever is appropriate for the command that you’re running. 
If the command runs for about an hour, then the waiter should be ready to wait about that long. 
They’re configurable like this:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">ssmWaiter</span> <span class="p">=</span> <span class="nc">SsmWaiter</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
  <span class="p">.</span><span class="nf">client</span><span class="p">(</span><span class="n">ssmClient</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">overrideConfiguration</span><span class="p">(</span>
    <span class="nc">WaiterOverrideConfiguration</span><span class="p">.</span><span class="nf">builder</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">maxAttempts</span><span class="p">(</span><span class="nc">Int</span><span class="p">.</span><span class="nc">MAX_VALUE</span><span class="p">)</span>
      <span class="p">.</span><span class="nf">waitTimeout</span><span class="p">(</span><span class="mi">60</span><span class="p">.</span><span class="n">minutes</span><span class="p">.</span><span class="nf">toJavaDuration</span><span class="p">())</span>
      <span class="p">.</span><span class="nf">backoffStrategyV2</span><span class="p">(</span><span class="nc">BackoffStrategy</span><span class="p">.</span><span class="nf">fixedDelay</span><span class="p">(</span><span class="mi">5</span><span class="p">.</span><span class="n">seconds</span><span class="p">.</span><span class="nf">toJavaDuration</span><span class="p">()))</span>
      <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
  <span class="p">)</span>
  <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<p>Read more about waiters at <a href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/waiters.html">Using waiters in the AWS SDK for Java 2.x</a> (<a href="https://archive.ph//JSqRD">archive</a>). Waiters are also available in <a href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrate-waiters-signers.html">other languages</a> (<a href="https://archive.ph/t4gqv">archive</a>).</p>]]></content><author><name></name></author><summary type="html"><![CDATA[2025-10-15: I’ve added an example of when a waiter exceeds its configured maxAttempts value.]]></summary></entry><entry><title type="html">Stop writing AWS SDK pagination loops that break</title><link href="https://kieran.casa/aws-sdk-paginators/" rel="alternate" type="text/html" title="Stop writing AWS SDK pagination loops that break" /><published>2025-07-26T00:00:00+00:00</published><updated>2025-07-26T00:00:00+00:00</updated><id>https://kieran.casa/use-aws-sdk-paginators</id><content type="html" xml:base="https://kieran.casa/aws-sdk-paginators/"><![CDATA[<p>2025-07-27: I updated this post to include details on paginators in other AWS SDKs.</p>

<p>I see this everywhere:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">allItems</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">let</span> <span class="nx">nextToken</span><span class="p">:</span> <span class="kr">string</span> <span class="o">|</span> <span class="kc">undefined</span><span class="p">;</span>

<span class="k">do</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">dynamodb</span><span class="p">.</span><span class="nf">scan</span><span class="p">({</span>
    <span class="na">TableName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">MyTable</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">ExclusiveStartKey</span><span class="p">:</span> <span class="nx">nextToken</span><span class="p">,</span>
  <span class="p">});</span>

  <span class="nx">allItems</span><span class="p">.</span><span class="nf">push</span><span class="p">(...(</span><span class="nx">response</span><span class="p">.</span><span class="nx">Items</span> <span class="o">||</span> <span class="p">[]));</span>
  <span class="nx">nextToken</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">LastEvaluatedKey</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while </span><span class="p">(</span><span class="nx">nextToken</span><span class="p">);</span>
</code></pre></div></div>

<p>But this approach has some problems:</p>

<ul>
  <li>There’s mutable state with <code class="language-plaintext highlighter-rouge">allItems</code> and <code class="language-plaintext highlighter-rouge">nextToken</code>. Looking at you, <code class="language-plaintext highlighter-rouge">let</code>.</li>
  <li>It’s easy to introduce bugs with loop conditions. You’ve got to make sure you’re doing a <code class="language-plaintext highlighter-rouge">do while</code> loop, not a <code class="language-plaintext highlighter-rouge">while</code> loop.</li>
  <li>There can be memory issues with large result sets. This solution doesn’t give you the chance to process just a single page of results.</li>
  <li>You have to be aware of the pagination token, and then handle it correctly. For example, with DDB, you must remember that the <code class="language-plaintext highlighter-rouge">LastEvaluatedKey</code> becomes the <code class="language-plaintext highlighter-rouge">ExclusiveStartKey</code>.</li>
</ul>

<p>To solve most of these problems, the AWS SDK ships built-in paginators for all paginated operations:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">paginateScan</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@aws-sdk/lib-dynamodb</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">allItems</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">page</span> <span class="k">of</span> <span class="nf">paginateScan</span><span class="p">(</span>
  <span class="cm">/* DynamoDBPaginationConfiguration */</span> <span class="p">{</span> <span class="na">client</span><span class="p">:</span> <span class="nx">dynamodb</span> <span class="p">},</span>
  <span class="cm">/* ScanCommandInput */</span> <span class="p">{</span> <span class="na">TableName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">MyTable</span><span class="dl">"</span> <span class="p">}</span>
<span class="p">))</span> <span class="p">{</span>
  <span class="nx">allItems</span><span class="p">.</span><span class="nf">push</span><span class="p">(...(</span><span class="nx">page</span><span class="p">.</span><span class="nx">Items</span> <span class="o">||</span> <span class="p">[]));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Their benefits are:</p>

<ul>
  <li>Eliminates <em>most</em> mutable state. No more <code class="language-plaintext highlighter-rouge">let</code> variables for pagination tokens.</li>
  <li>Automatic pagination tokens management. The SDK handles pagination tokens automatically.</li>
  <li>Simpler loop logic. Just iterate until finished, no <code class="language-plaintext highlighter-rouge">do while</code> complexity.</li>
</ul>

<p>Paginators are not just for JS/TS either. There’s paginator support in:</p>
<ul>
  <li><a href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/pagination.html">Java</a> (<a href="https://archive.is/G1Fkt">archive</a>)</li>
  <li><a href="https://boto3.amazonaws.com/v1/documentation/api/latest/guide/paginators.html">Python</a> (<a href="https://archive.is/6mgFt">archive</a>)</li>
  <li><a href="https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/using.html#using-operation-paginators">Go</a> (<a href="https://archive.is/WFAuS">archive</a>)</li>
</ul>

<p>And I’m sure others, too.</p>

<h2 id="some-more-examples">Some more examples</h2>

<p><strong>CloudWatch Logs:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">paginateDescribeLogGroups</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@aws-sdk/client-cloudwatch-logs</span><span class="dl">"</span><span class="p">;</span>

<span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">page</span> <span class="k">of</span> <span class="nf">paginateDescribeLogGroups</span><span class="p">(</span>
  <span class="p">{</span> <span class="na">client</span><span class="p">:</span> <span class="nx">cloudwatchLogs</span> <span class="p">},</span>
  <span class="p">{}</span>
<span class="p">))</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">logGroups</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>EC2 Images:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">paginateDescribeImages</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@aws-sdk/client-ec2</span><span class="dl">"</span><span class="p">;</span>

<span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">page</span> <span class="k">of</span> <span class="nf">paginateDescribeImages</span><span class="p">(</span>
  <span class="p">{</span> <span class="na">client</span><span class="p">:</span> <span class="nx">ec2</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">Owners</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">self</span><span class="dl">"</span><span class="p">]</span> <span class="p">}</span>
<span class="p">))</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">page</span><span class="p">.</span><span class="nx">Images</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Stop writing manual pagination loops. Use the SDK’s paginators instead.</p>

<h2 id="a-utility-function">A Utility Function</h2>

<p>For cases where you need all results in an array, this utility function is helpful:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">toArray</span> <span class="o">=</span> <span class="k">async</span> <span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">gen</span><span class="p">:</span> <span class="nx">AsyncIterable</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">[]</span><span class="o">&gt;</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="na">out</span><span class="p">:</span> <span class="nx">T</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="k">for</span> <span class="k">await </span><span class="p">(</span><span class="kd">const</span> <span class="nx">x</span> <span class="k">of</span> <span class="nx">gen</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">out</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">x</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">out</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// Use it like this</span>

<span class="kd">const</span> <span class="nx">pages</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">toArray</span><span class="p">(</span>
  <span class="nf">paginateScan</span><span class="p">({</span> <span class="na">client</span><span class="p">:</span> <span class="nx">dynamodb</span> <span class="p">},</span> <span class="p">{</span> <span class="na">TableName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">MyTable</span><span class="dl">"</span> <span class="p">})</span>
<span class="p">);</span>
<span class="kd">const</span> <span class="nx">allItems</span> <span class="o">=</span> <span class="nx">pages</span><span class="p">.</span><span class="nf">flatMap</span><span class="p">((</span><span class="nx">page</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">page</span><span class="p">.</span><span class="nx">Items</span> <span class="o">||</span> <span class="p">[]);</span>
</code></pre></div></div>

<p>You’ll notice that we got rid of <code class="language-plaintext highlighter-rouge">allItems</code> from above!
It is now hidden in the <code class="language-plaintext highlighter-rouge">toArray</code> function.
The mutability is constrained to the scope of the <code class="language-plaintext highlighter-rouge">toArray</code> function.</p>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://aws.amazon.com/blogs/developer/pagination-using-async-iterators-in-modular-aws-sdk-for-javascript/">Pagination using Async Iterators in modular AWS SDK for JavaScript | AWS Developer Tools Blog</a> (<a href="https://archive.is/Kt8T0">archive</a>)</li>
  <li><a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/Function/paginateScan/">paginateScan for the AWS SDK for JavaScript v3</a> (<a href="https://archive.is/zr4Em">archive</a>)</li>
  <li><a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-cloudwatch-logs/Function/paginateDescribeLogGroups/">paginateDescribeLogGroups for the AWS SDK for JavaScript v3</a> (<a href="https://archive.is/g7njR">archive</a>)</li>
  <li><a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-ec2/Function/paginateDescribeImages/">paginateDescribeImages for the AWS SDK for JavaScript v3</a> (<a href="https://archive.is/LX0MG">archive</a>)</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[2025-07-27: I updated this post to include details on paginators in other AWS SDKs.]]></summary></entry></feed>