SuperAPI AI Directory

ssd-bug-std-version-checker

Deployment & operations

View repository

Updated 10-15 21:35

About this skill

SSD Bug Fix: Version Checker — does not see the new version after deploy

Symptoms

  • the user deployed a new version but some users still see the old UI
  • the "version outdated" popup appears repeatedly on the same page
  • pressing refresh still gets the old version
  • config.json updated but the browser still reads the old value

Root Cause

  1. vite.config.ts: chunkFileNames no [hash] → browser caches the old JS chunk after deploy
  2. VersionChecker.tsx: emptyCache() is sync → window.location.reload() fires before the cache is actually cleared
  3. VersionChecker.tsx: no guard → getData() is called repeatedly, duplicate popups, repeated reloads
  4. VersionChecker.tsx: fetch uses browser cache → reads config.json old

Fix: 2 files


Step 1: Edit vite.config.ts

Add [hash] in chunkFileNames — most important, forces the browser to load new JS every time you deploy

output: {
    entryFileNames: `assets/js/[name].[hash].js`,
    assetFileNames: `assets/[ext]/[name].[hash].[ext]`,
    chunkFileNames: `assets/js/[name].[hash].js`,   // ก่อนหน้านี้ไม่มี [hash]
},

Step 2: Edit src/app/layout/components/VersionChecker.tsx

Replace the entire file with the fixed version:

import React, { useEffect, useRef } from "react";
import { APP_INFO, VERSION_CHECKER } from "../../../Const";
import { swalConfirm } from "../../modules/_common";

function VersionChecker() {
    const { version } = APP_INFO;
    const { CHECK_VERSION_EVERY_MINUTE, CONFIRM_BEFORE_REFRESH, ENABLE_VERSION_CHECKER } = VERSION_CHECKER;
    const isCheckingRef = useRef(false);
    const isPromptOpenRef = useRef(false);
    const isReloadingRef = useRef(false);
    const checkVersionLoop = 1000 * 60 * CHECK_VERSION_EVERY_MINUTE;

    const getData = async () => {
        // guard: ป้องกันเรียกซ้อน
        if (!ENABLE_VERSION_CHECKER || isCheckingRef.current || isReloadingRef.current) {
            return;
        }
        isCheckingRef.current = true;

        try {
            const response = await fetch(`${window.location.origin}/config.json`, {
                cache: "no-store",    // บังคับไม่ใช้ browser cache
                headers: {
                    "Content-Type": "application/json",
                    Accept: "application/json",
                },
            });

            if (!response.ok) {
                throw new Error(`Failed to fetch config.json: ${response.status}`);
            }

            const configData = await response.json();

            if (configData.version !== version) {
                if (CONFIRM_BEFORE_REFRESH) {
                    if (isPromptOpenRef.current) {
                        return;    // ป้องกัน popup ซ้ำ
                    }
                    isPromptOpenRef.current = true;
                    try {
                        const res = await swalConfirm("Warning", "Your version is outdated. Refresh now?");
                        if (res.isConfirmed) {
                            await emptyCache();
                        }
                    } finally {
                        isPromptOpenRef.current = false;
                    }
                } else {
                    await emptyCache();
                }
            }
        } catch (error) {
            console.error("Version check failed", error);    // ไม่ใช้ alert()
        } finally {
            isCheckingRef.current = false;
        }
    };

    const emptyCache = async () => {
        if (isReloadingRef.current) {
            return;    // ป้องกัน reload ซ้ำ
        }
        isReloadingRef.current = true;

        try {
            if ("caches" in window) {
                const names = await caches.keys();
                // await ให้ลบ cache เสร็จก่อนค่อย reload
                await Promise.all(names.map((name) => caches.delete(name)));
            }
        } finally {
            window.location.reload();
        }
    };

    useEffect(() => {
        void getData();
        const interval = setInterval(() => {
            void getData();
        }, checkVersionLoop);

        return () => clearInterval(interval);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    return <></>;
}

export default VersionChecker;

What changed and why

Changed point Before After Reason
chunkFileNames [name].js [name].[hash].js Forces the browser to load new JS after deploy
emptyCache sync, forEach async, Promise.all + await Guarantees the cache is cleared before reload
guard refs None isCheckingRef, isPromptOpenRef, isReloadingRef Prevents race condition
fetch cache browser default cache: "no-store" Prevents reading old config.json
error handling alert() console.error() Does not disturb the user when the network temporarily fails
prompt text "your version is out-dated..." "Your version is outdated. Refresh now?" More readable language

Step 3: Verify

# 1. Build ต้องผ่าน
npm run build

# 2. ตรวจว่า chunk files มี hash ใน dist/
ls dist/assets/js/
# ควรเห็น: index.abc123.js, vendor.def456.js (มี hash ทุกไฟล์)

# 3. ทดสอบ flow
# - เปิด DevTools → Network tab → เปิด "Disable cache"
# - Reload หน้า → ตรวจว่า config.json fetch มี Cache-Control: no-store
# - เปลี่ยน version ใน config.json → รอ interval หรือ reload → popup ต้องขึ้น 1 ครั้งเท่านั้น
# - กด refresh → หน้าต้องโหลด version ใหม่