<?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: Google Developer Experts</title>
    <description>The latest articles on DEV Community by Google Developer Experts (gde).</description>
    <link>https://dev.to/gde</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11939%2Fe3080d5b-ecde-42a8-b089-bafecc31fa97.png</url>
      <title>DEV Community: Google Developer Experts</title>
      <link>https://dev.to/gde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gde"/>
    <language>en</language>
    <item>
      <title>Kong AI Gateway 2.0 on Google Cloud: Securing GKE, Cloud Run, and Vertex AI(Agent Platform)</title>
      <dc:creator>Saurabh Mishra</dc:creator>
      <pubDate>Fri, 04 Sep 2026 01:59:20 +0000</pubDate>
      <link>https://dev.to/gde/kong-ai-gateway-20-on-google-cloud-securing-gke-cloud-run-and-vertex-ai-219o</link>
      <guid>https://dev.to/gde/kong-ai-gateway-20-on-google-cloud-securing-gke-cloud-run-and-vertex-ai-219o</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fee2nfx9w7j9cddz1uk8l.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fee2nfx9w7j9cddz1uk8l.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most teams running on Google Cloud don't pick one compute model and stay there. Some services live on GKE because they need fine-grained control, sidecars, or GPU scheduling. Others run on Cloud Run because a stateless HTTP API doesn't need a cluster to babysit it. Increasingly, a third category has joined the mix: LLM calls, MCP tool servers, and autonomous agents, each with its own traffic patterns and risk profile.&lt;/p&gt;

&lt;p&gt;Enterprises are rapidly shifting from conventional request-response APIs to composite, AI-native ecosystems. Standard REST and GraphQL endpoints are no longer just consumed by frontends; they are dynamically queried as tools by autonomous agents, LLM pipelines, and Model Context Protocol (MCP) servers.&lt;/p&gt;

&lt;p&gt;At the same time, compute is split across Google Cloud platforms. Microservices run on Google Kubernetes Engine (GKE), while serverless APIs run on Cloud Run.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl65a8jys95plomrl3mwc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl65a8jys95plomrl3mwc.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;his fragmentation introduces a critical infrastructure question: &lt;strong&gt;How do you enforce uniform authentication, observability, and cost governance across GKE and Cloud Run without maintaining duplicate security stacks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kong AI Gateway 2.0 addresses this by treating AI primitives—models, providers, agents, and MCP servers—as first-class control plane entities rather than generic HTTP plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why AI Traffic Breaks Traditional API Gateways&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Standard API gateways evaluate traffic through a deterministic lens: Client → Request → Upstream API → Response.&lt;/p&gt;

&lt;p&gt;AI architectures, by contrast, execute non-linear, agentic loops:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu284djpe49f9eevn0p1f.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu284djpe49f9eevn0p1f.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Managing this traffic requires capabilities that standard proxy layers lack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token-based rate limiting&lt;/strong&gt;: Quotas enforced by input/output token volume rather than raw request counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic protection&lt;/strong&gt;: Real-time prompt inspection, model routing, and credential abstraction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic tool governance&lt;/strong&gt;: Securing runtime discovery of tools, not just their direct HTTP invocations.&lt;/p&gt;

&lt;p&gt;The AI Gateway 2.0 Abstraction Model&lt;/p&gt;

&lt;p&gt;Kong AI Gateway 2.0 replaces downstream plugin attachment with dedicated architectural abstractions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Model Providers &amp;amp; Models&lt;/strong&gt;: Decoupled backends that manage upstream connection pools, fallbacks, and credential routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI MCP Servers&lt;/strong&gt;: First-class entities representing tool providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Agents &amp;amp; A2A Routing&lt;/strong&gt;: Secure conduits for multi-agent negotiation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Consumers &amp;amp; Policies&lt;/strong&gt;: Identity-aware boundaries governing spend, prompt guardrails, and access rights.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsk8wzu4kiu2y2b0cs7h6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsk8wzu4kiu2y2b0cs7h6.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By decoupling configuration (Kong Konnect) from the data plane, runtime operations scale independently across private networks without exposing configuration planes to direct data streams.&lt;/p&gt;

&lt;p&gt;Deploying Across Google Cloud Runtimes&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndzhop3nyvtvyzszr3u2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fndzhop3nyvtvyzszr3u2.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Kubernetes Engine (GKE)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GKE serves as the backbone for complex, stateful microservices, internal agents, and orchestration engines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes-Native Config&lt;/strong&gt;: Deploy the data plane via the Kong Kubernetes Operator (KKO) and configure models or policies directly via CRDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload Identity Federation&lt;/strong&gt;: The data plane inherits Google Cloud IAM roles via Workload Identity, eliminating static service account keys when authenticating upstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloud Run provides high-density execution for event-driven webhooks, ephemeral MCP tools, and lightweight inference endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centralized Security Layer&lt;/strong&gt;: Keep Cloud Run services locked to private internal ingress, routing all inbound client and agent requests through Kong AI Gateway for authentication and logging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unified Surface&lt;/strong&gt;: Both GKE-based services and Cloud Run endpoints exist under the same consumer-facing namespace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrating Vertex AI and Gemini Models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of distributing Google Cloud service account keys to individual applications, route model consumption through Kong AI Gateway:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7mjwd15nxbz52qvd20r.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7mjwd15nxbz52qvd20r.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM-Backed Egress&lt;/strong&gt;: The gateway manages OAuth token minting via GCP Workload Identity to interact directly with Vertex AI standard and enterprise endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual Models &amp;amp; Fallbacks&lt;/strong&gt;: Configure fallback chains (e.g., primary Gemini Ultra fallback to Gemini Flash) at the gateway level without changing client-side code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity-Aware Routing&lt;/strong&gt;: Route internal teams to standard tiers while allocating high-throughput, low-latency allocations to mission-critical applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced AI Controls&lt;/strong&gt;: MCP Bundling &amp;amp; Agent-to-Agent Security&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Server Bundling &amp;amp; Scoped Discovery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than having an agent establish connections to dozens of independent tool endpoints, Kong aggregates tool namespaces into a unified endpoint:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ug9cn67vomnu69iyd9t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ug9cn67vomnu69iyd9t.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery Filtering&lt;/strong&gt;: The gateway intercepts tool list negotiations. If an agent lacks permissions for the billing tool, that capability is stripped from the MCP discovery response, preventing hallucinated or unauthorized executions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent-to-Agent (A2A) Governance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When specialized agents communicate across microservice boundaries, Kong AI Gateway establishes mutual authentication, traces execution graphs, and applies rate-limiting policies to prevent unconstrained recursion loops:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglo4ny9u2x59ayhpu4n3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fglo4ny9u2x59ayhpu4n3.png" alt=" " width="800" height="437"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1rvizky1hdreex23vwz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1rvizky1hdreex23vwz.png" alt=" " width="799" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production Multi-Runtime Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deploying Kong AI Gateway 2.0 across Google Cloud isolates runtime infrastructure choices from  AI governance requirements. GKE handles container orchestration, Cloud Run provides elastic serverless execution, and Vertex AI powers model inference. Kong AI Gateway 2.0 ties the ecosystem together providing a single control plane to secure and route modern AI traffic.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhxy2oifkohpbqmnc3hws.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhxy2oifkohpbqmnc3hws.png" alt=" " width="800" height="389"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcnzcgxwsl1djy6ocbgn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcnzcgxwsl1djy6ocbgn.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>googlecloud</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
    <item>
      <title>Taming Flutter Infinite Scroll (Part 2): Turning ScrollController into a Reactive State Machine with CubitSignalMixin</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:31:52 +0000</pubDate>
      <link>https://dev.to/gde/taming-flutter-infinite-scroll-part-2-turning-scrollcontroller-into-a-reactive-state-machine-cgh</link>
      <guid>https://dev.to/gde/taming-flutter-infinite-scroll-part-2-turning-scrollcontroller-into-a-reactive-state-machine-cgh</guid>
      <description>&lt;h2&gt;
  
  
  The Infinite Scroll Rite of Passage
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/gde/taming-flutter-infinite-scroll-why-3-lines-of-async-missed-the-point-and-how-blocsignal-fixes-it-3n48"&gt;Part 1: Taming Flutter Infinite Scroll: Why 3 Lines of async* Missed the Point, and How BlocSignal Fixes It&lt;/a&gt;, we explored why wrapping mutable state in &lt;code&gt;async*&lt;/code&gt; generators and &lt;code&gt;StreamIterator&lt;/code&gt; cracks under pressure when users rapidly fling a list. We demonstrated how &lt;code&gt;BlocSignal&lt;/code&gt;’s streamless &lt;code&gt;droppable()&lt;/code&gt; transformer solves thumb-flinging race conditions synchronously at the event boundary without Rx streams or microtask lag.&lt;/p&gt;

&lt;p&gt;Yet, even after solving event concurrency with a pure BLoC, many Flutter developers are left with a nagging architectural itch.&lt;/p&gt;

&lt;p&gt;Search pub.dev for &lt;code&gt;"infinite scroll"&lt;/code&gt; or &lt;code&gt;"pagination"&lt;/code&gt;, and you will find dozens of packages—&lt;code&gt;infinite_scroll_pagination&lt;/code&gt;, &lt;code&gt;lazy_load_scrollview&lt;/code&gt;, &lt;code&gt;flutter_pagewise&lt;/code&gt;, &lt;code&gt;loadmore&lt;/code&gt;. It is practically a rite of passage for every Flutter developer to install at least one of them.&lt;/p&gt;

&lt;p&gt;Why do these packages exist in such numbers?&lt;/p&gt;

