<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://andrew.arace.info/feed.xml" rel="self" type="application/atom+xml" /><link href="https://andrew.arace.info/" rel="alternate" type="text/html" /><updated>2026-10-07T17:29:18+00:00</updated><id>https://andrew.arace.info/feed.xml</id><title type="html">Andrew Arace</title><subtitle></subtitle><author><name>Andrew Arace</name></author><entry><title type="html">Bounce</title><link href="https://andrew.arace.info/2026/09/03/bounce.html" rel="alternate" type="text/html" title="Bounce" /><published>2026-09-03T00:00:00+00:00</published><updated>2026-09-03T00:00:00+00:00</updated><id>https://andrew.arace.info/2026/09/03/bounce</id><content type="html" xml:base="https://andrew.arace.info/2026/09/03/bounce.html"><![CDATA[<p>I made a bouncing-ball screensaver that follows the real shape of a messy multi-monitor setup like mine:</p>

<p><img src="/images/20260903_bounce.png" alt="display settings" /></p>

<p>Most screensavers either treat the whole desktop as one big rectangle or run a separate copy on each screen. Bounce treats your monitor layout as one connected space. The ball (or several balls, or a logo) moves freely from one screen to the next and bounces off the edges where monitors of different sizes don’t line up, including the inside corners those mismatched edges create.</p>

<ul>
  <li>Source Code: <a href="https://github.com/aarace/bounce">on github</a></li>
</ul>

<h2 id="features">Features</h2>

<ul>
  <li>Uses your real arrangement from Settings → System → Display (monitors must be set to <strong>Extend</strong>)</li>
  <li>Multiple balls, or a custom image in place of the ball</li>
  <li>Adjustable color, size, and speed</li>
  <li>Optional flash when a ball hits a corner dead-on</li>
  <li>Optional fading trail, or a “Forever” trail that never fades. Leave the Forever trail running for a while and it should eventually paint over every screen, which makes it a handy way to check that the ball can reach every part of your layout.</li>
  <li>Works with mixed-DPI monitors</li>
  <li>Can be installed without admin rights</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<h3 id="the-play-area-is-the-union-of-your-monitors">The play area is the union of your monitors</h3>

<p><code class="language-plaintext highlighter-rouge">MonitorRegion</code> builds a GDI+ <code class="language-plaintext highlighter-rouge">Region</code> from the union of every <code class="language-plaintext highlighter-rouge">Screen.Bounds</code>. That gives the exact shape of your desktop, notches and all.</p>

<p>Monitors that look snapped together in Display Settings can still have a small gap between their actual pixel bounds. That happens with mixed DPI, or just from imprecise dragging. Without a fix, the ball would treat that gap as a solid wall. So any two monitors that face each other across a small gap get a “bridge” rectangle filling it:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// a's right edge facing b's left edge.</span>
<span class="kt">int</span> <span class="n">horizontalGap</span> <span class="p">=</span> <span class="n">b</span><span class="p">.</span><span class="n">Left</span> <span class="p">-</span> <span class="n">a</span><span class="p">.</span><span class="n">Right</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">horizontalGap</span> <span class="p">&gt;</span> <span class="m">0</span> <span class="p">&amp;&amp;</span> <span class="n">horizontalGap</span> <span class="p">&lt;=</span> <span class="n">MaxBridgeableGap</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">top</span> <span class="p">=</span> <span class="n">Math</span><span class="p">.</span><span class="nf">Max</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">Top</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="n">Top</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">bottom</span> <span class="p">=</span> <span class="n">Math</span><span class="p">.</span><span class="nf">Min</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">Bottom</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="n">Bottom</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">bottom</span> <span class="p">&gt;</span> <span class="n">top</span><span class="p">)</span>
        <span class="n">bridges</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">Rectangle</span><span class="p">.</span><span class="nf">FromLTRB</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">Right</span><span class="p">,</span> <span class="n">top</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="n">Left</span><span class="p">,</span> <span class="n">bottom</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Containment checks run every tick, so they’re kept cheap. Most of the time the ball is fully inside a single monitor, and plain rectangle math answers the question. The slower GDI+ region test only runs when the ball is straddling a seam.</p>

