<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gtio</title>
    <description>The latest articles on DEV Community by Gtio (@gtoxlili).</description>
    <link>https://dev.to/gtoxlili</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879044%2F71fe7f47-f683-4016-aae0-83bfcd48a6bf.jpeg</url>
      <title>DEV Community: Gtio</title>
      <link>https://dev.to/gtoxlili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gtoxlili"/>
    <language>en</language>
    <item>
      <title>I built a game where your AI coding agent writes the fighter's combat code</title>
      <dc:creator>Gtio</dc:creator>
      <pubDate>Thu, 11 Jun 2026 05:34:28 +0000</pubDate>
      <link>https://dev.to/gtoxlili/i-built-a-game-where-your-ai-coding-agent-writes-the-fighters-combat-code-8l0</link>
      <guid>https://dev.to/gtoxlili/i-built-a-game-where-your-ai-coding-agent-writes-the-fighters-combat-code-8l0</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I built &lt;a href="https://jianghu.gtio.work/" rel="noopener noreferrer"&gt;Jianghu&lt;/a&gt; — a 1v1 wuxia dueling game where you &lt;em&gt;don't&lt;/em&gt; control your fighter. Your AI coding agent (Claude Code, Codex, Cursor, …) writes a JavaScript function that does. Free, browser-based, and the leaderboard is basically agents fighting agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist
&lt;/h2&gt;

&lt;p&gt;You never press "attack." Instead you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a fighter and pick 1 of 8 sects.&lt;/li&gt;
&lt;li&gt;Get a hero key (an API token).&lt;/li&gt;
&lt;li&gt;Hand it to your AI coding agent along with the spec.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent reads the rules and writes one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chebyshev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;enemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renjian_heyi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qi&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renjian_heyi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// finisher&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toward_enemy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// close the gap&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sanhuan_taoyue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// basic strike&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's called every time your action queue empties, for the whole match. You're the coach: set the strategy, read the post-match diagnosis, iterate. (You can hand-edit the code too.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nhka88xi2ir9hqv7gdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nhka88xi2ir9hqv7gdx.png" alt="The onIdle editor and your sect's skills" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop &amp;amp; the leaderboard
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;simulate → publish → challenge (ranked) → diagnose → revise&lt;/code&gt;. There's also a co-op PvE mode (boss raids with telegraphed AoE and enrage phases) that takes a separate script.&lt;/p&gt;

&lt;p&gt;The leaderboard shows which agent each player used — Claude, Codex/OpenAI, and so on — so it turns into a quiet head-to-head &lt;em&gt;between agents&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farjnvn14fe6bsw66o070.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Farjnvn14fe6bsw66o070.png" alt="Leaderboard, with each player's agent shown" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend: Rust (axum).&lt;/strong&gt; Submitted JS runs in a &lt;strong&gt;QuickJS sandbox&lt;/strong&gt; — no host access (&lt;code&gt;me&lt;/code&gt;/&lt;code&gt;enemy&lt;/code&gt;/&lt;code&gt;game&lt;/code&gt; are hand-built whitelists, not raw engine structs), with an interrupt to kill infinite loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic simulation.&lt;/strong&gt; A match is a frame loop (~800 frames) with a seeded RNG, so the same code + seed always produces the same match. Replays are stored as JSON (no video), and the post-match diagnosis is computed by replaying them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend: React + Canvas.&lt;/strong&gt; The look is ink-wash (sumi-e).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmoexddlushwa7u6yk87h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmoexddlushwa7u6yk87h.png" alt="An ink-wash replay with the combat log" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing I didn't expect
&lt;/h2&gt;

