From c2960013f0b55611184c51d811875c4ae8c0751d Mon Sep 17 00:00:00 2001 From: Semih Yesilyurt Date: Mon, 25 May 2026 00:54:22 +0300 Subject: [PATCH] feat(web): speculative pre-decode on a valid VIN (faster counter lookups) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The moment the VIN reaches 17 valid chars, warm the full decode in the background (350ms debounce, deduped per VIN, fire-and-forget) so the "Şase Çöz" click hits the backend lock / 24h positive cache and returns near-instantly. If the warm is still in flight when the dealer clicks, the button's decode lock-waits on it and navigates the moment it lands. No analytics/state/navigation from the pre-decode — vin_decoded and the success/candidates/error events still fire only from the button path, so the CRO funnel stays = real intent. Raw fetch + AbortController (api.post takes no signal), mirroring the existing 17-char preview effect. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/routes/dashboard/search.tsx | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/web/src/routes/dashboard/search.tsx b/apps/web/src/routes/dashboard/search.tsx index e016001..9f24632 100644 --- a/apps/web/src/routes/dashboard/search.tsx +++ b/apps/web/src/routes/dashboard/search.tsx @@ -61,6 +61,7 @@ function SearchPage() { const candidatesShownAtRef = useRef(null); const lastAttemptedVinRef = useRef(null); const attemptCountRef = useRef(0); + const preDecodedVinRef = useRef(null); const [vin, setVin] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -147,6 +148,36 @@ function SearchPage() { return () => controller.abort(); }, [vin]); + // ─── Speculative pre-decode (the moment the VIN is valid) ────────────────── + // Warm the full decode in the background before the user clicks "Şase Çöz", so + // the button-click decode hits the backend lock / 24h positive cache and returns + // near-instantly — the dealer at the counter loses no time. Fire-and-forget: no + // state, no analytics, no navigation (the button path owns all of that). Deduped + // per VIN, debounced + aborted so typing/paste-then-edit doesn't spam decodes. + useEffect(() => { + const cleanVin = vin.toUpperCase().trim(); + if (!isValidVin(cleanVin) || preDecodedVinRef.current === cleanVin) return; + + const controller = new AbortController(); + const timer = window.setTimeout(() => { + preDecodedVinRef.current = cleanVin; + // Warm the backend decode chain; the result is intentionally discarded. + fetch("/api/vehicles/decode", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vin: cleanVin }), + signal: controller.signal, + }).catch(() => { + /* speculative warm — ignore result/errors; the button-click decode owns the outcome */ + }); + }, 350); + + return () => { + window.clearTimeout(timer); + controller.abort(); + }; + }, [vin]); + // ─── Auto-focus ──────────────────────────────────────────────────────────── useEffect(() => { inputRef.current?.focus();