<h3 id="collision-tests-each-axis-on-its-own">Collision tests each axis on its own</h3>

<p>Each tick, the ball tests three possible moves: X only, Y only, and both together. Comparing the results tells it what kind of edge it ran into:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">bool</span> <span class="n">xOk</span>    <span class="p">=</span> <span class="n">_region</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="k">new</span> <span class="nf">RectangleF</span><span class="p">(</span><span class="n">nx</span><span class="p">,</span> <span class="n">Position</span><span class="p">.</span><span class="n">Y</span><span class="p">,</span> <span class="n">Size</span><span class="p">,</span> <span class="n">Size</span><span class="p">));</span>
<span class="kt">bool</span> <span class="n">yOk</span>    <span class="p">=</span> <span class="n">_region</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="k">new</span> <span class="nf">RectangleF</span><span class="p">(</span><span class="n">Position</span><span class="p">.</span><span class="n">X</span><span class="p">,</span> <span class="n">ny</span><span class="p">,</span> <span class="n">Size</span><span class="p">,</span> <span class="n">Size</span><span class="p">));</span>
<span class="kt">bool</span> <span class="n">bothOk</span> <span class="p">=</span> <span class="n">_region</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="k">new</span> <span class="nf">RectangleF</span><span class="p">(</span><span class="n">nx</span><span class="p">,</span> <span class="n">ny</span><span class="p">,</span> <span class="n">Size</span><span class="p">,</span> <span class="n">Size</span><span class="p">));</span>
</code></pre></div></div>

<ul>
  <li><strong>Both OK:</strong> move diagonally as normal.</li>
  <li><strong>Only one axis OK:</strong> it hit a flat edge, so slide along it and reverse the other direction.</li>
  <li><strong>Each axis OK alone, but not together:</strong> this is the interesting case. The diagonal move would cut across the notch where two monitors of different sizes meet, through space that isn’t on any screen. The ball slides along whichever axis it’s moving faster on and bounces off the other.</li>
  <li><strong>Neither OK:</strong> a dead-on corner hit. Both directions reverse (and the corner flash fires, if it’s turned on).</li>
</ul>

<h3 id="one-window-per-monitor">One window per monitor</h3>

<p>Windows applies a single DPI scale to each top-level window. With one giant window stretched across monitors that have different DPI settings, Windows stretches the parts that aren’t on the window’s “home” monitor, which made the ball look squashed. So Bounce opens one borderless, topmost window per monitor. A single shared simulation drives all of them, and each window draws its own part of the desktop at native resolution.</p>

<h2 id="installing">Installing</h2>

<p>Every build also produces a <code class="language-plaintext highlighter-rouge">Bounce.scr</code>. Right-click it and choose <strong>Install</strong>. Or, if you don’t have admin rights, run <code class="language-plaintext highlighter-rouge">Install-Screensaver.ps1</code> from the repo. It points the per-user registry setting at the build output instead of copying anything into System32. The repo README also covers publishing a self-contained build for machines without the .NET 10 runtime.</p>]]></content><author><name>Andrew Arace</name></author><category term="dotnet" /><category term="windows" /><category term="screensaver" /><summary type="html"><![CDATA[A bouncing-ball Windows screensaver that follows the real shape of a multi-monitor layout.]]></summary></entry><entry><title type="html">Vestaboard</title><link href="https://andrew.arace.info/2026/06/25/vestaboard.html" rel="alternate" type="text/html" title="Vestaboard" /><published>2026-06-25T00:00:00+00:00</published><updated>2026-06-25T00:00:00+00:00</updated><id>https://andrew.arace.info/2026/06/25/vestaboard</id><content type="html" xml:base="https://andrew.arace.info/2026/06/25/vestaboard.html"><![CDATA[<p>A <a href="https://www.vestaboard.com/">Vestaboard</a> is a split-flap display with 6 rows of 22 characters (and they’re steadily growing with other sizes and options). The company brought this this classic “train station” display to the modern world with API support. I got one as a huge gift (thank you babydoll 🥰) and immediately started building integrations with it.</p>