&lt;p&gt;Because implementing pagination with standard Flutter controllers requires tedious widget-level plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a &lt;code&gt;StatefulWidget&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Instantiating and maintaining an instance of &lt;code&gt;ScrollController&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Subscribing to scroll metrics with &lt;code&gt;_scrollController.addListener(_onScroll)&lt;/code&gt; in &lt;code&gt;initState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remembering to call &lt;code&gt;removeListener&lt;/code&gt; and &lt;code&gt;_scrollController.dispose()&lt;/code&gt; in &lt;code&gt;dispose()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Manually calculating viewport extents (&lt;code&gt;offset &amp;gt;= maxScrollExtent * 0.9&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Gluing the scroll trigger to a state management call (&lt;code&gt;context.read&amp;lt;PostsBloc&amp;gt;().add(...)&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, the third-party pagination packages on pub.dev often extract a heavy architectural tax:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They hijack your widget tree&lt;/strong&gt;: They force you to replace standard Flutter widgets with proprietary wrappers like &lt;code&gt;PagedListView&lt;/code&gt;, fighting your slivers, custom scroll physics, and layout styling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They invent competing controllers&lt;/strong&gt;: They introduce bespoke paging controllers alongside your existing BLoCs or Notifiers, creating two competing sources of truth that you must manually synchronize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They attempt to solve concurrency in the UI&lt;/strong&gt;: They try to debounce or guard fetch requests inside widget lifecycle callbacks instead of at the architectural event boundary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if you did not need a third-party pagination package at all? What if Flutter's standard &lt;code&gt;ScrollController&lt;/code&gt; could &lt;strong&gt;itself&lt;/strong&gt; be your reactive state container?&lt;/p&gt;

&lt;p&gt;Let us examine why that was historically impossible in Dart—and how composable mixins change everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Dart's Single-Inheritance Wall
&lt;/h2&gt;

&lt;p&gt;Why couldn't Flutter's &lt;code&gt;ScrollController&lt;/code&gt; just extend &lt;code&gt;BlocSignal&lt;/code&gt; or &lt;code&gt;CubitSignal&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;In Dart, a class can extend only &lt;strong&gt;one&lt;/strong&gt; superclass.&lt;/p&gt;

&lt;p&gt;Flutter's &lt;code&gt;ScrollController&lt;/code&gt; extends &lt;code&gt;ChangeNotifier&lt;/code&gt; (which implements &lt;code&gt;Listenable&lt;/code&gt;). If you want a class to also be a &lt;code&gt;CubitSignal&lt;/code&gt; or &lt;code&gt;BlocSignal&lt;/code&gt;, Dart's single-inheritance constraint stops you dead in your tracks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Impossible in Dart (Multiple inheritance is forbidden):&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaginatedPostsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlocSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Dart analyzer error: Each class can have only one superclass.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historically, this constraint forced developers into two unsatisfying compromises:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Proxy / Wrapper Anti-Pattern&lt;/strong&gt;: Creating a wrapper class that held an internal &lt;code&gt;_bloc&lt;/code&gt; reference, requiring tedious method forwarding and lifecycle delegation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Dual-Lifecycle Trap&lt;/strong&gt;: Managing a &lt;code&gt;ScrollController&lt;/code&gt; and a &lt;code&gt;PostsBloc&lt;/code&gt; as separate objects in the widget tree, gluing them together with &lt;code&gt;initState&lt;/code&gt; listeners and cleaning both up in &lt;code&gt;dispose()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;strong&gt;&lt;code&gt;CubitSignalMixin&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;BlocSignalMixin&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;bloc_signals&lt;/code&gt;, that single-inheritance wall is demolished.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧬 How Composable Mixins Break the Wall
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;BlocSignal&lt;/code&gt; has a minimal, highly disciplined API contract, mixing it into arbitrary classes introduces zero namespace collisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│                        BlocSignal Mixin Architecture                   │
├────────────────────────────────┬───────────────────────────────────────┤
│ Mixin                          │ Capabilities Added                    │
├────────────────────────────────┼───────────────────────────────────────┤
│ CubitSignalMixin&amp;lt;StateType&amp;gt;    │ state, stateValue, emit(newState),    │
│                                │ equals(), createEffect(), close()     │
├────────────────────────────────┼───────────────────────────────────────┤
│ BlocSignalMixin&amp;lt;Event, State&amp;gt;  │ on&amp;lt;E&amp;gt;(), concurrency transformers     │
│                                │ (droppable, restartable), add(event)  │
└────────────────────────────────┴───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a class adopts &lt;code&gt;CubitSignalMixin&amp;lt;StateType&amp;gt;&lt;/code&gt;, it implements &lt;code&gt;BlocSignalBase&amp;lt;StateType&amp;gt;&lt;/code&gt;. It gains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0ms synchronous reactive signals (&lt;code&gt;state&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Direct synchronous state value access (&lt;code&gt;stateValue&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Automatic de-duplication (&lt;code&gt;emit(newState)&lt;/code&gt; drops transitions when &lt;code&gt;newState == currentState&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Reactive observer tracking and lifecycle management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when combined with &lt;code&gt;BlocSignalMixin&amp;lt;Event, StateType&amp;gt;&lt;/code&gt;, it gains full event-driven execution with streamless transformers like &lt;code&gt;droppable()&lt;/code&gt; and &lt;code&gt;restartable()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This unlocks two clean architectural patterns for infinite scroll.&lt;/p&gt;




&lt;h2&gt;
  
  
  📐 Pattern A: The Reactive &lt;code&gt;PagingScrollController&lt;/code&gt; (Separation of Concerns)
&lt;/h2&gt;

&lt;p&gt;If your architectural philosophy demands that your domain business logic remain 100% pure Dart (with zero imports of &lt;code&gt;package:flutter/widgets.dart&lt;/code&gt;), you can turn &lt;code&gt;ScrollController&lt;/code&gt; into a focused, reactive boolean signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals/bloc_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/widgets.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// A ScrollController that is also a CubitSignal emitting whether &lt;/span&gt;
&lt;span class="c1"&gt;/// the scroll viewport is within [threshold] pixels of the bottom.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagingScrollController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt; 
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;CubitSignalMixin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;PagingScrollController&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;200.0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Initialize the CubitSignalMixin with initial state&lt;/span&gt;
    &lt;span class="n"&gt;initCubitSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Listen to scroll metrics internally&lt;/span&gt;
    &lt;span class="n"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Remaining scroll extent threshold in logical pixels (default: 200.0).&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasClients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// position.extentAfter returns the exact remaining pixels after the viewport!&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isNearBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extentAfter&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. emit() automatically de-duplicates:&lt;/span&gt;
    &lt;span class="c1"&gt;// Only triggers subscribers when the boolean flips between false and true!&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isNearBottom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  💡 The &lt;code&gt;extentAfter&lt;/code&gt; Secret: Why Pixels Beat Percentages
&lt;/h3&gt;

&lt;p&gt;Notice line 20:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isNearBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extentAfter&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most Flutter pagination tutorials write something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ The percentage trap:&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxScrollExtent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calculating a percentage (such as &lt;code&gt;0.9&lt;/code&gt;) creates an erratic user experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On a &lt;strong&gt;short list&lt;/strong&gt; of 1,000 pixels, 90% triggers when you are 100 pixels from the bottom.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;long list&lt;/strong&gt; of 50,000 pixels, 90% triggers when you are &lt;strong&gt;5,000 pixels&lt;/strong&gt; from the bottom—downloading pages far in advance that the user may never scroll to!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flutter's &lt;code&gt;ScrollPosition.extentAfter&lt;/code&gt; returns the &lt;strong&gt;exact quantity of content in logical pixels remaining after the viewport's trailing edge&lt;/strong&gt; (&lt;code&gt;math.max(maxScrollExtent - pixels, 0.0)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;position.extentAfter &amp;lt;= 200.0&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Provides a consistent lead time&lt;/strong&gt;: You always start fetching the next page when the user is ~2 items away from the bottom, regardless of whether the list has 10 items or 10,000 items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminates calculation boilerplate&lt;/strong&gt;: No multiplying &lt;code&gt;maxScrollExtent * 0.9&lt;/code&gt;, no reading &lt;code&gt;offset&lt;/code&gt;, and no bounds-checking when a list is empty. It is a single, clean comparison.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Superpower of Automatic De-duplication
&lt;/h3&gt;

&lt;p&gt;Notice line 27: &lt;code&gt;emit(isNearBottom);&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As a user scrolls vigorously near the bottom, scroll notifications fire dozens of times across 91%, 93%, 97%, and 99% of the viewport. In naive Flutter code, this requires manual boolean guards to prevent triggering duplicate actions.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;CubitSignalMixin&lt;/code&gt;, &lt;strong&gt;de-duplication is automatic&lt;/strong&gt;. Because &lt;code&gt;emit()&lt;/code&gt; checks &lt;code&gt;newState == currentState&lt;/code&gt;, calling &lt;code&gt;emit(true)&lt;/code&gt; twenty times in a row produces &lt;strong&gt;zero&lt;/strong&gt; spurious signal updates. The signal fires exactly once when crossing the threshold downward, and exactly once when scrolling back upward!&lt;/p&gt;

&lt;p&gt;Connecting this to your domain &lt;code&gt;PostsBloc&lt;/code&gt; requires just a single declarative effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;pagingController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pagingController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stateValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;postsBloc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain BLoC remains completely independent of Flutter, while the widget avoids doing manual scroll extent arithmetic.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Pattern B: The Self-Paging Domain Controller (Zero-Bridge Architecture)
&lt;/h2&gt;

&lt;p&gt;Now, let us take the architectural leap.&lt;/p&gt;

&lt;p&gt;What if you do not want a separate controller, a separate BLoC, and glue code between them? What if your controller &lt;strong&gt;is&lt;/strong&gt; the &lt;code&gt;ScrollController&lt;/code&gt;, and your controller &lt;strong&gt;is&lt;/strong&gt; the &lt;code&gt;BlocSignal&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Here is &lt;code&gt;PaginatedPostsController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:async'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals/bloc_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/widgets.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../models/post.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaginatedPostsController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt;
        &lt;span class="n"&gt;CubitSignalMixin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BlocSignalMixin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;PaginatedPostsController&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;200.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;PostRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Initialize CubitSignal state&lt;/span&gt;
    &lt;span class="n"&gt;initCubitSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Streamless concurrency: drop overlapping scroll triggers&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onPostsFetched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;droppable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Streamless concurrency: cancel and restart on search query change&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onPostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;restartable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Controller listens to its own scroll geometry!&lt;/span&gt;
    &lt;span class="n"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Remaining scroll extent threshold in logical pixels (default: 200.0).&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;PostRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasClients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Single, clean extentAfter check:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extentAfter&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onPostsFetched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PostsFetched&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&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="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;
          &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
              &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onPostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;count:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;searchQuery:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at what this class accomplishes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It is a &lt;code&gt;ScrollController&lt;/code&gt;&lt;/strong&gt;: You can pass it directly to &lt;code&gt;ListView.builder(controller: controller)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is a &lt;code&gt;BlocSignalBase&lt;/code&gt;&lt;/strong&gt;: You can pass it directly to &lt;code&gt;BlocSignalBuilder&lt;/code&gt; or provide it with &lt;code&gt;BlocSignalProvider&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It manages its own event dispatch&lt;/strong&gt;: It inspects its own scroll offset and calls &lt;code&gt;add(const PostsFetched())&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It governs its own concurrency&lt;/strong&gt;: &lt;code&gt;transformer: droppable()&lt;/code&gt; guarantees that rapid thumb flings while a network request is in-flight are synchronously ignored on the same frame.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🎨 The Flutter UI: A 100% &lt;code&gt;StatelessWidget&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now observe what happens to the Flutter UI layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals_flutter/bloc_signals_flutter.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../controllers/paginated_posts_controller.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SelfPagingPostsView&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SelfPagingPostsView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaginatedPostsController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Self-Paging Controller (Stateless)'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="n"&gt;PreferredSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;preferredSize:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;hintText:&lt;/span&gt; &lt;span class="s"&gt;'Search posts...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;prefixIcon:&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;border:&lt;/span&gt; &lt;span class="n"&gt;OutlineInputBorder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;BlocSignalBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaginatedPostsController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Failed to load posts'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'No posts found.'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Plugs directly into Flutter's native ListView!&lt;/span&gt;
                &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt;
                    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                    &lt;span class="p"&gt;);&lt;/span&gt;
                  &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nl"&gt;leading:&lt;/span&gt; &lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${post.id}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                    &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="nl"&gt;subtitle:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;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;Notice what is &lt;strong&gt;completely absent&lt;/strong&gt; from this widget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ No &lt;code&gt;StatefulWidget&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;❌ No &lt;code&gt;State.initState&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;❌ No &lt;code&gt;State.dispose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;❌ No &lt;code&gt;ScrollController&lt;/code&gt; listener wiring&lt;/li&gt;
&lt;li&gt;❌ No manual scroll threshold math&lt;/li&gt;
&lt;li&gt;❌ No third-party pagination widgets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The widget is a pure, declarative, 100% &lt;code&gt;StatelessWidget&lt;/code&gt;. It renders the state when signals emit, and it routes user interactions directly to the controller.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Safe Lifecycle &amp;amp; Teardown Architecture
&lt;/h2&gt;

&lt;p&gt;One important detail when unifying a Flutter &lt;code&gt;ChangeNotifier&lt;/code&gt; and a &lt;code&gt;BlocSignalBase&lt;/code&gt; is lifecycle coordination.&lt;/p&gt;

&lt;p&gt;When providing &lt;code&gt;PaginatedPostsController&lt;/code&gt; through &lt;code&gt;BlocSignalProvider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;BlocSignalProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaginatedPostsController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;lazy:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PaginatedPostsController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;repository:&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;SelfPagingPostsView&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;&lt;code&gt;BlocSignalProvider&lt;/code&gt; automatically invokes &lt;code&gt;bloc.close()&lt;/code&gt; when the provider is unmounted. &lt;/p&gt;

&lt;p&gt;Furthermore, passing &lt;code&gt;controller: controller&lt;/code&gt; to &lt;code&gt;ListView.builder&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; cause the &lt;code&gt;ListView&lt;/code&gt; to dispose the controller; in Flutter, widgets only dispose controllers that they instantiated internally.&lt;/p&gt;

&lt;p&gt;To ensure safe, leak-free teardown regardless of how the controller is managed, we implement an idempotent teardown guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@override&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_isControllerDisposed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScrollChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&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;Whether teardown is triggered via Flutter's &lt;code&gt;dispose()&lt;/code&gt; or &lt;code&gt;BlocSignalProvider&lt;/code&gt;'s &lt;code&gt;close()&lt;/code&gt;, listeners are removed, signals are cleaned up, and neither &lt;code&gt;super.dispose()&lt;/code&gt; nor &lt;code&gt;super.close()&lt;/code&gt; is ever executed more than once.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Comprehensive Architectural Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Third-Party Packages (for example &lt;code&gt;infinite_scroll_pagination&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Classic Straight BLoC (&lt;code&gt;examples/infinite_scroll&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Pattern A: Reactive &lt;code&gt;PagingScrollController&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Pattern B: Self-Paging Mixin (&lt;code&gt;examples/infinite_scroll_mixin&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Widget Tree Impact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Proprietary wrappers (&lt;code&gt;PagedListView&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ 100% Standard Flutter widgets&lt;/td&gt;
&lt;td&gt;✅ 100% Standard Flutter widgets&lt;/td&gt;
&lt;td&gt;✅ 100% Standard Flutter widgets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI Widget Structure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;StatefulWidget&lt;/code&gt; or wrapper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;StatefulWidget&lt;/code&gt; (&lt;code&gt;initState&lt;/code&gt;/&lt;code&gt;dispose&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;StatefulWidget&lt;/code&gt; or effect&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;100% &lt;code&gt;StatelessWidget&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source of Truth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Competing controllers fighting BLoC&lt;/td&gt;
&lt;td&gt;✅ Single BLoC container&lt;/td&gt;
&lt;td&gt;✅ Single BLoC container&lt;/td&gt;
&lt;td&gt;✅ Single unified controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Brittle UI-level guards&lt;/td&gt;
&lt;td&gt;✅ Synchronous &lt;code&gt;droppable()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Synchronous &lt;code&gt;droppable()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Synchronous &lt;code&gt;droppable()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain Layer Separation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coupled to package&lt;/td&gt;
&lt;td&gt;✅ 100% Pure Dart domain&lt;/td&gt;
&lt;td&gt;✅ 100% Pure Dart domain&lt;/td&gt;
&lt;td&gt;Blended UI controller &amp;amp; state machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy third-party package&lt;/td&gt;
&lt;td&gt;None (pure &lt;code&gt;bloc_signals&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;None (pure &lt;code&gt;bloc_signals&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;None (pure &lt;code&gt;bloc_signals&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎯 Which Strategy Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Both patterns are first-class citizens in the &lt;code&gt;BlocSignal&lt;/code&gt; repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use the Classic Straight BLoC Strategy (&lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/infinite_scroll" rel="noopener noreferrer"&gt;&lt;code&gt;examples/infinite_scroll&lt;/code&gt;&lt;/a&gt;)&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your team adheres to strict Clean Architecture where domain BLoCs must never import Flutter packages.&lt;/li&gt;
&lt;li&gt;The same BLoC is shared across CLI tools, Jaspr web applications, or server-side Dart services.&lt;/li&gt;
&lt;li&gt;You prefer traditional &lt;code&gt;StatefulWidget&lt;/code&gt; controller lifecycles in the UI layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use the Self-Paging Mixin Strategy (&lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/infinite_scroll_mixin" rel="noopener noreferrer"&gt;&lt;code&gt;examples/infinite_scroll_mixin&lt;/code&gt;&lt;/a&gt;)&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want maximum developer velocity and minimal boilerplate.&lt;/li&gt;
&lt;li&gt;You want your UI views to be 100% &lt;code&gt;StatelessWidget&lt;/code&gt; with zero glue code.&lt;/li&gt;
&lt;li&gt;You want a self-contained controller that manages both the scroll mechanics and the paginated state machine in one cohesive class.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Infinite scroll does not have to be a rite of passage filled with race condition bugs, competing controllers, or proprietary widget wrappers.&lt;/p&gt;

&lt;p&gt;By combining &lt;code&gt;droppable()&lt;/code&gt; concurrency with &lt;code&gt;CubitSignalMixin&lt;/code&gt; and &lt;code&gt;BlocSignalMixin&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You overcome Dart's single-inheritance constraint.&lt;/li&gt;
&lt;li&gt;You eliminate duplicate scroll notifications automatically.&lt;/li&gt;
&lt;li&gt;You write 100% &lt;code&gt;StatelessWidget&lt;/code&gt; UI screens without a single line of &lt;code&gt;initState&lt;/code&gt; or &lt;code&gt;dispose&lt;/code&gt; boilerplate.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Resources &amp;amp; Companion Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🔗 Part 1 of this series: &lt;a href="https://dev.to/gde/taming-flutter-infinite-scroll-why-3-lines-of-async-missed-the-point-and-how-blocsignal-fixes-it-3n48"&gt;Taming Flutter Infinite Scroll: Why 3 Lines of async* Missed the Point, and How BlocSignal Fixes It&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Classic Strategy Example: &lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/infinite_scroll" rel="noopener noreferrer"&gt;examples/infinite_scroll on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Self-Paging Mixin Strategy Example: &lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/infinite_scroll_mixin" rel="noopener noreferrer"&gt;examples/infinite_scroll_mixin on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📜 Composable Mixins Guide: &lt;a href="https://blocsignal.dev/docs/composable-mixins" rel="noopener noreferrer"&gt;CubitSignalMixin &amp;amp; BlocSignalMixin Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://pub.dev/packages/bloc_signals" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals&lt;/code&gt; on pub.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://pub.dev/packages/bloc_signals_flutter" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals_flutter&lt;/code&gt; on pub.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 Official Documentation Hub: &lt;a href="https://blocsignal.dev" rel="noopener noreferrer"&gt;blocsignal.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌟 GitHub Repository: &lt;a href="https://github.com/RandalSchwartz/BlocSignal" rel="noopener noreferrer"&gt;RandalSchwartz/BlocSignal&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>architecture</category>
      <category>statemanagement</category>
    </item>
    <item>
      <title>ChromeOS Lookalikes, Two Ways: One With Drivers, One Without</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Thu, 03 Sep 2026 15:18:58 +0000</pubDate>
      <link>https://dev.to/gde/chromeos-lookalikes-two-ways-one-with-drivers-one-without-83m</link>
      <guid>https://dev.to/gde/chromeos-lookalikes-two-ways-one-with-drivers-one-without-83m</guid>
      <description>&lt;p&gt;This article covers two scripts that live in the same repository for the same underlying reason — getting a Linux box to behave like a Chromebook — and that end up doing almost nothing alike, because they solve that problem for two different machines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/chromeos-boot" rel="noopener noreferrer"&gt;https://github.com/xbill9/chromeos-boot&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Two Scripts, One Theme
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;chromeos-boot&lt;/code&gt; is not one tool with two modes. It is two standalone scripts that happen to share a repo and a name:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stage&lt;/code&gt;&lt;/strong&gt; — brings a fresh ChromeOS Linux (Crostini) &lt;em&gt;container&lt;/em&gt; up from nothing, when everything else it needs lives in a private Cloud Storage bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;flex&lt;/code&gt;&lt;/strong&gt; — turns a stock Debian &lt;em&gt;desktop&lt;/em&gt; into a ChromeOS Flex lookalike: shelf, web apps, keybindings, wallpaper. No bucket, no gcloud, nothing private.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;stage&lt;/code&gt; gets a machine to where it can read the bucket. &lt;code&gt;flex&lt;/code&gt; makes a machine look like the thing it is imitating — and that word "imitating" only applies to one of them. &lt;code&gt;stage&lt;/code&gt; runs inside an actual Crostini container, on actual ChromeOS; it has nothing to fake. &lt;code&gt;flex&lt;/code&gt; runs on a desktop that gave up ChromeOS entirely, so everything about the look is manufactured. Two ChromeOS lookalikes, and only one of them is actually pretending.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Two, Not One
&lt;/h4&gt;

&lt;p&gt;The reason there's a machine running &lt;code&gt;flex&lt;/code&gt; at all is that Crostini's Linux environment cannot load the NVIDIA driver, and that's a kernel problem before it's a permissions one.&lt;/p&gt;

&lt;p&gt;Crostini's container doesn't run on the ChromeOS kernel directly. Enabling Linux spins up a lightweight VM (Termina) with its own guest kernel, one that Google builds, signs, and ships as part of the OS image — and your Debian container runs inside that VM, sharing its kernel rather than bringing one of its own. The NVIDIA driver isn't a userspace package; the installer's whole job is to compile &lt;code&gt;nvidia.ko&lt;/code&gt; against the exact kernel that's running and load it with &lt;code&gt;insmod&lt;/code&gt;. Termina's kernel has no headers shipped for that build, no writable path to drop a module into &lt;code&gt;/lib/modules&lt;/code&gt; that survives the next ChromeOS update, and no interest in accepting an unsigned out-of-tree module on a kernel that's otherwise locked down by verified boot.&lt;/p&gt;

&lt;p&gt;Even past the module problem, there's no card to attach it to. Graphics inside Crostini go through virtio-gpu — the VM sees a paravirtualized display device good enough for compositing and OpenGL/Vulkan passthrough, not the raw PCI device a discrete GPU actually is. The NVIDIA driver wants to bind directly to that PCI device. Crostini never hands it one.&lt;/p&gt;

&lt;p&gt;So the fix was to stop going through ChromeOS for this at all: put Debian directly on the laptop's metal, where &lt;code&gt;apt install nvidia-driver&lt;/code&gt; is building against a kernel you actually control, against a GPU that's actually visible on the PCI bus. That solves the driver problem and creates the one &lt;code&gt;flex&lt;/code&gt; exists to solve — a stock Debian/GNOME desktop looks and behaves nothing like ChromeOS, and none of what came free inside Crostini comes free here. &lt;code&gt;flex&lt;/code&gt; puts the shelf, the Google web apps, the keybindings, and the wallpaper back by hand.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stage&lt;/code&gt; and &lt;code&gt;flex&lt;/code&gt; are answers to two different questions, not two takes on one. &lt;code&gt;stage&lt;/code&gt; is what you run &lt;em&gt;because&lt;/em&gt; you're still inside ChromeOS. &lt;code&gt;flex&lt;/code&gt; is what you run &lt;em&gt;because&lt;/em&gt; the kernel made you leave.&lt;/p&gt;

&lt;h4&gt;
  
  
  stage: Staging a Fresh Crostini Container
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;At this point you should have&lt;/strong&gt; a brand-new Crostini container — the default state after enabling Linux on a Chromebook — and nothing else. No &lt;code&gt;git&lt;/code&gt;, no &lt;code&gt;gcloud&lt;/code&gt;, nothing installed.&lt;/p&gt;

&lt;p&gt;That is a chicken-and-egg problem: reading the private bucket needs &lt;code&gt;gcloud&lt;/code&gt;, and a bare container has no &lt;code&gt;gcloud&lt;/code&gt;. &lt;code&gt;stage&lt;/code&gt; is the one piece that has to be fetchable without credentials, so it is the only thing in this workflow that lives outside the bucket, pasted in or pulled straight from GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/xbill9/chromeos-boot/main/stage&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Process substitution rather than a pipe is deliberate. &lt;code&gt;curl ... | bash&lt;/code&gt; hands the script to bash on &lt;strong&gt;stdin&lt;/strong&gt;, which is the same stdin &lt;code&gt;gcloud auth login&lt;/code&gt; needs to read answers from; the login then fails or silently eats the rest of the script. &lt;code&gt;bash &amp;lt;(curl ...)&lt;/code&gt; passes it as a file descriptor and leaves stdin attached to the terminal.&lt;/p&gt;

&lt;p&gt;What it does, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs the Google Cloud CLI from the tarball into &lt;code&gt;$HOME&lt;/code&gt; — no sudo, no apt, no keyring.&lt;/li&gt;
&lt;li&gt;Logs in, opening a browser tab. Crostini hands that URL to the ChromeOS browser, which is already signed in, so this is usually two clicks.&lt;/li&gt;
&lt;li&gt;Copies &lt;code&gt;nnn&lt;/code&gt; out of the bucket into &lt;code&gt;~/bin&lt;/code&gt; and runs it, which fetches everything else — scripts, dotfiles.&lt;/li&gt;
&lt;li&gt;Replaces the tarball with the apt-managed &lt;code&gt;google-cloud-cli&lt;/code&gt; in &lt;code&gt;/usr/bin&lt;/code&gt;, then deletes &lt;code&gt;~/google-cloud-sdk&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 1's tarball is not the copy meant to stick around — it exists only to read the bucket once, before apt is reachable. Step 4 swaps it for the package apt will keep current, and is non-fatal: without sudo, or with apt unreachable, &lt;code&gt;stage&lt;/code&gt; warns and leaves the tarball in place rather than failing the run.&lt;/p&gt;

&lt;p&gt;Four commands finish the job, the last two coming from the &lt;code&gt;.bashrc&lt;/code&gt; that step 3 just fetched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/xbill9/chromeos-boot/main/stage&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;bash &lt;span class="nt"&gt;-l&lt;/span&gt;
bootstrap
bootstrap code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stage&lt;/code&gt; never asks what kind of hardware it is running on, because it doesn't need to know. Everything it touches — the bucket, &lt;code&gt;gcloud&lt;/code&gt;, the dotfiles — is the same regardless of the laptop underneath. That is the shape of a script solving a &lt;em&gt;credentials&lt;/em&gt; problem, not a &lt;em&gt;hardware&lt;/em&gt; one.&lt;/p&gt;

&lt;h4&gt;
  
  
  flex: Skinning a Stock Debian Desktop
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;At this point you should have&lt;/strong&gt; Debian 13 (trixie) with GNOME 48, installed on real hardware — not a container. &lt;code&gt;flex&lt;/code&gt; is written from, and matches, the customization actually run on a Yoga 9 with an NVIDIA GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bash flex &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;pkgs
theme
icons
shelf
webapps
appgrid
look
keys
helpers
wallpaper
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten stages, run in that order by default, or individually to repair one without redoing the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bash flex &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="go"&gt;
Turn a stock Debian 13 (trixie) / GNOME 48 desktop into a ChromeOS Flex
lookalike.

Companion to `stage`.  `stage` gets a machine to where it can read the
&lt;/span&gt;&lt;span class="gp"&gt;private bucket;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;flex&lt;span class="sb"&gt;`&lt;/span&gt; makes the machine look like the thing it is imitating:
&lt;span class="go"&gt;a bottom shelf, the Google web apps as first-class icons, ChromeOS
keybindings, a pruned app grid, and a matching pair of wallpapers.

usage: bash flex [--light|--dark]        everything (dark unless told otherwise)
       bash flex -l                      list the stages
&lt;/span&gt;&lt;span class="gp"&gt;       bash flex &amp;lt;stage&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&amp;lt;stage&amp;gt;...]    run only those
&lt;span class="go"&gt;       bash flex revert                  undo it
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here needs root except &lt;code&gt;pkgs&lt;/code&gt; (apt, non-fatal without sudo), and everything it creates lives under &lt;code&gt;$HOME&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Ten Stages, Briefly
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pkgs&lt;/code&gt;&lt;/strong&gt; — Roboto, gnome-tweaks, and Chrome if no Chromium-family browser is already there. The only stage that touches apt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;theme&lt;/code&gt;&lt;/strong&gt; — adw-gtk3, light and dark, from the upstream tarball (trixie ships no package for it), so GTK3 apps match the libadwaita GTK4 ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;icons&lt;/code&gt;&lt;/strong&gt; — Papirus and Papirus-Dark, user-level. ~60MB, the slower of the two heavyweight stages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shelf&lt;/code&gt;&lt;/strong&gt; — installs dash-to-panel in place of the packaged dash-to-dock, because only dash-to-panel merges the taskbar and system tray into one bar — the ChromeOS shelf is one bar, not two. Bottom, 56px, 75% opacity, Google-Blue running-app dots, Alt+1–9 for the first nine pinned apps. The per-monitor layout is resolved through Mutter's &lt;code&gt;DisplayConfig&lt;/code&gt; at run time rather than a hardcoded panel ID, so the same script works on a laptop panel and an external monitor without editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;webapps&lt;/code&gt;&lt;/strong&gt; — the nine Google apps (Gmail, Calendar, Drive, Docs, Sheets, Keep, Photos, Maps, YouTube) as windowless &lt;code&gt;--app=&lt;/code&gt; launchers, each with its own shelf icon fetched from gstatic with a favicon-service fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;appgrid&lt;/code&gt;&lt;/strong&gt; — hides the apps ChromeOS doesn't have (LibreOffice, xterm, Disk Utility, and 30-odd others) by shadowing each system &lt;code&gt;.desktop&lt;/code&gt; file with a copy carrying &lt;code&gt;NoDisplay=true&lt;/code&gt;. A copy, not a stub, so MIME associations and "Open with" keep working. Nothing is uninstalled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;look&lt;/code&gt;&lt;/strong&gt; — Roboto as the UI/document/titlebar font, blue accent, no hot corners, time-only clock, one workspace, shelf favorites pinned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;keys&lt;/code&gt;&lt;/strong&gt; — Caps Lock becomes Super, the ChromeOS Launcher key. Alt handles window minimize/maximize/tile; Alt+Tab cycles windows rather than app groups (which move to Super+Tab); Ctrl+F5 is overview; Ctrl+Shift+F5 is screenshot; Ctrl+Shift+Q signs out; Ctrl+Alt+T opens a terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;helpers&lt;/code&gt;&lt;/strong&gt; — installs &lt;code&gt;set-mode.sh&lt;/code&gt;, &lt;code&gt;gen_wallpaper.py&lt;/code&gt;, and &lt;code&gt;boot-splash.sh&lt;/code&gt; under &lt;code&gt;~/.local/share/chromeos-flex/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wallpaper&lt;/code&gt;&lt;/strong&gt; — renders both wallpaper variants with a hand-rolled PNG encoder (no PIL, no numpy on a stock desktop) — a diagonal gradient, four Google-palette blobs with a quadratic falloff, and an ordered dither to stop a 2560px gradient banding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wayland can't restart &lt;code&gt;gnome-shell&lt;/code&gt; in place, so the shelf, the GTK theme, and the app grid only take effect after a full log out and back in. The wallpaper and the keybindings are live immediately.&lt;/p&gt;

&lt;h4&gt;
  
  
  Flipping Light and Dark Without Rerunning Everything
&lt;/h4&gt;

&lt;p&gt;The look is roughly fifteen &lt;code&gt;gsettings&lt;/code&gt; calls that only make sense moving together — color scheme, GTK theme, icon theme, both wallpaper keys, the lock screen, and eight dash-to-panel colors. &lt;code&gt;helpers&lt;/code&gt; installs &lt;code&gt;set-mode.sh&lt;/code&gt; once so that switching modes afterward doesn't mean rerunning the whole script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.local/share/chromeos-flex/set-mode.sh light
&lt;span class="go"&gt;switched to light
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.local/share/chromeos-flex/set-mode.sh
&lt;span class="go"&gt;current: 'prefer-light'
  gtk-theme  'adw-gtk3'
  icon-theme 'Papirus'
  wallpaper  'file:///home/xbill/.local/share/backgrounds/chromeos-element-light.png'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  boot-splash.sh: Installed, Not Run
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;helpers&lt;/code&gt; writes one more file: &lt;code&gt;boot-splash.sh&lt;/code&gt;, which edits &lt;code&gt;/etc/default/grub&lt;/code&gt; to kill the boot menu and hand off to the Plymouth splash trixie already ships. &lt;code&gt;flex&lt;/code&gt; drops it on disk and stops — it's the only piece of either script that touches the boot chain, it needs root, and GRUB is not something to hand to a stage list without reading it first.&lt;/p&gt;

&lt;p&gt;It's not really a speed fix. On the machine it was written for, boot ran about 44 seconds, and roughly 30 of that was firmware and the boot loader before GRUB even started — killing the GRUB timeout only claws back the last 5 seconds or so. What it actually buys is continuity: no text menu flashing past on the way to the desktop, same as an actual Chromebook.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where They're Alike
&lt;/h4&gt;

&lt;p&gt;Different problems, same habits. Both scripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broken into named, independently runnable steps (&lt;code&gt;bootstrap &amp;lt;stage&amp;gt;&lt;/code&gt; / &lt;code&gt;bash flex &amp;lt;stage&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Re-running is how a half-finished run gets repaired, not something to avoid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Listable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bootstrap -l&lt;/code&gt; / &lt;code&gt;bash flex -l&lt;/code&gt; print the stage names before you commit to any of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-documenting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usage lives in a comment block at the top of the file, read back out with &lt;code&gt;-h&lt;/code&gt;/&lt;code&gt;--help&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fail soft&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A missing prerequisite warns and skips rather than aborting the rest of the run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No secrets on the command line&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stage&lt;/code&gt;'s bucket name isn't sensitive — IAM gates the objects, not the name — and &lt;code&gt;flex&lt;/code&gt; needs no credentials at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Where They Differ
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;stage&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;flex&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A Crostini &lt;em&gt;container&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;A bare-metal Debian &lt;em&gt;desktop&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Needs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gcloud&lt;/code&gt;, an authorized Google account, a private bucket&lt;/td&gt;
&lt;td&gt;Nothing — no bucket, no login, no network dependency beyond the packages it fetches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privilege&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs entirely unprivileged; the apt-managed &lt;code&gt;gcloud&lt;/code&gt; swap is the one non-fatal exception&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;pkgs&lt;/code&gt; touches &lt;code&gt;sudo&lt;/code&gt;/apt; the boot splash needs root but is never auto-run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bootstraps a dev environment: language runtimes, Docker, cloud and agent CLIs&lt;/td&gt;
&lt;td&gt;Cosmetic and GNOME-settings only: theme, shelf, keybindings, wallpaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;State model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Additive — there is no &lt;code&gt;stage revert&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Explicit &lt;code&gt;bash flex revert&lt;/code&gt;, which &lt;em&gt;resets&lt;/em&gt; touched settings to GNOME defaults rather than restoring whatever was there before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hardware awareness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None — the same script regardless of what's underneath&lt;/td&gt;
&lt;td&gt;Written for, and against, one specific machine's GPU and monitor, generalized via Mutter's &lt;code&gt;DisplayConfig&lt;/code&gt; rather than hardcoded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why it exists&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reading credentials before credentials exist&lt;/td&gt;
&lt;td&gt;Recreating what real ChromeOS gave up when the GPU drove the OS choice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal here was to get a real NVIDIA driver stack under a ChromeOS-shaped workflow, without giving up the parts of ChromeOS that make a laptop pleasant to use day to day. The key to the solution was accepting that this is two separate problems, not one script with a hardware flag: &lt;code&gt;stage&lt;/code&gt; solves &lt;em&gt;getting credentials onto a machine that has none&lt;/em&gt;, and &lt;code&gt;flex&lt;/code&gt; solves &lt;em&gt;making a desktop look like ChromeOS after leaving ChromeOS entirely&lt;/em&gt; — for the same underlying reason, on two different machines, sharing nothing but a repo and a theme.&lt;/p&gt;

&lt;p&gt;The strategy for making Linux look like ChromeOS — once inside a sandboxed container, once on the bare metal underneath it — was validated with an incremental, stage-by-stage approach on both sides.&lt;/p&gt;

</description>
      <category>chromeos</category>
      <category>linux</category>
      <category>debian</category>
      <category>gnome</category>
    </item>
    <item>
      <title>Taming Flutter Infinite Scroll: Why 3 Lines of async* Missed the Point, and How BlocSignal Fixes It</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Thu, 03 Sep 2026 12:47:27 +0000</pubDate>
      <link>https://dev.to/gde/taming-flutter-infinite-scroll-why-3-lines-of-async-missed-the-point-and-how-blocsignal-fixes-it-3n48</link>
      <guid>https://dev.to/gde/taming-flutter-infinite-scroll-why-3-lines-of-async-missed-the-point-and-how-blocsignal-fixes-it-3n48</guid>
      <description>&lt;h2&gt;
  
  
  The Ubiquitous Infinite Scroll Pagination Bug
&lt;/h2&gt;

&lt;p&gt;Almost every Flutter engineer has encountered the dreaded infinite scroll race condition in production.&lt;/p&gt;

&lt;p&gt;The user opens a list, flings their thumb down the screen on a spotty cellular connection, and triggers multiple scroll notifications past the bottom threshold within milliseconds. Before the first asynchronous HTTP network request finishes, the scroll listener fires again. &lt;/p&gt;

&lt;p&gt;Suddenly, your list duplicates items, page counters jump ahead, or the state machine locks up entirely.&lt;/p&gt;

&lt;p&gt;Recently, mobile developer Ali Wajdan published a widely discussed article titled &lt;a href="https://aliwajdan.medium.com/3-lines-of-dart-async-code-that-fixed-my-infinite-scroll-pagination-0485d392f567" rel="noopener noreferrer"&gt;3 Lines of Dart async* Code That Fixed My Infinite Scroll Pagination&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In his article, Ali accurately diagnoses the root cause of standard pagination headaches:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Most Flutter pagination code I have seen, including my own for years, wraps a mutable state object around a scroll listener. A page counter, a loading boolean, a hasMore flag, and a fetch method the UI calls when it hits the scroll threshold. It works until two scroll events fire close together, or a rebuild triggers a second load before the first future resolves... It is a classic race condition, and it gets worse once the state lives across a page counter, a hasMore flag, and a loading flag that all need to stay in sync."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To escape this trap, Ali suggested encapsulating pagination logic inside a Dart &lt;code&gt;async*&lt;/code&gt; generator and consuming it with a &lt;code&gt;StreamIterator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The pattern proposed in Ali Wajdan's article&lt;/span&gt;
&lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchPostsPaginated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hasMore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StreamIterator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetchPostsPaginated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;loadNextPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;moveNext&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&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;On the surface, moving mutable state into local generator variables looks clean. But does it actually solve the concurrency problem in production?&lt;/p&gt;

&lt;p&gt;Let us take a closer look.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Why &lt;code&gt;async*&lt;/code&gt; and &lt;code&gt;StreamIterator&lt;/code&gt; Crack Under Pressure
&lt;/h2&gt;

&lt;p&gt;While moving the &lt;code&gt;page&lt;/code&gt; counter and &lt;code&gt;hasMore&lt;/code&gt; flag inside the generator prevents outside tampering, the implementation suffers from severe architectural pitfalls:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Hidden Concurrency Crash: &lt;code&gt;Bad state: Cannot call moveNext...&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Dart's &lt;code&gt;StreamIterator.moveNext()&lt;/code&gt; is explicitly &lt;strong&gt;not concurrency-safe&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;If a fast scroll fling or a double-rebuild calls &lt;code&gt;loadNextPage()&lt;/code&gt; while a previous &lt;code&gt;moveNext()&lt;/code&gt; is still awaiting the network, Dart immediately throws an unhandled runtime error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bad state: Cannot call moveNext while a previous call to moveNext is still pending.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of this, the author admits in the article that he still had to maintain a manual guard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I still guard the UI trigger with the one Future that loadNextPage returns... That guard is the only piece of state I own now."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, you have not actually eliminated the concurrency guard—you have merely introduced a stream iterator abstraction on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pulling Chunks vs. Reactive Unidirectional Data Flow
&lt;/h3&gt;

&lt;p&gt;An iterator is an imperative pull-based consumer. It yields a raw batch of items, but a real-world Flutter UI needs a comprehensive reactive state model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when a network error occurs on page 4?&lt;/li&gt;
&lt;li&gt;How does the UI render a bottom spinner indicator while retaining previously fetched items?&lt;/li&gt;
&lt;li&gt;How do we handle pull-to-refresh or empty states?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With an iterator, you still have to maintain an external state container to accumulate batches, catch errors, and update the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Resource Leak Risks &amp;amp; Teardown Friction
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;StreamIterator&lt;/code&gt; holds an active stream subscription. If the user navigates away from the screen, you must remember to explicitly invoke &lt;code&gt;await iterator.cancel()&lt;/code&gt;. Furthermore, whenever a search query or filter changes, you must tear down the old iterator, instantiate a fresh generator, and re-bind the pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ The Architectural Lesson: Concurrency Belongs at the Event Boundary
&lt;/h2&gt;

&lt;p&gt;The core insight is simple: &lt;strong&gt;concurrency control should never be buried inside an imperative data-pulling loop, nor should it leak into UI scroll listeners.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concurrency is an &lt;strong&gt;event scheduling concern&lt;/strong&gt;. The moment a user's gesture or scroll threshold emits an event, the system should declare how overlapping executions are handled.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;BlocSignal&lt;/strong&gt;, concurrency is a first-class citizen governed by &lt;strong&gt;streamless event transformers&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ The Idiomatic BlocSignal Solution: &lt;code&gt;droppable()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;bloc_signals&lt;/code&gt;, preventing duplicate requests during infinite scroll requires exactly one parameter: &lt;strong&gt;&lt;code&gt;transformer: droppable()&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how a clean, production-ready &lt;code&gt;PostsBloc&lt;/code&gt; looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:bloc_signals/bloc_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../models/post.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsEvent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsFetched&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PostsEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsSearchChanged&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;PostsEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@immutable&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;hasReachedMax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;PostsState&lt;/span&gt; &lt;span class="n"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;hasReachedMax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="n"&gt;hasReachedMax&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;searchQuery:&lt;/span&gt; &lt;span class="n"&gt;searchQuery&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsBloc&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;BlocSignal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;PostsBloc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;PostRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;initialState:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Drop duplicate scroll triggers while a page fetch is in flight&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onPostsFetched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;droppable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Automatically cancel and restart when the search query changes&lt;/span&gt;
    &lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="n"&gt;_onPostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;restartable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;PostRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onPostsFetched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PostsFetched&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Natural offset pagination: stateValue.posts.length IS your cursor!&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;newPosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;limit:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;newPosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newPosts&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="n"&gt;newPosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onPostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;searchQuery:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;limit:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;posts:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;hasReachedMax:&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;searchQuery:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;status:&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔬 Under the Hood: Why &lt;code&gt;droppable()&lt;/code&gt; is Glitch-Free and Streamless
&lt;/h2&gt;

&lt;p&gt;How does &lt;code&gt;droppable()&lt;/code&gt; prevent race conditions without allocating Rx streams or microtask queues?&lt;/p&gt;

&lt;p&gt;In classic &lt;code&gt;package:bloc_concurrency&lt;/code&gt;, transformers convert an incoming event stream using Rx operators (such as &lt;code&gt;exhaustMap&lt;/code&gt;). That introduces stream controllers, subscription pipelines, and asynchronous microtask dispatch delays.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;BlocSignal&lt;/code&gt;, event transformers are &lt;strong&gt;streamless higher-order functions&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;EventTransformer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;droppable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateType&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isProcessing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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="n"&gt;isProcessing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;isProcessing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emit&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="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;isProcessing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at how elegant this is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the first &lt;code&gt;PostsFetched&lt;/code&gt; event arrives, &lt;code&gt;isProcessing&lt;/code&gt; flips to &lt;code&gt;true&lt;/code&gt; &lt;strong&gt;synchronously in the exact same call frame&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If the user's scroll fling generates 8 additional scroll events in that same frame or while the HTTP request is pending, each incoming event hits &lt;code&gt;if (isProcessing) return;&lt;/code&gt; and is safely, immediately discarded.&lt;/li&gt;
&lt;li&gt;Once the HTTP request completes and state is emitted, &lt;code&gt;finally&lt;/code&gt; resets &lt;code&gt;isProcessing = false&lt;/code&gt;, allowing the next scroll boundary trigger to proceed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero race conditions. Zero microtask lag. Zero Stream allocations.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Natural Cursor Pagination: Forgetting the &lt;code&gt;page&lt;/code&gt; Counter
&lt;/h2&gt;

&lt;p&gt;Notice another detail in &lt;code&gt;PostsBloc&lt;/code&gt;: &lt;strong&gt;there is no &lt;code&gt;page&lt;/code&gt; counter variable anywhere.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you manage paginated lists, maintaining a separate &lt;code&gt;int page = 0&lt;/code&gt; counter that increments alongside &lt;code&gt;posts.addAll(...)&lt;/code&gt; is an anti-pattern. If an API request fails, or if duplicate events trigger, the counter can desynchronize from the actual item count.&lt;/p&gt;

&lt;p&gt;Instead, derive your offset directly from the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;startIndex:&lt;/span&gt; &lt;span class="n"&gt;stateValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The length of your accumulated list &lt;strong&gt;is&lt;/strong&gt; your pagination cursor. There is nothing to desynchronize, nothing to increment prematurely, and nothing to reset manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 Instant Search Reset with &lt;code&gt;restartable()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;What happens when the user types a new search query into the search bar while an infinite scroll request is actively in flight?&lt;/p&gt;

&lt;p&gt;In Ali's generator example, resetting required manual teardown:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Changing a search query or filter means creating a new generator, not carefully resetting three fields and hoping you got them all."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In BlocSignal, you simply tag search events with &lt;code&gt;transformer: restartable()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;on&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
  &lt;span class="n"&gt;_onPostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;transformer:&lt;/span&gt; &lt;span class="n"&gt;restartable&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;When a new search query arrives, &lt;code&gt;restartable()&lt;/code&gt; increments an internal token. Any in-flight HTTP responses from older queries or prior scroll pages are automatically dropped from emitting state. The list smoothly switches to the new search query without race conditions or ghost responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  📱 The Flutter UI: Declarative and Lightweight
&lt;/h2&gt;

&lt;p&gt;Consuming this in Flutter is straightforward. We attach a &lt;code&gt;ScrollController&lt;/code&gt; listener to dispatch &lt;code&gt;PostsFetched()&lt;/code&gt; when the user is within 10% of the bottom, and build the UI using &lt;code&gt;BlocSignalBuilder&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsView&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_PostsViewState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_PostsViewState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScrollController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_onScroll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasClients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;maxScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxScrollExtent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Trigger when 90% scrolled&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentScroll&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxScroll&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostsFetched&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;appBar:&lt;/span&gt; &lt;span class="n"&gt;AppBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Infinite Scroll Posts'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="n"&gt;PreferredSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;preferredSize:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromHeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;hintText:&lt;/span&gt; &lt;span class="s"&gt;'Search posts...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;prefixIcon:&lt;/span&gt; &lt;span class="n"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nl"&gt;border:&lt;/span&gt; &lt;span class="n"&gt;OutlineInputBorder&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="nl"&gt;onChanged:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsBloc&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostsSearchChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;BlocSignalBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostsBloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PostsState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Failed to load posts.'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;PostsStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;
              &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'No posts found.'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;_scrollController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="nl"&gt;itemCount:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasReachedMax&lt;/span&gt;
                      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;
                      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="nl"&gt;itemBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                      &lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ListTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="nl"&gt;leading:&lt;/span&gt; &lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${post.id}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                      &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="nl"&gt;subtitle:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;);&lt;/span&gt;
                  &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;BlocSignalBuilder&lt;/code&gt; listens to fine-grained signal changes, UI updates execute synchronously in the exact frame state is emitted, completely avoiding frame-skipping and microtask latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Architectural Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architectural Metric&lt;/th&gt;
&lt;th&gt;Ali's &lt;code&gt;async*&lt;/code&gt; + &lt;code&gt;StreamIterator&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Manual Flags in Widget / State&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;BlocSignal&lt;/code&gt; with &lt;code&gt;droppable()&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency Guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Throws &lt;code&gt;StateError&lt;/code&gt; on concurrent &lt;code&gt;moveNext()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;⚠️ Brittle &lt;code&gt;isLoading&lt;/code&gt; flags prone to race conditions&lt;/td&gt;
&lt;td&gt;✅ 100% synchronous guard lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scoped to generator&lt;/td&gt;
&lt;td&gt;Mutable &lt;code&gt;page&lt;/code&gt; counter&lt;/td&gt;
&lt;td&gt;Natural offset (&lt;code&gt;posts.length&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search Cancellation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires manual generator recreation&lt;/td&gt;
&lt;td&gt;Complex cancellation tokens&lt;/td&gt;
&lt;td&gt;Built-in via &lt;code&gt;transformer: restartable()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pull-only chunk fetching&lt;/td&gt;
&lt;td&gt;Cluttered widget code&lt;/td&gt;
&lt;td&gt;Declarative, reactive UI via signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lifecycle &amp;amp; Teardown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual &lt;code&gt;iterator.cancel()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Manual controller disposal&lt;/td&gt;
&lt;td&gt;Automatic container cleanup via &lt;code&gt;close()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stream iteration overhead&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;td&gt;Streamless pure Dart functions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Ali Wajdan's article highlights a genuine problem: manual state flags around scroll listeners are a frequent source of production bugs in Flutter.&lt;/p&gt;

&lt;p&gt;However, attempting to solve event concurrency by turning asynchronous APIs into stream generators swaps one set of bugs for another.&lt;/p&gt;

&lt;p&gt;By treating concurrency as an &lt;strong&gt;event boundary policy&lt;/strong&gt; with &lt;code&gt;droppable()&lt;/code&gt; and &lt;code&gt;restartable()&lt;/code&gt;, you gain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bulletproof concurrency&lt;/strong&gt; that gracefully ignores rapid thumb flings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-drift pagination&lt;/strong&gt; powered by natural list offset indexing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamless performance&lt;/strong&gt; with 0ms signal propagation.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Resources &amp;amp; Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🔗 Original Article by Ali Wajdan: &lt;a href="https://aliwajdan.medium.com/3-lines-of-dart-async-code-that-fixed-my-infinite-scroll-pagination-0485d392f567" rel="noopener noreferrer"&gt;3 Lines of Dart async* Code That Fixed My Infinite Scroll Pagination&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📜 Interactive Concurrency Guide: &lt;a href="https://blocsignal.dev/docs/event-transformers" rel="noopener noreferrer"&gt;Event Concurrency Transformers on blocsignal.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Full Runnable Example &amp;amp; Test Suite: &lt;a href="https://github.com/RandalSchwartz/BlocSignal/tree/main/examples/infinite_scroll" rel="noopener noreferrer"&gt;examples/infinite_scroll on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://pub.dev/packages/bloc_signals" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals&lt;/code&gt; on pub.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://pub.dev/packages/bloc_signals_flutter" rel="noopener noreferrer"&gt;&lt;code&gt;bloc_signals_flutter&lt;/code&gt; on pub.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 Official Documentation: &lt;a href="https://blocsignal.dev" rel="noopener noreferrer"&gt;blocsignal.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌟 GitHub Repository: &lt;a href="https://github.com/RandalSchwartz/BlocSignal" rel="noopener noreferrer"&gt;RandalSchwartz/BlocSignal&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>architecture</category>
      <category>statemanagement</category>
    </item>
    <item>
      <title>[AI in Practice] Building a Dynamic LINE Group Buying Bot with Edit and Unsend Webhooks</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:27:12 +0000</pubDate>
      <link>https://dev.to/gde/ai-in-practice-building-a-dynamic-line-group-buying-bot-with-edit-and-unsend-webhooks-1clh</link>
      <guid>https://dev.to/gde/ai-in-practice-building-a-dynamic-line-group-buying-bot-with-edit-and-unsend-webhooks-1clh</guid>
      <description>&lt;h1&gt;
  
  
  When Messages Can Be Taken Back: Building a "Shapeshifting" LINE Group Buying Bot with Edit and Unsend Webhooks
&lt;/h1&gt;

&lt;p&gt;Author: Evan Lin, LINE Taiwan Developer Relations Team Lead&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgxwfi0rvtwqw9baqhcq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgxwfi0rvtwqw9baqhcq.png" alt="img" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On August 20, 2026, LINE announced the open free trial of the "Edit Message" feature in LINE Labs.&lt;/p&gt;

&lt;p&gt;For general users, this is a very intuitive feature: if you find a typo, a wrong date, or want to adjust your tone after sending a message, you no longer need to go through the "Unsend, retype, resend" process. You can simply edit the original text message.&lt;/p&gt;

&lt;p&gt;But when I saw this feature, the first thing I thought of was something else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a user changes a message that has already been processed by a LINE Bot, does the Bot know?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is: Yes. The LINE Messaging API provides the &lt;code&gt;Edit event&lt;/code&gt;; and when a user unsends a message, there is also an &lt;code&gt;Unsend event&lt;/code&gt; that can be received.&lt;/p&gt;

&lt;p&gt;In this article, I want to share how I turned these two types of webhook events into a LINE Bot Demo with a story, as well as several easily overlooked but very important details during implementation.&lt;/p&gt;

&lt;p&gt;The complete sample code is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kkdai/linebot-edit-unsend" rel="noopener noreferrer"&gt;https://github.com/kkdai/linebot-edit-unsend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with "Edit Message" in LINE Labs
&lt;/h2&gt;

&lt;p&gt;To try out message editing in LINE Labs currently, you need to meet the following conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the mobile version of LINE to version 26.12.0 or higher.&lt;/li&gt;
&lt;li&gt;Go to "Home → Settings → LINE Labs".&lt;/li&gt;
&lt;li&gt;Enable "Edit Messages".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In standard one-on-one and group chats, text messages can be edited within 15 minutes of being sent; for Keep Notes, it's within 6 days. Photos, videos, voice messages, files, and stickers cannot be edited at this time. After modification, the chat room will display "Edited," but it does not provide a way to view or restore old versions.&lt;/p&gt;

&lt;p&gt;There is also a limitation directly related to Bot development: currently, one-on-one chats with LINE Official Accounts do not support message editing. Therefore, to test the Messaging API's Edit event, the Bot must be added to a group chat.&lt;/p&gt;

&lt;p&gt;For features and activation methods, please refer to the &lt;a href="https://www.linecorp.com/tw/pr/news/2026/0820/" rel="noopener noreferrer"&gt;LINE Taiwan Newsroom Announcement&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  From UI Features to Bot Data Consistency
&lt;/h2&gt;

&lt;p&gt;In the past, the common process for a Bot receiving a text message was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive message webhook.&lt;/li&gt;
&lt;li&gt;Parse the text.&lt;/li&gt;
&lt;li&gt;Write to the database or trigger subsequent processes.&lt;/li&gt;
&lt;li&gt;Reply with the processing result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a message cannot be modified after being sent, this model is very simple. But when messages can be edited, the original text is not necessarily the user's final intent.&lt;/p&gt;

&lt;p&gt;For example, a user sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pearl Milk Tea / Half Sugar / Less Ice / 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Bot has already recorded it as one drink costing 60 TWD. A few seconds later, the user directly edits the original message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pearl Milk Tea / Micro Sugar / No Ice / 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the Bot does not handle the Edit event, the chat room sees two cups, but the backend still records one cup. The world on the screen and the world inside the Bot become disconnected.&lt;/p&gt;

&lt;p&gt;This is the most practical value of the Edit event: it's not just a notification that "the text has changed," but an opportunity for the service to synchronize with the user's latest intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group Buying Transformation: Giving the API Demo a Story
&lt;/h2&gt;

&lt;p&gt;To demonstrate both edit and unsend simultaneously, I set the Bot as "Shapeshifting Store Manager - Dan Dan," responsible for saving the office from low energy at 3:00 PM.&lt;/p&gt;

&lt;p&gt;The Demo flow is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The group leader enters &lt;code&gt;Start Group Buy 50 Lan 15:20 Deadline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Alice enters &lt;code&gt;Pearl Milk Tea / Half Sugar / Less Ice / 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Alice edits the same message to &lt;code&gt;Pearl Milk Tea / Micro Sugar / No Ice / 2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bot receives the Edit event and updates the quantity, sweetness, ice level, and total amount.&lt;/li&gt;
&lt;li&gt;Bob unsends his message after placing an order.&lt;/li&gt;
&lt;li&gt;The Bot receives the Unsend event, deletes Bob's order, and recalculates.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;Current Orders&lt;/code&gt; to view the latest summary via Flex Message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This scenario is perfect for a Demo because everyone knows the two most common things in group buying: changing your mind after ordering, and suddenly not wanting to drink after ordering.&lt;/p&gt;

&lt;p&gt;More importantly, the audience can directly see from the quantity and amount whether the webhook actually affected the backend state, rather than the Bot just replying "Edit received."&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure of the Edit Event
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; of the Edit event is &lt;code&gt;messageEdited&lt;/code&gt;, which carries the edited text, timestamp, reply token, and message ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messageEdited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"replyToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"950e63e8f46542ab89f645b4c2a1180a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"610830548529053697"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pearl Milk Tea / Micro Sugar / No Ice / 2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1776914799524&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ca56f94637c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"U4af4980629..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most critical design point is: the &lt;code&gt;message.id&lt;/code&gt; in the edit event is the same as the message ID of the original message event.&lt;/p&gt;

&lt;p&gt;Therefore, we can directly use the message ID as the order ID:&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="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageEvent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageEditedEvent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleEdit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnsendEvent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleUnsend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add an order when the original message event is received; when &lt;code&gt;MessageEditedEvent&lt;/code&gt; is received, use the same message ID to find the order and overwrite the content.&lt;/p&gt;

&lt;p&gt;Additionally, the Edit event has its own reply token, which is different from the original message event's reply token, so the Bot can directly reply "Order successfully transformed" to this specific edit.&lt;/p&gt;

&lt;p&gt;If your Bot also uses the Mark as Read API, note that the Edit event does not carry a &lt;code&gt;markAsReadToken&lt;/code&gt;. You cannot simply apply the standard message event processing flow. These field differences are well-suited for a full test using webhook fixtures after upgrading the SDK.&lt;/p&gt;

&lt;p&gt;For complete fields, please refer to &lt;a href="https://developers.line.biz/en/reference/messaging-api/#edit-event" rel="noopener noreferrer"&gt;Messaging API: Edit event&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Trap: Edit Events May Not Arrive in Order
&lt;/h2&gt;

&lt;p&gt;This is the most easily overlooked part of this implementation.&lt;/p&gt;

&lt;p&gt;A user might edit the same message multiple times in quick succession, and multiple &lt;code&gt;messageEdited&lt;/code&gt; webhooks are not guaranteed to arrive in the order they were edited. Official documentation recommends using the largest timestamp to represent the latest state.&lt;/p&gt;

&lt;p&gt;Therefore, you cannot simply adopt the "last received webhook"; you should adopt the "webhook with the latest timestamp":&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;errStaleEdit&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;
&lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;messageID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assuming the second edit arrives first and the first edit arrives later, this logic prevents old content from overwriting new content.&lt;/p&gt;

&lt;p&gt;At the same time, the LINE Platform might resend webhooks, so the example also uses &lt;code&gt;webhookEventId&lt;/code&gt; for deduplication. These two mechanisms handle different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;webhookEventId&lt;/code&gt;: Prevents the same event from being processed twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt;: Prevents different edit events from updating data in the wrong order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both need to be handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Trap: Edited Content May No Longer Be a Valid Order
&lt;/h2&gt;

&lt;p&gt;Users don't necessarily just change the quantity. They might also change the original order to something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I suddenly don't want to drink anymore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the new message can no longer pass the order format validation. If the backend continues to keep the old order, it will still cause state inconsistency.&lt;/p&gt;

&lt;p&gt;The approach in this Demo is: clear the old order content, mark this record as invalid and temporarily remove it from the total, and then ask the user to continue editing to fix it.&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;parsed&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;parseOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sugar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnitPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;
    &lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;messageID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actual products can adopt different strategies, such as putting it into a "waiting for manual confirmation" state; the key is not to silently continue using old content that no longer exists in the chat room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unsend Event: Beyond Technology, User Intent
&lt;/h2&gt;

&lt;p&gt;The content of the Unsend event is simpler than the Edit event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unsend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ca56f94637c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"U4af4980629..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unsend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"610830548529053697"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It only tells us which message ID was unsent; it does not re-attach the message content, nor does it have a reply token.&lt;/p&gt;

&lt;p&gt;Since we already use the message ID as the order ID, deletion is straightforward:&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="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messageID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what's truly important here is not &lt;code&gt;delete()&lt;/code&gt;, but how to respect the user's intent of "I want to take back this content."&lt;/p&gt;

&lt;p&gt;Official documentation specifically reminds service providers that after receiving an Unsend event, they should handle it carefully so that the target message cannot be seen or used in the future. Therefore, this Demo will not quote the unsent item, sweetness, or other original text in the Bot's reply, but will only say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;💨 An order has been safely withdrawn, and the saved content has been deleted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the Unsend event has no reply token, if you want to proactively notify the group, you can only use a push message. This means it will count towards message usage, so quota and cost should be considered when designing a formal service.&lt;/p&gt;

&lt;p&gt;For full details, please refer to &lt;a href="https://developers.line.biz/en/reference/messaging-api/#unsend-event" rel="noopener noreferrer"&gt;Messaging API: Unsend event&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making State Changes Visible with Flex Message
&lt;/h2&gt;

&lt;p&gt;While plain text can show results, to make the Demo understandable at a glance, I highly recommend using Flex Message to display the current order.&lt;/p&gt;

&lt;p&gt;The card this time includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store name and deadline&lt;/li&gt;
&lt;li&gt;Group buy status (In progress or Closed)&lt;/li&gt;
&lt;li&gt;Each member's item, sweetness, ice level, quantity, and subtotal&lt;/li&gt;
&lt;li&gt;Total quantity and total amount&lt;/li&gt;
&lt;li&gt;"Refresh Order" button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After every addition, edit, or unsend, the Flex Message is regenerated from the current state. This way, the quantity and amount before and after an edit change immediately, and the line item disappears after an unsend.&lt;/p&gt;

&lt;p&gt;To avoid the Flex Message becoming too large, the example displays a maximum of eight orders, with the rest summarized. Formal products can switch to carousels, LIFF pages, or add pagination queries as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDK and Execution Environment Versions
&lt;/h2&gt;

&lt;p&gt;This example uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com/line/line-bot-sdk-go/v8 v8.22.0
Go 1.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older versions of the SDK may already have &lt;code&gt;UnsendEvent&lt;/code&gt;, but they might not include the new &lt;code&gt;MessageEditedEvent&lt;/code&gt; type. After upgrading the SDK, you also need to check the required Go version; when upgrading from the old example this time, the Go toolchain and CI workflow both needed adjustment.&lt;/p&gt;

&lt;p&gt;This is a common but easily missed upgrade issue: being able to compile locally doesn't mean Cloud Build or GitHub Actions are still using the same version.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Issues When Deploying to Cloud Run
&lt;/h2&gt;

&lt;p&gt;This Demo is deployed on Google Cloud Run. The channel secret and channel access token are injected via Secret Manager and are not committed to the repository.&lt;/p&gt;

&lt;p&gt;To keep the example simple, current orders are stored in the program's memory. This brings two limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the instance restarts or scales to zero, orders will disappear.&lt;/li&gt;
&lt;li&gt;When multiple instances exist simultaneously, each instance will have a different order state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Therefore, the Demo environment sets the maximum instances to 1 to avoid webhooks for the same group buy being split. But this is only suitable for demonstration, not a complete solution for a production environment.&lt;/p&gt;

&lt;p&gt;Formal services should use Redis, Firestore, or other shared storage, and further handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Atomic updates and concurrency control&lt;/li&gt;
&lt;li&gt;Webhook idempotency&lt;/li&gt;
&lt;li&gt;Data retention periods&lt;/li&gt;
&lt;li&gt;Cross-system deletion after unsend&lt;/li&gt;
&lt;li&gt;Push Message failure retries&lt;/li&gt;
&lt;li&gt;The boundary between audit logs and privacy requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially for unsend, if message content has already been sent to search indices, analysis platforms, or other downstream services, deleting only the main database is incomplete. Data flow design should know where content went from the start to be able to truly complete a deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Run This Demo
&lt;/h2&gt;

&lt;p&gt;First, please enable webhooks in the LINE Developers Console and allow the Bot to join group chats. Point the webhook URL to the &lt;code&gt;/callback&lt;/code&gt; of your deployed service.&lt;/p&gt;

&lt;p&gt;Then, enter the following in the group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start Group Buy 50 Lan 15:20 Deadline
Pearl Milk Tea / Half Sugar / Less Ice / 1
Current Orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then directly edit the second message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pearl Milk Tea / Micro Sugar / No Ice / 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see the Bot reply "Order successfully transformed," and the quantity and total amount in the Flex Message will also update.&lt;/p&gt;

&lt;p&gt;Finally, unsend that order message, and the Bot will delete the content and show a new order summary.&lt;/p&gt;

&lt;p&gt;The menu and prices in the example are fixed Demo data to make amount changes clearly visible. For complete startup instructions, environment variables, and test commands, please refer to the README in the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kkdai/linebot-edit-unsend" rel="noopener noreferrer"&gt;https://github.com/kkdai/linebot-edit-unsend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Else Can This Be Applied To?
&lt;/h2&gt;

&lt;p&gt;Group buying is just one easy-to-understand story. The same event model can be extended to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Itinerary Bots: Update the itinerary when the meeting time is edited, and cancel the activity when the message is unsent.&lt;/li&gt;
&lt;li&gt;Task Bots: Edit the assignee or deadline, and remove the task when the message is unsent.&lt;/li&gt;
&lt;li&gt;Reservation Bots: Modify the time slot or number of people, and cancel the reservation when the message is unsent.&lt;/li&gt;
&lt;li&gt;Announcement Bots: Update announcement content, and synchronize the removal of copies on other channels after unsend.&lt;/li&gt;
&lt;li&gt;Interactive Story Bots: Rewrite the plot when a choice is edited, and return to the previous node when a message is unsent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as a Bot has ever converted "a message" into some kind of system state, it's worth re-checking: when the message is edited or unsent, should that state also change?&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Message editing looks like an improvement to the chat interface, but from a Bot developer's perspective, it actually changes the lifecycle of an event.&lt;/p&gt;

&lt;p&gt;A message is no longer just the moment it is "sent." It might be updated, or it might be withdrawn. Backend services need to understand these events, maintain the correct order, and respect the user's latest intent.&lt;/p&gt;

&lt;p&gt;This time, I used an afternoon tea group buy story to string together Edit event, Unsend event, Flex Message, and Cloud Run. I hope this small example helps everyone master the new features faster and start thinking about how their own LINE Bots should respond after a message is changed.&lt;/p&gt;

&lt;p&gt;Feel free to refer to the code, and I look forward to seeing everyone create more interesting applications:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kkdai/linebot-edit-unsend" rel="noopener noreferrer"&gt;https://github.com/kkdai/linebot-edit-unsend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linecorp.com/tw/pr/news/2026/0820/" rel="noopener noreferrer"&gt;LINE Premium "Edit Message" feature open for free trial in LINE Labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/messaging-api/#edit-event" rel="noopener noreferrer"&gt;LINE Messaging API reference: Edit event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.line.biz/en/reference/messaging-api/#unsend-event" rel="noopener noreferrer"&gt;LINE Messaging API reference: Unsend event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://techblog.lycorp.co.jp/zh-hant/linebot-mark-as-read" rel="noopener noreferrer"&gt;Introduction to New LINE Messaging API Features: Mark as Read API lets your chatbot mark messages as read&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>development</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Hearing the Mountain's Roar: How Antigravity CLI's AI Agents &amp; IoT Data Track Volcanic Shockwaves</title>
      <dc:creator>Tanaike</dc:creator>
      <pubDate>Wed, 02 Sep 2026 01:29:20 +0000</pubDate>
      <link>https://dev.to/gde/hearing-the-mountains-roar-how-antigravity-clis-ai-agents-iot-data-track-volcanic-shockwaves-13hp</link>
      <guid>https://dev.to/gde/hearing-the-mountains-roar-how-antigravity-clis-ai-agents-iot-data-track-volcanic-shockwaves-13hp</guid>
      <description>&lt;h2&gt;
  
  
  Turning 29k home weather stations and Gemini AI agents into a 15-minute volcanic warning shield
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published Paper&lt;/strong&gt;: &lt;a href="https://essopenarchive.org/doi/full/10.22541/essoar.15008116/v1" rel="noopener noreferrer"&gt;Capturing Volcanic Lamb Waves and Blast Yields with Netatmo Weather Networks (ESS Open Archive)&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Abstract
&lt;/h3&gt;

&lt;p&gt;Explosive volcanic eruptions generate powerful atmospheric Lamb waves—acoustic-gravity waves trapped in the planetary boundary layer that travel horizontally across the globe at sound speed. Operating on a continuous background ingest where Google Apps Script (GAS) logs 20-minute weather records to Google Drive, we mapped two benchmark historical eruptions: the 2018 phreatic blast at Mt. Kusatsu-Shirane and the 2018 magmatic explosion at Mt. Shinmoedake. We framed these events within first-principles continuum mechanics inside the &lt;strong&gt;Antigravity CLI&lt;/strong&gt; environment powered by &lt;strong&gt;Gemini&lt;/strong&gt; and the autonomous R&amp;amp;D framework &lt;strong&gt;&lt;code&gt;tanaike-lab&lt;/code&gt;&lt;/strong&gt;. Across 29,334 authentic Netatmo observation records in Japan, our framework reconstructs temperature-dependent shockwave velocities (304.38 m/s in sub-zero winter air and 313.27 m/s in mild spring conditions, matching theoretical sound speeds within 98.5% precision), localizes unmonitored volcanic crater azimuths to within 1.78 degrees, quantifies explosive yields (178.8 to 1,041.1 tons of TNT equivalent), and delivers &lt;strong&gt;2.5 to 15 minutes of advance early warning&lt;/strong&gt; with &lt;strong&gt;100% false alarm elimination (0.0% False Positive Rate)&lt;/strong&gt; during violent storm baselines. These findings demonstrate how grounding generative AI in first-principles physics yields robust planetary sensing systems.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Introduction
&lt;/h3&gt;

&lt;p&gt;Conventional volcanic monitoring infrastructures (such as specialized infrasound microphones and seismographs operated by government agencies and global nuclear monitoring treaties) operate under a severe spatial limitation: observation stations are separated by 100 to 500 kilometers. This extreme spatial sparsity creates massive blind zones, suffers from severe spatial aliasing, and leaves isolated sensors vulnerable to wind turbulence.&lt;/p&gt;

&lt;p&gt;To overcome the spatial aliasing of sparse government arrays, we turn to crowdsourced consumer IoT barometers. By treating thousands of residential Netatmo stations across Japan as a continent-scale acoustic antenna (totaling &lt;strong&gt;29,334 authentic observation records&lt;/strong&gt;), we demonstrate that balcony-mounted pressure sensors can provide &lt;strong&gt;2.5 to 15 minutes of advance warning&lt;/strong&gt; before volcanic shockwaves and ash strike downstream urban centers.&lt;/p&gt;

&lt;p&gt;This research project advanced through an integrated, four-stage research workflow: combining background cloud archiving in Google Drive via Google Apps Script, historical event mapping across past volcanic eruptions, deductive physical theory construction, and Gemini AI multi-agent pattern mining inside the &lt;strong&gt;Antigravity CLI&lt;/strong&gt; terminal environment guided by the AI Co-Researcher framework &lt;strong&gt;&lt;code&gt;tanaike-lab&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;【Academic Note】&lt;/strong&gt;: The physical models, data interpretations, inferences, and precursor warning conclusions presented in this paper represent &lt;strong&gt;one of many academic perspectives and methodologies&lt;/strong&gt; regarding volcanic infrasound and atmospheric wave dynamics within the broad geophysics community. Given the inherent complexity of volcanic processes, these findings contribute a novel framework to the ongoing scientific discourse, inviting further empirical validation and community dialogue.&lt;/p&gt;

&lt;p&gt;In this article, written for AI practitioners, software architects, and technology leaders, we explore how human-AI collaboration on Antigravity CLI turned ultra-dense IoT sensor data into a proactive planetary defense platform.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. The Automated Big-Data Pipeline &amp;amp; Research Project Workflow
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmt875nofhbde7c5gbrw6.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmt875nofhbde7c5gbrw6.jpg" alt="Volcanic-Lamb-IoT Framework: Turning Cities into a Giant Planetary Acoustic Antenna" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 1: Executive Infographic of the Volcanic-Lamb-IoT Framework illustrating an explosive volcanic eruption generating 2D cylindrical Lamb waves traveling horizontally along the Earth's surface at 1,134 km/h (speed of sound), intercepted by thousands of residential IoT home weather stations across urban centers, and processed in real time by cloud analytics to compute blast yield in Tons of TNT and deliver a 2.5 to 15-minute early warning countdown.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The operational foundation of this research rests on a continuous, fully automated cloud ingestion system running in the background.&lt;/p&gt;

&lt;p&gt;Using &lt;strong&gt;Google Apps Script (GAS)&lt;/strong&gt;, an automated serverless cron trigger executes every 20 minutes, calling the &lt;strong&gt;Netatmo Connect API&lt;/strong&gt; to fetch multi-parametric meteorological records (Barometric Pressure, Outdoor Temperature, and Relative Humidity) from hundreds of residential IoT stations distributed across Japanese metropolitan areas and mountain foothills. These incoming data streams are continuously indexed, structured, and archived directly into a centralized &lt;strong&gt;Google Drive repository&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0hoipbifks5cm5rwwxa.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv0hoipbifks5cm5rwwxa.jpg" alt="Automated IoT Big-Data Pipeline and Volcanic Research Project Workflow on Antigravity CLI" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 2: Automated IoT Big-Data Pipeline and Volcanic Research Project Workflow on Antigravity CLI showing: (1) Automated Cloud Ingestion via Google Apps Script (GAS) every 20 minutes into Google Drive; (2) Historical Event Mapping extracting 24-hour observation windows (+/- 12 hours) across past volcanic eruptions; (3) Human Physical Theory &amp;amp; Logic Guidance injecting thermodynamic and wave mechanics constraints; and (4) Antigravity CLI Powered by Gemini AI where the tanaike-lab multi-agent matrix executes in a terminal CLI workspace to discover hidden precursor patterns and compute shockwave velocities with 0.0% false alarms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Under this continuous data accumulation setup, our research project advanced through a structured 4-stage workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1: Automated Live Ingestion&lt;/strong&gt;: Google Apps Script operates as an autonomous background ingest engine, polling the Netatmo API every 20 minutes to maintain an uninterrupted, longitudinal meteorological archive in Google Drive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2: Historical Event Mapping&lt;/strong&gt;: We mapped past historical volcanic eruptions against this vast archive, extracting targeted 24-hour observation windows (from 12 hours before onset to 12 hours after: $T_0 - 12\text{ h} \to T_0 + 12\text{ h}$) for both the 2018 Mt. Kusatsu-Shirane steam blast and the 2018 Mt. Shinmoedake magmatic eruption, as well as baseline calm and stormy control days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3: Pre- and Post-Eruption Physical Anomaly Investigation&lt;/strong&gt;: By analyzing atmospheric time series before and after the blasts, we investigated whether subtle hydrothermal degassing anomalies emerge prior to rupture and how explosive atmospheric shockwaves propagate across urban centers after eruption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4: Theoretical Modeling &amp;amp; Validation on Antigravity CLI&lt;/strong&gt;: We constructed unified mathematical models (Bolton enthalpy, 2D compressible Lamb wave equations, slowness beamforming, and cylindrical energy flux integration) and supplied them to Gemini AI running inside Antigravity CLI to verify that volcanic early warnings and crater diagnostics operate with zero false alarms.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Accessible Atmospheric Physics: 4 Intuitive Everyday Analogies
&lt;/h3&gt;

&lt;p&gt;How does an uncalibrated $150 weather gadget on an apartment balcony capture a volcanic explosion hundreds of kilometers away? The underlying physics can be understood through four intuitive real-world analogies:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybq5889nwnl5yv30ncoo.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybq5889nwnl5yv30ncoo.jpg" alt="Understanding Volcanic Shockwaves: 4 Intuitive Everyday Analogies" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 3: Understanding Volcanic Shockwaves: 4 Intuitive Everyday Analogies. Panel 1 shows Lightning vs. Thunder (lightspeed cloud data outrunning 1,134 km/h sound waves to give early warning). Panel 2 shows Ripples in a Pond (2D cylindrical wave trapping along the Earth's surface). Panel 3 shows 150km Binaural Hearing (home weather sensors working like ears to pinpoint crater azimuth). Panel 4 shows the Speed Gate &amp;amp; Chorus Test (filtering out messy wind turbulence).&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  1. "Lightning vs. Thunder" — The Race Between Speed of Light and Speed of Sound
&lt;/h4&gt;

&lt;p&gt;When lightning strikes in the distance, you see the brilliant flash instantly, but the rumble of thunder arrives many seconds later. That is because light travels at 300,000 km/s, while sound crawls through the air at roughly 340 m/s.&lt;/p&gt;

&lt;p&gt;Our IoT early warning framework exploits this fundamental physical gap. When a volcano erupts, the atmospheric shockwave travels horizontally at acoustic sound speed (approximately 315 m/s, or 18.9 km per minute). Meanwhile, IoT weather stations sitting in the near-field (10 to 30 km from the crater) detect the initial pressure pulse within 30 to 90 seconds and beam their digital telemetry to the cloud at the speed of light.&lt;/p&gt;

&lt;p&gt;A perceptive question naturally arises: &lt;em&gt;If individual IoT stations report on 20-minute batch intervals, how is a 2.5 to 15-minute advance lead time operationally possible?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer lies in two interlocking physical and network principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Acoustic Travel-Time Horizon Across Distance&lt;/strong&gt;: Atmospheric shockwaves require finite physical time to propagate across geography. For downstream infrastructure located 50 to 300 km away from the crater, the acoustic travel time creates an unavoidable physical delay: approximately 2.6 minutes for mountain communities at 50 km, 5.3 minutes at 100 km, 9.5 minutes for Tokyo at 180 km (from Mt. Kusatsu-Shirane), and 14.8 minutes (nearly 15 minutes) for the Fukuoka transit corridor at 280 km (from Mt. Shinmoedake). Because near-crater sensors transmit their detections to cloud servers in under a minute, downstream cities receive their alerts well before the physical wave arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous Temporal Staggering Across Dense Spatial Clusters&lt;/strong&gt;: In crowdsourced networks, individual home weather stations do not report in synchronized lockstep; their 20-minute transmission cycles are asynchronously distributed across time. Across a dense regional cluster of 100 stations, an updated packet reaches the cloud every 12 seconds on average ($\Delta t_{  ext{effective}} pprox 1,200    ext{ s} / 100 = 12  ext{ s}$). This dense staggering transforms discrete 20-minute station updates into a continuous, high-frequency spatial tracking stream, while modern edge-triggered webhooks can stream instantaneous sub-second bursts upon detecting steep pressure gradients ($\Delta P / \Delta t$).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. "Ripples in a Pond" — 2D Cylindrical Wave Trapping
&lt;/h4&gt;

&lt;p&gt;Unconfined atmospheric explosions radiate energy spherically in three dimensions. As the wavefront expands, acoustic energy thins out rapidly, dissipating with the square of the distance ($\propto 1/r^2$).&lt;/p&gt;

&lt;p&gt;Volcanic Lamb waves behave differently. Trapped against the Earth's surface by gravity and stratified air density, they propagate as a two-dimensional cylindrical ring. Because acoustic energy cannot escape vertically, the pressure pulse decays much more slowly ($\propto 1/\sqrt{r}$). This acoustic waveguide effect preserves signal integrity across hundreds of kilometers, allowing uncalibrated balcony barometers in downtown Tokyo to record sharp micro-barometric pulses from craters 180 km away.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. "Binaural Hearing Across 150 Kilometers" — Slowness Beamforming
&lt;/h4&gt;

&lt;p&gt;Humans can close their eyes and instantly point toward where a sound came from because the brain calculates the microsecond difference in arrival time between the left and right ears.&lt;/p&gt;

&lt;p&gt;Our system applies this exact principle across an entire region. By measuring millisecond arrival time differences across 25 to 88 IoT stations spaced over a 150-kilometer baseline, the slowness delay-and-sum beamforming algorithm overlays recorded waveforms across all 360 degrees of azimuth. The exact angle where all waveforms constructively reinforce each other pinpoints the volcanic crater with sub-degree accuracy, without requiring prior knowledge of the crater coordinates.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. "The Speed Gate &amp;amp; The Chorus Test" — Silencing Violent Storm Gusts
&lt;/h4&gt;

&lt;p&gt;A critical instrumentation challenge immediately emerges: local wind turbulence. Under Bernoulli's principle, wind gusts blowing across an open balcony create sharp localized pressure drops. On stormy days, a single station can record hundreds of false pressure spikes.&lt;/p&gt;

&lt;p&gt;To eliminate false alarms, &lt;code&gt;tanaike-lab&lt;/code&gt; established two physical hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Speed Gate&lt;/strong&gt;: Meteorological wind gusts travel with the weather front at 10 to 25 m/s (36 to 90 km/h), whereas authentic volcanic Lamb waves travel at sound speed (300 to 330 m/s, or ~1,134 km/h). Any signal moving slower than sound is instantly rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Regional Chorus Test&lt;/strong&gt;: A wind gust is a localized, uncorrelated event hitting one home at a time. A volcanic shockwave is a massive orchestral wavefront that strikes dozens of homes across an entire prefecture in geometric cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By requiring multi-station phase coherence (Semblance greater than or equal to 0.70), all 378 storm gust spikes recorded during severe gales were filtered out, achieving &lt;strong&gt;100% false alarm elimination (0.0% False Positive Rate)&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Data Processing Pipeline &amp;amp; The Philosophy of AI Pattern Discovery
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75rl9r217wvrk85d35o2.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75rl9r217wvrk85d35o2.jpg" alt="Multi-Parametric IoT Data Processing Pipeline: From Balcony Sensors to Planetary Intelligence" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 4: Multi-Parametric IoT Data Processing Pipeline showing the 5-step transition from residential weather measurements to planetary intelligence: (1) Raw IoT Weather Ingestion (Pressure, Temp, Humidity), (2) Zero-Phase Waveform Filtering (removing tidal drift), (3) Sound Speed &amp;amp; Temperature Inversion (reconstructing 304-313 m/s velocities), (4) Binaural Beamforming (isolating crater bearing within 1.78 degrees), and (5) Blast Energy &amp;amp; Ash Shadow (computing TNT explosive tons and mapping ash cooling).&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Core Philosophy: Guiding AI's Pattern Discovery with Physical Logic
&lt;/h4&gt;

&lt;p&gt;Generative AI excels at high-dimensional pattern recognition. Yet, when applied blindly to noisy meteorological time series, unconstrained models risk latching onto spurious correlations and generating ungrounded artifacts.&lt;/p&gt;

&lt;p&gt;Transforming raw AI output into verifiable scientific discovery requires a hybrid approach: coupling pattern-mining agents directly with &lt;strong&gt;first-principles theoretical frameworks and domain logic&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Guiding Paradigm&lt;/strong&gt;: When we supply Generative AI not only with raw numbers, but with the exact physical laws, thermodynamic equations, and geometric constraints governing the phenomenon, we provide the AI with a structured cognitive lens. This dramatically magnifies the probability of discovering essential physical patterns while filtering out spurious noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Unified Methodology Across Planetary Disasters&lt;/strong&gt;: This philosophy is the common intellectual thread uniting all our published disaster prediction breakthroughs:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;strong&gt;Earthquake Prediction (Unified LAIC-AGW Theory)&lt;/strong&gt;, we supplied AI with lithosphere-atmosphere coupling equations and seismic moment tensors to discover pre-seismic radon ionization and thermodynamic enthalpy spikes ($\delta \theta_e = 5.96\text{--}15.47\text{ K}$) hours before fault rupture.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Urban Torrential Rain Forecasting (LFD-TRP Framework)&lt;/strong&gt;, we supplied AI with 3D Navier-Stokes momentum and thermodynamic helicity equations to capture localized convective initiation 45 minutes in advance.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Volcanic Early Warning (Volcanic-Lamb-IoT Framework)&lt;/strong&gt;, we supplied AI with 2D compressible Lamb wave equations, Bolton steam enthalpy, and slowness beamforming constraints to isolate volcanic shockwave patterns and crater bearings from 29,334 noisy IoT records.&lt;/p&gt;

&lt;h4&gt;
  
  
  The 6-Stage Multi-Parametric Pipeline
&lt;/h4&gt;

&lt;p&gt;Driven by this human-guided AI paradigm, our processing pipeline executes an end-to-end derivation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-Eruption Hydrothermal Enthalpy Tracking&lt;/strong&gt;: Outdoor temperature, humidity, and barometric pressure are converted into Equivalent Potential Temperature using Bolton's formulation. Subtracting regional baseline trends reveals subtle thermal-steam anomalies (plus 1.5 to 3.2 Kelvin) downwind of the volcano hours before rupture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Phase Digital Filtering&lt;/strong&gt;: Continuous barometric pressure series are conditioned using bidirectional zero-phase Butterworth bandpass filters (0.0005 to 0.02 Hz), removing 24-hour diurnal tidal pressure cycles while preserving exact wave arrival timestamps without phase distortion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature-Dependent Travel-Time Inversion&lt;/strong&gt;: Rather than assuming a constant speed of sound, the model performs line-integrals over local temperature fields ($c = \sqrt{\gamma R T}$), accounting for sound speed deceleration across freezing mountain plateaus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slowness Delay-and-Sum Beamforming&lt;/strong&gt;: Waveforms from regional sub-arrays are shifted and stacked across a 360-degree slowness grid to compute semblance coherence, isolating back-azimuth bearings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cylindrical Acoustic Energy Flux Integration&lt;/strong&gt;: Integrating pressure variance over the 2D cylindrical surface and scaling by atmospheric scale height (approximately 8.4 km) quantifies total acoustic energy and converts it to TNT explosive yields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volcanic Ash Shadow Cooling Tracking&lt;/strong&gt;: Dense suburban temperature sensors detect direct solar radiation shielding under drifting ash clouds (causing local temperature drops of minus 4.10 to minus 6.13 degrees Celsius).&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  5. Empirical Results Across 29,334 Authentic Netatmo Records
&lt;/h3&gt;

&lt;p&gt;Our study validated the Volcanic-Lamb-IoT Framework using &lt;strong&gt;29,334 authentic 20-minute observation records&lt;/strong&gt; across two major volcanic eruptions and two severe weather control events:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event &amp;amp; Scenario&lt;/th&gt;
&lt;th&gt;Analysis Window ($\pm 12\text{ h}$)&lt;/th&gt;
&lt;th&gt;Station Count &amp;amp; Region&lt;/th&gt;
&lt;th&gt;Inverted Wave Speed ($c_{\text{app}}$)&lt;/th&gt;
&lt;th&gt;Crater Azimuth Error&lt;/th&gt;
&lt;th&gt;Acoustic Energy Yield ($E_{\text{acoustic}}$)&lt;/th&gt;
&lt;th&gt;Ash Shadow Cooling ($\Delta T$)&lt;/th&gt;
&lt;th&gt;False Positive Rate (FPR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;① Mt. Kusatsu-Shirane (Phreatic Blast)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jan 22 22:00 – Jan 23 22:00 JST&lt;/td&gt;
&lt;td&gt;88 stations (Kanto / Chubu)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;304.38 $\pm$ 4.73 m/s&lt;/strong&gt; ($R^2=0.980$)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.10$^\circ$&lt;/strong&gt; ($S_c=0.88$)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;7.48 $\times 10^{11}$ J&lt;/strong&gt; (178.8 t TNT)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$-$6.13 $^\circ$C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% (Detected)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;② Mt. Shinmoedake (Magmatic Blast)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mar 06 02:00 – Mar 07 03:00 JST&lt;/td&gt;
&lt;td&gt;59 stations (Kyushu / Fukuoka)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;313.27 $\pm$ 4.20 m/s&lt;/strong&gt; ($R^2=0.990$)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.78$^\circ$&lt;/strong&gt; ($S_c=0.90$)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4.36 $\times 10^{12}$ J&lt;/strong&gt; (1,041.1 t TNT)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$-$4.10 $^\circ$C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% (Detected)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;③ Winter Quiet Baseline (Control 1)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jan 15 00:00 – Jan 15 23:59 JST&lt;/td&gt;
&lt;td&gt;229 stations (Tokyo &amp;amp; Kyushu)&lt;/td&gt;
&lt;td&gt;No coherent wave detected&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Background noise&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% (Zero Alert)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;④ Spring Storm Baseline (Control 2)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mar 01 00:00 – Mar 01 23:59 JST&lt;/td&gt;
&lt;td&gt;146 stations (Tokyo &amp;amp; Kyushu)&lt;/td&gt;
&lt;td&gt;Non-coherent wind turbulence&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Background noise&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0% (Zero Alert)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwei4k3shv0rwfx461kx.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwei4k3shv0rwfx461kx.jpg" alt="Empirical Breakthroughs: Freezing Steam Blast vs. Magmatic Blast and Crater Pinpointing" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 5: Empirical Breakthroughs: Freezing Steam Blast vs. Magmatic Blast. Panel A illustrates Mt. Kusatsu-Shirane's phreatic explosion generating 304 m/s shockwaves in sub-zero winter air (-5 deg C) with 178 Tons of TNT energy. Panel B illustrates Mt. Shinmoedake's magmatic explosion generating 313 m/s shockwaves in 12 deg C spring air with 1,041 Tons of TNT energy, while residential IoT beamforming pinpoints the crater bearing within 1.78 degrees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As shown in Figure 5 and Table 1, our framework achieved decisive empirical breakthroughs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolving Temperature-Dependent Wave Speeds&lt;/strong&gt;: During the Mt. Kusatsu-Shirane phreatic event, the observed shockwave propagated at $304.38 \pm 4.73\text{ m/s}$. While standard sea-level sound speed is approximately $340\text{ m/s}$, sound slows in cold, dense air according to $c = \sqrt{\gamma R T}$. On that sub-zero January morning ($-5^\circ\text{C}$ across the Gunma highlands), theoretical acoustic velocity drops to $304.8\text{ m/s}$. Our uncalibrated IoT array recovered this local thermal deceleration with 99.8% empirical accuracy ($R^2 = 0.980$).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinpointing Unknown Craters from 150 km Away&lt;/strong&gt;: At Mt. Shinmoedake, beamforming across Kyushu stations localized the crater bearing to 169.49 degrees (true bearing 167.72 degrees), an error of &lt;strong&gt;just 1.78 degrees&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantifying Steam vs. Magma Blast Yields&lt;/strong&gt;: Total radiated acoustic energy was 7.48 x 10^11 Joules (&lt;strong&gt;178.8 tons TNT equivalent&lt;/strong&gt;) for the Kusatsu-Shirane steam blast, and 4.36 x 10^12 Joules (&lt;strong&gt;1,041.1 tons TNT equivalent&lt;/strong&gt;) for the Shinmoedake magmatic blast—demonstrating that magmatic explosions release roughly 6 times more atmospheric energy than phreatic events.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsyesupgurs88tuqeq5c.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjsyesupgurs88tuqeq5c.jpg" alt="Speed Gate and Regional Chorus Test for 100% False Alarm Elimination" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 6: The Speed Gate and Regional Chorus Test: 100% False Alarm Elimination. Illustrates the dual-filtering mechanics: (1) The Speed Gate rejects slow 10-25 m/s wind gusts while accepting 300-330 m/s volcanic shockwaves, and (2) The Regional Chorus Test filters out isolated single-house wind noise while verifying coherent multi-station wavefronts (Semblance &amp;gt; 0.70), guaranteeing zero false alarms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As illustrated in Figure 6, the Popperian falsification audit across 15,000+ control records and 378 storm gust spikes confirmed a &lt;strong&gt;0.0% false positive rate&lt;/strong&gt;, verifying that the network avoids false alarms during severe atmospheric disturbances.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Actionable Infrastructure Mitigation: 3 Real-World Applications
&lt;/h3&gt;

&lt;p&gt;Transforming residential weather stations into real-time geophysical intelligence unlocks three transformative societal protection capabilities:&lt;/p&gt;

&lt;h4&gt;
  
  
  Application 1: Aviation Safety &amp;amp; Urban Infrastructure Early Warning
&lt;/h4&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fie8pa92srvtaxkpuk0hw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fie8pa92srvtaxkpuk0hw.png" alt="Aviation Early Warning and Urban Infrastructure Protection Network" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 7: Application 1: Aviation Safety and Urban Infrastructure Protection Network illustrating automated aircraft plume rerouting, subway intake louvers sealing, hospital HEPA filter cycling, and high-speed rail pre-deceleration triggered 2.5 to 15 minutes before shockwave arrival.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Volcanic ash melts inside jet turbine engines (above 1,400 degrees Celsius), turning into liquid glass and causing total engine failure. Our IoT network estimates explosive yield and crater coordinates within seconds, sending immediate alerts to Air Traffic Control (ATC) and Flight Management Systems (FMS) to dynamically reroute aircraft away from ascending plumes. Downwind cities receive a 5 to 15-minute countdown to automatically seal subway ventilation shafts, switch hospital HVAC systems to internal HEPA filtration, and pause outdoor construction.&lt;/p&gt;

&lt;h4&gt;
  
  
  Application 2: Volcanic Tsunami Early Detection &amp;amp; Metropolitan Shield
&lt;/h4&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiij0bamkqz8qzrttfls.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiij0bamkqz8qzrttfls.png" alt="Volcanic Tsunami Early Detection and Multi-Hazard Urban Shield" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 8: Application 2: Volcanic Tsunami Early Detection and Metropolitan Shield illustrating coastal IoT barometers capturing atmospheric shockwaves outrunning deep-sea tsunamis, delivering 15 to 45 minutes of advance coastal evacuation lead time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When submarine calderas explode or island flanks collapse (e.g., Krakatoa in 1883, Hunga Tonga in 2022), they generate deadly tsunamis. While deep-ocean tsunamis travel at roughly 200 m/s (slowing to 30–50 m/s in shallow coastal waters), atmospheric Lamb waves travel at 315 m/s—outrunning the ocean wave. Coastal IoT barometers detecting the atmospheric shockwave deliver &lt;strong&gt;15 to 45 minutes of advance tsunami evacuation lead time&lt;/strong&gt; before the physical water arrives, triggering automated seawall gate closures and coastal rail halts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Application 3: Blind Crater Localization for Dormant &amp;amp; Unmonitored Volcanoes
&lt;/h4&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ozen72gemzmahrik5ig.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ozen72gemzmahrik5ig.png" alt="Blind Crater Localization and Precursor Back-Tracking for Dormant Volcanoes" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 9: Application 3: Blind Crater Localization for Dormant Volcanoes illustrating pre-eruption hydrothermal plume back-tracking via adjoint advection-diffusion and post-eruption multi-array cross-bearing triangulation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The greatest hazard in volcanology is the surprise eruption from dormant mountains or unmonitored flank fissures where no government instruments exist. In our framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-Eruption Hydrothermal Tracking&lt;/strong&gt;: Prior to rupture, escaping steam creates subtle localized equivalent potential temperature anomalies (plus 1.5 to 3.2 Kelvin). Solving the adjoint advection-diffusion equation tracks the plume back to its source, mapping the coordinates of impending hydrothermal degassing days in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-Eruption Multi-Array Triangulation&lt;/strong&gt;: When an uncataloged explosion occurs, independent regional IoT sub-arrays (e.g., Kanto, Nagoya, Kyushu) calculate cross-bearing azimuth lines (within 0.48 degrees error), pinpointing the active crater coordinates to within 840 meters within minutes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7. Inside the AI Co-Researcher Framework (&lt;code&gt;tanaike-lab&lt;/code&gt;) on Antigravity CLI
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjqz3y98zye8ysb9erlxm.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjqz3y98zye8ysb9erlxm.jpg" alt="Autonomous Virtual R&amp;amp;D Laboratory Workflow on Antigravity CLI" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Figure 10: Autonomous Virtual R&amp;amp;D Laboratory Workflow on Antigravity CLI powered by Gemini AI and &lt;code&gt;tanaike-lab&lt;/code&gt;. Illustrates the Human PI providing high-level vision and initial research prompts, orchestrating the Data Ingestion Agent (29,334 authentic records), Modeling &amp;amp; Simulation Agent (2D wave mechanics), Automated Code Developer (Python scripts with assertion hooks), Popperian Falsification Agent, and Automated Manuscript Peer Review Panel.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Executing complex continuum mechanics equations, ingesting 29,334 authentic IoT records, running Popperian falsification checks, and preparing publication-ready LaTeX manuscripts was accomplished using &lt;strong&gt;Antigravity CLI&lt;/strong&gt; powered by &lt;strong&gt;Gemini&lt;/strong&gt; and the auxiliary R&amp;amp;D framework &lt;strong&gt;&lt;code&gt;tanaike-lab&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. How the Project Was Started, Executed, and Completed on Antigravity CLI
&lt;/h4&gt;

&lt;p&gt;The end-to-end execution of this research on Antigravity CLI followed a structured, 5-phase lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Human Strategic Ignition &amp;amp; Comprehensive Initial Prompting&lt;/strong&gt;: Rather than initiating the study with an open-ended prompt, the human PI provided Antigravity CLI with a mathematically constrained research charter. This prompt established six non-negotiable architectural requirements: (i) construct a micro-barometric Lamb wave detection framework using citizen-science IoT time series; (ii) require 24-hour observation windows ($\pm 12\text{ h}$) across the 2018 Kusatsu-Shirane and Shinmoedake eruptions, backed by storm baselines to test falsifiability; (iii) apply Bolton's equivalent potential temperature ($\theta_e$) to isolate pre-eruptive hydrothermal heat fluxes; (iv) enforce bidirectional zero-phase bandpass filtering to preserve exact wavefront phase timestamps; (v) invert propagation velocities using spatially varying temperature fields; and (vi) implement slowness delay-and-sum beamforming to calculate crater azimuths and blast yields directly from multi-station arrival differentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous Plan Audit &amp;amp; Mathematical Dry-Runs&lt;/strong&gt;: Specialized subagents within &lt;code&gt;tanaike-lab&lt;/code&gt; (&lt;code&gt;plan_audit_dryrun_agent&lt;/code&gt; and &lt;code&gt;theoretical_feasibility_evaluator&lt;/code&gt;) audited the research plan, verifying mathematical consistency, validating the 2D cylindrical energy flux integration model, and formulating the temperature path-integral equation to account for mountain topography.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Pipeline Development &amp;amp; Popperian Assertion Hooks&lt;/strong&gt;: The code development subagent (&lt;code&gt;experiment_code_developer&lt;/code&gt;) constructed isolated Python processing pipelines inside the CLI environment. Automated assertion hooks were embedded into every script, ensuring that data ingestion from Google Drive archives via OAuth2, zero-phase bandpass filtering, and slowness beamforming executed with 100% authentic data and zero synthetic mock arrays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empirical Analysis &amp;amp; Agent-to-Agent (A2A) Peer Discussions&lt;/strong&gt;: Specialized auditing subagents (&lt;code&gt;experimental_results_auditor&lt;/code&gt;, &lt;code&gt;data_integrity_sentinel&lt;/code&gt;, &lt;code&gt;theory_discussion_reviewer&lt;/code&gt;) executed A2A discussions across the 29,334 records. They verified the 304.38 m/s freezing mountain sound speed match, evaluated crater back-azimuth accuracy, and confirmed 100% false alarm rejection across the 378 storm gust spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manuscript Peer Review &amp;amp; Publication Delivery&lt;/strong&gt;: The 5-axis simulated peer review panel, &lt;code&gt;style_compliance_officer&lt;/code&gt;, and &lt;code&gt;typesetting_linter&lt;/code&gt; performed pre-submission audits, ensuring 100% 1-to-1 in-text citation matching, compiling complete LaTeX manuscripts, and producing high-resolution, color-universal infographics before final Human PI authorization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  2. Real-Time Self-Crystallization &amp;amp; CLI Auto-Reinstallation Engine
&lt;/h4&gt;

&lt;p&gt;Unlike static agent scripts, &lt;code&gt;tanaike-lab&lt;/code&gt; features an active &lt;strong&gt;Real-Time Self-Crystallization &amp;amp; CLI Auto-Reinstallation Engine&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Directive Capture&lt;/strong&gt;: Human steering directives and verified execution lessons are captured on the fly during project runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SKILL.md Auto-Crystallization&lt;/strong&gt;: Lessons are written immediately into &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI Plugin Auto-Reinstallation&lt;/strong&gt;: An automated export engine exports and reinstalls the updated skill matrix directly into the local Antigravity CLI plugin directory on the fly, performing instant Git remote sync to keep the framework continuously evolving.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Academic and Systemic Positioning: The Human-AI Synergy Model
&lt;/h4&gt;

&lt;p&gt;Within modern technology R&amp;amp;D, &lt;code&gt;tanaike-lab&lt;/code&gt; is positioned as a &lt;strong&gt;"Human-Centric Dynamic Virtual R&amp;amp;D Laboratory OS"&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Positioning Against "AI Slop" vs. Human-AI Synergy&lt;/strong&gt;: Unsupervised AI generation frequently suffers from hallucinations and superficial text ("AI slop"). &lt;code&gt;tanaike-lab&lt;/code&gt; enforces a &lt;strong&gt;Human-AI Synergy Model&lt;/strong&gt;, where the human PI retains strategic direction while AI subagent matrices accelerate logical formulation, code execution, empirical auditing, and multi-axis peer reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positioning as a Cognitive Friction Eliminator&lt;/strong&gt;: Traditional scientific workflows consume massive cognitive bandwidth on operational friction—debugging scripts, interpolating non-uniform temporal grids, adjusting graphics for color universal design, and fixing LaTeX compilation errors. &lt;code&gt;tanaike-lab&lt;/code&gt; acts as a &lt;strong&gt;cognitive accelerator&lt;/strong&gt;, eliminating operational friction so human researchers can focus on high-level strategic reasoning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triple-Domain Architecture&lt;/strong&gt;: &lt;code&gt;tanaike-lab&lt;/code&gt; operates across three interconnected domain pillars: (1) &lt;strong&gt;Academic &amp;amp; Natural Sciences&lt;/strong&gt; (geophysical modeling, computational fluid dynamics, quantum chemistry, and material science), (2) &lt;strong&gt;Generative AI &amp;amp; LLM Engineering&lt;/strong&gt; (prompt architecture, RAG vector retrieval pipelines, Multi-Agent orchestration, token budget compression, and LLM benchmarks), and (3) &lt;strong&gt;Google Ecosystem&lt;/strong&gt; (Google Apps Script libraries, Google Workspace automation for Drive, Sheets, Docs, Gmail, Forms, and Google Cloud/Workspace APIs with quota limit management).&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  8. Summary
&lt;/h3&gt;

&lt;p&gt;The release of our manuscript &lt;a href="https://essopenarchive.org/doi/full/10.22541/essoar.15008116/v1" rel="noopener noreferrer"&gt;&lt;em&gt;Capturing Volcanic Lamb Waves and Blast Yields with Netatmo Weather Networks&lt;/em&gt;&lt;/a&gt; on ESS Open Archive marks a major advance in volcanology and planetary sensing. By proving that atmospheric Lamb waves and blast energies can be captured using crowdsourced IoT weather networks with 100% false alarm elimination (0.0% False Positive Rate), this research demonstrates how residential weather gadgets can be transformed into a continent-scale early warning shield.&lt;/p&gt;

&lt;p&gt;The analytical results, physical interpretations, and conclusions presented in this study represent &lt;strong&gt;one of many diverse scientific perspectives and theoretical approaches&lt;/strong&gt; within the evolving domain of volcanic physics. Continuous empirical validation and open community dialogue remain essential to building upon these findings.&lt;/p&gt;

&lt;p&gt;Beyond geophysics, the real-time self-evolving Human-AI synergy framework embodied by &lt;code&gt;tanaike-lab&lt;/code&gt; on Antigravity CLI provides a scalable blueprint for AI engineers and researchers across climate adaptation, LLM multi-agent engineering, and enterprise Google API automation. We invite the global geophysics, meteorology, generative AI, and smart-city engineering communities to read the full open-access paper on ESS Open Archive.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Three Gemma 4 Deployments on One T4G for Under $3: What the Runtime Changes, and What It Doesn't</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 01 Sep 2026 18:25:48 +0000</pubDate>
      <link>https://dev.to/gde/three-gemma-4-deployments-on-one-t4g-for-under-3-what-the-runtime-changes-and-what-it-doesnt-jo3</link>
      <guid>https://dev.to/gde/three-gemma-4-deployments-on-one-t4g-for-under-3-what-the-runtime-changes-and-what-it-doesnt-jo3</guid>
      <description>&lt;p&gt;This article provides a step by step comparison of three Gemma 4 deployments on a single AWS hosted GPU enabled system. A suite of Python MCP tools is built to simplify management of each deployment, and one benchmark harness is shared across all three so that the runtime is the only variable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole exercise cost under three dollars, and that is the part worth keeping. Nineteen instances and about four and a half instance-hours bought three serving sweeps, nine timed boots and a handful of A/B restarts. It also bought five wrong claims, each caught by measuring instead of reasoning. On hardware where a run is expensive, the cheapest of those five would have shipped with a caveat attached.&lt;/p&gt;

&lt;h4&gt;
  
  
  What is this project trying to Do?
&lt;/h4&gt;

&lt;p&gt;Three rigs in this monorepo serve &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; on an AWS G5g instance. One runs vLLM, one runs a pure JAX port, one runs PyTorch with transformers. The hardware is identical and only the runtime slot moves, so this should be the cleanest A/B available.&lt;/p&gt;

&lt;p&gt;For months it was not, because each rig measured itself with its own harness and quoted its own number. Three harnesses computing three statistics is not a comparison.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prerequisites
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with G-family quota in &lt;code&gt;us-east-1&lt;/code&gt;. Each &lt;code&gt;g5g.2xlarge&lt;/code&gt; is 8 vCPU, so 16 vCPU
of spot quota runs two at once.&lt;/li&gt;
&lt;li&gt;A subnet, a security group opening TCP 8000, and an instance profile carrying
&lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; plus read on the Hugging Face token secret.&lt;/li&gt;
&lt;li&gt;A Hugging Face token in Secrets Manager. It is fetched at boot into a root-only
&lt;code&gt;EnvironmentFile&lt;/code&gt; and never appears in user data.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;boto3&lt;/code&gt; and the standard credential chain. No AWS CLI shell-outs, no inbound SSH rule, and
no private key anywhere in the flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  AWS EC2 G5g
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g5g.2xlarge&lt;/code&gt; — 8 vCPU, 16 GiB host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host CPU&lt;/td&gt;
&lt;td&gt;AWS Graviton2, aarch64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;1x NVIDIA T4G, Turing, SM 7.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU memory&lt;/td&gt;
&lt;td&gt;15,360 MiB per &lt;code&gt;nvidia-smi&lt;/code&gt;; AWS lists 16,384 nominal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;G5g is the only family AWS ships that puts an NVIDIA GPU behind a Graviton host, which makes it the only place to get aarch64 and compute capability 7.5 together.&lt;/p&gt;

&lt;h4&gt;
  
  
  Gemma 4 E2B
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt; is the reference instruction-tuned release. It is 2B effective from about 5B total, and the split matters here: most of what is resident is a per-layer-embedding table that decode reads as a gather and never streams through a matmul.&lt;/p&gt;

&lt;p&gt;The dense checkpoint fits. 9.5 GiB of float16 weights go into 15,360 MiB of device memory with room for the KV cache, which at roughly 18 KiB per token is tens of megabytes at this context and never the binding constraint.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Three Runtimes
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;engine&lt;/th&gt;
&lt;th&gt;how it serves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;v0.27.2rc0, built from source for &lt;code&gt;sm_75&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;continuous batching, paged KV, prefix caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;this project's own port&lt;/td&gt;
&lt;td&gt;hand-written KV ring with a bucket ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AutoModelForCausalLM&lt;/code&gt; + transformers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;past_key_values&lt;/code&gt;, one request at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Turing has no bfloat16 datapath, so all three run float16. It has no fp8 either, which rules out the KV-cache tricks that work on newer parts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Check the Quotas
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;check_g5g_quotas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Quota | vCPUs |
| Running On-Demand G and VT instances (vCPU) | 16 |
| All G and VT Spot Instance Requests (vCPU) | 16 |

&lt;span class="sb"&gt;`g5g.2xlarge`&lt;/span&gt; needs 8 vCPUs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the constraint behind every launch below: two rigs in parallel, and no more.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Sweep Could Not See vLLM
&lt;/h4&gt;

&lt;p&gt;The sweep script read its throughput figure straight out of the response body:&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decode_tps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decode_tokens_per_second&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;usage.decode_tokens_per_second&lt;/code&gt; is a field our own servers invent. vLLM does not emit it, and neither does anything else, so the harness could not be pointed at the vLLM rig at all. The three-way comparison had never actually been run.&lt;/p&gt;

&lt;p&gt;Re-running a rig does not fix that. Only a common statistic does.&lt;/p&gt;

&lt;h4&gt;
  
  
  One Statistic, Three Servers
&lt;/h4&gt;

&lt;p&gt;Every OpenAI-compatible server streams, so the portable measurement is the gap between tokens on the wire.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sweep.py &lt;span class="nt"&gt;--help&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; decode-source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nt"&gt;--decode-source&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;auto,usage,stream,both&lt;span class="o"&gt;}&lt;/span&gt;
                        where the decode figure comes from&lt;span class="p"&gt;;&lt;/span&gt; see the module
                        docstring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stream&lt;/code&gt; path uses &lt;code&gt;vllm bench serve&lt;/code&gt;'s exact TPOT definition, &lt;code&gt;(latency - ttft) / (output_len - 1)&lt;/code&gt;, so a number from this harness is directly comparable to that tool's published figures. &lt;code&gt;auto&lt;/code&gt; probes the endpoint once and picks &lt;code&gt;both&lt;/code&gt; where the server emits its own gauge, &lt;code&gt;stream&lt;/code&gt; where it does not.&lt;/p&gt;

&lt;h4&gt;
  
  
  Is the Calibration Transferable?
&lt;/h4&gt;

&lt;p&gt;No, and that is worth a measurement rather than an assumption. Running &lt;code&gt;both&lt;/code&gt; measures each rig's offset between the two statistics.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rig&lt;/th&gt;
&lt;th&gt;server gauge&lt;/th&gt;
&lt;th&gt;client stream&lt;/th&gt;
&lt;th&gt;stream/gauge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;12.962&lt;/td&gt;
&lt;td&gt;12.687&lt;/td&gt;
&lt;td&gt;0.9799&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;10.814&lt;/td&gt;
&lt;td&gt;10.243&lt;/td&gt;
&lt;td&gt;0.9543&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two percent against 4.6 percent, on the same day and the same instance shape. Borrowing one rig's ratio to convert the other's number would inject a 2.6 percent error into a comparison whose smallest interesting gap is 24 percent. The cross-rig table below is therefore built from &lt;code&gt;stream&lt;/code&gt; throughout.&lt;/p&gt;

&lt;h4&gt;
  
  
  A Gauge Rounded to One Decimal
&lt;/h4&gt;

&lt;p&gt;The JAX server emitted its decode gauge with one decimal place:&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tpu_jax_decode_tokens_per_second{{model=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}} &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;METRICS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_tokens_per_second&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At about 13 tok/s, one decimal is 0.78 percent resolution. Every sweep that rig had produced showed all three repeats of a cell as byte-identical: 12.8, 12.8, 12.8. That is not reproducibility, it is the measurement floor. The rig had been used to argue about two percent effects it could not resolve.&lt;/p&gt;

&lt;p&gt;Two characters fixed it. The first run afterwards reads 12.962, where before it would have said 13.0.&lt;/p&gt;

&lt;h4&gt;
  
  
  Launch the Instance
&lt;/h4&gt;

&lt;p&gt;Capacity for the whole G5g family was exhausted across all four availability zones several times, so the launcher cycles them with a sixty second backoff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[12:51:45] round 5 us-east-1c: ❌ AWS InsufficientInstanceCapacity
[12:52:47] us-east-1a: ✅ Launching `i-02e79988a6cbeecbf` (g5g.2xlarge, spot, 1x T4G) in `us-east-1`.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The walkthrough from here follows the PyTorch rig on &lt;code&gt;i-02e79988a6cbeecbf&lt;/code&gt;; the other two run the same steps against their own instances. All three landed in &lt;code&gt;us-east-1a&lt;/code&gt; within hours of each other. Note that AWS names the other zones as available in every one of those errors — that text describes on-demand capacity and says nothing about spot.&lt;/p&gt;

&lt;h4&gt;
  
  
  Watch the Install
&lt;/h4&gt;

&lt;p&gt;Cloud-init installs the runtime and then backgrounds itself, so the progress tool reports cloud-init's own state as well as the install log. A dead bootstrap and a slow one must not render identically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;get_install_progress i-02e79988a6cbeecbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;INSTALL COMPLETE&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt; &lt;span class="s"&gt;cloud-init ---&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;done&lt;/span&gt;
&lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a wheel install, not a build. Across the three timed boots the install finishes a median 113.55 s after launch, against the hours the vLLM rig needs for a from-source build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Verify the GPU
&lt;/h4&gt;

&lt;p&gt;A config flag being accepted proves nothing, so the probe runs a real matmul on the device.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;verify_gpu_arch i-02e79988a6cbeecbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;NVIDIA&lt;/span&gt; &lt;span class="n"&gt;T4G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;7.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15360&lt;/span&gt; &lt;span class="n"&gt;MiB&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cu132&lt;/span&gt;
&lt;span class="n"&gt;arch_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_75&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_80&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_90&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_110&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sm_120&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;capability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;compute_dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;float16&lt;/span&gt;
&lt;span class="n"&gt;fp16&lt;/span&gt; &lt;span class="n"&gt;matmul&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="err"&gt;✅&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt; &lt;span class="n"&gt;reached&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;GPU&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;real&lt;/span&gt; &lt;span class="n"&gt;fp16&lt;/span&gt; &lt;span class="n"&gt;matmul&lt;/span&gt; &lt;span class="n"&gt;executed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DLAMI's torch carries &lt;code&gt;sm_75&lt;/code&gt;. Upstream PyPI aarch64 wheels do not, so a &lt;code&gt;pip install torch&lt;/code&gt; on this box would serve on CPU without saying so.&lt;/p&gt;

&lt;h4&gt;
  
  
  Deploy the Server
&lt;/h4&gt;

&lt;p&gt;The payload is the rig's own source, shipped over SSM as a gzipped tarball because user data caps at 16 KiB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deploy_torch_server i-02e79988a6cbeecbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;✅ Deployed 3 files (16 KiB base64) to &lt;span class="sb"&gt;`i-02e79988a6cbeecbf`&lt;/span&gt;.

Payload root: &lt;span class="sb"&gt;`/home/xbill/gemma4-dev/gpu-pytorch-g5g-2b`&lt;/span&gt;
Build id: &lt;span class="sb"&gt;`060a572aeb55`&lt;/span&gt; — verify_model_health checks the running server reports this.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Verify the Installation
&lt;/h4&gt;

&lt;p&gt;A non-empty reply is not evidence of health. One sibling was once measured answering &lt;code&gt;': ok: ok: ok…'&lt;/code&gt;, so the check reads the server's own degenerate-response counter either side of its probe, and compares the served build id against the local payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;verify_model_health i-02e79988a6cbeecbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;✅ health=200 tokens=5 reply='ok'
&lt;span class="p"&gt;
-&lt;/span&gt; Degenerate (server's own verdict on the full text): &lt;span class="gs"&gt;**no**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Build id served: &lt;span class="sb"&gt;`060a572aeb55`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Build id matches the local payload (&lt;span class="sb"&gt;`060a572aeb55`&lt;/span&gt;).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Run the Sweep
&lt;/h4&gt;

&lt;p&gt;The same command runs against all three rigs. Only the endpoint changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 sweep.py &lt;span class="nt"&gt;--base&lt;/span&gt; http://&amp;lt;ip&amp;gt;:8000/v1 &lt;span class="nt"&gt;--out&lt;/span&gt; benchmarks/runs/&amp;lt;run&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--contexts&lt;/span&gt; 64,512,1024,2048,3072,3800 &lt;span class="nt"&gt;--outputs&lt;/span&gt; 32,128 &lt;span class="nt"&gt;--repeats&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--decode-source&lt;/span&gt; both
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;decode-source: both -&amp;gt; both
ctx~512 out=32: in=633 out=32 decode=10.96 tok/s  e2e=9.83 tok/s (warmup 11.08)  stream/usage=0.9616
ctx~2048 out=128: in=2501 out=89 decode=10.66 tok/s  e2e=9.16 tok/s (warmup 10.61)  stream/usage=0.9540
ctx~3800 out=32: FAILED HTTP Error 400 {"detail":"prompt is 4630 tokens and the context
  bound is 4096, leaving no room to decode. Start the server with a larger --seq."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cells that cannot exist on the hardware are recorded as &lt;code&gt;infeasible&lt;/code&gt; rather than dropped. An absent cell is indistinguishable from an untried one, which is how a sweep overstates its own coverage.&lt;/p&gt;

&lt;h4&gt;
  
  
  Decode at Concurrency One
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;decode tok/s&lt;/th&gt;
&lt;th&gt;% of ceiling&lt;/th&gt;
&lt;th&gt;vs PyTorch&lt;/th&gt;
&lt;th&gt;cells&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;vLLM v0.27.2rc0&lt;/td&gt;
&lt;td&gt;32.53&lt;/td&gt;
&lt;td&gt;53.0%&lt;/td&gt;
&lt;td&gt;3.18x&lt;/td&gt;
&lt;td&gt;12/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;12.69&lt;/td&gt;
&lt;td&gt;20.7%&lt;/td&gt;
&lt;td&gt;1.24x&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;PyTorch + transformers&lt;/td&gt;
&lt;td&gt;10.24&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  How Close Is That to the Hardware?
&lt;/h4&gt;

&lt;p&gt;The ceiling is arithmetic, not a measurement. E2B streams 4.514 GB of weights per decode step against a measured 277 GB/s, giving 16.30 ms per step and &lt;strong&gt;61.4 tok/s&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The PLE table is excluded from that figure because it is a gather and never a matmul. Quantising it from 9.257 GB to 5.752 GB moved decode by 0.00 tok/s across three cells, which is what confirms it never streams.&lt;/p&gt;

&lt;p&gt;All three runtimes sit far below the ceiling, so &lt;strong&gt;none of them is bandwidth-bound at batch one&lt;/strong&gt;. The PyTorch profile shows why: about 5,650 kernel launches per step at one to three microseconds each, on a chip whose launch overhead is five to ten.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Number I Did Not Expect
&lt;/h4&gt;

&lt;p&gt;Time to first token was the result of the run, right up until it was checked.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;input tok&lt;/th&gt;
&lt;th&gt;vLLM&lt;/th&gt;
&lt;th&gt;JAX&lt;/th&gt;
&lt;th&gt;PyTorch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;103 ms&lt;/td&gt;
&lt;td&gt;225 ms&lt;/td&gt;
&lt;td&gt;164 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,259&lt;/td&gt;
&lt;td&gt;118 ms&lt;/td&gt;
&lt;td&gt;1,615 ms&lt;/td&gt;
&lt;td&gt;657 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3,746&lt;/td&gt;
&lt;td&gt;178 ms&lt;/td&gt;
&lt;td&gt;5,352 ms&lt;/td&gt;
&lt;td&gt;2,339 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 30x advantage, far larger than the 3.2x on decode. It is also impossible. Prefill at 3,746 tokens is roughly 14 TFLOP against a T4G's realistic 20 to 30 TFLOP/s, which is 460 ms at best. vLLM's row says 178 ms.&lt;/p&gt;

&lt;p&gt;It did not. vLLM ships &lt;code&gt;enable_prefix_caching=True&lt;/code&gt;, and its own metrics say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^vllm:prefix_cache_(queries|hits)_total"&lt;/span&gt; metrics.prom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;vllm:prefix_cache_queries_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mf"&gt;102898.0&lt;/span&gt;
&lt;span class="n"&gt;vllm:prefix_cache_hits_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;     &lt;span class="mf"&gt;97440.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A 94.7 percent hit rate.&lt;/strong&gt; vLLM genuinely prefilled 5.3 percent of the tokens it was sent, because the harness reused one prompt for a cell's warm-up and all three repeats. Neither sibling has a prefix cache, so both paid full prefill every time.&lt;/p&gt;

&lt;p&gt;The fix places a nonce first in the prompt, since a shared prefix is exactly what the cache keys on and a trailing nonce would not have defeated it. That property is now a unit test.&lt;/p&gt;

&lt;h4&gt;
  
  
  What the Prefill Data Does Support
&lt;/h4&gt;

&lt;p&gt;Strip the contaminated column and a real result remains. Neither of the other two runtimes caches prefixes, and both saw identical prompts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;TTFT slope&lt;/th&gt;
&lt;th&gt;at 3,746 tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;1.403 ms/token&lt;/td&gt;
&lt;td&gt;5,352 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;0.595 ms/token&lt;/td&gt;
&lt;td&gt;2,339 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;JAX prefills 2.4x slower than PyTorch&lt;/strong&gt;, consistently across all five shared context lengths. On an interactive workload with real context that dominates user-visible latency, and it runs in the opposite direction to the 1.24x decode advantage the same rig enjoys.&lt;/p&gt;

&lt;h4&gt;
  
  
  Boot Time Reverses the Ranking
&lt;/h4&gt;

&lt;p&gt;Nine cold boots, three per runtime, plus nine warm reloads. The start line is the moment &lt;code&gt;run_instances&lt;/code&gt; returns an id, because capacity wait measures AWS rather than the rig.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;cold boot&lt;/th&gt;
&lt;th&gt;spread&lt;/th&gt;
&lt;th&gt;warm reload&lt;/th&gt;
&lt;th&gt;cold/warm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;195.2 s&lt;/td&gt;
&lt;td&gt;11.8%&lt;/td&gt;
&lt;td&gt;24.5 s&lt;/td&gt;
&lt;td&gt;8.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;242.2 s&lt;/td&gt;
&lt;td&gt;11.5%&lt;/td&gt;
&lt;td&gt;74.1 s&lt;/td&gt;
&lt;td&gt;3.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;1417.8 s&lt;/td&gt;
&lt;td&gt;12.6%&lt;/td&gt;
&lt;td&gt;264.3 s&lt;/td&gt;
&lt;td&gt;5.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;vLLM takes 23m 38s to serve, from a prebuilt AMI that downloads nothing.&lt;/strong&gt; PyTorch installs its runtime from wheels and pulls the 9.54 GiB checkpoint over the network, and is still 7.3x faster.&lt;/p&gt;

&lt;p&gt;Boot variance is 11.5, 11.8 and 12.6 percent — too consistent across three different runtimes to be a property of any of them. This family's decode noise floor is 1.7 percent, measured by running an identical build on two hosts, so boot is about seven times noisier and a single boot measurement is close to worthless.&lt;/p&gt;

&lt;h4&gt;
  
  
  Is Health 200 the Same as Ready?
&lt;/h4&gt;

&lt;p&gt;Not on every runtime, which is why the harness records two stop lines.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;first completion, cold&lt;/th&gt;
&lt;th&gt;warm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;td&gt;0.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;1.0 s&lt;/td&gt;
&lt;td&gt;0.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;22.9 s&lt;/td&gt;
&lt;td&gt;9.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JAX returns health 200 and then compiles XLA per shape bucket on the first real request. Quoting health alone understates its time to serving by 22 seconds, and the cost does not vanish when warm. vLLM is the mirror image: slowest to boot, fastest first token, because graph capture is paid before the port binds.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Does a Code Change Cost?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;runtime&lt;/th&gt;
&lt;th&gt;to change serving code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PyTorch&lt;/td&gt;
&lt;td&gt;ship 3 files over SSM, restart — 25 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAX&lt;/td&gt;
&lt;td&gt;same mechanism, plus 9.2 s of compile — 83 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM&lt;/td&gt;
&lt;td&gt;no deploy path exists: rebuild from source, ~67 min, and reapply an out-of-tree Turing patch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vLLM's 264 s warm figure is a &lt;code&gt;systemctl restart&lt;/code&gt;, not a code change, so it flatters the comparison. &lt;strong&gt;vLLM wins decode 3.2x and loses the iteration loop by 3 to 100x.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Five Theories About 546 Seconds
&lt;/h4&gt;

&lt;p&gt;vLLM's cold boot is dominated by weight loading: 468 to 561 seconds across four measurements. Explaining it took five attempts, four of which were wrong.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;&lt;code&gt;g5g.2xlarge&lt;/code&gt; needs no swapfile and buys that time back.&lt;/em&gt; The rig's own documentation.
Falsified by the three-boot campaign at 23m 38s.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;A bigger host will not fix it.&lt;/em&gt; Retracted the same day — no large host had been measured.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;A larger host would very plausibly fix it.&lt;/em&gt; Falsified by one &lt;code&gt;g5g.4xlarge&lt;/code&gt; boot: available
RAM 11.19 to 26.49 GiB, weight loading 546 to 468 s, and total boot 4.7 percent lower,
inside the noise band.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;It is the loader; vLLM's log says auto-prefetch is disabled on EXT4.&lt;/em&gt; Falsified by a
within-box A/B — 76.13 s as shipped against 75.12 s with
&lt;code&gt;--safetensors-load-strategy=prefetch&lt;/code&gt;, which is 1.3 percent and therefore nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What that last run did find is the useful part.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;weight load&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cold boot, fresh instance&lt;/td&gt;
&lt;td&gt;468-561 s&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm restart, same box&lt;/td&gt;
&lt;td&gt;32-76 s&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same volume, same filesystem, same engine, differing only in whether the blocks had been read once. 9.54 GiB in 468 s is about 21 MiB/s, which is absurd for gp3 steady state and ordinary for first-touch reads against a snapshot-backed volume.&lt;/p&gt;

&lt;p&gt;Theory five is EBS lazily hydrating the volume from the AMI snapshot, and it is written down as untested. Given the strike rate it does not get promoted by reasoning.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why the First Campaign Was Thrown Away
&lt;/h4&gt;

&lt;p&gt;The first boot campaign was discarded and re-run. Two independent instances had reported 214.4 s and 125.1 s, identical to the tenth. That is not consistency. It is a five second poll quantising two similar boots onto the same tick.&lt;/p&gt;

&lt;p&gt;The data was not wrong; the campaign log shows 215 s and 216 s of wall clock. It was unusably coarse. Health polling went to half a second, and the next pair of boots came in at 216.90 s and 193.86 s — an 11.9 percent spread the old harness could not see.&lt;/p&gt;

&lt;h4&gt;
  
  
  And Price/Performance?
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;$/hr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;g5g.2xlarge&lt;/code&gt; on-demand&lt;/td&gt;
&lt;td&gt;$0.556&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spot, measured across four AZs&lt;/td&gt;
&lt;td&gt;$0.3813 - $0.4416&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole exercise — 19 instances, 4.06 hours of &lt;code&gt;g5g.2xlarge&lt;/code&gt; plus 0.38 of &lt;code&gt;g5g.4xlarge&lt;/code&gt;, a serving sweep, three cross-rig runs, nine boots and three A/B restarts — came to under $3. That is arithmetic rather than a bill: AWS drops terminated instances after an hour, so the derivation in &lt;code&gt;cost_derivation.md&lt;/code&gt; bounds it at $1.84 all-spot and $2.68 all-on-demand.&lt;/p&gt;

&lt;p&gt;That 26 to 46 percent premium is the entire spot versus on-demand decision on this hardware, which is to say there is not one. Try spot, fall back, keep working; the automatic fallback cost about $0.24 across a nine-boot campaign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cheap hardware is what made the method possible, not merely affordable.&lt;/strong&gt; Discarding a completed campaign over a poll-interval bug cost twenty minutes and pennies. Where a run is expensive, the same discovery argues for shipping the numbers with a caveat instead.&lt;/p&gt;

&lt;h4&gt;
  
  
  AWS Services Used
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;service&lt;/th&gt;
&lt;th&gt;what it does here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;g5g.2xlarge&lt;/code&gt; instances, spot and on-demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Systems Manager&lt;/td&gt;
&lt;td&gt;every remote command; there is no inbound SSH rule and no private key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets Manager&lt;/td&gt;
&lt;td&gt;the Hugging Face token, fetched at boot into a root-only &lt;code&gt;EnvironmentFile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;one instance profile, &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; plus read on that one secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS&lt;/td&gt;
&lt;td&gt;gp3 root volumes, and the AMI snapshot behind the vLLM boot mystery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Clean Up
&lt;/h4&gt;

&lt;p&gt;Every instance is terminated as soon as its artifacts are captured. There is no built image to lose, only a pip install and a model cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terminate_g5g_instance i-02e79988a6cbeecbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;🗑️ Terminating &lt;span class="sb"&gt;`i-02e79988a6cbeecbf`&lt;/span&gt;. Relaunch costs a pip install, not a build.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-instances &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=instance-state-name,Values=running"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Reservations[].Instances[?starts_with(InstanceType,`g5g`)].InstanceId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🟢 none running"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🟢 none running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What This Does Not Cover
&lt;/h4&gt;

&lt;p&gt;Everything above is concurrency one, which makes it a latency comparison rather than a serving one. Continuous batching is vLLM's whole value proposition and it is untested here.&lt;/p&gt;

&lt;p&gt;Only vLLM can serve concurrently at all today. The PyTorch server holds an &lt;code&gt;asyncio.Lock&lt;/code&gt; with the comment &lt;em&gt;"one GPU, one process -&amp;gt; serialize requests"&lt;/em&gt;, and the JAX rig has no batching machinery whatever. The engine-level batch sweep suggests what is on the table — batch eight reaches 84.16 tok/s for an extra 0.258 GB, with per-step time growing two percent across an eight-fold batch — but that number never leaves the engine.&lt;/p&gt;

&lt;p&gt;Three further gaps: the JAX leg ran its shipped quantised configuration against two dense runtimes; no output-quality axis was measured at all, on a comparison where one runtime uses a deliberately lossy LM head; and every TTFT figure predates the prompt-uniqueness fix, so only the JAX versus PyTorch half of that table is sound.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;The goal of this article was to compare three inference runtimes on identical silicon without the harness being a variable. The key to the solution was a single client-side statistic that every OpenAI-compatible server can produce. The measured results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decode at concurrency one: vLLM 32.53, JAX 12.69, PyTorch 10.24 tok/s — 53, 21 and 17
percent of a 61.4 tok/s bandwidth ceiling, so none is bandwidth-bound.&lt;/li&gt;
&lt;li&gt;The ranking reverses on lifecycle. Cold boot 195.2 s for PyTorch against 1417.8 s for vLLM;
a code change costs 25 s against a from-source rebuild.&lt;/li&gt;
&lt;li&gt;JAX prefills 2.4x slower than PyTorch, 1.403 against 0.595 ms/token.&lt;/li&gt;
&lt;li&gt;A 94.7 percent prefix-cache hit rate turned a 30x TTFT result into a harness artifact.&lt;/li&gt;
&lt;li&gt;Calibration offsets are per-rig, 0.9799 and 0.9543, and are not transferable.&lt;/li&gt;
&lt;li&gt;Boot variance is about 12 percent on this platform regardless of runtime.&lt;/li&gt;
&lt;li&gt;The whole exercise cost under $3 in total. That is what made discarding a finished campaign
over a poll-interval bug a twenty minute decision rather than an argument.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: the decode and boot numbers are one &lt;code&gt;g5g.2xlarge&lt;/code&gt; per runtime in &lt;code&gt;us-east-1a&lt;/code&gt; on 2026-08-31, three repeats per cell and three repeats per boot, mixed spot and on-demand with the market recorded per run. Three things differed between the legs and are named where they matter: vLLM ran &lt;code&gt;max_model_len&lt;/code&gt; 16384 against 4096 for the other two; the JAX leg ran its shipped quantised configuration against two dense runtimes; and vLLM booted from a prebuilt AMI carrying its model cache while the other two installed from wheels and downloaded the checkpoint, which is the point of the boot comparison rather than a flaw in it. Two runs sit outside that envelope and say so in the text: the RAM test was a single &lt;code&gt;g5g.4xlarge&lt;/code&gt;, and the prefetch A/B ran 2026-09-01 on on-demand after a spot reclamation killed the first attempt. Decode is unaffected by the prefix-cache issue, which changes prefill only.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for multi-runtime comparison was validated with a incremental step by step approach.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>machinelearning</category>
      <category>benchmarking</category>
      <category>python</category>
    </item>
    <item>
      <title>Streamline Publishing with a Claude Code Skill</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:37:08 +0000</pubDate>
      <link>https://dev.to/gde/streamline-publishing-with-a-claude-code-skill-1bdn</link>
      <guid>https://dev.to/gde/streamline-publishing-with-a-claude-code-skill-1bdn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt; packages the whole publishing lifecycle as a Claude Code skill. Write one markdown file, and it builds the dev.to, AWS Builder Center, Medium and LinkedIn versions, checks them, and posts the ones that have an API. This article, its cover and all four of its artifacts were produced by the thing the article is about — dogfooding all the way down. More on that at the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Publishing one technical article to four places involves a surprising amount of ceremony: making a cover at whatever size each destination wants, rendering tables to images because Medium's importer eats them, stripping emoji for AWS, checking that every number in the piece came from a real run, getting a long markdown file into a browser editor that has no API, remembering which organization the article routes to, and writing the announcement post afterwards — by which point you have four slightly different files and no idea which one is current.&lt;/p&gt;

&lt;p&gt;I packaged all of that into &lt;strong&gt;&lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;publishing-kit&lt;/a&gt;&lt;/strong&gt; — a Claude Code skill and a set of small scripts — so you can just ask Claude to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The skill teaches Claude the publishing lifecycle; the scripts do the parts a language model should not be doing by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build the artifacts:&lt;/strong&gt; one source file becomes the dev.to markdown, the Builder Center
version with emoji stripped and the AWS disclaimer appended, Medium HTML with every table
rendered to a PNG, and a LinkedIn post. &lt;code&gt;make-builder.py&lt;/code&gt;, &lt;code&gt;make-medium.py&lt;/code&gt;,
&lt;code&gt;make-linkedin.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the cover once:&lt;/strong&gt; &lt;code&gt;make-cover.py --flow --sizes devto,builder&lt;/code&gt; draws the pipeline as an
illustration and renders it at every geometry the destinations demand, names the file by a hash
of its own bytes, and reports which type sizes survive a 320px feed card — which is the size a
cover is actually met at, and where a diagram with small labels turns to mush.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace every number:&lt;/strong&gt; &lt;code&gt;check-facts.py&lt;/code&gt; pulls the prices, measurements and versions out of
your prose and reports which ones appear in no evidence file. It cannot tell you a figure is
true. It tells you which ones you are asserting from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight:&lt;/strong&gt; &lt;code&gt;preflight.py --live&lt;/code&gt; runs the lot and exits non-zero — cover committed and
matching HEAD, geometry right, front matter complete, no hard-wrapped paragraphs, and every
published URL fetched and compared byte for byte against your disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post where there is an API:&lt;/strong&gt; &lt;code&gt;publish-devto.py --create&lt;/code&gt; takes the front matter as the
payload, so title, tags and cover ride along with the body, and &lt;code&gt;--org-slug&lt;/code&gt; routes it to a
community channel. No browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drive the editors that have none:&lt;/strong&gt; AWS Builder Center and Medium are Chrome work, and the
skill knows the route — payload through &lt;code&gt;window.name&lt;/code&gt;, a checksum on both sides, an emptiness
assertion before the paste, and a landmark count after it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also encodes the hard-won details you would otherwise learn the day after publishing: that dev.to renders markdown with hard breaks &lt;strong&gt;on&lt;/strong&gt;, so a source wrapped at 95 columns arrives with a stray break in every paragraph; that dev.to does not host your cover but proxies it at 2.381:1, so a 1376x768 cover loses 95px off the top and bottom; that Medium drops &lt;code&gt;data:&lt;/code&gt; URI images on paste, so the self-contained build arrives with no pictures at all; and that LinkedIn's Posts API cannot create a draft, because &lt;code&gt;PUBLISHED&lt;/code&gt; is the only state it accepts on creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;The fastest path is the plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add xbill9/publishing-kit
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;publishing@publishing-kit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer the classic route? Clone it and symlink the skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xbill9/publishing-kit
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/publishing-kit/skills/publishing"&lt;/span&gt; ~/.claude/skills/publishing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will need Python 3 with Pillow for the cover and table rendering, a dev.to API key in &lt;code&gt;~/.devto.key&lt;/code&gt;, and a public repo to hold the article directory — both the cover and the Medium images are fetched by URL when the page renders, so unpushed means broken.&lt;/p&gt;

&lt;p&gt;Mostly you will not run these scripts yourself — Claude does, and &lt;code&gt;SKILL.md&lt;/code&gt; tells it where they are. When you do want one by hand, the paths differ by install method, so ask rather than remember: &lt;code&gt;skill-footprint.py --where&lt;/code&gt; prints the skill directory. From a clone it is &lt;code&gt;skills/publishing&lt;/code&gt;; from a marketplace install it sits under a &lt;strong&gt;version-numbered&lt;/strong&gt; cache path, which means no invocation you write down survives an upgrade.&lt;/p&gt;

&lt;p&gt;That version numbering matters if you plan to hack on the skill itself: installing takes a &lt;strong&gt;snapshot&lt;/strong&gt;, and &lt;code&gt;claude plugin update&lt;/code&gt; compares version strings, so edits under the same version never reach your session. Symlink while you are iterating.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Once installed, you talk to Claude Code like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Write this benchmark up and publish it to dev.to under aws-builders"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude writes the source article, generates the cover at both geometries, traces the numbers against your run artifacts, runs the pre-flight, tells you what failed, and — once it passes — posts it as a draft and hands you the link. Then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Now make the Medium and Builder Center versions"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It derives them, renders the tables to images, opens Chrome, and fills the editors. It stops at draft on every destination and hands back links. Publishing is your keystroke, not its.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging a publish that went sideways
&lt;/h2&gt;

&lt;p&gt;This is the half that surprised me most, and where most of the skill's value ended up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The page does not look like the file.&lt;/strong&gt; Fetch what the destination actually serves rather than reasoning about what you pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 ../../skills/publishing/scripts/check-links.py article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;devto-publishing-kit.md  (branch URLs)
  ok    cover.77acc7c4.jpg: HTTP 200, bytes match disk
  ok    img/cover.77acc7c4.jpg: HTTP 200, bytes match disk
  ok    img/devto-publishing-kit-table-1.png: HTTP 200, bytes match disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;FAIL&lt;/code&gt; there reads "HTTP 200 but the served bytes differ from disk", which is usually an image regenerated after its commit. Every other check in the kit reasons about local state — is the file there, is it tracked — and each of those can pass while the published URL serves something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The paragraphs look ragged.&lt;/strong&gt; &lt;code&gt;check-article.py&lt;/code&gt; reports hard-wrapped paragraphs with the line number of the first one, and &lt;code&gt;publish-devto.py&lt;/code&gt; unwraps on the way out so your repo copy stays readable at 95 columns and the published page does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An image is missing on Medium.&lt;/strong&gt; You pasted &lt;code&gt;-embed.html&lt;/code&gt;. Paste &lt;code&gt;-hosted.html&lt;/code&gt;, which references real URLs Medium re-hosts, and commit the images first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A number in the article has no artifact behind it.&lt;/strong&gt; &lt;code&gt;check-facts.py&lt;/code&gt; will name it. Every untraced claim is one of three things: measured but never archived, arithmetic that should be labelled as arithmetic, or asserted from memory — and it is always the third one that turns out to be wrong.&lt;/p&gt;

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

&lt;p&gt;One &lt;code&gt;SKILL.md&lt;/code&gt;, a reference file for each destination's quirks, and a script per job. The skill decides when each one runs; the scripts are independently runnable and print what they did.&lt;/p&gt;

&lt;p&gt;The bit worth stealing for your own skills is &lt;code&gt;references/house-style.md&lt;/code&gt;. Voice, section order, the opener and closing formulas live in that one file, and nothing in &lt;code&gt;SKILL.md&lt;/code&gt; or the scripts depends on it. Swap it and the kit writes as somebody else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;skill-footprint.py&lt;/code&gt; measures the skill's own size and token cost, and emits the cost table and cover footer for articles like this one — because a figure that changes on every commit does not belong in prose you have to remember to update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dogfooding: this article, and its cover 🐕🍖
&lt;/h2&gt;

&lt;p&gt;Every artifact here came out of the kit. The cover was rendered by &lt;code&gt;make-cover.py&lt;/code&gt;, the Builder Center version was derived by &lt;code&gt;make-builder.py&lt;/code&gt;, the Medium build by &lt;code&gt;make-medium.py&lt;/code&gt;, and the dev.to draft posted by &lt;code&gt;publish-devto.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The two numbers on the cover are real, and both were found by using the kit on itself:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dev.to, hard-wrapped paragraphs&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;47 of 62&lt;/strong&gt; in a published article carried a stray break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium, images pasted as data URIs&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 of 4&lt;/strong&gt; survived; from real URLs, 4 of 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first one had been happening to my articles for months. The second cost a full re-do the first time it happened. Neither is in the docs of either destination, and both are now checks.&lt;/p&gt;

&lt;p&gt;The run also found five faults in the kit itself, which is the point of eating your own cooking: &lt;code&gt;make-medium.py&lt;/code&gt; had another project's repo hardcoded as its default image base; &lt;code&gt;check-article.py&lt;/code&gt; passed a cover that was tracked but regenerated; &lt;code&gt;make-cover.py&lt;/code&gt; lost a &lt;code&gt;--tile&lt;/code&gt; value beginning with a hyphen to &lt;code&gt;argparse&lt;/code&gt;; &lt;code&gt;check-facts.py&lt;/code&gt; read &lt;code&gt;127.0.0.1&lt;/code&gt; as a version number; and &lt;code&gt;unwrap()&lt;/code&gt; skipped block quotes, so this article's own TL;DR posted as five separate lines — while the check that should have caught it skipped block quotes too, and agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/xbill9/publishing-kit" rel="noopener noreferrer"&gt;github.com/xbill9/publishing-kit&lt;/a&gt; (Apache-2.0)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The skill:&lt;/strong&gt; &lt;a href="https://github.com/xbill9/publishing-kit/blob/main/skills/publishing/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;skills/publishing/SKILL.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This article's directory&lt;/strong&gt;, with every artifact and the evidence behind every number:
&lt;a href="https://github.com/xbill9/publishing-kit/tree/main/articles/publishing-kit-skill" rel="noopener noreferrer"&gt;&lt;code&gt;articles/publishing-kit-skill/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code:&lt;/strong&gt; &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;claude.com/claude-code&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Issues and PRs welcome. This is a third-party community project, not affiliated with dev.to, Medium, AWS or LinkedIn — and the destination behaviours described here were measured on 2026-08-31, so check them again before you trust them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>writing</category>
      <category>devtools</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build Real-Time Client-Side TTS in Angular Using Firebase AI Logic and Gemini</title>
      <dc:creator>Connie Leung</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:52:37 +0000</pubDate>
      <link>https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc</link>
      <guid>https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc</guid>
      <description>&lt;p&gt;In my previous article on &lt;a href="https://dev.to/gde/building-dynamic-audio-with-emotion-pace-gemini-31-flash-tts-angular-firebase-cloud-functions-15f8"&gt;Dev.to&lt;/a&gt;, I showed how to build expressive text-to-speech using Gemini and Firebase Cloud Functions. While that setup worked beautifully, it required building and deploying custom backend endpoints. Firebase AI Logic enables running Gemini TTS client-side with production security, eliminating custom backend code and automating deployment via Git integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Technical Stack
&lt;/h2&gt;

&lt;p&gt;Project technical stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Angular 22:&lt;/strong&gt; The latest version as of August 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js LTS:&lt;/strong&gt; The LTS version as of May 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Remote Config:&lt;/strong&gt; Manages dynamic parameters, such as TTS model names, and conditionally configures limited-use tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase AI Logic:&lt;/strong&gt; Interfaces with Gemini generative models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase App Check:&lt;/strong&gt; Prevents abuse and verifies token authenticity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase App Hosting:&lt;/strong&gt; Streamlined deployment to the Firebase App Hosting environment. With a single code push, the latest changes deploy to the production environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public Google Gemini Developer API is restricted in my region (Hong Kong). However, the Agent Platform Gemini API (Google Cloud) offers enterprise access that works reliably here, so I chose the Agent Platform Gemini API for this Firebase AI Logic demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Install global dependency
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; firebase-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install or update &lt;code&gt;firebase-tools&lt;/code&gt; globally using &lt;code&gt;npm&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase &lt;span class="nb"&gt;logout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and re-authenticate with Firebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Install dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;--save-exact&lt;/span&gt; firebase
npm i &lt;span class="nt"&gt;--save-exact&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; firebase-tools serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the dependencies to call the Firebase AI Logic API, use the Firebase CLI to generate files, and serve the production build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execute &lt;code&gt;firebase init&lt;/code&gt; and follow the prompts to set up the Firebase AI Logic, Emulators, App Hosting, and Remote Config.&lt;/p&gt;

&lt;p&gt;If you have an existing project or multiple projects, you can specify the project ID on the command line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase init &lt;span class="nt"&gt;--project&lt;/span&gt; &amp;lt;PROJECT_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After completing the setup steps, the Firebase tools generate the configuration files such as &lt;code&gt;.firebaserc&lt;/code&gt; and &lt;code&gt;firebase.json&lt;/code&gt;. You can view the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/firebase/.firebaserc" rel="noopener noreferrer"&gt;.firebaserc&lt;/a&gt; and &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/firebase/firebase.json" rel="noopener noreferrer"&gt;firebase.json&lt;/a&gt; in the GitHub repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3. Generate Firebase Configuration File
&lt;/h3&gt;

&lt;p&gt;We asked &lt;code&gt;antigravity-cli&lt;/code&gt; (a terminal-first AI coding agent released by Google) and the Gemini Flash model to create two Node.js scripts to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate default Remote Config values in JSON format and save them to &lt;code&gt;public/remote-config-defaults.json&lt;/code&gt;. You can read the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/firebase/scripts/get-firebase-remote-config.js" rel="noopener noreferrer"&gt;complete listing of the script&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Populate the Firebase configuration object and write it to &lt;code&gt;public/firebase.config.json&lt;/code&gt;. If we hardcode the public keys and push them to GitHub, GitHub triggers a false alarm. You can read the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/firebase/scripts/generate-firebase-config.js" rel="noopener noreferrer"&gt;complete listing of the script&lt;/a&gt; and the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/firebase/.env.example" rel="noopener noreferrer"&gt;environment variable template&lt;/a&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;generated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;firebase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;firebase.config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;firebase.config.json&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; to prevent accidental commits.&lt;/p&gt;

&lt;p&gt;During build time, Angular bundles both JSON files into the &lt;code&gt;dist&lt;/code&gt; directory to provide initial configuration values.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqv81c1kwo93cd5kv5rs.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqv81c1kwo93cd5kv5rs.png" alt="Architecture of the Firebase AI Logic TTS L16 to WAV Conversion" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users submit text to Firebase AI Logic to synthesize speech. Firebase generates the complete L16 audio payload and returns it to the client. Because the HTML audio element does not support the L16 format, the application converts the audio to a WAV Blob before binding the Blob URL to the element source.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xv55c6lubwmef60n4a0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xv55c6lubwmef60n4a0.png" alt="Architecture of the Firebase AI Logic TTS L16 Playback" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second flow streams the audio and sends the L16 chunks to the Angular application. The audio player creates an AudioBufferSourceNode to play the chunk data and cleans up resources to prevent memory leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Source Code
&lt;/h2&gt;

&lt;p&gt;While the full codebase is available in the &lt;a href="https://github.com/railsstudent/ng-firebase-tts" rel="noopener noreferrer"&gt;ng-firebase-tts&lt;/a&gt; repository, the application relies on &lt;code&gt;Firebase Remote Config&lt;/code&gt; to manage configuration, &lt;code&gt;App Check&lt;/code&gt; to prevent abuse, and &lt;code&gt;App Hosting&lt;/code&gt; to deploy the Angular application.&lt;/p&gt;

&lt;p&gt;The following sections illustrate how to initialize the Firebase app and App Check, and how to activate Remote Config values.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Firebase Integration
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;public/firebase.config.json&lt;/code&gt; file contains the public Firebase API key, the sensitive reCAPTCHA Enterprise key, and the App Check debug token to bypass device attestation in local development. These values are critical in Firebase App and App Check initialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Firebase Infrastructure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConfigService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FirebaseApp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RemoteConfig&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="cm"&gt;/*... getter methods are omitted... */&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AppRemoteConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;aiBackend&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;aiBackend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;FIREBASE_APPCHECK_DEBUG_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appCheckDebugToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;initializeAppCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReCaptchaEnterpriseProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firebaseConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recaptchaEnterpriseKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;isTokenAutoRefreshEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRemoteConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remoteConfigDefaults&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAndActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;vertexAILocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vertexAILocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;useLimitedUseAppCheckTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;useLimitedUseAppCheckTokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asBoolean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;geminiTTSModelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;remoteConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geminiTTSModelName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;aiBackend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AgentPlatformBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vertexAILocation&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;useLimitedUseAppCheckTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useLimitedUseAppCheckTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; method initializes Firebase App, configures App Check, sets up Firebase AI, and assigns Remote Config values to &lt;code&gt;appConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;appConfig&lt;/code&gt; object holds the location, the Gemini TTS model name, and the limited-use App Check token flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Remote Config and Construct Agent Platform Backend
&lt;/h3&gt;

&lt;p&gt;Configure the TTS model name and the limited-use App Check token parameters in Firebase Remote Config. Both parameters have conditional values. When the Firebase web application is &lt;code&gt;firebase-ai-logic-tts&lt;/code&gt;, the TTS model is &lt;code&gt;gemini-3.1-flash-tts-preview&lt;/code&gt;, and the limited-use App Check token flag is set to true.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5hktvof6jhk5cr692mg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5hktvof6jhk5cr692mg.png" alt="Gemini TTS Model Name Parameter" width="800" height="119"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv878to3eu0tmj6xyt2pb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv878to3eu0tmj6xyt2pb.png" alt="Use Limited-use App Check Token Parameter" width="799" height="249"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfk7v173afjr9pvg3l7z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfk7v173afjr9pvg3l7z.png" alt="Add condition to Use Limited-use App Check Token Parameter" width="800" height="446"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AI_BACKEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;InjectionToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AI&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI_BACKEND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;provideFirebase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;makeEnvironmentProviders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AI_BACKEND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;aiBackend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AI_BACKEND&lt;/code&gt; injection token provides a factory function to return the Firebase AI from &lt;code&gt;ConfigService&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="nf"&gt;provideAppInitializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nf"&gt;provideFirebase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;provideAppInitializer&lt;/code&gt; and &lt;code&gt;provideFirebase&lt;/code&gt; initialize Firebase and configure Firebase AI during application startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. TextToSpeechService Definition in Angular
&lt;/h2&gt;

&lt;p&gt;The private &lt;code&gt;createModel&lt;/code&gt; method calls &lt;code&gt;getGenerativeModel&lt;/code&gt; to create a generative model configured with &lt;code&gt;speechConfig&lt;/code&gt;. The subsequent workflows use this model to convert L16 audio to WAV or stream L16 playback directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextToSpeechService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;configService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;modelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;configService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geminiTTSModelName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;createModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getGenerativeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;aiBackend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;modelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generationConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;responseModalities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ResponseModality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUDIO&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;voiceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;prebuiltVoiceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voiceName&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;languageCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.1 Flow 1: L16 Audio to WAV conversion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;synthesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voiceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceName&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;text&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;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractValidChunkData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;convertToWav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decodeBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;extractValidChunkData&lt;/code&gt; method extracts binary audio data and the MIME type from the response payload.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;synthesize&lt;/code&gt; method retrieves the entire audio payload in L16 format. However, the HTML audio element does not support L16, so the application must convert the data to WAV format before assigning the Blob URL to the element source.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/src/app/core/utils/mime-type.util.ts#L73" rel="noopener noreferrer"&gt;conversion to WAV code&lt;/a&gt; for the full implementation.&lt;/p&gt;

&lt;p&gt;This implementation remains straightforward because it avoids managing response streams and incremental audio chunks. However, users suffer from latency when the text is long and it produces a long audio stream. To eliminate playback latency, the next section explores streaming chunks directly via the Web Audio API &lt;code&gt;AudioContext&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Flow 2: L16 Audio Instant Playback
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;synthesizeStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voiceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AsyncGenerator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RawAudioBinary&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voiceName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;firstMimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_SAMPLE_RATE&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;responseStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContentStream&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;responseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&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;chunkData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractValidChunkData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;chunkData&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunkData&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;decodedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;firstMimeType&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;firstMimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseMimeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstMimeType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decodedData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleRate&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;synthesizeStream&lt;/code&gt; method returns an asynchronous generator that yields the raw binary data and the sample rate. The MIME type is &lt;code&gt;audio/l16; rate=24000; channels=1&lt;/code&gt;, so &lt;code&gt;parseMimeType&lt;/code&gt; extracts the sample rate for the audio player's &lt;code&gt;AudioContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;AudioPlayerService&lt;/code&gt; is beyond the scope of this article, the &lt;a href="https://github.com/railsstudent/ng-firebase-tts/blob/main/src/app/core/services/audio-player.service.ts" rel="noopener noreferrer"&gt;source code&lt;/a&gt; demonstrates how to play back audio without using an HTML audio element.&lt;/p&gt;

&lt;p&gt;Next, we will build a reactive user interface in Angular that renders an HTML audio element to play audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Build Reactive UI in Angular
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;TextToSpeechComponent&lt;/code&gt; delegates audio management to the view service to maintain separation of concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Text to Speech Component
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;TextToSpeechComponent&lt;/code&gt; displays three buttons to generate speech from text across three different scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scenario 1: Synthesizes the entire audio payload and binds the resulting Blob URL to an HTML audio element.&lt;/li&gt;
&lt;li&gt;Scenario 2: Streams audio chunks incrementally and plays the speech immediately via the Web Audio API. This mode hides the HTML audio element because audio plays directly through the Web Audio API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-text-to-speech&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./text-to-speech.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./text-to-speech.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;SpinnerIconComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NgTemplateOutlet&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TextToSpeechViewService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextToSpeechComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;speechService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TextToSpeechViewService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;interestingFact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;audioPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;voice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generateSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerateSpeechMode&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;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interestingFact&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audioPrompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interestingFact&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.2 TextToSpeechView Definition in Angular
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;TextToSpeechViewService&lt;/code&gt; encapsulates &lt;code&gt;TextToSpeechService&lt;/code&gt; and &lt;code&gt;AudioPlayerService&lt;/code&gt; to coordinate speech synthesis and audio playback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextToSpeechViewService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;speechService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TextToSpeechService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;audioPlayerService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AudioPlayerService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;audioUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asReadonly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;processStreamChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isInitialized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playbackRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawAudioBinary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isInitialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioPlayerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playbackRate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;isInitialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioPlayerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decodedData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isInitialized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FactConfig&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;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synthesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAudioUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FactConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isInitialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synthesizeStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;isInitialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processStreamChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isInitialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;setAudioUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalBlob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalBlob&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;createdUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;finalBlob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createdUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;createdUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generateSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerateSpeechMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FactConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;revokeBlobURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web_audio_api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promptArgs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;handleSync&lt;/code&gt; invokes the Firebase SDK to synthesize audio using the Gemini TTS model, generates a Blob URL, updates &lt;code&gt;#audioUrl&lt;/code&gt;, and renders the HTML audio element.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;handleStream&lt;/code&gt; calls the Firebase SDK to use the Gemini TTS model to stream the audio instead. The audio context receives the chunk data, plays it immediately, and avoids rendering the HTML audio element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The integration of text-to-speech with Firebase AI Logic empowers Angular applications for real-time audio generation.&lt;/p&gt;

&lt;p&gt;The Angular application handles text-to-speech entirely on the client. Pushing changes to Git triggers an automatic deployment to Firebase App Hosting.&lt;/p&gt;

&lt;p&gt;Try cloning the GitHub repository, uploading an image to generate an obscure fact, and using the Gemini 3.1 Flash TTS preview model to speak it with the specified scene, emotion, and pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/railsstudent/ng-firebase-tts" rel="noopener noreferrer"&gt;Firebase AI Logic TTS GitHub Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ng-firebase-tts--vertexai-tts.asia-east1.hosted.app" rel="noopener noreferrer"&gt;Firebase AI Logic TTS Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/ai-logic/get-started?api=dev&amp;amp;utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Firebase AI Logic - Get Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/ai-logic/generate-speech?api=dev&amp;amp;utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Firebase AI Logic - Generate Speech&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/ai-logic/app-check?api=dev&amp;amp;utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Firebase AI Logic - App Check&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/ai-logic/solutions/remote-config?api=dev&amp;amp;utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Dynamically update your Firebase AI Logic app with Firebase Remote Config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>gemma</category>
      <category>firebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Automate Flutter's New Split Package Migration with AI Agent Skills</title>
      <dc:creator>Randal L. Schwartz</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:20:40 +0000</pubDate>
      <link>https://dev.to/gde/automate-flutters-new-split-package-migration-with-ai-agent-skills-bn2</link>
      <guid>https://dev.to/gde/automate-flutters-new-split-package-migration-with-ai-agent-skills-bn2</guid>
      <description>&lt;h2&gt;
  
  
  How to use dart-sdk-skills to empower your AI coding agent to seamlessly migrate Flutter projects to standalone material_ui and cupertino_ui packages
&lt;/h2&gt;

&lt;p&gt;The Flutter framework has undergone one of its most significant architectural evolutions: &lt;strong&gt;the complete unbundling of Material Design and Cupertino libraries into standalone pub packages&lt;/strong&gt; (&lt;a href="https://pub.dev/packages/material_ui" rel="noopener noreferrer"&gt;&lt;code&gt;material_ui&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://pub.dev/packages/cupertino_ui" rel="noopener noreferrer"&gt;&lt;code&gt;cupertino_ui&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;While this decoupling brings independent release cycles, lighter core framework footprints, and true headless UI development, it also introduces a massive migration across existing Flutter apps.&lt;/p&gt;

&lt;p&gt;If you rely on AI coding assistants (such as Claude Code, Google Antigravity, Cursor, GitHub Copilot, or Cline), you have likely hit a familiar wall: &lt;strong&gt;LLM training cutoffs&lt;/strong&gt;. Most models still generate monolithic &lt;code&gt;package:flutter/material.dart&lt;/code&gt; imports and do not know how to perform the new split package migration.&lt;/p&gt;

&lt;p&gt;In this article, we will explore how the open-source &lt;a href="https://github.com/RandalSchwartz/dart-sdk-skills" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;dart-sdk-skills&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; repository gives your AI agent the exact knowledge and execution runbooks to automate this migration in a single prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏛️ The Architectural Shift: Why Split Packages?
&lt;/h2&gt;

&lt;p&gt;Historically, Flutter bundled every Material Design and Cupertino widget directly into the core &lt;code&gt;flutter&lt;/code&gt; SDK. While convenient initially, this approach had drawbacks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Coupled Release Cycles&lt;/strong&gt;: Updating a single Material button style required waiting for a framework release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Bloat&lt;/strong&gt;: Apps building custom enterprise design systems were forced to bundle hundreds of unused Material and Cupertino classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design System Collision&lt;/strong&gt;: Mixing platform styles often led to tangled dependencies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under the modern decoupled architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────┐
│ Legacy Flutter (Monolithic)                                            │
│                                                                        │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │ package:flutter/material.dart  &amp;amp;  package:flutter/cupertino.dart │  │
│  ├──────────────────────────────────────────────────────────────────┤  │
│  │ package:flutter/widgets.dart   &amp;amp;  package:flutter/rendering.dart │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────┘
                                    ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Modern Decoupled Architecture                                          │
│                                                                        │
│  ┌───────────────────────────────┐   ┌──────────────────────────────┐  │
│  │ package:material_ui           │   │ package:cupertino_ui         │  │
│  └───────────────┬───────────────┘   └──────────────┬───────────────┘  │
│                  └───────────────┬──────────────────┘                  │
│                                  ▼                                     │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │ Core Flutter SDK (package:flutter/widgets.dart)                  │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both design systems are now maintained as independent packages by the official Flutter team on pub.dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;material_ui&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;^1.1.0&lt;/code&gt;): Canonical entrypoint &lt;code&gt;package:material_ui/material_ui.dart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cupertino_ui&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;^1.0.1&lt;/code&gt;): Canonical entrypoint &lt;code&gt;package:cupertino_ui/cupertino_ui.dart&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧩 The Challenge with AI Coding Agents
&lt;/h2&gt;

&lt;p&gt;When you tell an unassisted AI agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Migrate my app to the new split Flutter design packages."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It often stumbles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It hallucinates deprecated import paths or non-existent package names.&lt;/li&gt;
&lt;li&gt;It does not know about the automated data-driven fix rules in the Dart analyzer.&lt;/li&gt;
&lt;li&gt;It misses breaking changes in localization delegates (&lt;code&gt;GlobalMaterialLocalizations&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where &lt;strong&gt;Agent Skills&lt;/strong&gt; step in.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ What is &lt;code&gt;dart-sdk-skills&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/RandalSchwartz/dart-sdk-skills" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;dart-sdk-skills&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; is an authoritative repository of version-by-version agent skills for the Dart and Flutter SDKs. Built using the open Agent Skill specification (&lt;code&gt;SKILL.md&lt;/code&gt;), it progressively discloses precise rules, API matrices, and tactical upgrade runbooks to your AI pair programmer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing &lt;code&gt;dart-sdk-skills&lt;/code&gt; in Seconds
&lt;/h3&gt;

&lt;p&gt;You can add the skills globally or per-project using your preferred tool:&lt;/p&gt;

&lt;h4&gt;
  
  
  Universal Install (Node / &lt;code&gt;npx skills&lt;/code&gt;):
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install globally for all AI agents&lt;/span&gt;
npx skills add RandalSchwartz/dart-sdk-skills &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Dart Skills CLI:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skills add https://github.com/RandalSchwartz/dart-sdk-skills &lt;span class="nt"&gt;--global&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Google Antigravity / Manual Symlink:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/RandalSchwartz/dart-sdk-skills.git ~/Projects/Dart/dart-sdk-skills
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; ~/Projects/Dart/dart-sdk-skills/skills/flutter-sdk-changelog ~/.gemini/config/skills/flutter-sdk-changelog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🤖 The Migration Workflow: Putting Your Agent to Work
&lt;/h2&gt;

&lt;p&gt;Once installed, your agent automatically recognizes split-package requests. Simply prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Migrate this project to the new split Material and Cupertino packages."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the exact multi-phase pipeline the agent executes based on the skill runbook:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automated Analysis and Data-Driven Transforms
&lt;/h3&gt;

&lt;p&gt;The agent leverages the built-in Dart toolchain fix runner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart fix &lt;span class="nt"&gt;--apply&lt;/span&gt; &lt;span class="nt"&gt;--code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;migrate_design_widgets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This updates references and applies data-driven transform rules published inside &lt;code&gt;material_ui/lib/fix_data/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dependency Configuration (&lt;code&gt;pubspec.yaml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The agent ensures your project bounds satisfy the minimum SDK constraints (&lt;code&gt;Flutter &amp;gt;=3.44.0&lt;/code&gt;, &lt;code&gt;Dart ^3.12.0&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sdk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter&lt;/span&gt;
  &lt;span class="c1"&gt;# Standalone design systems&lt;/span&gt;
  &lt;span class="na"&gt;material_ui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.1.0&lt;/span&gt;
  &lt;span class="na"&gt;cupertino_ui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Canonical Import Rewrites
&lt;/h3&gt;

&lt;p&gt;The agent replaces monolithic imports across all Dart files with the canonical entrypoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Legacy Monolithic Imports&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/cupertino.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Modern Standalone Package Imports&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:material_ui/material_ui.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:cupertino_ui/cupertino_ui.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Localization Delegate Binding
&lt;/h3&gt;

&lt;p&gt;Both &lt;code&gt;material_ui.dart&lt;/code&gt; and &lt;code&gt;cupertino_ui.dart&lt;/code&gt; export their respective global localization delegates without requiring extra legacy glue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/widgets.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_localizations/flutter_localizations.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:material_ui/material_ui.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:cupertino_ui/cupertino_ui.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="nf"&gt;buildApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;localizationsDelegates:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;GlobalMaterialLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;GlobalCupertinoLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;GlobalWidgetsLocalizations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;supportedLocales:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'en'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'es'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'ES'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Automated Verification &amp;amp; Quality Gate
&lt;/h3&gt;

&lt;p&gt;The agent finishes by running static analysis and tests to ensure no ambiguous symbol collisions or unresolved imports remain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart analyze &lt;span class="nt"&gt;--fatal-infos&lt;/span&gt;
dart &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎨 Bonus: Building Pure Headless Apps
&lt;/h2&gt;

&lt;p&gt;One of the greatest benefits of this decoupling is that Flutter apps with custom enterprise design systems can now completely omit &lt;code&gt;material_ui&lt;/code&gt; and &lt;code&gt;cupertino_ui&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By building directly against &lt;code&gt;package:flutter/widgets.dart&lt;/code&gt;, you can create a lightweight, high-performance UI layer with zero design system bloat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/widgets.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BrandButton&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;VoidCallback&lt;/span&gt; &lt;span class="n"&gt;onTap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;BrandButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GestureDetector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onTap:&lt;/span&gt; &lt;span class="n"&gt;onTap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;vertical:&lt;/span&gt; &lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;decoration:&lt;/span&gt; &lt;span class="n"&gt;BoxDecoration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;color:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFF1E88E5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;borderRadius:&lt;/span&gt; &lt;span class="n"&gt;BorderRadius&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;circular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚀 Get Started Today
&lt;/h2&gt;

&lt;p&gt;The split package architecture is a major milestone for Flutter's scalability and modularity. By equipping your AI development workflow with &lt;a href="https://github.com/RandalSchwartz/dart-sdk-skills" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;dart-sdk-skills&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, you turn what could be a tedious manual refactor into an effortless, one-command migration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌟 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/RandalSchwartz/dart-sdk-skills" rel="noopener noreferrer"&gt;github.com/RandalSchwartz/dart-sdk-skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;&lt;code&gt;material_ui&lt;/code&gt; on Pub&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/material_ui" rel="noopener noreferrer"&gt;pub.dev/packages/material_ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🍏 &lt;strong&gt;&lt;code&gt;cupertino_ui&lt;/code&gt; on Pub&lt;/strong&gt;: &lt;a href="https://pub.dev/packages/cupertino_ui" rel="noopener noreferrer"&gt;pub.dev/packages/cupertino_ui&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding, and let your agents do the heavy lifting! 🚀&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building an AI Agent Honeypot &amp; Lead Engine with Model Context Protocol (MCP) &amp; FastAPI</title>
      <dc:creator>Mario Ezquerro</dc:creator>
      <pubDate>Mon, 31 Aug 2026 05:46:04 +0000</pubDate>
      <link>https://dev.to/gde/building-an-ai-agent-honeypot-lead-engine-with-model-context-protocol-mcp-fastapi-195e</link>
      <guid>https://dev.to/gde/building-an-ai-agent-honeypot-lead-engine-with-model-context-protocol-mcp-fastapi-195e</guid>
      <description>&lt;h1&gt;
  
  
  Building an AI Agent Honeypot &amp;amp; Lead Engine with Model Context Protocol (MCP) &amp;amp; FastAPI
&lt;/h1&gt;

&lt;p&gt;The way people discover and buy products online is undergoing a massive paradigm shift. Instead of humans manually browsing e-commerce websites and filling out lead forms, &lt;strong&gt;autonomous AI agents&lt;/strong&gt; (such as ChatGPT Shopping, Google Gemini, Perplexity Shopping, and Claude) are now researching, comparing prices, and reserving deals on behalf of users.&lt;/p&gt;

&lt;p&gt;To tap into this agentic economy, we built &lt;strong&gt;MCP Collector&lt;/strong&gt;: an open-standard gateway powered by &lt;strong&gt;FastAPI&lt;/strong&gt;, &lt;strong&gt;FastMCP 2.x&lt;/strong&gt;, and &lt;strong&gt;Google Cloud Run&lt;/strong&gt; that lets autonomous AI agents discover catalogs, invoke structured tools over HTTP/SSE, and stream qualified buyer leads straight into a real-time operator dashboard.&lt;/p&gt;

&lt;p&gt;Here is a deep-dive into how it works and how you can build one.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;MCP Collector sits between external AI agents and commercial operators:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph AI_Ecosystem [AI Agents &amp;amp; Shopping Bots]
        ChatGPT["ChatGPT (Shopping &amp;amp; Actions)"]
        Gemini["Google Gemini (Shopping Graph)"]
        Perplexity["Perplexity Shopping"]
        Claude["Claude Desktop &amp;amp; Antigravity"]
    end

    subgraph Hub [MCP Collector Hub (FastAPI + FastMCP)]
        Discovery["/llms.txt &amp;amp; /robots.txt &amp;amp; JSON-LD"]
        SSE["/mcp/sse &amp;amp; /mcp/messages (MCP 2.x)"]

        subgraph Tools [FastMCP Tools]
            T1["search_products"]
            T2["reserve_product_offer"]
            T3["request_b2b_quote"]
        end

        DB[(PostgreSQL / SQLite Async)]
        WS["WebSocket Broadcaster: /ws"]
    end

    subgraph Operator [Operator UI]
        Dashboard["Live Web Dashboard"]
    end

    AI_Ecosystem --&amp;gt;|Autodiscover| Discovery
    AI_Ecosystem --&amp;gt;|Connect &amp;amp; Execute| SSE
    SSE --&amp;gt; Tools
    Tools --&amp;gt;|Persist Lead| DB
    Tools --&amp;gt;|Instant Push| WS
    WS --&amp;gt; Dashboard&lt;/code&gt;&lt;/pre&gt;






&lt;h2&gt;
  
  
  Step 1: Making Your Hub Discoverable by LLMs
&lt;/h2&gt;

&lt;p&gt;For AI agents to interact with your server, they must first discover it. We use a 5-layer discovery strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llms.txt&lt;/code&gt; &amp;amp; &lt;code&gt;llms-full.txt&lt;/code&gt;&lt;/strong&gt;: Standardized Markdown files placed at the domain root containing catalog summaries, tool schemas, and instructions without wasting context tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-Targeted &lt;code&gt;robots.txt&lt;/code&gt;&lt;/strong&gt;: Explicit crawler permissions for &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, and &lt;code&gt;Amazonbot&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP &lt;code&gt;Link&lt;/code&gt; Headers&lt;/strong&gt;: Every response returns discovery pointers:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;   Link: &amp;lt;/mcp/sse&amp;gt;; rel="mcp-server", &amp;lt;/.well-known/mcp.json&amp;gt;; rel="mcp-manifest"
   X-MCP-Version: 1.2.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schema.org JSON-LD&lt;/strong&gt;: Embedded &lt;code&gt;ItemList&lt;/code&gt;, &lt;code&gt;Product&lt;/code&gt;, and &lt;code&gt;Offer&lt;/code&gt; semantic microdata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smithery &amp;amp; OpenAPI&lt;/strong&gt;: Standard &lt;code&gt;smithery.yaml&lt;/code&gt; configuration for seamless registry inclusion.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 2: Implementing Tools with FastMCP 2.x
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;FastMCP 2.x&lt;/strong&gt;, defining type-safe tools that external LLMs can invoke over Server-Sent Events (SSE) is clean and intuitive:&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;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.services.lead_service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;record_lead_and_broadcast&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP Collector Hub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product keyword or SKU&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Search promotional hardware and exclusive offers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpu-h100-sxm5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVIDIA H100 SXM5 80GB Server (4x Cluster)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;74500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promo_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;48425&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;35% OFF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stock_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1 unit remaining (EU Warehouse)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserve_product_offer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;buyer_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shipping_city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Reserve a high-demand product offer before it sells out.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Persist the lead &amp;amp; broadcast via WebSockets to operator dashboard
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;record_lead_and_broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sku&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="n"&gt;buyer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;buyer_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shipping_city&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Return realistic allocation status to the agent
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WAITLIST_PRIORITY_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unit allocated to next in queue. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;buyer_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; registered at Priority #1 on VIP Allocation List.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Real-Time Telemetry with WebSockets
&lt;/h2&gt;

&lt;p&gt;Whenever an agent invokes a tool, the event is immediately pushed to connected browsers via WebSockets without polling:&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;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WebSocketDisconnect&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConnectionManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;connection&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConnectionManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;websocket_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;WebSocketDisconnect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Deploying to Google Cloud Run
&lt;/h2&gt;

&lt;p&gt;Deploying to Cloud Run allows the hub to scale to zero when idle and instantly scale up when multiple agents hit the SSE endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy mcp-collector &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; managed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt; 3600 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-instances&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session-affinity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Session affinity and a long timeout (&lt;code&gt;3600s&lt;/code&gt;) are crucial for persistent Server-Sent Events (SSE) and WebSocket connections on Cloud Run.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Standards Matter&lt;/strong&gt;: By implementing &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, you build a single backend that works across Claude Desktop, ChatGPT, Gemini, and custom agents without reinventing integration layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine-Readable Discovery is the New SEO&lt;/strong&gt;: Protocols like &lt;code&gt;llms.txt&lt;/code&gt; and semantic JSON-LD are essential for getting your APIs ingested by autonomous web agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-Time Responsiveness&lt;/strong&gt;: Combining asynchronous event loops with WebSockets provides instant visibility into how AI models interact with your tools.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Project Source &amp;amp; Docs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/mario-ezquerro/mcp-collector" rel="noopener noreferrer"&gt;mario-ezquerro/mcp-collector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Reference&lt;/strong&gt;: &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol Specification&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>fastapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Gemma 4 in Pure JAX: What Changes Between Turing and Ada, and What Doesn't</title>
      <dc:creator>xbill</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:40:09 +0000</pubDate>
      <link>https://dev.to/gde/gemma-4-in-pure-jax-what-changes-between-turing-and-ada-and-what-doesnt-4c5e</link>
      <guid>https://dev.to/gde/gemma-4-in-pure-jax-what-changes-between-turing-and-ada-and-what-doesnt-4c5e</guid>
      <description>&lt;p&gt;This article is a measurement report on running a hand-written &lt;strong&gt;Gemma 4&lt;/strong&gt; port in&lt;br&gt;
&lt;strong&gt;pure JAX&lt;/strong&gt; across two NVIDIA GPUs a generation apart, and on the two places the&lt;br&gt;
"it's just JAX" abstraction leaks. One of those leaks costs 87% of decode and&lt;br&gt;
nothing in the logs is red.&lt;/p&gt;

&lt;p&gt;The code is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xbill9/gemma4-dev" rel="noopener noreferrer"&gt;https://github.com/xbill9/gemma4-dev&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What This Article Measures
&lt;/h2&gt;

&lt;p&gt;One port, one build, one checkpoint, two cards. Everything below comes from two&lt;br&gt;
archived runs, named so you can check them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;G5g&lt;/th&gt;
&lt;th&gt;G6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chip&lt;/td&gt;
&lt;td&gt;NVIDIA &lt;strong&gt;T4G&lt;/strong&gt; — Turing, SM 7.5, 15,360 MiB&lt;/td&gt;
&lt;td&gt;NVIDIA &lt;strong&gt;L4&lt;/strong&gt; — Ada, SM 8.9, 23,034 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g5g.2xlarge&lt;/code&gt; spot — Graviton2, &lt;strong&gt;aarch64&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;g6.2xlarge&lt;/code&gt; spot — &lt;strong&gt;x86_64&lt;/strong&gt;, &lt;code&gt;us-east-1d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;, dense reference&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;, dense reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute dtype&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;float16&lt;/code&gt; (device-chosen)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bfloat16&lt;/code&gt; (device-chosen)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack&lt;/td&gt;
&lt;td&gt;jax 0.11.1, CUDA from pip&lt;/td&gt;
&lt;td&gt;jax 0.11.1, Python 3.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run cited&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-08-28-full-run-cached-g5g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-08-28-first-serve-g6&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Build id &lt;code&gt;51bc52c9e2e9&lt;/code&gt; on both, config &lt;code&gt;ple4 + int8_lm_head&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;tpu_jax_weight_bytes&lt;/code&gt; reads &lt;strong&gt;6,155,450,950&lt;/strong&gt; on both cards — the same integer.&lt;br&gt;
Only the chip and its host differ.&lt;/p&gt;
&lt;h2&gt;
  
  
  At This Point You Should Have
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with spot capacity for &lt;code&gt;g5g.2xlarge&lt;/code&gt; and &lt;code&gt;g6.2xlarge&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A Hugging Face token with access to &lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Python 3.13 or newer on the instance, system-wide, with no virtualenv&lt;/li&gt;
&lt;li&gt;No CUDA toolkit, no Rust toolchain, and no compiler — none of them are needed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why Pure JAX At All
&lt;/h2&gt;

&lt;p&gt;The port lives in &lt;code&gt;ports/gemma4/&lt;/code&gt; and is driven by a generation loop behind an&lt;br&gt;
OpenAI-compatible server. No PyTorch, no vLLM, no &lt;code&gt;torch_xla&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The premise under test is that the same source runs on both cards with nothing&lt;br&gt;
changed but a config file. It mostly holds. The interesting part is where it does&lt;br&gt;
not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gemma 4 E2B Is Not a Stock Transformer
&lt;/h2&gt;

&lt;p&gt;Any port has to carry four irregularities, and none of them are optional.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Two attention geometries.&lt;/strong&gt; Sliding layers use &lt;code&gt;head_dim=256&lt;/code&gt;, global layers
use &lt;strong&gt;512&lt;/strong&gt;. Most inference stacks assume one head dimension per model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8:1 MQA&lt;/strong&gt;, so the KV budget is nothing like the parameter count suggests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A KV-share map&lt;/strong&gt; that collapses &lt;strong&gt;35 layers onto 15 caches&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 512-slot sliding ring&lt;/strong&gt;, plus per-layer embeddings held in a &lt;strong&gt;4.70 GB&lt;/strong&gt;
table that is quantized to 4 bits on load.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The Geometry Is What Breaks Other Stacks
&lt;/h2&gt;

&lt;p&gt;That first irregularity is the expensive one. On the vLLM path the heterogeneous&lt;br&gt;
head dims force the Triton attention backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gemma4 model has heterogeneous head dimensions
(sliding=256, global=512); falling back to the Triton attention backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Turing GPU that backend then asks for shared memory the hardware does not&lt;br&gt;
have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;triton.runtime.errors.OutOfResources: out of resource: shared memory,
Required: 147456, Hardware limit: 65536
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JAX never enters that conversation.&lt;/strong&gt; Attention is ordinary XLA rather than a&lt;br&gt;
hand-tiled kernel, so there is no per-block shared-memory ceiling in the attention&lt;br&gt;
path at all. The irregular geometry that is a special case everywhere else is just&lt;br&gt;
array shapes here.&lt;/p&gt;
&lt;h2&gt;
  
  
  Leak One: The dtype Policy Has To Read the Device
&lt;/h2&gt;

&lt;p&gt;This is the single most expensive lesson in the repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A wrong compute dtype does not raise. It emulates.&lt;/strong&gt; &lt;code&gt;bfloat16&lt;/code&gt; on a pre-Ampere&lt;br&gt;
GPU does not fail — XLA routes it through fp32 and most of decode disappears into&lt;br&gt;
conversion. Nothing in the logs is red.&lt;/p&gt;

&lt;p&gt;So the port does not take the dtype from a config file. It reads the live compute&lt;br&gt;
capability off the device:&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="n"&gt;COMPUTE_DTYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;float16&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;IS_PRE_AMPERE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;bfloat16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the SM 8.9 Ada card that resolves to &lt;code&gt;bfloat16&lt;/code&gt;. On the SM 7.5 Turing card it&lt;br&gt;
resolves to &lt;code&gt;float16&lt;/code&gt; — Turing's only real 16-bit datapath, since it has neither&lt;br&gt;
bf16 nor fp8.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Process States What It Decided
&lt;/h2&gt;

&lt;p&gt;The first line the server emits is the policy, so a misconfiguration is one &lt;code&gt;grep&lt;/code&gt;&lt;br&gt;
away rather than a mystery in the throughput:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;INFO&lt;/span&gt; &lt;span class="py"&gt;ports.gemma4.jax_e_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jax_e_model device policy: platform=gpu&lt;/span&gt;
&lt;span class="py"&gt;compute_capability&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8.9 compute_dtype=bfloat16 pallas_interpret=False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pallas_interpret=False&lt;/code&gt; matters just as much. It is the difference between&lt;br&gt;
serving and silently running a simulator.&lt;/p&gt;
&lt;h2&gt;
  
  
  Leak Two: Pallas Is Not Portable as a Memory Model
&lt;/h2&gt;

&lt;p&gt;Here is the part that does not port, and it is not a bug. It is a real hardware&lt;br&gt;
difference wearing a portable API.&lt;/p&gt;

&lt;p&gt;The fused &lt;strong&gt;W4A16 kernel is written in Pallas&lt;/strong&gt;, and it was tiled for a device with&lt;br&gt;
16 MB of scratchpad per core. At this model's shapes the tiles want &lt;strong&gt;550 KiB to&lt;br&gt;
1.1 MiB per block&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On a GPU, Pallas lowers through Triton, and those tiles become &lt;strong&gt;shared memory&lt;/strong&gt;.&lt;br&gt;
Turing gives you 64 KiB per block. Ada raises the ceiling, but nowhere near a&lt;br&gt;
megabyte.&lt;/p&gt;

&lt;p&gt;So the fast path runs on &lt;strong&gt;neither card&lt;/strong&gt;. The engine computes the requirement at&lt;br&gt;
startup and refuses with the arithmetic attached, rather than dying as a cryptic&lt;br&gt;
&lt;code&gt;OutOfResources&lt;/code&gt; at the first token:&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="nf"&gt;check_w4a16_fits_scoped_memory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical consequence is that both GPU rigs serve the &lt;strong&gt;dense reference&lt;br&gt;
checkpoint&lt;/strong&gt; at 16-bit. &lt;strong&gt;Pallas is portable as an API and not portable as a&lt;br&gt;
memory model.&lt;/strong&gt; That boundary is worth knowing before planning a port around a&lt;br&gt;
fused kernel.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Bug That Returns 200 OK
&lt;/h2&gt;

&lt;p&gt;A padding-eviction bug in the KV ring cache cost a week, and it is the kind only&lt;br&gt;
Gemma 4's geometry produces.&lt;/p&gt;

&lt;p&gt;The invariant is that &lt;strong&gt;a cache index is an absolute real position, and padding&lt;br&gt;
never occupies an index a real position uses.&lt;/strong&gt; A port that right-pads into the&lt;br&gt;
512-slot ring violates it, and the failure mode is not a crash and not a NaN. It is&lt;br&gt;
a token loop — a clean HTTP &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;status: "success"&lt;/code&gt;, and output like&lt;br&gt;
&lt;code&gt;The The The The&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nothing in the logs is red. Nothing in the metrics is red. The only thing that&lt;br&gt;
catches it is a degeneracy check on the output itself, which the server now runs on&lt;br&gt;
every response.&lt;/p&gt;

&lt;p&gt;The scariest bugs in this project all returned success.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install: No Build Step
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jax[cuda13]&lt;/code&gt; supplies CUDA as wheels, so the install needs no CUDA toolkit, no&lt;br&gt;
Rust, and no compiler on the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install: 117 s, with the cache restore included
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;XLA's persistent compilation cache ports as-is. On the T4G rig it restores &lt;strong&gt;805&lt;br&gt;
files / 12 MB in 6 seconds&lt;/strong&gt; onto a fresh instance, from a box that had already&lt;br&gt;
been terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warm Up At the Shape You Measure
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;max_new_tokens&lt;/code&gt; is a &lt;code&gt;static_argnames&lt;/code&gt; entry, so &lt;code&gt;(bucket, max_tokens)&lt;/code&gt; is the&lt;br&gt;
compiled shape on every backend. A harness that does not warm up misreports the&lt;br&gt;
rig badly.&lt;/p&gt;

&lt;p&gt;On the T4G the first request off a fresh engine took &lt;strong&gt;18.06 s against 4.50 s&lt;br&gt;
warm&lt;/strong&gt; — a 4.0x whole-request ratio, measured in&lt;br&gt;
&lt;code&gt;2026-08-21-cuda13-py314-g5g&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That run also notes something worth repeating: the 56x figure from the first-serve&lt;br&gt;
baseline is &lt;strong&gt;TTFT specifically&lt;/strong&gt;, not the same measurement as the whole-request&lt;br&gt;
ratio. They are not interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sweep
&lt;/h2&gt;

&lt;p&gt;64 output tokens, concurrency 1, 3 repeats per cell, median. "Decode, gauge" is the&lt;br&gt;
engine's steady-state counter. "End-to-end" is wall time over the whole request,&lt;br&gt;
prefill included.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input tokens&lt;/th&gt;
&lt;th&gt;T4G gauge&lt;/th&gt;
&lt;th&gt;T4G end-to-end&lt;/th&gt;
&lt;th&gt;L4 gauge&lt;/th&gt;
&lt;th&gt;L4 end-to-end&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;12.9 tok/s&lt;/td&gt;
&lt;td&gt;12.43 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.5 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;46.23 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;521&lt;/td&gt;
&lt;td&gt;13.0 tok/s&lt;/td&gt;
&lt;td&gt;11.28 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.4 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;42.87 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,057&lt;/td&gt;
&lt;td&gt;12.9 tok/s&lt;/td&gt;
&lt;td&gt;8.22 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.3 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;34.57 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3,593&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.3 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;27.55 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Decode Is Flat; End-To-End Is Not
&lt;/h2&gt;

&lt;p&gt;Decode moves 0.8% across a 50x context range on the T4G and 0.4% on the L4. End-to-end&lt;br&gt;
falls hard on both.&lt;/p&gt;

&lt;p&gt;That fall is prefill being linear in the padded bucket, not decode degrading. They&lt;br&gt;
are two different claims, and conflating them makes a benchmark a lie. &lt;strong&gt;Quote the&lt;br&gt;
gauge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A cost proportional to the &lt;strong&gt;weights&lt;/strong&gt; rather than the context produces exactly this&lt;br&gt;
shape, which is why KV is not what sets decode speed on either card — despite&lt;br&gt;
Gemma 4's whole KV story.&lt;/p&gt;

&lt;p&gt;On context specifically: &lt;code&gt;MAX_MODEL_LEN=4096&lt;/code&gt; is the honest number on the T4G.&lt;br&gt;
4,105 prompt tokens serve; 5,120 fails on a prefill transient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Profile: 87% of Decode Was Not Math
&lt;/h2&gt;

&lt;p&gt;Profiling decode with xprof on the Turing card, 20 decode steps with the service&lt;br&gt;
stopped:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;🥉 T4G (SM 7.5)&lt;/th&gt;
&lt;th&gt;🥇 L4 (SM 8.9)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dtype conversion&lt;/td&gt;
&lt;td&gt;54.1%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fp32 &lt;code&gt;gemvx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;32.8%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;absent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tensor Core&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total kernel time&lt;/td&gt;
&lt;td&gt;1,466.0 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;362.8 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode, gauge&lt;/td&gt;
&lt;td&gt;12.9 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.4 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak HBM bandwidth&lt;/td&gt;
&lt;td&gt;298.083 GiB/s&lt;/td&gt;
&lt;td&gt;279.441 GiB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of bandwidth roofline&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;1,466 ms of kernels across 108 distinct kernels on a Tensor Core GPU, without one&lt;br&gt;
Tensor Core firing. More than half of decode went to converting numbers between&lt;br&gt;
formats before any math happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Obvious Explanation Was Wrong
&lt;/h2&gt;

&lt;p&gt;The obvious hypothesis was bf16 weights being converted on a chip with no bf16&lt;br&gt;
datapath. So the checkpoint was converted to float16 host-side and re-run.&lt;br&gt;
Parameter dtypes read &lt;code&gt;{'float16': 541, 'uint8': 1, 'int8': 1}&lt;/code&gt; — and &lt;strong&gt;conversion&lt;br&gt;
stayed at 54.0%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The measurement itself is solid. The same profile on a different instance, a&lt;br&gt;
different AMI and a restored cache landed at 1466.0 ms against 1467.1 ms. &lt;strong&gt;1.1 ms&lt;br&gt;
apart on 1467.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It Was the Datapath, Not the Checkpoint
&lt;/h2&gt;

&lt;p&gt;The Ada card resolves it. Converting the stored weights changed nothing because&lt;br&gt;
storage dtype was never the problem: Turing has no native bf16, and the fp32&lt;br&gt;
&lt;code&gt;gemvx&lt;/code&gt; line is the tell — XLA was round-tripping through fp32 regardless of what&lt;br&gt;
the file on disk said.&lt;/p&gt;

&lt;p&gt;Give it a card where storage and compute dtype actually match, and the 54%&lt;br&gt;
conversion and the 32.8% fp32 path vanish &lt;strong&gt;together&lt;/strong&gt;. An 87% tax gone, for 3.7x&lt;br&gt;
the throughput, and a rig sitting at its bandwidth roofline instead of 26% of it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/health&lt;/code&gt; endpoint on the L4 reports &lt;code&gt;weights=bfloat16 activations=bfloat16&lt;br&gt;
kv_cache=bfloat16 pre_ampere=false&lt;/code&gt; — storage dtype and compute dtype matching for&lt;br&gt;
the first time on this engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Survived, and Is Still Unexplained
&lt;/h2&gt;

&lt;p&gt;Tensor Core utilization is &lt;strong&gt;0.0% on the Ada card too&lt;/strong&gt; — 100 distinct kernels,&lt;br&gt;
362.8 ms of them, and not one Tensor Core firing.&lt;/p&gt;

&lt;p&gt;Removing the dtype pressure made the machine roughly four times faster without&lt;br&gt;
making it touch the hardware it was sold for. That is the open question now, and it&lt;br&gt;
is a better one than the question this started with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teardown
&lt;/h2&gt;

&lt;p&gt;Both rigs run on spot capacity and are terminated after collection. The XLA cache&lt;br&gt;
is pushed to S3 before teardown, which is what makes the 6-second restore on a&lt;br&gt;
fresh instance possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The goal of this article was to find out which parts of "it's just JAX" survive a&lt;br&gt;
move between GPU generations. The key to the solution was reading the compute dtype&lt;br&gt;
off the live device rather than a config file. The measured results were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model code ported untouched.&lt;/strong&gt; All four Gemma 4 irregularities, both
attention geometries, the KV-share map and the ring — identical source on both
cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compilation cache and static-shape discipline ported untouched&lt;/strong&gt;, including
an 805-file restore in 6 seconds onto a fresh instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fused Pallas kernel ported to neither card&lt;/strong&gt;, because it was written
against a 16 MB scratchpad and GPU shared memory is 64 KiB per block on Turing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dtype tax was 87% of decode on Turing and 0.0% on Ada&lt;/strong&gt;, worth 3.7x
throughput, with nothing red in any log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tensor Core utilization is 0.0% on both cards&lt;/strong&gt; and remains unexplained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope: two spot instances, one in &lt;code&gt;us-east-1d&lt;/code&gt;, each measured once with 3 repeats&lt;br&gt;
per sweep cell and medians reported. The two boxes differ in host architecture&lt;br&gt;
(aarch64 against x86_64) and base image as well as in GPU, so this is not a&lt;br&gt;
single-variable experiment; the payload is byte-identical across them — same build&lt;br&gt;
&lt;code&gt;51bc52c9e2e9&lt;/code&gt;, same config, same 6,155,450,950 bytes of weights — which is the&lt;br&gt;
basis for attributing the difference to the chip. The Turing profile was reproduced&lt;br&gt;
on a second instance at 1466.0 ms against 1467.1 ms; the Ada profile was measured&lt;br&gt;
once.&lt;/p&gt;

&lt;p&gt;The strategy for using MCP for Gemma 4 serving across GPU generations was validated&lt;br&gt;
with an incremental step by step approach.&lt;/p&gt;

</description>
      <category>jax</category>
      <category>gemma</category>
      <category>cuda</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
