diff --git a/web/app/actions.ts b/web/app/actions.ts index 68a954f..d3a79bb 100644 --- a/web/app/actions.ts +++ b/web/app/actions.ts @@ -1,14 +1,19 @@ "use server"; -import { revalidatePath } from "next/cache"; -import { fetchApi } from "@/lib/api"; +import { revalidatePath, revalidateTag } from "next/cache"; +import { ACCOUNTS_TAG, USERS_TAG, fetchApi } from "@/lib/api"; import type { AccUpdate, UserUpdate } from "@/lib/types"; export type ActionResult = { ok: true } | { ok: false; error: string }; +// Each mutation evicts the matching tag so the next GET bypasses the +// 30s data cache and re-reads from MySQL. revalidatePath then tells +// Next.js to re-render the dependent route on the next request. + export async function updateAccount(data: AccUpdate): Promise { try { await fetchApi("/update-acc-data", { method: "POST", body: data }); + revalidateTag(ACCOUNTS_TAG); revalidatePath("/"); return { ok: true }; } catch (err) { @@ -19,6 +24,7 @@ export async function updateAccount(data: AccUpdate): Promise { export async function updateUser(data: UserUpdate): Promise { try { await fetchApi("/update-user-data", { method: "POST", body: data }); + revalidateTag(USERS_TAG); revalidatePath("/users"); return { ok: true }; } catch (err) { @@ -29,6 +35,7 @@ export async function updateUser(data: UserUpdate): Promise { export async function createAccount(data: AccUpdate): Promise { try { await fetchApi("/create-acc-data", { method: "POST", body: data }); + revalidateTag(ACCOUNTS_TAG); revalidatePath("/"); return { ok: true }; } catch (err) { @@ -39,6 +46,7 @@ export async function createAccount(data: AccUpdate): Promise { export async function createUser(data: UserUpdate): Promise { try { await fetchApi("/create-user-data", { method: "POST", body: data }); + revalidateTag(USERS_TAG); revalidatePath("/users"); return { ok: true }; } catch (err) { @@ -49,6 +57,7 @@ export async function createUser(data: UserUpdate): Promise { export async function deleteAccount(username: string): Promise { try { await fetchApi("/delete-acc-data", { method: "POST", body: { username } }); + revalidateTag(ACCOUNTS_TAG); revalidatePath("/"); return { ok: true }; } catch (err) { @@ -59,6 +68,7 @@ export async function deleteAccount(username: string): Promise { export async function deleteUser(f_username: string): Promise { try { await fetchApi("/delete-user-data", { method: "POST", body: { f_username } }); + revalidateTag(USERS_TAG); revalidatePath("/users"); return { ok: true }; } catch (err) { diff --git a/web/lib/api.ts b/web/lib/api.ts index 8a97313..1909fdc 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -2,20 +2,35 @@ import type { Acc, User } from "./types"; const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000"; +// How long the Next.js data cache holds an /acc/ or /user/ response +// before considering it stale. Tab switches and auto-refreshes within +// this window are served from memory — no api-server / MySQL round-trip. +// Mutations (update/create/delete) call revalidateTag() so the next +// request always sees fresh data after a write. +const CACHE_REVALIDATE_SECONDS = 30; + +export const ACCOUNTS_TAG = "accounts"; +export const USERS_TAG = "users"; + type FetchInit = { method?: "GET" | "POST"; body?: unknown; cache?: RequestCache; + next?: { revalidate?: number; tags?: string[] }; }; export async function fetchApi(path: string, options: FetchInit = {}): Promise { const url = `${API_BASE_URL}${path}`; - const init: RequestInit = { + const init: RequestInit & { next?: FetchInit["next"] } = { method: options.method ?? "GET", - cache: options.cache ?? "no-store", headers: options.body ? { "content-type": "application/json" } : undefined, body: options.body ? JSON.stringify(options.body) : undefined, }; + if (options.next) { + init.next = options.next; + } else { + init.cache = options.cache ?? "no-store"; + } const res = await fetch(url, init); if (!res.ok) { throw new Error(`API ${options.method ?? "GET"} ${path} failed: ${res.status}`); @@ -24,11 +39,15 @@ export async function fetchApi(path: string, options: FetchInit = {}): Promise { - const data = await fetchApi("/acc/"); + const data = await fetchApi("/acc/", { + next: { revalidate: CACHE_REVALIDATE_SECONDS, tags: [ACCOUNTS_TAG] }, + }); return data as Acc[]; } export async function getUsers(): Promise { - const data = await fetchApi("/user/"); + const data = await fetchApi("/user/", { + next: { revalidate: CACHE_REVALIDATE_SECONDS, tags: [USERS_TAG] }, + }); return data as User[]; }