<p>I wrote three small Node.js services for mine: a scheduler that posts a morning briefing, plus Slack and Discord bots that let other people post messages to it.</p>

<ul>
  <li><a href="https://github.com/aarace/vestabot-scheduler">Scheduler Source Code on github</a></li>
  <li><a href="https://github.com/aarace/vestabot-slack">Slack Integration Source Code on github</a></li>
  <li><a href="https://github.com/aarace/vestabot-discord">Discord Integration Source Code on github</a></li>
</ul>

<p>All three use the same setup: a Vestaboard API token in a <code class="language-plaintext highlighter-rouge">.env</code> file, then a single <code class="language-plaintext highlighter-rouge">node index.js</code> command, or a Docker container with <code class="language-plaintext highlighter-rouge">--restart unless-stopped</code> so it keeps running.</p>

<h2 id="morning-briefing-scheduler">Morning Briefing (Scheduler)</h2>

<p>Every morning at 6:30 the scheduler posts the date, weather, sunrise and sunset, and the day’s tides (I live in a beach town):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     MONDAY JUNE 22ND
  PARTLY CLOUDY 72°/58°
 RISE 5:12AM SET 8:34PM
HIGH               LOW
10:23AM        3:45PM
 4:12PM        9:58PM
</code></pre></div></div>

<p>Both data sources are free and need no API key. Weather comes from <a href="https://open-meteo.com/">Open-Meteo</a>, and tides come from <a href="https://tidesandcurrents.noaa.gov/api-guide.html">NOAA Tides &amp; Currents</a>. The tide station, location, time zone, and schedule are all set in the <code class="language-plaintext highlighter-rouge">.env</code> file, and they default to Cohasset Harbor. The scheduler uses <a href="https://github.com/hexagon/croner">croner</a> for the timing:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Cron</span><span class="p">(</span><span class="nx">MORNING_CRON</span><span class="p">,</span> <span class="p">{</span> <span class="na">timezone</span><span class="p">:</span> <span class="nx">TIMEZONE</span> <span class="p">},</span> <span class="nx">runMorningBriefing</span><span class="p">);</span>
</code></pre></div></div>

<p>Both APIs are called at the same time, and their results are combined into the grid:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">weather</span><span class="p">,</span> <span class="nx">tides</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="nx">getCurrentWeather</span><span class="p">(),</span> <span class="nx">getTodayTides</span><span class="p">()]);</span>
<span class="kd">const</span> <span class="nx">grid</span> <span class="o">=</span> <span class="nx">buildMorningGrid</span><span class="p">(</span><span class="nx">weather</span><span class="p">,</span> <span class="nx">tides</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">postGridToVestaboard</span><span class="p">(</span><span class="nx">grid</span><span class="p">);</span>
</code></pre></div></div>

<p>Pass <code class="language-plaintext highlighter-rouge">--now</code> to post a briefing right away, which is handy for testing.</p>

<h3 id="laying-out-the-board">Laying out the board</h3>

<p>Plain text sent to the Vestaboard API gets laid out by Vestaboard, so you don’t control where anything goes. To line up columns like the tide table, you send a 6×22 array of character codes instead. Each character on the board has a numeric code:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">CHAR_CODES</span> <span class="o">=</span> <span class="p">{</span>
  <span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">:</span>  <span class="mi">0</span><span class="p">,</span>
  <span class="na">A</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span>  <span class="na">B</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span>  <span class="na">C</span><span class="p">:</span><span class="mi">3</span><span class="p">,</span>  <span class="cm">/* ... */</span>  <span class="na">Z</span><span class="p">:</span><span class="mi">26</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">:</span><span class="mi">27</span><span class="p">,</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">:</span><span class="mi">28</span><span class="p">,</span> <span class="cm">/* ... */</span>  <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">:</span><span class="mi">36</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">!</span><span class="dl">'</span><span class="p">:</span><span class="mi">37</span><span class="p">,</span> <span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">:</span><span class="mi">38</span><span class="p">,</span> <span class="cm">/* ... */</span>  <span class="dl">'</span><span class="s1">°</span><span class="dl">'</span><span class="p">:</span><span class="mi">62</span><span class="p">,</span>
<span class="p">};</span>