&lt;p&gt;When an agent hits unexplained behavior (e.g. a &lt;code&gt;move&lt;/code&gt; that didn't go through because of terrain), it often concludes &lt;em&gt;"the engine is buggy"&lt;/em&gt; and writes workarounds for a bug that isn't there. So a lot of the design ended up being about &lt;strong&gt;surfacing &lt;em&gt;why&lt;/em&gt; something happened&lt;/strong&gt; — a post-match "mirror" — instead of silently dropping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's free and runs in the browser; you can watch past replays without signing in. If you use a coding agent, pointing it at this is a weirdly fun benchmark.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://jianghu.gtio.work/" rel="noopener noreferrer"&gt;https://jianghu.gtio.work/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy to talk about the sandbox / determinism / agent-facing API design in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I needed the stream URL when casting video — so I built a fake TV</title>
      <dc:creator>Gtio</dc:creator>
      <pubDate>Wed, 15 Apr 2026 09:48:24 +0000</pubDate>
      <link>https://dev.to/gtoxlili/i-needed-the-stream-url-when-casting-video-so-i-built-a-fake-tv-4ag4</link>
      <guid>https://dev.to/gtoxlili/i-needed-the-stream-url-when-casting-video-so-i-built-a-fake-tv-4ag4</guid>
      <description>&lt;p&gt;I wanted to grab the actual stream URL when casting a video from my phone to the TV. Not to watch it on the TV — I wanted the raw m3u8 link so I could record it with ffmpeg or play it in VLC on my laptop.&lt;/p&gt;

&lt;p&gt;Turns out, DLNA casting works by sending the media URL from the phone app to the TV via a standard SOAP request (&lt;code&gt;SetAVTransportURI&lt;/code&gt;). The TV then fetches and plays the stream on its own. The phone is just a remote control.&lt;/p&gt;

&lt;p&gt;So... what if your computer pretended to be a TV?&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;If you advertise a fake UPnP MediaRenderer on the local network, any app that supports DLNA casting (Bilibili, iQiyi, Youku, TikTok, and many more) will happily send you the real stream URL when you hit "cast."&lt;/p&gt;

&lt;p&gt;The app can't tell the difference — it's standard protocol, same as talking to a Samsung or Sony TV.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SSDP multicast&lt;/strong&gt; on &lt;code&gt;239.255.255.250:1900&lt;/code&gt; — announce ourselves as a MediaRenderer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UPnP device description&lt;/strong&gt; — reply with a minimal XML descriptor when queried&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOAP control&lt;/strong&gt; — the app sends &lt;code&gt;SetAVTransportURI&lt;/code&gt; with the actual stream URL. We extract it and we're done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing is ~500 lines of Python stdlib. No dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;wechat-finder-dlna

&lt;span class="c"&gt;# Print the captured URL&lt;/span&gt;
wechat-finder-dlna

&lt;span class="c"&gt;# Record directly with ffmpeg&lt;/span&gt;
wechat-finder-dlna &lt;span class="nt"&gt;--record&lt;/span&gt; stream.mp4 &lt;span class="nt"&gt;--duration&lt;/span&gt; 01:00:00

&lt;span class="c"&gt;# Pipe to VLC&lt;/span&gt;
wechat-finder-dlna | xargs vlc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use it as a library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wechat_finder_dlna&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Living Room TV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# url is the raw m3u8/mp4 link — do whatever you want with it
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phone and computer need to be on the same WiFi. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use an existing DLNA library?
&lt;/h2&gt;

&lt;p&gt;I looked at a few (like dlnap, macast), but they're full renderers — they actually play the video. I just wanted the URL. So I built the smallest possible MediaRenderer that only implements device discovery and &lt;code&gt;SetAVTransportURI&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Zero external dependencies. Pure stdlib (&lt;code&gt;http.server&lt;/code&gt;, &lt;code&gt;socket&lt;/code&gt;, &lt;code&gt;threading&lt;/code&gt;, &lt;code&gt;xml&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What apps work with this?
&lt;/h2&gt;

&lt;p&gt;Anything that supports DLNA/UPnP casting. I've tested with several Chinese streaming apps (they all use DLNA for casting), but it should work with any app that can cast to a smart TV on your network.&lt;/p&gt;

&lt;p&gt;The project is at &lt;a href="https://github.com/gtoxlili/wechat-finder-dlna" rel="noopener noreferrer"&gt;github.com/gtoxlili/wechat-finder-dlna&lt;/a&gt;. Feedback welcome — especially if you've tested it with apps I haven't tried.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dlna</category>
      <category>networking</category>
      <category>streaming</category>
    </item>
    <item>
      <title>I needed resumable LLM streams in Go — so I built streamhub</title>
      <dc:creator>Gtio</dc:creator>
      <pubDate>Tue, 14 Apr 2026 17:10:55 +0000</pubDate>
      <link>https://dev.to/gtoxlili/i-needed-resumable-llm-streams-in-go-so-i-built-streamhub-349g</link>
      <guid>https://dev.to/gtoxlili/i-needed-resumable-llm-streams-in-go-so-i-built-streamhub-349g</guid>
      <description>&lt;p&gt;If you've built anything that streams LLM responses over SSE, you've probably hit this: the user refreshes the page, or their network blips, or the load balancer routes the reconnect to a different instance — and the stream is just gone. The generation keeps burning tokens on your backend, but the client sees nothing.&lt;/p&gt;

&lt;p&gt;In the JS/TS world this is mostly solved. Vercel shipped &lt;a href="https://github.com/vercel/resumable-stream" rel="noopener noreferrer"&gt;resumable-stream&lt;/a&gt;, there's &lt;a href="https://github.com/zirkelc/ai-resumable-stream" rel="noopener noreferrer"&gt;ai-resumable-stream&lt;/a&gt;, Ably has a whole &lt;a href="https://ably.com/blog/token-streaming-for-ai-ux" rel="noopener noreferrer"&gt;token streaming product&lt;/a&gt;. But if your backend is in Go? Nothing.&lt;/p&gt;

&lt;p&gt;I ran into this while working on a project where the LLM worker and the HTTP handler live in different processes. I needed something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;persists chunks so reconnecting clients can replay what they missed&lt;/li&gt;
&lt;li&gt;delivers cancel signals across instances (user clicks "stop" on one node, generation stops on another)&lt;/li&gt;
&lt;li&gt;prevents duplicate producers (two requests racing to start the same session)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/gtoxlili/streamhub" rel="noopener noreferrer"&gt;streamhub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Two Redis primitives, that's it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Streams&lt;/strong&gt; store chunks. New subscribers read history first, then get live data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Pub/Sub&lt;/strong&gt; carries cancel signals. Fast, fire-and-forget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each producer gets a generation ID that acts as a fencing token — if a stale producer tries to write after losing ownership, the writes are rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code looks like
&lt;/h2&gt;

&lt;p&gt;Producer side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// called when someone cancels this session&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// another instance already owns this&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" world"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumer side (can be a completely different process):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// replays existing chunks first, then streams live&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flusher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cancel from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat:123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why not just use X?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Just use Redis Streams directly"&lt;/strong&gt; — you can, but you'll end up reimplementing subscriber fan-out, replay-then-live handoff, generation fencing, and the cancel side-channel. That's what streamhub is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Use Centrifuge/Centrifugo"&lt;/strong&gt; — great project, but it's a full real-time messaging framework. If all you need is to make your LLM streams durable, it's a lot of surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Use vercel/resumable-stream"&lt;/strong&gt; — TypeScript only, tightly coupled to the Vercel AI SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;Early days. The API surface might still change. If you're dealing with this same problem in Go, I'd appreciate feedback: &lt;a href="https://github.com/gtoxlili/streamhub" rel="noopener noreferrer"&gt;github.com/gtoxlili/streamhub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>redis</category>
      <category>ai</category>
      <category>streaming</category>
    </item>
  </channel>
</rss>
