<?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: L Anil Kumar Singha</title>
    <description>The latest articles on DEV Community by L Anil Kumar Singha (@anilloutombam).</description>
    <link>https://dev.to/anilloutombam</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F381275%2F7c05e551-19bf-4978-b673-1fbb9db63c52.jpeg</url>
      <title>DEV Community: L Anil Kumar Singha</title>
      <link>https://dev.to/anilloutombam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anilloutombam"/>
    <language>en</language>
    <item>
      <title>Incident Context: tracing production failures without guessing</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Sun, 20 Sep 2026 16:58:42 +0000</pubDate>
      <link>https://dev.to/anilloutombam/incident-context-tracing-production-failures-without-guessing-5b6i</link>
      <guid>https://dev.to/anilloutombam/incident-context-tracing-production-failures-without-guessing-5b6i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Incident Context is an incident-investigation agent for questions that cannot afford a confident but unsupported answer.&lt;/p&gt;

&lt;p&gt;During an outage, evidence is rarely contained in one document. It is spread across services and dependencies, deployments and released versions, configuration changes, previous incidents, and version-specific runbooks.&lt;/p&gt;

&lt;p&gt;Incident Context models those records as connected Sanity documents. The agent follows their relationships through a Sanity Context MCP Knowledge Base, separates confirmed evidence from inference, and preserves the source paths behind every report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A keyword search can find "timeout" in both INC-142 and INC-208. But correlation is not causation.&lt;/p&gt;

&lt;p&gt;Both incidents were production outages on consecutive days. Both involved configuration changes. Both mentioned timeouts. That is not enough to determine whether they share a root cause or require the same remediation.&lt;/p&gt;

&lt;p&gt;The useful answer is not a matching paragraph. It is a supported path across an incident, an affected service, a deployment, a configuration change, and an applicable runbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The agent models operational records as a connected graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service    → depends on → Service
Deployment → belongs to → Service
Deployment → includes   → Change
Change     → modifies   → Service
Runbook    → applies to → Service
Incident   → affects    → Service
Incident   → relates to → Deployment
Incident   → relates to → Change
Incident   → references → Runbook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you ask it to compare the two incidents, the agent follows the structured path, not keyword matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INC-208
→ affected checkout-api
→ related deployment checkout-api v2.3.0
→ included PAYMENT_TIMEOUT_MS configuration change
→ reduced from 5000ms to 1500ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This traversal becomes an evidence trail in the interface:&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%2Fyjam9osp0qqmod6nnpht.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%2Fyjam9osp0qqmod6nnpht.png" alt="Incident Context evidence report showing comparison of INC-142 and INC-208" width="800" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the evidence trail at the top. It shows the relationships the agent followed: INC-142 affected payment-api and was caused by a DB_POOL_SIZE reduction; INC-208 affected checkout-api and was caused by a PAYMENT_TIMEOUT_MS reduction. Two different paths. Two different root causes.&lt;/p&gt;

&lt;p&gt;Below that: 4 confirmed evidence items, 0 inferences, 2 sources. The model did not guess or correlate. It followed the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Operational data contains many correlations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a deployment happened before an incident&lt;/li&gt;
&lt;li&gt;two incidents mention timeouts&lt;/li&gt;
&lt;li&gt;a runbook belongs to an affected service&lt;/li&gt;
&lt;li&gt;a configuration value changed near an outage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those relationships are useful evidence. They are not automatically proof of causation.&lt;/p&gt;