<span class="kd">function</span> <span class="nx">textRow</span><span class="p">(</span><span class="nx">text</span><span class="p">,</span> <span class="nx">width</span> <span class="o">=</span> <span class="mi">22</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">codes</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">text</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">)].</span><span class="nx">map</span><span class="p">(</span><span class="nx">charCode</span><span class="p">);</span>
  <span class="k">while</span> <span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="nx">width</span><span class="p">)</span> <span class="nx">codes</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">BLANK</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">codes</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>From there, each row is built with ordinary string padding. Here, high tide times are left-aligned and low tide times are right-aligned:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">tideTimeRow</span><span class="p">(</span><span class="nx">high</span><span class="p">,</span> <span class="nx">low</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">h</span> <span class="o">=</span> <span class="p">(</span><span class="nx">high</span> <span class="p">?</span> <span class="nx">formatTideTime</span><span class="p">(</span><span class="nx">high</span><span class="p">.</span><span class="nx">time</span><span class="p">)</span> <span class="p">:</span> <span class="dl">''</span><span class="p">).</span><span class="nx">padEnd</span><span class="p">(</span><span class="mi">13</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">13</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">l</span> <span class="o">=</span> <span class="p">(</span><span class="nx">low</span>  <span class="p">?</span> <span class="nx">formatTideTime</span><span class="p">(</span><span class="nx">low</span><span class="p">.</span><span class="nx">time</span><span class="p">)</span>  <span class="p">:</span> <span class="dl">''</span><span class="p">).</span><span class="nx">padStart</span><span class="p">(</span><span class="mi">9</span><span class="p">).</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">textRow</span><span class="p">(</span><span class="nx">h</span> <span class="o">+</span> <span class="nx">l</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Open-Meteo reports conditions as <a href="https://open-meteo.com/en/docs#weathervariables">WMO weather codes</a>. These get mapped to short labels like <code class="language-plaintext highlighter-rouge">PARTLY CLOUDY</code> or <code class="language-plaintext highlighter-rouge">TSTORM+HAIL</code>, so the conditions and temperatures fit on one 22-character line.</p>

<h2 id="slack">Slack</h2>

<p>The Slack app adds two slash commands:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/vesta [message]</code> posts any text to the board</li>
  <li><code class="language-plaintext highlighter-rouge">/tides</code> posts the day’s high and low tides as a grid, built the same way as the scheduler’s</li>
</ul>

<p>It uses <a href="https://slack.dev/bolt-js/">Bolt</a> in Socket Mode, which means it connects out to Slack. There’s no public URL to host and nothing to open on the firewall, so it can run on any machine at home.</p>

<p>To keep the board from getting spammed, each user can post once per minute. If someone tries again too soon, they get a private reply only they can see:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">lastPost</span> <span class="o">=</span> <span class="nx">lastPostTime</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">userId</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">lastPost</span> <span class="o">&amp;&amp;</span> <span class="nx">now</span> <span class="o">-</span> <span class="nx">lastPost</span> <span class="o">&lt;</span> <span class="nx">RATE_LIMIT_MS</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">secondsLeft</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">((</span><span class="nx">RATE_LIMIT_MS</span> <span class="o">-</span> <span class="p">(</span><span class="nx">now</span> <span class="o">-</span> <span class="nx">lastPost</span><span class="p">))</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">);</span>
  <span class="k">await</span> <span class="nx">respond</span><span class="p">({</span>
    <span class="na">response_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ephemeral</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">text</span><span class="p">:</span> <span class="s2">`⏳ You can only post once per minute. Please wait </span><span class="p">${</span><span class="nx">secondsLeft</span><span class="p">}</span><span class="s2"> more second</span><span class="p">${</span><span class="nx">secondsLeft</span> <span class="o">===</span> <span class="mi">1</span> <span class="p">?</span> <span class="dl">''</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">s</span><span class="dl">'</span><span class="p">}</span><span class="s2">.`</span><span class="p">,</span>
  <span class="p">});</span>
  <span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When a post works, the bot replies in the channel so everyone can see who posted what.</p>

