From 8eafddae509ce475392ba325ab2083f2c26a07e7 Mon Sep 17 00:00:00 2001 From: sirpy Date: Sun, 11 Oct 2026 15:45:01 +0300 Subject: [PATCH 1/3] feat: add inactivity detection to stop background API calls - Create inactivityStore to track user interactions (mouse, keyboard, touch, scroll) - Stop API polling when tab is inactive for 5+ minutes - Add inactivity check to amountsStore and tokensStore - Prevents unnecessary background balance/token fetches - Resumes immediately on user interaction --- .../addresses/[address]/balance/route.ts | 4 +- .../useTokenBalances/stores/amountsStore.ts | 4 +- .../stores/inactivityStore.ts | 54 +++++++++++++++++++ .../useTokenBalances/stores/tokensStore.ts | 9 +++- 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useTokenBalances/stores/inactivityStore.ts diff --git a/src/app/api/chains/[family]/addresses/[address]/balance/route.ts b/src/app/api/chains/[family]/addresses/[address]/balance/route.ts index 33abf9d..76a4c87 100644 --- a/src/app/api/chains/[family]/addresses/[address]/balance/route.ts +++ b/src/app/api/chains/[family]/addresses/[address]/balance/route.ts @@ -11,7 +11,9 @@ import { throwIfQueryParamIsNull } from "../../../../../utils/throwIfQueryParamI // cache balance for 5 seconds const headers = { headers: { - "Cache-Control": `public, max-age=${5}`, + "Cache-Control": "public, s-maxage=5", + "CDN-Cache-Control": "public, s-maxage=5", + "Vercel-CDN-Cache-Control": "public, s-maxage=5", }, } export async function GET( diff --git a/src/hooks/useTokenBalances/stores/amountsStore.ts b/src/hooks/useTokenBalances/stores/amountsStore.ts index dda7471..30301e8 100644 --- a/src/hooks/useTokenBalances/stores/amountsStore.ts +++ b/src/hooks/useTokenBalances/stores/amountsStore.ts @@ -10,6 +10,7 @@ import type { Tokens } from "@/tokens/types" import type { NormalizedAddressMap } from "../NormalizedAddressMap" import { focusStore } from "./focusStore" +import { inactivityStore } from "./inactivityStore" import { tokensStore } from "./tokensStore" const AMOUNTS_REFRESH_INTERVAL_MS = 1000 * 60 * 3 @@ -99,7 +100,8 @@ subscribeKey(sessionState, "addresses", (addresses) => { setInterval(() => { const now = Date.now() - if (!focusStore.isWalletInFocus) { + // Skip if not focused OR inactive + if (!focusStore.isWalletInFocus || !inactivityStore.isActive) { return } diff --git a/src/hooks/useTokenBalances/stores/inactivityStore.ts b/src/hooks/useTokenBalances/stores/inactivityStore.ts new file mode 100644 index 0000000..b611d5e --- /dev/null +++ b/src/hooks/useTokenBalances/stores/inactivityStore.ts @@ -0,0 +1,54 @@ +import { proxy } from "valtio" + +type InactivityStore = { + isActive: boolean + lastInteractionTime: number + inactivityTimeoutMs: number +} + +export const inactivityStore = proxy({ + isActive: true, + lastInteractionTime: Date.now(), + inactivityTimeoutMs: 1000 * 60 * 5, // 5 minutes +}) + +const updateInactivityStore = () => { + const resetInactivity = () => { + inactivityStore.lastInteractionTime = Date.now() + inactivityStore.isActive = true + } + + if (!window) { + return () => {} + } + + // Track user interactions + const events = [ + "mousemove", + "keydown", + "click", + "touchstart", + "scroll", + "wheel", + ] + events.forEach((event) => { + window.addEventListener(event, resetInactivity, { passive: true }) + }) + + // Check inactivity every 10 seconds + const inactivityCheckInterval = setInterval(() => { + const timeSinceLastInteraction = + Date.now() - inactivityStore.lastInteractionTime + inactivityStore.isActive = + timeSinceLastInteraction < inactivityStore.inactivityTimeoutMs + }, 10_000) + + return () => { + events.forEach((event) => { + window.removeEventListener(event, resetInactivity) + }) + clearInterval(inactivityCheckInterval) + } +} + +updateInactivityStore() diff --git a/src/hooks/useTokenBalances/stores/tokensStore.ts b/src/hooks/useTokenBalances/stores/tokensStore.ts index ad97a23..4d7ed3f 100644 --- a/src/hooks/useTokenBalances/stores/tokensStore.ts +++ b/src/hooks/useTokenBalances/stores/tokensStore.ts @@ -3,6 +3,8 @@ import { proxy } from "valtio" import type { Tokens } from "@/tokens/types" import { getTokens } from "../fetchers/tokens" +import { focusStore } from "./focusStore" +import { inactivityStore } from "./inactivityStore" const TOKENS_REFRESH_INTERVAL_MS = 1000 * 60 * 60 @@ -37,4 +39,9 @@ export const tokensStore = proxy({ }) refreshTokens() -setInterval(refreshTokens, TOKENS_REFRESH_INTERVAL_MS) +setInterval(() => { + if (!focusStore.isWalletInFocus || !inactivityStore.isActive) { + return + } + refreshTokens() +}, TOKENS_REFRESH_INTERVAL_MS) From 26f6ec32777dc3568fbc4de2543a6ea230e11f9b Mon Sep 17 00:00:00 2001 From: sirpy Date: Mon, 12 Oct 2026 10:37:19 +0300 Subject: [PATCH 2/3] Check for window type before accessing it Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: sirpy --- src/hooks/useTokenBalances/stores/inactivityStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useTokenBalances/stores/inactivityStore.ts b/src/hooks/useTokenBalances/stores/inactivityStore.ts index b611d5e..30f999a 100644 --- a/src/hooks/useTokenBalances/stores/inactivityStore.ts +++ b/src/hooks/useTokenBalances/stores/inactivityStore.ts @@ -18,7 +18,7 @@ const updateInactivityStore = () => { inactivityStore.isActive = true } - if (!window) { + if (typeof window === "undefined") { return () => {} } From 7d9920576199ecbe2566bdcfba5a60abe1dd999f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:39:14 +0000 Subject: [PATCH 3/3] Refresh tokens after inactive period Co-authored-by: sirpy <671095+sirpy@users.noreply.github.com> --- src/hooks/useTokenBalances/stores/tokensStore.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/hooks/useTokenBalances/stores/tokensStore.ts b/src/hooks/useTokenBalances/stores/tokensStore.ts index 4d7ed3f..de59e4f 100644 --- a/src/hooks/useTokenBalances/stores/tokensStore.ts +++ b/src/hooks/useTokenBalances/stores/tokensStore.ts @@ -1,4 +1,5 @@ import { proxy } from "valtio" +import { subscribeKey } from "valtio/utils" import type { Tokens } from "@/tokens/types" @@ -7,6 +8,7 @@ import { focusStore } from "./focusStore" import { inactivityStore } from "./inactivityStore" const TOKENS_REFRESH_INTERVAL_MS = 1000 * 60 * 60 +let nextRefreshAt = Date.now() + TOKENS_REFRESH_INTERVAL_MS type TokensStore = { tokens: Tokens | undefined @@ -39,9 +41,18 @@ export const tokensStore = proxy({ }) refreshTokens() -setInterval(() => { +const refreshTokensIfDue = () => { if (!focusStore.isWalletInFocus || !inactivityStore.isActive) { return } + + const now = Date.now() + if (now < nextRefreshAt) return + + nextRefreshAt = now + TOKENS_REFRESH_INTERVAL_MS refreshTokens() -}, TOKENS_REFRESH_INTERVAL_MS) +} + +setInterval(refreshTokensIfDue, TOKENS_REFRESH_INTERVAL_MS) +subscribeKey(focusStore, "isWalletInFocus", refreshTokensIfDue) +subscribeKey(inactivityStore, "isActive", refreshTokensIfDue)