&lt;p&gt;The agent preserves the distinction. Every investigation returns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A direct answer&lt;/strong&gt; — plain language, grounded in the evidence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An evidence trail&lt;/strong&gt; — the relationship path the agent followed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed evidence&lt;/strong&gt; — facts from the Knowledge Base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inferences&lt;/strong&gt; — what the model had to infer (or in this case, nothing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A recommended next step&lt;/strong&gt; — actionable based on what was discovered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sources&lt;/strong&gt; — exact Sanity paths for verification&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If Sanity Context cannot be reached, the application does not generate an unsupported fallback answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity is not being used as a generic document store behind a chat box. Its references form the investigation graph.&lt;/p&gt;

&lt;p&gt;The project defines five document types: &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;deployment&lt;/code&gt;, &lt;code&gt;change&lt;/code&gt;, &lt;code&gt;runbook&lt;/code&gt;, &lt;code&gt;incident&lt;/code&gt;. Their references encode the operational relationships.&lt;/p&gt;

&lt;p&gt;During the build, I included the references required for an investigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;service.dependencies[] → service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deployment.service → service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deployment.changes[] → change&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;change.service → service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runbook.service → service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;incident.affectedServices[] → service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;incident.relatedDeployments[] → deployment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;incident.relatedChanges[] → change&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;incident.relatedRunbook → runbook&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sanity Context distilled those connected documents into navigable, source-linked entries. At investigation time, the agent reads the Knowledge Base outline through &lt;code&gt;initial_context&lt;/code&gt;, selects relevant entries, retrieves them through Knowledge Base tools, and traces explicit relationships. The system prompt explicitly forbids turning correlation into confirmed causation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Demo
&lt;/h2&gt;

&lt;p&gt;Live application: &lt;a href="https://incident-context.vercel.app/" rel="noopener noreferrer"&gt;https://incident-context.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No login is required.&lt;/p&gt;

&lt;p&gt;The dataset represents a small production system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web-app → checkout-api → payment-api → postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try these questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What changed before INC-208?
Trace the services affected by INC-208.
Which runbook applies to INC-142?
Compare INC-142 and INC-208.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison question is the strongest demonstration. It asks the agent to distinguish incidents that share related terminology but have different evidence paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/anilloutombam/incident-context" rel="noopener noreferrer"&gt;https://github.com/anilloutombam/incident-context&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository contains two standalone applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent/   Next.js investigation interface and API
sanity/  Sanity Studio, schemas, and demo data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Runtime Architecture
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    User[Incident question] --&amp;gt; UI[Next.js interface]
    UI --&amp;gt;|POST /api/chat| API[Next.js API]
    API --&amp;gt; Loop[AI SDK agent loop]
    Loop &amp;lt;--&amp;gt;|model requests| Gemini[Gemini 3.5 Flash-Lite]
    Loop &amp;lt;--&amp;gt;|MCP tool calls| MCP[Sanity Context MCP]

    Content[Sanity Content Lake&amp;lt;br/&amp;gt;Services · Deployments · Changes&amp;lt;br/&amp;gt;Runbooks · Incidents]
    Content --&amp;gt;|builds| KB[Sanity Knowledge Base]
    KB --&amp;gt;|serves cited entries| MCP

    Loop --&amp;gt; Report[Zod-validated report]
    Report --&amp;gt;|JSON response| UI
    UI --&amp;gt; Output[Evidence trail · Confirmed evidence&amp;lt;br/&amp;gt;Inferences · Next step · Sources]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The Gemini key and Sanity organization token remain on the server. The browser receives a validated report, not credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Output
&lt;/h3&gt;

&lt;p&gt;The API validates the final response against a Zod schema instead of asking the model to return arbitrary Markdown:&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="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;evidenceTrail&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;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;confirmedEvidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inferences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recommendedNextStep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sources&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;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;path&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;This allows the interface to render relationships, facts, inferences, and sources as distinct product elements instead of parsing presentation from a generated string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Safeguards
&lt;/h3&gt;

&lt;p&gt;The deployed agent includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-client request rate limiting&lt;/li&gt;
&lt;li&gt;a 500-character question limit&lt;/li&gt;
&lt;li&gt;a 30-second request timeout&lt;/li&gt;
&lt;li&gt;explicit Gemini, Sanity, timeout, and rate-limit errors&lt;/li&gt;
&lt;li&gt;no unsupported answer when the Knowledge Base is unavailable&lt;/li&gt;
&lt;li&gt;a health endpoint at &lt;code&gt;/api/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;server-only Gemini and Sanity credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing the MCP Dependency
&lt;/h3&gt;

&lt;p&gt;The project uses the published &lt;a href="https://www.npmjs.com/package/mcp-failure-lab" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-failure-lab&lt;/code&gt;&lt;/a&gt; package to test the dependency the agent cannot operate without.&lt;/p&gt;

&lt;p&gt;The live compatibility scenario calls &lt;code&gt;initial_context&lt;/code&gt; and verifies that the Sanity Context endpoint responds within ten seconds.&lt;/p&gt;

&lt;p&gt;A separate local resilience suite covers bounded delays, hanging requests, malformed MCP responses, and connection loss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:mcp
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:mcp:live
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;:mcp:faults
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The difficult part was not connecting a model to an MCP endpoint. It was deciding what the model was allowed to claim.&lt;/p&gt;

&lt;p&gt;I initially let the model connect any dots it found—deployments before incidents, shared keywords, related services. The output looked authoritative. But INC-142 and INC-208 both mentioned timeouts and both affected the payment path. One was a database connection exhaustion issue. One was a configuration change that had been too aggressive. The model could see both, but it could not reliably say which caused which.&lt;/p&gt;

&lt;p&gt;I had to split facts from guesses in the output, enforce it in the system prompt, and then &lt;em&gt;show that separation in the UI&lt;/em&gt;. Because incident responders need to know what you are certain about.&lt;/p&gt;

&lt;p&gt;The useful answer is not a matching paragraph. It is a supported path, with source citations, and an explicit boundary between evidence and inference.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Injected the Same Failures Into 5 MCP SDKs. Here’s What I Found.</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:24:28 +0000</pubDate>
      <link>https://dev.to/anilloutombam/i-injected-the-same-failures-into-5-mcp-sdks-heres-what-i-found-2384</link>
      <guid>https://dev.to/anilloutombam/i-injected-the-same-failures-into-5-mcp-sdks-heres-what-i-found-2384</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://github.com/anilloutombam/mcp-failure-lab" rel="noopener noreferrer"&gt;MCP Failure Lab&lt;/a&gt;, a deterministic failure-injection toolkit for testing Model Context Protocol clients and servers beyond the happy path.&lt;/p&gt;

&lt;p&gt;Instead of asking whether an MCP client can successfully call a tool, I wanted to ask a different set of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when the connection disappears?&lt;/li&gt;
&lt;li&gt;What happens when a response arrives twice?&lt;/li&gt;
&lt;li&gt;What happens when the server sends malformed JSON-RPC?&lt;/li&gt;
&lt;li&gt;Does the client recover?&lt;/li&gt;
&lt;li&gt;Is the same behavior observable across SDKs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over the last few releases, I've been running the same failures against multiple official MCP SDKs.&lt;/p&gt;

&lt;p&gt;Some of the differences turned out to be more interesting than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test setup
&lt;/h2&gt;

&lt;p&gt;The important part of these tests is determinism.&lt;/p&gt;

&lt;p&gt;MCP Failure Lab deliberately produces a specific failure so that the same scenario can be replayed against different clients.&lt;/p&gt;

&lt;p&gt;For the latest duplicate-response tests, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-failure-lab@0.10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stdio&lt;/li&gt;
&lt;li&gt;Streamable HTTP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clients tested were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript SDK&lt;/td&gt;
&lt;td&gt;1.30.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python SDK&lt;/td&gt;
&lt;td&gt;2.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go SDK&lt;/td&gt;
&lt;td&gt;1.7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust SDK (&lt;code&gt;rmcp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;3.4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C# SDK&lt;/td&gt;
&lt;td&gt;2.2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The runtime environment was macOS arm64 with Node.js 26.5.0, Python 3.12.14, Go 1.27.1, Rust 1.98.1 and .NET SDK 10.0.401.&lt;/p&gt;

&lt;p&gt;The goal wasn't simply to see whether a request failed.&lt;/p&gt;

&lt;p&gt;I also wanted to know whether the &lt;strong&gt;same session remained usable afterward&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding #1: HTTP disconnect recovery differed between TypeScript and Python
&lt;/h2&gt;

&lt;p&gt;One of the earlier tests injected an HTTP disconnect and then attempted another request.&lt;/p&gt;

&lt;p&gt;The TypeScript SDK recovered and the next request succeeded.&lt;/p&gt;

&lt;p&gt;With Python SDK 2.2.0, the connection closed and the following request failed in the scenario I tested.&lt;/p&gt;

&lt;p&gt;That was interesting because the injected failure was identical, but the observable recovery behavior wasn't.&lt;/p&gt;

&lt;p&gt;I reported the reproduction upstream:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/python-sdk/issues/3522" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/python-sdk/issues/3522&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was one of the first results that convinced me that failure testing across implementations was worth pursuing.&lt;/p&gt;

&lt;p&gt;Happy-path interoperability doesn't necessarily imply recovery-path interoperability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding #2: &lt;code&gt;rmcp&lt;/code&gt; accepted a malformed JSON-RPC response
&lt;/h2&gt;

&lt;p&gt;The next finding came from the Rust SDK.&lt;/p&gt;

&lt;p&gt;MCP Failure Lab's &lt;code&gt;malformed_message&lt;/code&gt; tool can intentionally return a JSON-RPC response containing both &lt;code&gt;result&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&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;"content"&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;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&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;"error"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32603&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="s2"&gt;"injected error"&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;A JSON-RPC response should contain either &lt;code&gt;result&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;, not both.&lt;/p&gt;

&lt;p&gt;I expected the client to reject it.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;rmcp&lt;/code&gt; 3.4.0, however, &lt;code&gt;call_tool&lt;/code&gt; returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result was accepted and the &lt;code&gt;error&lt;/code&gt; field was effectively ignored.&lt;/p&gt;

&lt;p&gt;I reproduced this over both stdio and Streamable HTTP.&lt;/p&gt;

&lt;p&gt;A subsequent normal request also succeeded, so this wasn't a connection-recovery problem. It was a message-validation difference.&lt;/p&gt;

&lt;p&gt;The C# MCP SDK 2.2.0 rejected the same malformed wire response.&lt;/p&gt;

&lt;p&gt;The likely Rust parsing path involves the untagged &lt;code&gt;JsonRpcMessage&lt;/code&gt; representation: the response can deserialize into the &lt;code&gt;JsonRpcResponse&lt;/code&gt; shape while Serde ignores the unexpected &lt;code&gt;error&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;I reported that upstream as well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/rust-sdk/issues/1283" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/rust-sdk/issues/1283&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is exactly the kind of issue that's difficult to notice when every server you're testing against sends valid messages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding #3: Five SDKs survived a duplicate response, but exposed it differently
&lt;/h2&gt;

&lt;p&gt;For MCP Failure Lab 0.10.0, I added a focused duplicate-response compatibility test.&lt;/p&gt;

&lt;p&gt;The sequence was deliberately simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client
  |
  | tools/call
  v
server
  |
  | response #1
  | response #1 again
  v
client
  |
  | ping using the SAME session
  v
server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A passing recovery check required the final &lt;code&gt;ping&lt;/code&gt; to succeed.&lt;/p&gt;

&lt;p&gt;Here were the results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;stdio&lt;/th&gt;
&lt;th&gt;Streamable HTTP&lt;/th&gt;
&lt;th&gt;Duplicate behavior&lt;/th&gt;
&lt;th&gt;Same-session ping&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Unknown-response-ID diagnostic&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No call-level duplicate error observed&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No call-level duplicate error observed&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No call-level duplicate error observed&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;No call-level duplicate error observed&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The good news is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;all five clients remained usable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of them allowed the duplicate response to corrupt the session.&lt;/p&gt;

&lt;p&gt;But their observable behavior wasn't identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript made the duplicate visible
&lt;/h3&gt;

&lt;p&gt;The TypeScript SDK accepted the first response normally.&lt;/p&gt;

&lt;p&gt;When the duplicate arrived, that request ID was no longer pending, so the SDK reported the repeated response through its error callback as a response for an unknown ID.&lt;/p&gt;

&lt;p&gt;The diagnostic did not close the session.&lt;/p&gt;

&lt;p&gt;The next &lt;code&gt;ping&lt;/code&gt; succeeded.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other four didn't expose a call-level error
&lt;/h3&gt;

&lt;p&gt;Python, Go, Rust and C# also returned the first result and successfully completed the following &lt;code&gt;ping&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But my harness didn't observe an equivalent call-level duplicate diagnostic from those clients.&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean they failed to detect the duplicate.&lt;/p&gt;

&lt;p&gt;There are several possible implementations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response arrives
       |
lookup pending request ID
       |
       +-- found --&amp;gt; resolve request
       |
       +-- missing --&amp;gt; report?
                      log?
                      discard?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current experiment only establishes what was observable through the public paths instrumented by the harness.&lt;/p&gt;

&lt;p&gt;I did not instrument private SDK internals, so I can't claim that those four SDKs silently ignored the response internally.&lt;/p&gt;

&lt;p&gt;That's a distinction worth preserving.&lt;/p&gt;




&lt;h2&gt;
  
  
  A "pass" isn't always the whole result
&lt;/h2&gt;

&lt;p&gt;This has been one of the more useful lessons from these experiments.&lt;/p&gt;

&lt;p&gt;If I reduced the duplicate-response test to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript: PASS
Python:     PASS
Go:         PASS
Rust:       PASS
C#:         PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd lose most of the interesting information.&lt;/p&gt;

&lt;p&gt;All five passed the recovery invariant.&lt;/p&gt;

&lt;p&gt;But one implementation exposed an unexpected response to the application while the others didn't expose an equivalent call-level diagnostic in my harness.&lt;/p&gt;

&lt;p&gt;That matters when you're debugging a production MCP system.&lt;/p&gt;

&lt;p&gt;Two clients can both recover successfully while giving developers very different visibility into what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why test the request after the failure?
&lt;/h2&gt;

&lt;p&gt;I've started treating this as one of the most important parts of the test.&lt;/p&gt;

&lt;p&gt;Triggering a fault only tells you what happened during the fault.&lt;/p&gt;

&lt;p&gt;It doesn't tell you whether the protocol state survived it.&lt;/p&gt;

&lt;p&gt;So a typical Failure Lab scenario now looks conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normal operation
      ↓
inject deterministic failure
      ↓
observe client behavior
      ↓
send normal request
      ↓
verify recovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout that returns an error but leaves the session healthy is very different from a timeout that poisons every request afterward.&lt;/p&gt;

&lt;p&gt;Likewise, rejecting malformed JSON-RPC is different from accepting it while keeping the connection alive.&lt;/p&gt;

&lt;p&gt;The post-failure request gives us that second dimension.&lt;/p&gt;




&lt;h2&gt;
  
  
  These aren't necessarily SDK bugs
&lt;/h2&gt;

&lt;p&gt;Another thing I've tried to avoid is turning every behavioral difference into an upstream issue.&lt;/p&gt;

&lt;p&gt;Different observable behavior isn't automatically incorrect behavior.&lt;/p&gt;

&lt;p&gt;For example, the duplicate-response test currently shows an interoperability difference in diagnostics, but all five clients recover.&lt;/p&gt;

&lt;p&gt;Before calling that a bug, the next step is to trace how each SDK handles a response whose request ID has already been completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;duplicate response
       ↓
request ID no longer pending
       ↓
what does the SDK do?
       ↓
report / log / discard / reject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an implementation violates a protocol requirement, that's worth reporting.&lt;/p&gt;

&lt;p&gt;If it's simply an intentional observability choice, it's better documented as a compatibility difference.&lt;/p&gt;

&lt;p&gt;The malformed Rust response was different: there was a concrete validation behavior to reproduce and compare, so I opened an upstream issue.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm testing next
&lt;/h2&gt;

&lt;p&gt;The next step isn't simply adding more SDK names to a table.&lt;/p&gt;

&lt;p&gt;I'm more interested in expanding the failure dimensions.&lt;/p&gt;

&lt;p&gt;Some of the scenarios I'm looking at include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;late responses after client timeout&lt;/li&gt;
&lt;li&gt;session loss&lt;/li&gt;
&lt;li&gt;server restart and recovery&lt;/li&gt;
&lt;li&gt;duplicate SSE delivery&lt;/li&gt;
&lt;li&gt;malformed or partial transport messages&lt;/li&gt;
&lt;li&gt;cancellation races&lt;/li&gt;
&lt;li&gt;responses with unexpected IDs&lt;/li&gt;
&lt;li&gt;transient HTTP failures followed by recovery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the same failure can be replayed against multiple implementations.&lt;/p&gt;

&lt;p&gt;That should eventually produce something more useful than a basic compatibility matrix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a map of how MCP implementations behave when the protocol stops being perfect.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducing the tests
&lt;/h2&gt;

&lt;p&gt;MCP Failure Lab is open source:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab" rel="noopener noreferrer"&gt;https://github.com/anilloutombam/mcp-failure-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The published package is available through npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-failure-lab@0.10.0 serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The focused 0.10.0 duplicate-response compatibility report, including exact SDK and runtime versions, is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab/blob/main/docs/compatibility/v0.10.0.md" rel="noopener noreferrer"&gt;https://github.com/anilloutombam/mcp-failure-lab/blob/main/docs/compatibility/v0.10.0.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project documentation is also available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mcplab.dev/" rel="noopener noreferrer"&gt;https://mcplab.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're maintaining an MCP client, SDK or host and there is a failure mode you think would be useful to reproduce deterministically, I'd be interested in testing it.&lt;/p&gt;

&lt;p&gt;The happy path tells us whether implementations can talk to each other.&lt;/p&gt;

&lt;p&gt;I'm more interested in what happens after something goes wrong.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>opensource</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>What Happens When an MCP Tool Call Never Returns?</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:57:27 +0000</pubDate>
      <link>https://dev.to/anilloutombam/what-happens-when-an-mcp-tool-call-never-returns-3gdd</link>
      <guid>https://dev.to/anilloutombam/what-happens-when-an-mcp-tool-call-never-returns-3gdd</guid>
      <description>&lt;p&gt;A normal MCP tool call is straightforward: the client sends a request, the server runs the tool, and a response comes back.&lt;/p&gt;

&lt;p&gt;A hang is different. The server accepts the call but never returns a result or an error.&lt;/p&gt;

&lt;p&gt;I wanted to test how an MCP client behaves in that case without relying on network tricks or manually killing a process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing a hang
&lt;/h2&gt;

&lt;p&gt;MCP Failure Lab has a &lt;code&gt;hang&lt;/code&gt; tool specifically for this.&lt;/p&gt;

&lt;p&gt;The behavior is intentionally simple: once called, it doesn't resolve.&lt;/p&gt;

&lt;p&gt;That gives us a repeatable failure instead of trying to approximate one with a very long delay.&lt;/p&gt;

&lt;p&gt;A scenario for it looks like this:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hung tool call times out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"call"&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;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hang"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeoutMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expect"&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;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeout"&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 server behavior and the expected client behavior are separate here.&lt;/p&gt;

&lt;p&gt;The server hangs.&lt;/p&gt;

&lt;p&gt;The client is expected to time out after one second.&lt;/p&gt;

&lt;p&gt;That distinction matters because a hung server doesn't produce a timeout by itself. The timeout has to be enforced by the client or by something around the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hang vs delay
&lt;/h2&gt;

&lt;p&gt;I originally treated a long delay as being close enough to a hang, but they're useful for testing different things.&lt;/p&gt;

&lt;p&gt;A delayed call still has a completion point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request ---- 5 seconds ----&amp;gt; response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hung call doesn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request --------------------&amp;gt;
        --------------------&amp;gt;
        --------------------&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a delay, a response can still arrive after the client has timed out.&lt;/p&gt;

&lt;p&gt;With a hang, there is no eventual response.&lt;/p&gt;

&lt;p&gt;That difference starts to matter when testing cancellation and cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout only tells you what the client saw
&lt;/h2&gt;

&lt;p&gt;Suppose a client calls a tool and gets a timeout.&lt;/p&gt;

&lt;p&gt;It's tempting to treat that as meaning the operation failed.&lt;/p&gt;

&lt;p&gt;That's not necessarily true.&lt;/p&gt;

&lt;p&gt;Consider a tool that changes some state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client                    server
   |                         |
   | ---- request ----------&amp;gt;|
   |                         | changes state
   |                         |
   |       response lost     X
   |
   | ---- timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client saw a timeout, but the server may have already completed the operation.&lt;/p&gt;

&lt;p&gt;If the client automatically retries, the operation might run twice.&lt;/p&gt;

&lt;p&gt;This is one reason I'm interested in keeping the observed outcome separate from server state in Failure Lab.&lt;/p&gt;

&lt;p&gt;It's also where simple timeout testing stops being enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should happen after the timeout?
&lt;/h2&gt;

&lt;p&gt;There are a few things worth checking beyond whether a timeout was thrown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the original operation cancelled?&lt;/li&gt;
&lt;li&gt;Is the MCP session still usable?&lt;/li&gt;
&lt;li&gt;Can another tool call succeed?&lt;/li&gt;
&lt;li&gt;Does the client retry?&lt;/li&gt;
&lt;li&gt;Can you determine whether the original operation changed state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is particularly useful for tools with side effects.&lt;/p&gt;

&lt;p&gt;Failure Lab supports an independent &lt;code&gt;observe&lt;/code&gt; call for scenarios where state needs to be checked after the main call.&lt;/p&gt;

&lt;p&gt;That lets a test distinguish between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client observed: timeout
server state: unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client observed: timeout
server state: changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are very different outcomes even though the client reported the same error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the failure deterministic
&lt;/h2&gt;

&lt;p&gt;The main reason I built the hang fault wasn't to simulate an unreliable network.&lt;/p&gt;

&lt;p&gt;It was to remove the unreliable part from the test.&lt;/p&gt;

&lt;p&gt;If the server hangs deterministically, I can run different clients against the same behavior and compare what they do.&lt;/p&gt;

&lt;p&gt;The path still goes through MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scenario
   ↓
MCP client
   ↓
transport
   ↓
MCP server
   ↓
hang
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the failure is controlled.&lt;/p&gt;

&lt;p&gt;That makes bugs around timeout handling much easier to reproduce.&lt;/p&gt;

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

&lt;p&gt;The project is open source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-failure-lab demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/anilloutombam/mcp-failure-lab" rel="noopener noreferrer"&gt;https://github.com/anilloutombam/mcp-failure-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm working through other failure cases as well, particularly cancellation, session loss, malformed responses, and cases where the client reports failure even though the server changed state.&lt;/p&gt;

&lt;p&gt;If you've hit an MCP failure that was difficult to reproduce, open an issue. I'd rather turn real failure cases into deterministic scenarios than invent them.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>typescript</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing MCP Failure Lab: Testing MCP Beyond the Happy Path</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Sat, 15 Aug 2026 19:21:09 +0000</pubDate>
      <link>https://dev.to/anilloutombam/introducing-mcp-failure-lab-testing-mcp-beyond-the-happy-path-2m22</link>
      <guid>https://dev.to/anilloutombam/introducing-mcp-failure-lab-testing-mcp-beyond-the-happy-path-2m22</guid>
      <description>&lt;p&gt;Most MCP examples focus on successful tool calls. That is useful for getting started, but production systems also need to handle calls that stall, exceed their timeout, get cancelled, or lose their connection.&lt;/p&gt;

&lt;p&gt;I started building &lt;strong&gt;MCP Failure Lab&lt;/strong&gt; to make those failure paths controlled, repeatable, and easier to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP Failure Lab?
&lt;/h2&gt;

&lt;p&gt;MCP Failure Lab is an open-source TypeScript toolkit for testing how MCP clients and related systems respond to failure conditions.&lt;/p&gt;

&lt;p&gt;The project currently includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic &lt;code&gt;delay&lt;/code&gt;, &lt;code&gt;hang&lt;/code&gt;, and &lt;code&gt;disconnect&lt;/code&gt; fault tools&lt;/li&gt;
&lt;li&gt;A TypeScript CLI and stdio MCP server&lt;/li&gt;
&lt;li&gt;A code-first scenario runner&lt;/li&gt;
&lt;li&gt;Success, error, timeout, and duration assertions&lt;/li&gt;
&lt;li&gt;Unit and integration tests&lt;/li&gt;
&lt;li&gt;CI, coverage, dependency, and security checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on unreliable network conditions or arbitrary sleeps, the goal is to provide explicit failure behavior that can be reproduced in a test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why build this?
&lt;/h2&gt;

&lt;p&gt;A successful tool call only proves the happy path.&lt;/p&gt;

&lt;p&gt;It does not tell us whether a client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforces request timeouts&lt;/li&gt;
&lt;li&gt;Cancels work correctly&lt;/li&gt;
&lt;li&gt;Cleans up resources after failure&lt;/li&gt;
&lt;li&gt;Distinguishes tool errors from transport failures&lt;/li&gt;
&lt;li&gt;Recovers after a connection is interrupted&lt;/li&gt;
&lt;li&gt;Produces useful diagnostic information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These behaviors become increasingly important as MCP integrations move beyond local experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current architecture
&lt;/h2&gt;

&lt;p&gt;Fault tools are registered directly on an MCP server. Tests connect through MCP transports and exercise the same protocol path used by real tool calls.&lt;/p&gt;

&lt;p&gt;The scenario runner adds a small testing layer around those calls. A scenario describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tool to call&lt;/li&gt;
&lt;li&gt;Which arguments to provide&lt;/li&gt;
&lt;li&gt;The request timeout&lt;/li&gt;
&lt;li&gt;The expected outcome&lt;/li&gt;
&lt;li&gt;Optional duration limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runner records the observed result and evaluates the configured assertions.&lt;/p&gt;

&lt;p&gt;This keeps the project code-first, type-safe, and suitable for existing TypeScript test suites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What comes next?
&lt;/h2&gt;

&lt;p&gt;The project is still early, and there are several areas where contributions would be valuable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab/issues/9" rel="noopener noreferrer"&gt;Add end-to-end CLI coverage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab/issues/15" rel="noopener noreferrer"&gt;Add structured scenario reports&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab/issues/16" rel="noopener noreferrer"&gt;Define a target-client adapter contract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab/issues/17" rel="noopener noreferrer"&gt;Add Streamable HTTP support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each issue includes its expected behavior, acceptance criteria, limitations, and relevant files.&lt;/p&gt;

&lt;p&gt;Before starting substantial work, please comment on the issue with your proposed approach and ask to be assigned. This helps avoid duplicated work and gives us a place to discuss the design first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributions are welcome
&lt;/h2&gt;

&lt;p&gt;If you work with MCP, TypeScript, resilience testing, or developer tooling, I would value your feedback.&lt;/p&gt;

&lt;p&gt;You can contribute code, review the design, improve documentation, test the project, or suggest failure modes that are worth supporting.&lt;/p&gt;

&lt;p&gt;MCP Failure Lab is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anilloutombam/mcp-failure-lab" rel="noopener noreferrer"&gt;https://github.com/anilloutombam/mcp-failure-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the project is useful to you, consider starring it, opening a discussion, or picking up one of the contributor-ready issues. I would be glad to build it with others.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>mcp</category>
      <category>testing</category>
    </item>
    <item>
      <title>How I Designed a Production Global Search System on PostgreSQL</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 14 Aug 2026 16:41:28 +0000</pubDate>
      <link>https://dev.to/anilloutombam/how-i-designed-a-production-global-search-system-on-postgresql-113</link>
      <guid>https://dev.to/anilloutombam/how-i-designed-a-production-global-search-system-on-postgresql-113</guid>
      <description>&lt;p&gt;Search often starts as a text box.&lt;/p&gt;

&lt;p&gt;Then the requirements arrive.&lt;/p&gt;

&lt;p&gt;Users want to search by a specific field. Then multiple fields. Then exclusions. Date ranges. Multi-select filters. &lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;NOT&lt;/code&gt;. Parentheses. Nested conditions.&lt;/p&gt;

&lt;p&gt;Eventually, this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;turns into this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  "payment approval"
  OR (
    department:finance
    AND (status:open OR status:pending)
  )
)
AND NOT (
  owner:system
  OR category:archived
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, you're no longer building a text box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're designing a query language.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I ran into exactly this while designing a global search system for a production application.&lt;/p&gt;

&lt;p&gt;From the user's perspective, I wanted the experience to remain simple: one global search surface, similar to the issue-search experience developers are familiar with in tools like GitHub or GitLab.&lt;/p&gt;

&lt;p&gt;Underneath, the requirements evolved into a Lucene-style query model supporting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;free-text search&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key:value&lt;/code&gt; filters&lt;/li&gt;
&lt;li&gt;quoted phrases&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, and &lt;code&gt;NOT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;parentheses and grouping&lt;/li&gt;
&lt;li&gt;recursively nested expressions&lt;/li&gt;
&lt;li&gt;text filters&lt;/li&gt;
&lt;li&gt;date ranges&lt;/li&gt;
&lt;li&gt;multi-select fields&lt;/li&gt;
&lt;li&gt;column-level filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there was one requirement I considered just as important as the query language itself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Users shouldn't need to understand the query language to use it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the system supported two ways of expressing the same search intent.&lt;/p&gt;

&lt;p&gt;Power users could write the query directly.&lt;/p&gt;

&lt;p&gt;Everyone else could build it through column-level filters. Depending on the field, the UI provided a text input, date-range picker, multi-select dropdown, or Boolean condition and constructed the expression as the user filtered.&lt;/p&gt;

&lt;p&gt;Then the production dataset grew, and a second problem emerged:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;free-text search performance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That eventually led to another architectural decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do I introduce a dedicated search engine, or can PostgreSQL continue owning search?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I kept PostgreSQL.&lt;/p&gt;

&lt;p&gt;After profiling the expensive path and moving the free-text workload to a GIN-backed indexing strategy, measured search latency dropped from roughly &lt;strong&gt;40ms to 12ms&lt;/strong&gt; on a production dataset containing more than &lt;strong&gt;20,000 records&lt;/strong&gt; at the time.&lt;/p&gt;

&lt;p&gt;The performance number is useful, but it isn't the most interesting part of the story.&lt;/p&gt;

&lt;p&gt;What interested me more was how one search box ended up touching UX design, language parsing, recursive data structures, database performance, and infrastructure trade-offs.&lt;/p&gt;




&lt;h2&gt;
  
  
  One search box, different levels of precision
&lt;/h2&gt;

&lt;p&gt;I didn't want advanced search to make basic search harder.&lt;/p&gt;

&lt;p&gt;Someone should always be able to type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and search normally.&lt;/p&gt;

&lt;p&gt;Nothing else should be required.&lt;/p&gt;

&lt;p&gt;A user who knows exactly what they're looking for can be more specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;owner:"John Doe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free text and structured conditions can be combined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"payment approval" AND status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And power users can go much further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;("payment approval" OR refund)
AND (status:open OR status:pending)
AND NOT owner:system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave the search experience &lt;strong&gt;progressive complexity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don't need to understand the language to start searching.&lt;/p&gt;

&lt;p&gt;But when you need more precision, the language is there.&lt;/p&gt;

&lt;p&gt;The next problem was usability.&lt;/p&gt;

&lt;p&gt;Most users shouldn't have to learn that syntax.&lt;/p&gt;

&lt;p&gt;So I didn't make them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The UI could write the query for you
&lt;/h2&gt;

&lt;p&gt;Alongside the global search input, I designed column-level filtering.&lt;/p&gt;

&lt;p&gt;The control shown to the user depended on the type of data being filtered.&lt;/p&gt;

&lt;p&gt;For text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner
┌────────────────────────┐
│ John                   │
└────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created Date

From: 01 Aug 2026
To:   14 Aug 2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For predefined values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status

✓ Open
✓ Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditions could also include or exclude values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status
AND
Open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status
NOT
Archived
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As users interacted with those controls, the application constructed the corresponding search expression.&lt;/p&gt;

&lt;p&gt;A user might interact with 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;Search: payment approval

Status:
  ✓ Open
  ✓ Pending

Owner:
  NOT System

Created:
  Aug 1 → Aug 14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the system represented the same intent as something equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"payment approval"
AND (status:open OR status:pending)
AND NOT owner:system
AND createdDate:[2026-08-01 TO 2026-08-14]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user didn't need to write that.&lt;/p&gt;

&lt;p&gt;The UI did it for them.&lt;/p&gt;

&lt;p&gt;That became an important principle in the design:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A system can support a sophisticated query language without requiring users to speak that language.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Two interfaces, one search model
&lt;/h2&gt;

&lt;p&gt;I didn't want the visual filter builder and the advanced query input to become two independent search implementations.&lt;/p&gt;

&lt;p&gt;That would eventually create two sets of semantics, two places for bugs, and two implementations that could disagree about what the same filter means.&lt;/p&gt;

&lt;p&gt;Instead, both interfaces converged on the same search model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ┌─────────────────────────┐
        │    Global Search Box    │
        │                         │
        │ Lucene-style syntax     │
        └────────────┬────────────┘
                     │
                     ▼
              Search Expression
                     ▲
                     │
        ┌────────────┴────────────┐
        │ Column Filter Builder   │
        │                         │
        │ • Text input            │
        │ • Date range            │
        │ • Multi-select          │
        │ • AND / NOT             │
        └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the expression entered the same processing pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search Expression
       │
       ▼
   Tokenizer
       │
       ▼
     Parser
       │
       ▼
      AST
       │
       ▼
   Validation
       │
       ▼
 Query Compiler
       │
       ▼
 Prisma / SQL
       │
       ▼
  PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There weren't two search engines.&lt;/p&gt;

&lt;p&gt;There were &lt;strong&gt;two ways of expressing the same search intent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction kept the architecture much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  When search became a language
&lt;/h2&gt;

&lt;p&gt;Boolean operators changed the nature of the problem.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status:open OR status:pending AND priority:high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With normal Boolean precedence, that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status:open
OR
(status:pending AND priority:high)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which is different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(status:open OR status:pending)
AND priority:high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add &lt;code&gt;NOT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(status:open OR status:pending)
AND NOT owner:system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then nested grouping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  status:open
  OR (
    status:pending
    AND (
      priority:high
      OR priority:critical
    )
  )
)
AND NOT owner:system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, &lt;code&gt;.split("AND")&lt;/code&gt; isn't an architecture.&lt;/p&gt;

&lt;p&gt;Neither is continuing to grow a regular expression until nobody wants to touch it.&lt;/p&gt;

&lt;p&gt;The requirement had crossed an architectural boundary.&lt;/p&gt;

&lt;p&gt;I needed a parser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Treating search like a small compiler
&lt;/h2&gt;

&lt;p&gt;I ended up treating the search input similarly to a small language-processing pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw Query
    │
    ▼
Tokenizer
    │
    ▼
 Parser
    │
    ▼
   AST
    │
    ▼
Validator
    │
    ▼
Compiler
    │
    ▼
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage had a narrow responsibility.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;tokenizer&lt;/strong&gt; identified meaningful pieces of the language.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;parser&lt;/strong&gt; determined how those pieces related to each other.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AST&lt;/strong&gt; represented the user's intent independently of the original string.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;validator&lt;/strong&gt; ensured that only supported fields and operations could be used.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;compiler&lt;/strong&gt; translated that structured representation into database conditions.&lt;/p&gt;

&lt;p&gt;That separation mattered.&lt;/p&gt;

&lt;p&gt;The parser didn't need to understand how PostgreSQL executed search.&lt;/p&gt;

&lt;p&gt;PostgreSQL didn't need to understand our user-facing syntax.&lt;/p&gt;

&lt;p&gt;And the database layer didn't need to repeatedly reinterpret an arbitrary search string.&lt;/p&gt;




&lt;h2&gt;
  
  
  From a query string to structured data
&lt;/h2&gt;

&lt;p&gt;Take:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(payment OR refund) AND status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tokenizer can identify units such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LPAREN
TEXT(payment)
OR
TEXT(refund)
RPAREN
AND
FIELD(status, open)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quoted values remain intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;owner:"John Doe"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as do quoted free-text phrases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"payment approval"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parentheses and Boolean operators remain explicit because they determine the structure of the expression.&lt;/p&gt;

&lt;p&gt;Now consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(payment OR refund) AND NOT status:archived
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of carrying that string through the application, the parser can represent its meaning as a tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 AND
                /   \
              OR     NOT
             /  \      \
      payment  refund   FIELD
                         │
                   status:archived
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified TypeScript representation could look like:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SearchNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;value&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;field&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="nl"&gt;value&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;and&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;or&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchNode&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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;operand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchNode&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 exact TypeScript isn't the interesting part.&lt;/p&gt;

&lt;p&gt;The boundary is.&lt;/p&gt;

&lt;p&gt;The user's arbitrary string has become &lt;strong&gt;structured data&lt;/strong&gt; that can be validated, transformed, tested, and compiled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Supporting N-level nested search
&lt;/h2&gt;

&lt;p&gt;I didn't want the grammar to contain a hard-coded concept of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;level 1
level 2
level 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expressions are recursive.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expression :=
    FreeText
  | FieldCondition
  | NOT Expression
  | (Expression)
  | Expression AND Expression
  | Expression OR Expression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An expression can contain another expression, which can contain another expression.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A AND (B OR (C AND (D OR (E AND NOT F))))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't require special handling.&lt;/p&gt;

&lt;p&gt;It's simply a deeper tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             AND
            /   \
           A     OR
                /  \
               B    AND
                   /   \
                  C     OR
                       /  \
                      D    AND
                          /   \
                         E    NOT
                               │
                               F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler recursively walks the tree.&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no fixed application-level nesting depth in the search grammar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That lets users continue composing conditions when they need more precision instead of running into an arbitrary maximum number of filter groups.&lt;/p&gt;

&lt;p&gt;Of course, that doesn't mean execution has infinite resources.&lt;/p&gt;

&lt;p&gt;Request size, runtime behavior, query complexity, timeouts, and infrastructure still create practical boundaries.&lt;/p&gt;

&lt;p&gt;Those are operational constraints rather than artificial limits in the grammar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Search fields are an API, not database columns
&lt;/h2&gt;

&lt;p&gt;Supporting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;raises another question.&lt;/p&gt;

&lt;p&gt;What happens if someone tries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;someInternalDatabaseField:value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A search language shouldn't automatically become an interface to the underlying database schema.&lt;/p&gt;

&lt;p&gt;Instead, searchable fields should be explicitly supported.&lt;/p&gt;

&lt;p&gt;A simplified mapping might look like:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchableFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* internal mapping */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* internal mapping */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* internal mapping */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* internal mapping */&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 actual implementation can contain considerably more metadata because different fields require different controls, validation rules, and database behavior.&lt;/p&gt;

&lt;p&gt;But the principle is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The search language has its own contract.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A field becomes searchable because the application deliberately exposes it, not because a similarly named PostgreSQL column happens to exist.&lt;/p&gt;

&lt;p&gt;That also gives the database schema room to evolve without necessarily breaking the query syntax users already know.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then the dataset grew
&lt;/h2&gt;

&lt;p&gt;The most complicated-looking part of this architecture wasn't where I encountered the main performance problem.&lt;/p&gt;

&lt;p&gt;The parser worked.&lt;/p&gt;

&lt;p&gt;Recursive expressions worked.&lt;/p&gt;

&lt;p&gt;Structured filters worked.&lt;/p&gt;

&lt;p&gt;The problem appeared somewhere much less exotic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;free-text search.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the dataset was smaller, the existing implementation performed well enough.&lt;/p&gt;

&lt;p&gt;Then production data accumulated.&lt;/p&gt;

&lt;p&gt;The same kind of free-text search now had increasingly more data to work through.&lt;/p&gt;

&lt;p&gt;The code hadn't necessarily regressed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workload had changed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's an important distinction.&lt;/p&gt;

&lt;p&gt;A query that behaves perfectly well at one data volume can have very different characteristics as that volume grows.&lt;/p&gt;

&lt;p&gt;So I didn't start by optimizing the parser.&lt;/p&gt;

&lt;p&gt;I started by measuring the database path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measure before changing the architecture
&lt;/h2&gt;

&lt;p&gt;I wanted to understand what PostgreSQL was actually doing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; was part of that investigation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was looking for answers to concrete questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many rows are being examined?&lt;/li&gt;
&lt;li&gt;Are we scanning more data than necessary?&lt;/li&gt;
&lt;li&gt;Are the indexes I expect actually being used?&lt;/li&gt;
&lt;li&gt;Which part of the generated condition is expensive?&lt;/li&gt;
&lt;li&gt;Where is execution time being spent?&lt;/li&gt;
&lt;li&gt;How does the execution plan behave as the dataset grows?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction mattered.&lt;/p&gt;

&lt;p&gt;The system contained sophisticated application code, but optimizing the most complicated-looking code wouldn't help if PostgreSQL was doing the expensive work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Optimize what the measurements tell you is expensive, not what looks complicated.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why GIN fit the workload
&lt;/h2&gt;

&lt;p&gt;Structured filters and free text have different access patterns.&lt;/p&gt;

&lt;p&gt;A B-tree index is a natural fit for many structured conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OPEN'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free-text search asks a different kind of question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which records contain these searchable terms?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where an inverted index becomes useful.&lt;/p&gt;

&lt;p&gt;PostgreSQL already provides this capability through &lt;strong&gt;GIN — Generalized Inverted Index&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At a high level, an inverted index maintains relationships between searchable terms and the records containing them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment ─────► Record 12
        ├────► Record 48
        └────► Record 91

approval ────► Record 12
         └───► Record 103

invoice ─────► Record 27
        └────► Record 91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified PostgreSQL example might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_records_search&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;searchable_text&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;with a corresponding full-text condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;searchable_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'payment approval'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production implementation was more involved because free text could appear alongside structured conditions and recursively nested Boolean expressions.&lt;/p&gt;

&lt;p&gt;Architecturally, though, the responsibilities remained separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                     Search AST
                         │
               ┌─────────┴─────────┐
               ▼                   ▼
         Structured            Free Text
          Conditions              Search
               │                   │
               ▼                   ▼
          Appropriate             GIN
           indexes               index
               │                   │
               └─────────┬─────────┘
                         ▼
                     PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query language remained expressive while the expensive free-text path could be optimized independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  From roughly 40ms to 12ms
&lt;/h2&gt;

&lt;p&gt;After optimizing the free-text path around the appropriate PostgreSQL indexing strategy, measured search latency moved approximately from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before       After
 ~40ms   →    ~12ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's roughly a &lt;strong&gt;70% reduction&lt;/strong&gt; for the workload we measured.&lt;/p&gt;

&lt;p&gt;The production dataset contained more than &lt;strong&gt;20,000 records&lt;/strong&gt; at the time.&lt;/p&gt;

&lt;p&gt;That result needs context.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add GIN and PostgreSQL becomes 70% faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Database performance depends on schema, data distribution, query shape, hardware, cache state, and workload.&lt;/p&gt;

&lt;p&gt;The useful part is the process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observe
   ↓
Measure
   ↓
Inspect the execution plan
   ↓
Understand the access pattern
   ↓
Choose the appropriate index
   ↓
Measure again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GIN fit our access pattern.&lt;/p&gt;

&lt;p&gt;The decision came from the workload, not the other way around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I didn't introduce Elasticsearch
&lt;/h2&gt;

&lt;p&gt;Once free-text search became a performance discussion, a dedicated search engine was an obvious option to consider.&lt;/p&gt;

&lt;p&gt;Elasticsearch could handle this kind of search.&lt;/p&gt;

&lt;p&gt;But capability alone wasn't enough reason for me to introduce another system.&lt;/p&gt;

&lt;p&gt;PostgreSQL was already part of the production architecture and remained the source of truth.&lt;/p&gt;

&lt;p&gt;Keeping search there meant the architecture remained roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Application
                   │
                   ▼
              PostgreSQL
             ┌─────┴─────┐
             │           │
       Structured     Free Text
         Search         Search
             │           │
        Appropriate     GIN
         indexes        index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Introducing another search datastore changes that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   Application
                        │
                ┌───────┴───────┐
                ▼               ▼
           PostgreSQL      Search Engine
                │               ▲
                └───── Sync ────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That synchronization arrow looks small on an architecture diagram.&lt;/p&gt;

&lt;p&gt;Operationally, it isn't.&lt;/p&gt;

&lt;p&gt;It introduces concerns around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;data synchronization&lt;/li&gt;
&lt;li&gt;eventual consistency&lt;/li&gt;
&lt;li&gt;failed indexing operations&lt;/li&gt;
&lt;li&gt;retry and replay&lt;/li&gt;
&lt;li&gt;index rebuilding&lt;/li&gt;
&lt;li&gt;mappings&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;deployments and upgrades&lt;/li&gt;
&lt;li&gt;infrastructure cost&lt;/li&gt;
&lt;li&gt;another failure domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those make Elasticsearch a bad choice.&lt;/p&gt;

&lt;p&gt;They're simply part of the cost of owning another system.&lt;/p&gt;

&lt;p&gt;For our workload, PostgreSQL was already there, GIN matched the free-text access pattern, and the resulting performance satisfied the requirements.&lt;/p&gt;

&lt;p&gt;Adding another datastore would have increased the operational surface without solving a problem we still had.&lt;/p&gt;

&lt;p&gt;So I didn't add one.&lt;/p&gt;




&lt;h2&gt;
  
  
  GIN was an architectural decision, not just an index
&lt;/h2&gt;

&lt;p&gt;It's easy to summarize the change as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I added a GIN index and made search faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's technically part of what happened.&lt;/p&gt;

&lt;p&gt;But the architectural decision was larger.&lt;/p&gt;

&lt;p&gt;I effectively had three directions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  Growing Search Cost
                         │
            ┌────────────┼────────────┐
            ▼            ▼            ▼
         Leave it     Optimize     Introduce
          as-is       PostgreSQL   Search Engine
                          │
                          ▼
                         GIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing nothing wasn't going to age well.&lt;/p&gt;

&lt;p&gt;A dedicated search engine could solve the problem, but it would introduce another operational component.&lt;/p&gt;

&lt;p&gt;Optimizing PostgreSQL gave me a middle path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use more of the capabilities of the system we already operate before introducing another one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That kept PostgreSQL as the source of truth.&lt;/p&gt;

&lt;p&gt;No search synchronization pipeline.&lt;/p&gt;

&lt;p&gt;No additional datastore.&lt;/p&gt;

&lt;p&gt;No additional operational dependency.&lt;/p&gt;

&lt;p&gt;And it met the performance requirements.&lt;/p&gt;

&lt;p&gt;The question wasn't which technology was more powerful.&lt;/p&gt;

&lt;p&gt;The question was which architecture introduced the right amount of complexity for the problem we actually had.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I would introduce a dedicated search engine
&lt;/h2&gt;

&lt;p&gt;This isn't an argument that PostgreSQL should replace Elasticsearch.&lt;/p&gt;

&lt;p&gt;I'd revisit the architecture if the requirements started demanding things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sophisticated relevance ranking&lt;/li&gt;
&lt;li&gt;advanced fuzzy matching and typo tolerance&lt;/li&gt;
&lt;li&gt;complex language-specific analysis&lt;/li&gt;
&lt;li&gt;significantly larger search workloads&lt;/li&gt;
&lt;li&gt;independent scaling of search&lt;/li&gt;
&lt;li&gt;complex search aggregations&lt;/li&gt;
&lt;li&gt;search-specific availability requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, specialized search infrastructure may provide enough value to justify its operational cost.&lt;/p&gt;

&lt;p&gt;The useful question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL or Elasticsearch?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Have the requirements crossed the point where owning another system is justified?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For us, they hadn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Leaving room to evolve without building the future
&lt;/h2&gt;

&lt;p&gt;There was another reason I was comfortable keeping PostgreSQL.&lt;/p&gt;

&lt;p&gt;The query language wasn't directly coupled to it.&lt;/p&gt;

&lt;p&gt;The boundary looked roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Intent
     │
     ▼
Search Expression
     │
     ▼
    AST
     │
     ▼
Validation
     │
     ▼
PostgreSQL Compiler
     │
     ▼
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL was the current execution strategy.&lt;/p&gt;

&lt;p&gt;It wasn't the definition of the search language.&lt;/p&gt;

&lt;p&gt;If the requirements eventually justify another backend, there's a natural architectural seam:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       AST
                        │
                ┌───────┴───────┐
                ▼               ▼
          PostgreSQL        Dedicated
           Compiler       Search Compiler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wouldn't implement that second compiler today.&lt;/p&gt;

&lt;p&gt;That's an important distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designing a boundary is useful. Building hypothetical infrastructure isn't.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently today
&lt;/h2&gt;

&lt;p&gt;If I were starting this feature again, I'd formalize the query language earlier.&lt;/p&gt;

&lt;p&gt;Search requirements tend to grow incrementally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment AND status:open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment AND (status:open OR status:pending)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and eventually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(
  payment
  OR (
    refund
    AND (status:open OR status:pending)
  )
)
AND NOT owner:system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At some point, you've created a language without explicitly deciding to create one.&lt;/p&gt;

&lt;p&gt;Once structured fields, Boolean operators, and grouping start appearing in the requirements, I'd define the boundaries early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grammar
   ↓
Tokenizer
   ↓
Parser
   ↓
AST
   ↓
Validation
   ↓
Compilation
   ↓
Execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes everything that follows easier to reason about and test.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I took away from building it
&lt;/h2&gt;

&lt;p&gt;Looking back, what appeared to be one feature was really three different engineering problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expressiveness
&lt;/h3&gt;

&lt;p&gt;Free text gradually became a query language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;free text
key:value
AND / OR / NOT
parentheses
recursive expressions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That required treating search as structured input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tokenize → parse → AST → validate → compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usability
&lt;/h3&gt;

&lt;p&gt;The query language could be powerful without becoming a prerequisite for using search.&lt;/p&gt;

&lt;p&gt;The column-level filter builder exposed controls appropriate to the underlying data—text inputs, date ranges, multi-selects, and Boolean conditions—and generated the same search representation underneath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two interfaces. One search model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The complexity stayed in the system instead of being pushed onto every user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scale
&lt;/h3&gt;

&lt;p&gt;The part that looked complicated wasn't the part that eventually became expensive.&lt;/p&gt;

&lt;p&gt;As the dataset grew, free-text search became the bottleneck.&lt;/p&gt;

&lt;p&gt;The response wasn't to rewrite the parser or immediately introduce another datastore.&lt;/p&gt;

&lt;p&gt;It was to measure the query path, inspect what PostgreSQL was doing, and optimize the access pattern that was actually expensive.&lt;/p&gt;

&lt;p&gt;For our workload, a GIN-backed strategy brought measured search latency from roughly &lt;strong&gt;40ms to 12ms&lt;/strong&gt; while allowing PostgreSQL to remain the source of truth.&lt;/p&gt;

&lt;p&gt;Could a dedicated search engine become the right architecture later?&lt;/p&gt;

&lt;p&gt;Absolutely.&lt;/p&gt;

&lt;p&gt;But I didn't want to pay the synchronization, consistency, infrastructure, and operational costs of another system before the requirements justified them.&lt;/p&gt;

&lt;p&gt;That's probably the main thing I took away from building this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good architecture isn't about eliminating complexity. It's about putting complexity in the right place—and only introducing it when the problem actually requires it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>postgressql</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Your Dog Has Thoughts. I Built an AI to Translate Them 🐶</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:03:00 +0000</pubDate>
      <link>https://dev.to/anilloutombam/your-dog-has-thoughts-i-built-an-ai-to-translate-them-3fk9</link>
      <guid>https://dev.to/anilloutombam/your-dog-has-thoughts-i-built-an-ai-to-translate-them-3fk9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Dog Mind&lt;/strong&gt;, a playful AI-powered web app that turns a dog photo into an entertaining interpretation of the dog’s mood, visible body language, personality signals, and imaginary inner monologue.&lt;/p&gt;

&lt;p&gt;Upload a photo and Dog Mind generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cautious breed or dog-type guess&lt;/li&gt;
&lt;li&gt;A mood and confidence score&lt;/li&gt;
&lt;li&gt;Happiness, energy, and mischief ratings&lt;/li&gt;
&lt;li&gt;Observations based on visible body-language signals&lt;/li&gt;
&lt;li&gt;A short personality summary&lt;/li&gt;
&lt;li&gt;A funny, family-friendly imaginary thought&lt;/li&gt;
&lt;li&gt;A matching voice personality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the analysis, users can hear the dog’s imagined thought spoken aloud, ask up to three follow-up questions, and download or share the final result card.&lt;/p&gt;

&lt;p&gt;No dog photo available? The &lt;strong&gt;Try a sample dog&lt;/strong&gt; button opens a pre-generated experience without consuming an AI request.&lt;/p&gt;

&lt;p&gt;Dog Mind is made for entertainment. It does not claim to read a dog’s mind or provide veterinary or behavioral advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🐶 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://dog-mind-eight.vercel.app" rel="noopener noreferrer"&gt;dog-mind-eight.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To try it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload a JPG, PNG, or WebP photo of a dog.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Analyze my dog&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Review the dog’s mood, signals, and imaginary inner monologue.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Hear this dog’s voice&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ask the dog up to three playful questions.&lt;/li&gt;
&lt;li&gt;Share or download the result card.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also select &lt;strong&gt;Try a sample dog&lt;/strong&gt; for an instant preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

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

&lt;p&gt;🐙 &lt;a href="https://github.com/anilloutombam/dog-mind" rel="noopener noreferrer"&gt;github.com/anilloutombam/dog-mind&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Dog Mind uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Google Gemini&lt;/li&gt;
&lt;li&gt;ElevenLabs&lt;/li&gt;
&lt;li&gt;Zod&lt;/li&gt;
&lt;li&gt;pnpm&lt;/li&gt;
&lt;li&gt;Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Multimodal dog analysis with Google Gemini
&lt;/h3&gt;

&lt;p&gt;When a user uploads a photo, the browser sends it to a server-side Next.js route as multipart form data.&lt;/p&gt;

&lt;p&gt;Before contacting Gemini, the server checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The declared MIME type&lt;/li&gt;
&lt;li&gt;The file size&lt;/li&gt;
&lt;li&gt;The actual binary image signature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents renamed or unsupported files from being accepted based only on their extensions.&lt;/p&gt;

&lt;p&gt;Gemini first determines whether the primary subject is a dog. For a valid dog photo, it returns structured JSON containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;breedGuess&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;breedConfidence&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dogSize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;voiceStyle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mood&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;confidence&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signals&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;observations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thought&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shared Zod schema validates the complete response before it reaches the interface. It also enforces the same character and number limits expected by downstream features.&lt;/p&gt;

&lt;p&gt;The prompt asks Gemini to interpret only clearly visible signals, avoid medical claims, make cautious breed guesses, and keep the imaginary thought short and family-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Giving the dog a voice with ElevenLabs
&lt;/h3&gt;

&lt;p&gt;Dog Mind sends the generated inner monologue to ElevenLabs only after the user selects &lt;strong&gt;Hear this dog’s voice&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Gemini recommends one of six controlled voice personalities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bright&lt;/li&gt;
&lt;li&gt;Warm&lt;/li&gt;
&lt;li&gt;Bold&lt;/li&gt;
&lt;li&gt;Dramatic&lt;/li&gt;
&lt;li&gt;Gentle&lt;/li&gt;
&lt;li&gt;Gruff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each personality maps to approved ElevenLabs stability, style, and speed settings.&lt;/p&gt;

&lt;p&gt;The voice choice is inspired by the dog’s visible size, expression, and energy. It is a playful creative decision, not a scientific claim about breed behavior.&lt;/p&gt;

&lt;p&gt;Generated audio is cached by text and voice personality. Replaying the same voice can reuse the existing audio, while &lt;strong&gt;New take&lt;/strong&gt; deliberately requests a fresh performance.&lt;/p&gt;

&lt;p&gt;The audio cache uses a fixed size and removes older entries so it cannot grow indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask Your Dog
&lt;/h3&gt;

&lt;p&gt;After the initial analysis, users can continue the imaginary conversation by asking the dog up to three questions.&lt;/p&gt;

&lt;p&gt;The follow-up request sends only the established dog persona and the new question. It does not upload the image again.&lt;/p&gt;

&lt;p&gt;Gemini also acts as a topic guard. It rejects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unrelated questions&lt;/li&gt;
&lt;li&gt;Role-changing instructions&lt;/li&gt;
&lt;li&gt;Prompt-injection attempts&lt;/li&gt;
&lt;li&gt;Requests to reveal internal prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dog health questions receive no diagnosis or treatment advice.&lt;/p&gt;

&lt;p&gt;The three-question limit is enforced on the server instead of relying only on a disabled client button. Conversation records expire after 24 hours, expired entries are pruned, and the process-local store has a maximum size.&lt;/p&gt;

&lt;p&gt;The client persists the successful question count and synchronizes it between browser tabs. When the limit is reached, the API returns a machine-readable terminal state so the interface hides the form instead of displaying a misleading retry button.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching repeated image analysis
&lt;/h3&gt;

&lt;p&gt;Gemini API requests are valuable, especially while working within free-tier limits.&lt;/p&gt;

&lt;p&gt;Dog Mind calculates a SHA-256 digest of each uploaded image and uses it as a cache key. If the same image is analyzed again during the browser session, the validated result can be reused without another Gemini request.&lt;/p&gt;

&lt;p&gt;Cached values are validated when they are read and written. Invalid stored values are removed instead of being trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling stale requests
&lt;/h3&gt;

&lt;p&gt;A user can select another image, reset the experience, or open the sample result while an earlier request is still running.&lt;/p&gt;

&lt;p&gt;To prevent an old response from replacing a newer state, Dog Mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assigns a generation number to each analysis&lt;/li&gt;
&lt;li&gt;Cancels superseded requests with &lt;code&gt;AbortController&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Checks the generation after asynchronous operations&lt;/li&gt;
&lt;li&gt;Discards responses that no longer belong to the active image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the UI consistent even when users move quickly between states.&lt;/p&gt;

&lt;h3&gt;
  
  
  Friendly failure and quota handling
&lt;/h3&gt;

&lt;p&gt;AI APIs can fail because of temporary capacity, rate limits, exhausted quotas, network problems, or invalid responses.&lt;/p&gt;

&lt;p&gt;Dog Mind provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Friendly Gemini 429 messages&lt;/li&gt;
&lt;li&gt;Retry timing from response metadata&lt;/li&gt;
&lt;li&gt;Live retry countdowns&lt;/li&gt;
&lt;li&gt;Disabled retry controls during cooldowns&lt;/li&gt;
&lt;li&gt;Retry actions for analysis, voice generation, and dog chat&lt;/li&gt;
&lt;li&gt;Provider timeouts&lt;/li&gt;
&lt;li&gt;Safe request IDs for diagnostics&lt;/li&gt;
&lt;li&gt;Graceful handling for non-dog images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app also includes a pre-generated sample so visitors can still explore the main interface when the AI provider is temporarily unavailable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security and privacy
&lt;/h3&gt;

&lt;p&gt;Security was an important part of the implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini and ElevenLabs credentials remain server-side&lt;/li&gt;
&lt;li&gt;Images are checked in both the browser and server&lt;/li&gt;
&lt;li&gt;Binary signatures are verified&lt;/li&gt;
&lt;li&gt;Uploads are limited to 5 MB&lt;/li&gt;
&lt;li&gt;AI responses are schema-validated&lt;/li&gt;
&lt;li&gt;Client errors do not reveal credentials or stack traces&lt;/li&gt;
&lt;li&gt;Follow-up prompts receive minimal dog context&lt;/li&gt;
&lt;li&gt;API routes use request throttling and provider timeouts&lt;/li&gt;
&lt;li&gt;Security headers prevent framing and MIME sniffing&lt;/li&gt;
&lt;li&gt;Camera, microphone, and location access are disabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building the experience
&lt;/h3&gt;

&lt;p&gt;I wanted Dog Mind to feel playful without becoming confusing.&lt;/p&gt;

&lt;p&gt;The interface includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag-and-drop uploads&lt;/li&gt;
&lt;li&gt;Full-image previews without cropping&lt;/li&gt;
&lt;li&gt;Animated analysis progress&lt;/li&gt;
&lt;li&gt;A running dog progress indicator&lt;/li&gt;
&lt;li&gt;Rotating dog facts&lt;/li&gt;
&lt;li&gt;Screen-reader announcements&lt;/li&gt;
&lt;li&gt;Reduced-motion support&lt;/li&gt;
&lt;li&gt;Responsive layouts&lt;/li&gt;
&lt;li&gt;Native sharing where supported&lt;/li&gt;
&lt;li&gt;Downloadable square result cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The codebase uses a feature-first structure. Route files compose the page, feature modules own the workflow, and server utilities centralize validation, throttling, and safe error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keeping playful AI output honest
&lt;/h3&gt;

&lt;p&gt;Dog Mind creates a humorous interpretation of a dog’s expression, but the result should never be confused with a diagnosis or a factual reading of the animal’s thoughts.&lt;/p&gt;

&lt;p&gt;The prompts, interface copy, and error messages consistently describe the output as an imaginary interpretation based on visible clues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coordinating multiple AI services
&lt;/h3&gt;

&lt;p&gt;Gemini and ElevenLabs have different request formats, limits, errors, and response types.&lt;/p&gt;

&lt;p&gt;Keeping both integrations behind server routes gave the browser one consistent application API while protecting provider credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designing useful recovery states
&lt;/h3&gt;

&lt;p&gt;A failed request should not leave the user stuck.&lt;/p&gt;

&lt;p&gt;Retry buttons, cooldown countdowns, cached results, audio reuse, stale-request cancellation, and the sample experience all help users continue without restarting the entire flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Best Use of Google AI
&lt;/h3&gt;

&lt;p&gt;Google Gemini powers the core multimodal experience. It examines dog photos, identifies visible body-language signals, produces structured mood data, creates the imaginary inner monologue, recommends a controlled voice personality, and handles guarded follow-up conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Use of ElevenLabs
&lt;/h3&gt;

&lt;p&gt;ElevenLabs transforms the dog’s imaginary thought and optional follow-up replies into expressive voice performances. Controlled presets adapt stability, speed, and expression to complement the visual analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Dog Mind began with a funny question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if AI could translate the look your dog gives you?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building it showed me that playful AI products still need careful engineering.&lt;/p&gt;

&lt;p&gt;Schema validation, file-signature checks, caching, rate-limit handling, stale-request protection, prompt-injection resistance, accessibility, and honest disclaimers made the experience more reliable and enjoyable.&lt;/p&gt;

&lt;p&gt;The best AI experience is not only about generating an answer. It is also about presenting that answer with the right personality, boundaries, and recovery path when something goes wrong.&lt;/p&gt;

&lt;p&gt;Thanks for checking out Dog Mind! 🐾&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>googleai</category>
      <category>elevenlabs</category>
    </item>
    <item>
      <title>Fixing an Infinite Loading State in the npmx Code Browser</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:23:15 +0000</pubDate>
      <link>https://dev.to/anilloutombam/fixing-an-infinite-loading-state-in-the-npmx-code-browser-n25</link>
      <guid>https://dev.to/anilloutombam/fixing-an-infinite-loading-state-in-the-npmx-code-browser-n25</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://npmx.dev" rel="noopener noreferrer"&gt;npmx&lt;/a&gt; is an open-source package explorer for the npm ecosystem. It helps developers inspect package metadata, releases, dependencies, documentation, comparisons, and published source files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The npmx code browser has a fallback for files that exceed its supported display size. It should show a “File too large” warning and provide an option to open the raw file.&lt;/p&gt;

&lt;p&gt;Instead, selecting an oversized file left the page displaying its loading state indefinitely.&lt;/p&gt;

&lt;p&gt;The issue can be reproduced here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://npmx.dev/package-code/@types/vscode/v/1.118.0/index.d.ts" rel="noopener noreferrer"&gt;https://npmx.dev/package-code/@types/vscode/v/1.118.0/index.d.ts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The corrected behaviour can be verified on the PR preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://npmx-h3s403v0g-npmx.vercel.app/package-code/@types/vscode/v/1.118.0/index.d.ts" rel="noopener noreferrer"&gt;https://npmx-h3s403v0g-npmx.vercel.app/package-code/@types/vscode/v/1.118.0/index.d.ts&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub issue: &lt;a href="https://github.com/npmx-dev/npmx.dev/issues/2732" rel="noopener noreferrer"&gt;https://github.com/npmx-dev/npmx.dev/issues/2732&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pull request: &lt;a href="https://github.com/npmx-dev/npmx.dev/pull/3120" rel="noopener noreferrer"&gt;https://github.com/npmx-dev/npmx.dev/pull/3120&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The production fix is one line:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFileTooLarge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was added to the page’s loading-state calculation:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;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="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;isViewingFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;treeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;treeStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&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;isFileTooLarge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fileStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fileStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fileStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The page already knew when a selected file exceeded the size limit. In that situation, it intentionally skipped the file-content request.&lt;/p&gt;

&lt;p&gt;Because the request never started, its status remained &lt;code&gt;idle&lt;/code&gt;. The loading logic treated that status as if the file were still waiting to load, so the loading skeleton prevented the existing fallback from appearing.&lt;/p&gt;

&lt;p&gt;The new condition makes the page stop reporting a loading state when the file is already known to be too large. This allows the existing warning and raw-file action to render without changing the server limit or downloading unnecessary content.&lt;/p&gt;

&lt;p&gt;I also added a regression test that verifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The large-file warning is displayed.&lt;/li&gt;
&lt;li&gt;The page does not remain in its loading state.&lt;/li&gt;
&lt;li&gt;The file-content endpoint is not called.&lt;/li&gt;
&lt;li&gt;The raw-file action points to the correct jsDelivr URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code change is small, but it fixes an important state-handling edge case: an &lt;code&gt;idle&lt;/code&gt; request is not always waiting to begin. Sometimes the application has intentionally decided that it should not run.&lt;/p&gt;




&lt;p&gt;If you're interested in more of my open-source work and engineering projects, you can explore my &lt;a href="https://anilsingha.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=npmx-infinite-loading" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt; or follow my work on &lt;a href="https://github.com/anilloutombam" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Fixing a Search Race Condition in npmx</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Sat, 01 Aug 2026 07:55:07 +0000</pubDate>
      <link>https://dev.to/anilloutombam/fixing-a-search-race-condition-in-npmx-4la3</link>
      <guid>https://dev.to/anilloutombam/fixing-a-search-race-condition-in-npmx-4la3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://npmx.dev" rel="noopener noreferrer"&gt;npmx&lt;/a&gt; is a modern browser for the npm registry. It provides package search, version timelines, dependency information, comparisons, and other tools for exploring npm packages.&lt;/p&gt;

&lt;p&gt;I've been contributing to npmx recently, mostly around search behavior and edge cases. One of those contributions started with a particularly interesting symptom:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search failed the first time, but worked after a refresh.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The bug was reported in &lt;a href="https://github.com/npmx-dev/npmx.dev/issues/2617" rel="noopener noreferrer"&gt;issue #2617&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The reproduction was quite specific.&lt;/p&gt;

&lt;p&gt;With instant search disabled and a saved page size of 50 or higher:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the search page.&lt;/li&gt;
&lt;li&gt;Search for a package.&lt;/li&gt;
&lt;li&gt;No package results appear.&lt;/li&gt;
&lt;li&gt;The organization suggestion can still appear.&lt;/li&gt;
&lt;li&gt;Refresh the exact same page.&lt;/li&gt;
&lt;li&gt;The package results suddenly show up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same query. Same page size. Same application.&lt;/p&gt;

&lt;p&gt;The refresh behavior was the interesting part.&lt;/p&gt;

&lt;p&gt;If the packages didn't exist, refreshing shouldn't make them appear. And if the search request itself was fundamentally broken, I'd expect the second attempt to fail too.&lt;/p&gt;

&lt;p&gt;That pointed away from the search data itself and toward what was happening during the &lt;strong&gt;initialization of the page&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following the timing
&lt;/h3&gt;

&lt;p&gt;npmx stores the user's preferred search page size locally.&lt;/p&gt;

&lt;p&gt;On the first navigation to the search page, however, the initial search could begin before that saved preference had finished loading.&lt;/p&gt;

&lt;p&gt;The sequence looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search page opens
        ↓
Initial search starts
(page size = 25)
        ↓
Saved preference loads
        ↓
Page size changes to 50
        ↓
fetchMore() runs
        ↓
Initial search is still pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Individually, none of these operations were wrong.&lt;/p&gt;

&lt;p&gt;The problem was &lt;strong&gt;when they happened relative to each other&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fetchMore()&lt;/code&gt; could run while the initial Algolia search was still pending.&lt;/p&gt;

&lt;p&gt;At that moment, the search result hadn't arrived yet. &lt;code&gt;fetchMore()&lt;/code&gt; could therefore read the temporary empty response and store that value in the search cache.&lt;/p&gt;

&lt;p&gt;Then the original search completed with the real package results.&lt;/p&gt;

&lt;p&gt;But it was too late.&lt;/p&gt;

&lt;p&gt;The empty cached value could take priority, leaving the UI showing no packages even though the search had successfully returned them.&lt;/p&gt;

&lt;p&gt;That also explained why refreshing appeared to magically fix the problem.&lt;/p&gt;

&lt;p&gt;It wasn't magic.&lt;/p&gt;

&lt;p&gt;The initialization timing had changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The fix is in &lt;a href="https://github.com/npmx-dev/npmx.dev/pull/3109" rel="noopener noreferrer"&gt;npmx-dev/npmx.dev#3109&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of allowing &lt;code&gt;fetchMore()&lt;/code&gt; to continue while the initial search is unresolved, it now checks whether that search is still pending and waits for it:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asyncData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;asyncData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dedupe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail here is:&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="nx"&gt;dedupe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't want &lt;code&gt;fetchMore()&lt;/code&gt; to cancel the search that was already running and replace it with another request.&lt;/p&gt;

&lt;p&gt;It needed to &lt;strong&gt;join the existing in-flight request&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the flow changed from 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;Initial search ────────────────► package results
        │
        └── fetchMore()
                ↓
          reads empty state
                ↓
          empty value cached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial search ────────────────► package results
                                      ↓
                               fetchMore() continues
                                      ↓
                               results preserved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small change in code, but it changes the ordering guarantee between the two asynchronous operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The main fix was making &lt;code&gt;fetchMore()&lt;/code&gt; wait for the pending initial search before consuming its result.&lt;/p&gt;

&lt;p&gt;But a race-condition fix also needs a test that can reliably reproduce the timing that caused it.&lt;/p&gt;

&lt;p&gt;Otherwise, the test might pass simply because the operations happened in a convenient order that particular time.&lt;/p&gt;

&lt;p&gt;So I added a regression test that deliberately recreates the problematic sequence.&lt;/p&gt;

&lt;p&gt;The test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Starts the initial Algolia search.&lt;/li&gt;
&lt;li&gt;Keeps that request pending.&lt;/li&gt;
&lt;li&gt;Changes the requested page size.&lt;/li&gt;
&lt;li&gt;Triggers the additional result-loading path.&lt;/li&gt;
&lt;li&gt;Resolves the original search.&lt;/li&gt;
&lt;li&gt;Verifies that the completed package results are preserved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That was important because the test isn't relying on the race condition happening naturally.&lt;/p&gt;

&lt;p&gt;It &lt;strong&gt;controls the timing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After the fix, users with a saved page size of 50 or higher get their package results on the first search without needing to refresh the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away from this bug
&lt;/h3&gt;

&lt;p&gt;What made this issue interesting wasn't the amount of code required to fix it.&lt;/p&gt;

&lt;p&gt;It was the symptom:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It doesn't work the first time, but refreshing fixes it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That can be a useful debugging clue.&lt;/p&gt;

&lt;p&gt;When persisted client state, initialization, caching, and asynchronous requests interact, a refresh can change the order in which those operations happen.&lt;/p&gt;

&lt;p&gt;In this case, the individual pieces were working.&lt;/p&gt;

&lt;p&gt;The initial search worked.&lt;/p&gt;

&lt;p&gt;The saved preference worked.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fetchMore()&lt;/code&gt; worked.&lt;/p&gt;

&lt;p&gt;The problem only appeared when they ran in a particular order.&lt;/p&gt;

&lt;p&gt;The final fix wasn't to make the search faster or add another request.&lt;/p&gt;

&lt;p&gt;It was simply to make one asynchronous operation respect another operation that was &lt;strong&gt;already in progress&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes fixing a race condition is less about doing more work and more about making sure the work you already have happens in the right order.&lt;/p&gt;




&lt;p&gt;If you're interested in more of my open-source work and engineering projects, you can explore my &lt;a href="https://anilsingha.dev/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=npmx-search-race-condition" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt; or follow my work on &lt;a href="https://github.com/anilloutombam" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Midnight Ramen: A Rainy CSS Art Love Letter to Comfort Food</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:29:29 +0000</pubDate>
      <link>https://dev.to/anilloutombam/midnight-ramen-a-rainy-css-art-love-letter-to-comfort-food-hed</link>
      <guid>https://dev.to/anilloutombam/midnight-ramen-a-rainy-css-art-love-letter-to-comfort-food-hed</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;My inspiration was a warm bowl of ramen on a quiet, rainy night—the kind of meal that feels comforting before you even take the first bite.&lt;/p&gt;

&lt;p&gt;I wanted to capture more than the food itself, so I created a complete midnight ramen-shop scene. The steaming bowl is surrounded by a glowing lantern, falling rain, a handwritten order slip, shelves of ingredients, and a tiny shop cat peeking over the counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/anilloutombam/embed/019fb8b9-6447-7117-9662-e41035bfc2c4?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can also explore the source code on &lt;a href="https://github.com/anilloutombam/midnight-ramen-css-art" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I created the illustration using HTML and CSS, without image assets or JavaScript. The bowl, broth, noodles, egg, chashu pork, nori, narutomaki, mushrooms, scallions, lantern, window, furniture, and cat are all composed from regular HTML elements.&lt;/p&gt;

&lt;p&gt;The visual details rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layered CSS gradients&lt;/li&gt;
&lt;li&gt;Borders and border radii&lt;/li&gt;
&lt;li&gt;Pseudo-elements&lt;/li&gt;
&lt;li&gt;Transforms and clipping&lt;/li&gt;
&lt;li&gt;CSS custom properties&lt;/li&gt;
&lt;li&gt;Responsive media queries&lt;/li&gt;
&lt;li&gt;Subtle keyframe animations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scene also includes CSS-only interactions. The chili-oil control changes the appearance of the broth and introduces small animated spice sparks. A compact day/night control changes the window, lantern, steam, and surrounding color palette without JavaScript.&lt;/p&gt;

&lt;p&gt;The ingredient labels can be revealed with a mouse or keyboard. I also included &lt;code&gt;prefers-reduced-motion&lt;/code&gt; support so visitors who disable animation receive a calmer version of the scene.&lt;/p&gt;

&lt;p&gt;The most challenging part was creating depth while keeping the markup understandable. The ramen bowl contains several overlapping layers, and each topping needs to remain recognizable without using an image. Careful positioning, contrast, shadows, and restrained animation helped the individual pieces feel like one cohesive illustration.&lt;/p&gt;

&lt;p&gt;I am particularly proud that the finished project requires only two files for the artwork: &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;style.css&lt;/code&gt;. It can run directly in a browser without a framework, build process, or external JavaScript.&lt;/p&gt;

&lt;p&gt;This project reminded me that CSS is not only a styling language—it can also be an expressive illustration medium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;p&gt;The complete source is available on &lt;a href="https://github.com/anilloutombam/midnight-ramen-css-art" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The project is shared under the MIT License.&lt;/p&gt;

&lt;p&gt;If you'd like to see more of what I'm building, you can explore my &lt;a href="https://dev.toPASTE_THE_ARTICLE_UTM_URL_HERE"&gt;portfolio&lt;/a&gt; or find me on &lt;a href="https://github.com/anilloutombam" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Fixing Exact Package Search Relevance in npmx</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:27:09 +0000</pubDate>
      <link>https://dev.to/anilloutombam/fixing-exact-package-search-relevance-in-npmx-34c7</link>
      <guid>https://dev.to/anilloutombam/fixing-exact-package-search-relevance-in-npmx-34c7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://npmx.dev" rel="noopener noreferrer"&gt;npmx&lt;/a&gt; is an open-source alternative interface for exploring packages from the npm registry. It provides package details, documentation, version history, dependencies, search tools, and other information for JavaScript developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;I worked on &lt;a href="https://github.com/npmx-dev/npmx.dev/issues/2978" rel="noopener noreferrer"&gt;issue #2978&lt;/a&gt;, which was reported by another contributor.&lt;/p&gt;

&lt;p&gt;When Algolia was selected as the search provider, the page displayed “Relevance” as the active sorting option. However, an exact package-name match did not always appear first.&lt;/p&gt;

&lt;p&gt;For example, searching for &lt;code&gt;napkin&lt;/code&gt; could show other packages above the exact match. Switching to “Downloads/wk” and then back to “Relevance” changed the results and moved &lt;code&gt;napkin&lt;/code&gt; to the top.&lt;/p&gt;

&lt;p&gt;I reproduced this behavior locally before investigating the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/npmx-dev/npmx.dev/issues/2978" rel="noopener noreferrer"&gt;Issue #2978&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pull request: &lt;a href="https://github.com/npmx-dev/npmx.dev/pull/3103" rel="noopener noreferrer"&gt;https://github.com/npmx-dev/npmx.dev/pull/3103&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://private-user-images.githubusercontent.com/23106365/629641010-7b90dc30-62b3-419a-9442-36a198edd8e3.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODU0OTM3MzYsIm5iZiI6MTc4NTQ5MzQzNiwicGF0aCI6Ii8yMzEwNjM2NS82Mjk2NDEwMTAtN2I5MGRjMzAtNjJiMy00MTlhLTk0NDItMzZhMTk4ZWRkOGUzLm1vdj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MzElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzMxVDEwMjM1NlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWUxMzEwYjI3OGUxN2I0M2ExZGI1ZjM5MTAzMTk0OWRhYmI0NjIzMzc2M2Q4YjBkZmNhNGNkNTE1MWNjY2RmZGYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT12aWRlbyUyRnF1aWNrdGltZSJ9.jYPPoG_cEHKfZk_oXZ1Qy1_0D2WPHbcGASouXU0PEpE" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;private-user-images.githubusercontent.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  After
&lt;/h3&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://private-user-images.githubusercontent.com/23106365/629641622-e73ea88e-e74c-40c6-a1a6-462760619c4b.mov?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODU0OTM3NTksIm5iZiI6MTc4NTQ5MzQ1OSwicGF0aCI6Ii8yMzEwNjM2NS82Mjk2NDE2MjItZTczZWE4OGUtZTc0Yy00MGM2LWExYTYtNDYyNzYwNjE5YzRiLm1vdj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA3MzElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNzMxVDEwMjQxOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTMzNzkxNmE1MTMyODRiNWIyNTg3MWIyN2MwNGI5MThlM2RlNmYzNWIzNjZhOGVhNTViZGE5OGM5NmQ0NGZlNGMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT12aWRlbyUyRnF1aWNrdGltZSJ9.DDshiEDIyCohCupPhqxI7XlriGJAovK3XOnrHSflnRc" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;private-user-images.githubusercontent.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;npmx already made an additional Algolia request to check whether a package with the exact searched name existed.&lt;/p&gt;

&lt;p&gt;However, this request retrieved only the package name and used the result only as an existence check. If the exact package was missing from the initial batch of results, the application knew it existed but did not have enough information to display it.&lt;/p&gt;

&lt;p&gt;I updated the exact-package request to retrieve the complete package information. When the returned package name matches the searched name, it is placed at the beginning of the results.&lt;/p&gt;

&lt;p&gt;Before adding it, the code removes any existing occurrence of the same package. This prevents duplicate results.&lt;/p&gt;

&lt;p&gt;I also added two safety checks during review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A missing Algolia &lt;code&gt;hits&lt;/code&gt; array falls back to an empty array.&lt;/li&gt;
&lt;li&gt;A returned hit is promoted only when its name matches the requested package.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, I added a regression test that supplies a regular search result and a separate exact-package result. The test verifies that the exact package is detected and placed first.&lt;/p&gt;

&lt;p&gt;I validated the change by running the focused Nuxt tests, TypeScript checks, lint and formatting checks, and by testing the behavior manually in the local UI.&lt;/p&gt;

&lt;p&gt;After the fix, an exact package-name match appears at the top immediately when “Relevance” is selected.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Fixing a False “TypeScript Types Removed” Event in npmx</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 31 Jul 2026 08:43:14 +0000</pubDate>
      <link>https://dev.to/anilloutombam/fixing-a-false-typescript-types-removed-event-in-npmx-5bj6</link>
      <guid>https://dev.to/anilloutombam/fixing-a-false-typescript-types-removed-event-in-npmx-5bj6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://npmx.dev/" rel="noopener noreferrer"&gt;npmx&lt;/a&gt; is an open-source browser for the npm registry. It helps developers explore package metadata, versions, dependencies, documentation, and other information.&lt;/p&gt;

&lt;p&gt;I worked on &lt;a href="https://github.com/npmx-dev/npmx.dev/issues/2791" rel="noopener noreferrer"&gt;npmx issue #2791&lt;/a&gt;, which reported an incorrect event in the package-version timeline. npmx could say that a package had removed its TypeScript types even though valid declaration files were still included in the published package.&lt;/p&gt;

&lt;p&gt;My fix is available in &lt;a href="https://github.com/npmx-dev/npmx.dev/pull/3102" rel="noopener noreferrer"&gt;npmx PR #3102&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;A package can expose TypeScript declarations explicitly through fields such as &lt;code&gt;types&lt;/code&gt; or &lt;code&gt;typings&lt;/code&gt;. It can also provide declarations implicitly by placing them beside its JavaScript entry points.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dist/index.mjs
dist/index.d.mts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript can use &lt;code&gt;dist/index.d.mts&lt;/code&gt; even when &lt;code&gt;package.json&lt;/code&gt; does not contain a top-level &lt;code&gt;types&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;npmx already understood this pattern on its package-analysis page. However, the timeline endpoint used &lt;code&gt;hasBuiltInTypes()&lt;/code&gt;, which only checked package metadata.&lt;/p&gt;

&lt;p&gt;This caused the two parts of npmx to disagree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The package page correctly showed “Types included.”&lt;/li&gt;
&lt;li&gt;The timeline incorrectly showed “TypeScript types removed.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The fix keeps the fast metadata check as the first step. It performs the more expensive published-file analysis only for visible versions that could be part of a type-removal sequence.&lt;/p&gt;

&lt;p&gt;The latest implementation selects every untyped version that has an older typed version somewhere earlier in the package’s history:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;possibleTypeRemovals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;visibleVersions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasTypes&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;false&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;versionIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allVersions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&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;allVersions&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;versionIndex&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previousVersion&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasTypes&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;Checking the full sequence is important. Consider these versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.0.0 — explicit types field
2.0.0 — colocated .d.mts declaration
3.0.0 — colocated .d.mts declaration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An earlier version of my fix analyzed only &lt;code&gt;2.0.0&lt;/code&gt;, because it was directly beside the metadata transition. The latest version also analyzes &lt;code&gt;3.0.0&lt;/code&gt; and any other affected version in the sequence.&lt;/p&gt;

&lt;p&gt;For each candidate, the timeline fetches the package metadata and published file tree. It then reuses the project’s existing &lt;code&gt;analyzePackage()&lt;/code&gt; utility:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;typesPackage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&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="nf"&gt;fetchPackageWithTypesAndFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;packageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&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;analysis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyzePackage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;typesPackage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;files&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;analysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;included&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="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasTypes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the additional lookup fails, the endpoint preserves the original metadata-only result instead of failing the complete timeline request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;I started by running the existing test suite and confirming that all 1,716 tests passed before making changes.&lt;/p&gt;

&lt;p&gt;I then added a regression test with three package versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;1.0.0&lt;/code&gt; declares types explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2.0.0&lt;/code&gt; publishes &lt;code&gt;index.mjs&lt;/code&gt; and &lt;code&gt;index.d.mts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;3.0.0&lt;/code&gt; also publishes &lt;code&gt;index.mjs&lt;/code&gt; and &lt;code&gt;index.d.mts&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test verifies that both implicit versions are analyzed and that all three versions report TypeScript types.&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchPackageWithTypesAndFilesMock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-pkg&lt;/span&gt;&lt;span class="dl"&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;2.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchPackageWithTypesAndFilesMock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-pkg&lt;/span&gt;&lt;span class="dl"&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;3.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;expect&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;versions&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;expect&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;versions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;hasTypes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&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;versions&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;hasTypes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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="nf"&gt;expect&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;versions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;hasTypes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also added a genuine-removal test. In that case, the newer version publishes &lt;code&gt;index.mjs&lt;/code&gt; without a matching declaration file, so &lt;code&gt;hasTypes&lt;/code&gt; correctly remains unset.&lt;/p&gt;

&lt;p&gt;The final validation included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript type checking&lt;/li&gt;
&lt;li&gt;Repository lint checks&lt;/li&gt;
&lt;li&gt;The focused timeline test suite&lt;/li&gt;
&lt;li&gt;All 82 unit-test files&lt;/li&gt;
&lt;li&gt;The complete unit-test suite with the new regression coverage&lt;/li&gt;
&lt;li&gt;GitHub continuous-integration checks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The final change improves the timeline in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It recognizes colocated &lt;code&gt;.d.mts&lt;/code&gt; declarations.&lt;/li&gt;
&lt;li&gt;It reuses the existing package-analysis implementation.&lt;/li&gt;
&lt;li&gt;It handles consecutive implicit-type versions after a metadata transition.&lt;/li&gt;
&lt;li&gt;It preserves genuine “types removed” events.&lt;/li&gt;
&lt;li&gt;It avoids failing the entire timeline when an additional file lookup is unavailable.&lt;/li&gt;
&lt;li&gt;Its tests verify array length before using optional indexed access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The main lesson was that package metadata does not always describe everything inside a published npm package. Removing a &lt;code&gt;types&lt;/code&gt; field does not necessarily mean that TypeScript support was removed.&lt;/p&gt;

&lt;p&gt;I also learned why regression tests should cover a sequence rather than only one transition. Testing just &lt;code&gt;1.0.0 → 2.0.0&lt;/code&gt; missed the behavior of &lt;code&gt;3.0.0&lt;/code&gt;. Extending the test revealed that every affected version after the transition needed file-aware analysis.&lt;/p&gt;

&lt;p&gt;Review feedback also improved the test quality. I replaced non-null assertions on indexed versions with an explicit length assertion and optional access.&lt;/p&gt;

&lt;p&gt;Most importantly, I reused npmx’s existing analysis logic instead of implementing another declaration-detection algorithm. This keeps the package page and timeline consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and After
&lt;/h2&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.0.0 — TypeScript types included
2.0.0 — TypeScript types removed
3.0.0 — TypeScript types not detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could appear even when &lt;code&gt;2.0.0&lt;/code&gt; and &lt;code&gt;3.0.0&lt;/code&gt; contained valid &lt;code&gt;.d.mts&lt;/code&gt; declarations.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.0.0 — TypeScript types included
2.0.0 — TypeScript types included
3.0.0 — TypeScript types included
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A release that genuinely removes both the metadata and the declaration files is still treated as a type removal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Request
&lt;/h2&gt;

&lt;p&gt;The implementation, tests, and review history are available in &lt;a href="https://github.com/npmx-dev/npmx.dev/pull/3102" rel="noopener noreferrer"&gt;npmx PR #3102&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Miso &amp; Memory: A CSS-Illustrated Ramen Landing Page for Rainy Nights</title>
      <dc:creator>L Anil Kumar Singha</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:08:24 +0000</pubDate>
      <link>https://dev.to/anilloutombam/miso-memory-a-css-illustrated-ramen-landing-page-for-rainy-nights-4i1c</link>
      <guid>https://dev.to/anilloutombam/miso-memory-a-css-illustrated-ramen-landing-page-for-rainy-nights-4i1c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge — Comfort Food Edition, Perfect Landing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Miso &amp;amp; Memory&lt;/strong&gt;, a landing page for an imaginary late-night ramen counter.&lt;/p&gt;

&lt;p&gt;The concept is inspired by the feeling of coming home to a hot bowl after a long, rainy day: slow broth, springy noodles, warm light, and the good kind of quiet.&lt;/p&gt;

&lt;p&gt;The visual direction combines editorial typography, an earthy restaurant-inspired palette, Japanese print-inspired details, and a steaming ramen bowl illustrated with custom CSS.&lt;/p&gt;

&lt;p&gt;The landing page includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A responsive CSS-art ramen hero&lt;/li&gt;
&lt;li&gt;Animated steam and a scrolling kitchen-principles banner&lt;/li&gt;
&lt;li&gt;A curated menu of comfort bowls&lt;/li&gt;
&lt;li&gt;The story behind the imaginary restaurant&lt;/li&gt;
&lt;li&gt;An interactive “build your comfort bowl” experience&lt;/li&gt;
&lt;li&gt;Dynamic topping selection and price calculation&lt;/li&gt;
&lt;li&gt;Keyboard-friendly controls and accessible labels&lt;/li&gt;
&lt;li&gt;Visible selected and confirmation states&lt;/li&gt;
&lt;li&gt;Reduced-motion support&lt;/li&gt;
&lt;li&gt;Responsive mobile, tablet, and desktop layouts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://miso-and-memory.nlkr50.workers.dev" rel="noopener noreferrer"&gt;Open Miso &amp;amp; Memory&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Source Code
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/anilloutombam/miso-and-memory" rel="noopener noreferrer"&gt;View the source code on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I wanted this landing page to feel handcrafted rather than relying on conventional restaurant photography.&lt;/p&gt;

&lt;p&gt;The hero illustration is constructed from CSS shapes, borders, clipping paths, shadows, and animation. The bowl contains noodles, a jammy egg, mushrooms, greens, narutomaki, chopsticks, and rising steam.&lt;/p&gt;

&lt;p&gt;One of my favorite parts is how the illustration and interface share the same visual language. The rust-red sun becomes the primary action color, broth-inspired gold highlights selected toppings, and forest green carries through the typography and bowl outlines.&lt;/p&gt;

&lt;p&gt;I also wanted the page to be functional rather than purely decorative. Visitors can choose from three bowls, add or remove toppings, see the total update immediately, and save their selection.&lt;/p&gt;

&lt;p&gt;The interactive controls use semantic buttons, &lt;code&gt;aria-pressed&lt;/code&gt; states, and a status message when the bowl is saved. The page also respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt; for visitors who prefer a calmer experience.&lt;/p&gt;

&lt;p&gt;The project is built with React, TypeScript, Vinext, Vite, and custom CSS. It is deployed publicly on Cloudflare Workers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned
&lt;/h3&gt;

&lt;p&gt;This project reminded me how much atmosphere can be created with a restrained color palette, expressive typography, and carefully composed CSS.&lt;/p&gt;

&lt;p&gt;I also learned more about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Constructing detailed illustrations from CSS primitives&lt;/li&gt;
&lt;li&gt;Balancing animation with accessibility&lt;/li&gt;
&lt;li&gt;Designing responsive editorial layouts&lt;/li&gt;
&lt;li&gt;Building keyboard-friendly selection controls&lt;/li&gt;
&lt;li&gt;Deploying a Vinext application and its static assets to Cloudflare Workers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Assistance
&lt;/h3&gt;

&lt;p&gt;I used Codex as a collaborative development tool for design exploration, implementation, accessibility improvements, troubleshooting, and deployment guidance.&lt;/p&gt;

&lt;p&gt;I reviewed and refined the project’s creative direction, interface, content, and interactions before submitting it. The social-preview image was created using an AI image-generation tool. The ramen illustration visible in the website’s hero section is implemented with CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  License
&lt;/h3&gt;

&lt;p&gt;The source code is available under the MIT License.&lt;/p&gt;

&lt;p&gt;Thanks for stopping by. Your seat at the counter is waiting. 🍜&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