<h2 id="discord">Discord</h2>

<p>The Discord bot does the same thing for a Discord server with a <code class="language-plaintext highlighter-rouge">/vesta</code> slash command and the same one-post-per-minute limit. It registers the slash command itself when it starts up. Because the call to the Vestaboard API can take a moment, the bot defers its reply first. That tells Discord the bot is working on it, so the interaction doesn’t time out:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">interaction</span><span class="p">.</span><span class="nx">deferReply</span><span class="p">();</span>
<span class="c1">// ... post to the Vestaboard ...</span>
<span class="k">await</span> <span class="nx">interaction</span><span class="p">.</span><span class="nx">editReply</span><span class="p">(</span><span class="s2">`Posted to the Vestaboard: **</span><span class="p">${</span><span class="nx">text</span><span class="p">}</span><span class="s2">**`</span><span class="p">);</span>
</code></pre></div></div>]]></content><author><name>Andrew Arace</name></author><category term="javascript" /><category term="nodejs" /><category term="integrations" /><summary type="html"><![CDATA[A morning weather and tide briefing, plus Slack and Discord bots, for my Vestaboard.]]></summary></entry><entry><title type="html">TemporalBag</title><link href="https://andrew.arace.info/2026/04/09/temporalbag.html" rel="alternate" type="text/html" title="TemporalBag" /><published>2026-04-09T00:00:00+00:00</published><updated>2026-04-09T00:00:00+00:00</updated><id>https://andrew.arace.info/2026/04/09/temporalbag</id><content type="html" xml:base="https://andrew.arace.info/2026/04/09/temporalbag.html"><![CDATA[<p>On a side project I needed a .NET collection where entries expire automatically after a set time. It also had to support:</p>
<ul>
  <li>thread safety</li>
  <li>enumeration (<code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code>, so LINQ just works)</li>
  <li>point-in-time snapshots</li>
  <li>cleanup that doesn’t block readers or writers</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">MemoryCache</code> is keyed and heavier than I wanted. <code class="language-plaintext highlighter-rouge">ConcurrentBag</code> has no concept of time. Nothing I found covered all four, so I wrote my own and published it as an open source package.</p>

<ul>
  <li>Source Code: <a href="https://github.com/aarace/TemporalBag">on github</a></li>
  <li>Nuget Package: <a href="https://www.nuget.org/packages/TemporalBag">on nuget</a></li>
</ul>

<h2 id="usage">Usage</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package TemporalBag
</code></pre></div></div>

<p>Create a bag with a default time-to-live, add items, and enumerate. Expired items are never returned.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">TemporalBag.Core</span><span class="p">;</span>

<span class="c1">// Items expire after 30 seconds; background cleanup runs every 10 seconds (default).</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">bag</span> <span class="p">=</span> <span class="k">new</span> <span class="n">TemporalBag</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(</span><span class="n">maxAge</span><span class="p">:</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">));</span>

<span class="n">bag</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"hello"</span><span class="p">);</span>
<span class="n">bag</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"short-lived"</span><span class="p">,</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">));</span>  <span class="c1">// per-item TTL override</span>
<span class="n">bag</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"long-lived"</span><span class="p">,</span>  <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromHours</span><span class="p">(</span><span class="m">1</span><span class="p">));</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">item</span> <span class="k">in</span> <span class="n">bag</span><span class="p">)</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">matches</span> <span class="p">=</span> <span class="n">bag</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="s">"long"</span><span class="p">)).</span><span class="nf">ToList</span><span class="p">();</span>
<span class="kt">bool</span> <span class="n">exists</span> <span class="p">=</span> <span class="n">bag</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"hello"</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">live</span>    <span class="p">=</span> <span class="n">bag</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AsLive()</code> returns each item along with how much time it has left:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="p">(</span><span class="k">value</span><span class="p">,</span> <span class="n">remaining</span><span class="p">)</span> <span class="k">in</span> <span class="n">bag</span><span class="p">.</span><span class="nf">AsLive</span><span class="p">())</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="k">value</span><span class="p">}</span><span class="s"> expires in </span><span class="p">{</span><span class="n">remaining</span><span class="p">.</span><span class="n">TotalSeconds</span><span class="p">:</span><span class="n">F1</span><span class="p">}</span><span class="s">s"</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="how-it-works">How it works</h2>

<h3 id="each-item-stores-its-own-expiration">Each item stores its own expiration</h3>

<p>Every value is wrapped with a deadline measured in <code class="language-plaintext highlighter-rouge">Stopwatch</code> ticks. <code class="language-plaintext highlighter-rouge">Stopwatch</code> uses a monotonic clock, so changes to the system clock (NTP sync, DST) can’t make items expire early or late.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">class</span> <span class="nc">TtlValue</span><span class="p">(</span><span class="n">TValue</span> <span class="k">value</span><span class="p">,</span> <span class="n">TimeSpan</span> <span class="n">ttl</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">TValue</span> <span class="n">Value</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">value</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="kt">long</span> <span class="n">_tickCountWhenToKill</span> <span class="p">=</span>
        <span class="n">Stopwatch</span><span class="p">.</span><span class="nf">GetTimestamp</span><span class="p">()</span> <span class="p">+</span> <span class="p">(</span><span class="kt">long</span><span class="p">)(</span><span class="n">ttl</span><span class="p">.</span><span class="n">TotalSeconds</span> <span class="p">*</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="n">Frequency</span><span class="p">);</span>

    <span class="k">public</span> <span class="kt">bool</span> <span class="nf">IsExpired</span><span class="p">(</span><span class="kt">long</span> <span class="n">currTickCount</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">currTickCount</span> <span class="p">&gt;</span> <span class="n">_tickCountWhenToKill</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="reads-filter-out-expired-items">Reads filter out expired items</h3>

<p>Enumeration, <code class="language-plaintext highlighter-rouge">Count</code>, and <code class="language-plaintext highlighter-rouge">Contains</code> all read the clock once at the start and skip anything past its deadline. Every item in one pass is checked against the same instant, so you get a consistent snapshot. Because of that, reads are always correct whether or not cleanup has run yet.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="n">IEnumerator</span><span class="p">&lt;</span><span class="n">TValue</span><span class="p">&gt;</span> <span class="nf">GetEnumeratorCore</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">currTime</span> <span class="p">=</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="nf">GetTimestamp</span><span class="p">();</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">item</span> <span class="k">in</span> <span class="n">_list</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">item</span><span class="p">.</span><span class="nf">IsExpired</span><span class="p">(</span><span class="n">currTime</span><span class="p">))</span>
            <span class="k">yield</span> <span class="k">return</span> <span class="n">item</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="cleanup-swaps-in-a-new-bag">Cleanup swaps in a new bag</h3>

<p>The items live in a <code class="language-plaintext highlighter-rouge">ConcurrentBag</code>. A background timer periodically compacts it. Instead of removing items one at a time, it atomically swaps in an empty bag and copies over only the items that are still live. Any <code class="language-plaintext highlighter-rouge">Add()</code> that happens during cleanup goes straight into the new bag, so nothing is lost and writers never wait on a lock.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">DoCleanUp</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">currTime</span> <span class="p">=</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="nf">GetTimestamp</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">oldList</span> <span class="p">=</span> <span class="n">Interlocked</span><span class="p">.</span><span class="nf">Exchange</span><span class="p">(</span><span class="k">ref</span> <span class="n">_list</span><span class="p">,</span> <span class="k">new</span> <span class="n">ConcurrentBag</span><span class="p">&lt;</span><span class="n">TtlValue</span><span class="p">&gt;());</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">item</span> <span class="k">in</span> <span class="n">oldList</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">item</span><span class="p">.</span><span class="nf">IsExpired</span><span class="p">(</span><span class="n">currTime</span><span class="p">))</span>
            <span class="n">_list</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Clear()</code> uses the same swap, so emptying the bag is a single atomic operation no matter how many items it holds.</p>

<p>A <code class="language-plaintext highlighter-rouge">SemaphoreSlim</code> makes sure only one cleanup runs at a time. A manual <code class="language-plaintext highlighter-rouge">CleanUp()</code> call uses <code class="language-plaintext highlighter-rouge">Wait(0)</code>, so if a cleanup is already running it returns <code class="language-plaintext highlighter-rouge">false</code> right away instead of waiting:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="kt">bool</span> <span class="nf">CleanUp</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">_cleanupLock</span><span class="p">.</span><span class="nf">Wait</span><span class="p">(</span><span class="m">0</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="k">try</span> <span class="p">{</span> <span class="nf">DoCleanUp</span><span class="p">();</span> <span class="k">return</span> <span class="k">true</span><span class="p">;</span> <span class="p">}</span>
        <span class="k">finally</span> <span class="p">{</span> <span class="n">_cleanupLock</span><span class="p">.</span><span class="nf">Release</span><span class="p">();</span> <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="k">false</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The bag implements <code class="language-plaintext highlighter-rouge">IDisposable</code> to stop the background timer, so use it in a <code class="language-plaintext highlighter-rouge">using</code> block or dispose it when you’re done.</p>

<h2 id="targets">Targets</h2>

<p><code class="language-plaintext highlighter-rouge">netstandard2.0</code>, <code class="language-plaintext highlighter-rouge">net8.0</code>, and <code class="language-plaintext highlighter-rouge">net10.0</code>. MIT licensed.</p>]]></content><author><name>Andrew Arace</name></author><category term="dotnet" /><category term="open-source" /><category term="nuget" /><summary type="html"><![CDATA[A thread-safe .NET collection whose items expire automatically, published on NuGet.]]></summary></entry><entry><title type="html">Zombies!</title><link href="https://andrew.arace.info/2026/03/13/zombies.html" rel="alternate" type="text/html" title="Zombies!" /><published>2026-03-13T00:00:00+00:00</published><updated>2026-03-13T00:00:00+00:00</updated><id>https://andrew.arace.info/2026/03/13/zombies</id><content type="html" xml:base="https://andrew.arace.info/2026/03/13/zombies.html"><![CDATA[<p>Converted an old screensaver into an interactive website: <a href="https://aarace.github.io/zombie/">Zombie Infection Simulator</a></p>

<p><img src="/images/20260313_zombies.png" alt="zombie page" /></p>

<p>Source Code: <a href="https://github.com/aarace/zombie">on github</a>
Running Example: <a href="https://aarace.github.io/zombie/">on github pages</a></p>]]></content><author><name>Andrew Arace</name></author><category term="javascript" /><category term="simulation" /><category term="screensaver" /><summary type="html"><![CDATA[An old zombie-infection screensaver, rebuilt as an interactive website.]]></summary></entry></feed>