From 6f5e83112728417da664dec331a6f8bc7fb80366 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Mon, 8 Jun 2026 19:10:36 +0200 Subject: [PATCH] PR comments - Account loading now dedupes in-flight requests per network instead of sharing one global promise across all networks. - Regression tests cover same-network reuse and cross-network isolation. --- nym-wallet/src/context/main.tsx | 19 +++----- .../src/utils/dedupeInflightByKey.test.ts | 43 +++++++++++++++++++ nym-wallet/src/utils/dedupeInflightByKey.ts | 17 ++++++++ 3 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 nym-wallet/src/utils/dedupeInflightByKey.test.ts create mode 100644 nym-wallet/src/utils/dedupeInflightByKey.ts diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index eef88eb645..73ebb3c8e0 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -21,6 +21,7 @@ import { Console } from '../utils/console'; import { createSignInWindow, getReactState, setReactState } from '../requests/app'; import { fetchNymPriceDeduped, getNetworkOverviewEndpoints, clearNymPriceCache } from '../api/networkOverview'; import { signInAndNavigateToBalance } from '../utils/signInAndNavigateToBalance'; +import { dedupeInflightByKey } from '../utils/dedupeInflightByKey'; import { toDisplay } from '../utils'; export const urls = (networkName?: Network) => @@ -103,7 +104,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { const [loginType, setLoginType] = useState<'mnemonic' | 'password'>(); const [isLoading, setIsLoadingInternal] = useState(false); const hadClientDetailsRef = useRef(false); - const accountLoadInflightRef = useRef | null>(null); + const accountLoadInflightRef = useRef>>(new Map()); const [loadingPresentation, setLoadingPresentation] = useState('auth-splash'); const [loadingOverlayTitle, setLoadingOverlayTitle] = useState(''); const [loadingOverlaySubtitle, setLoadingOverlaySubtitle] = useState(); @@ -161,12 +162,8 @@ export const AppProvider: FCWithChildren = ({ children }) => { setMixnodeDetails(null); }; - const loadAccount = async (n: Network): Promise => { - if (accountLoadInflightRef.current) { - return accountLoadInflightRef.current; - } - - const pending = (async () => { + const loadAccount = async (n: Network): Promise => + dedupeInflightByKey(accountLoadInflightRef.current, n, async () => { try { const client = await selectNetwork(n); setClientDetails(client); @@ -175,14 +172,8 @@ export const AppProvider: FCWithChildren = ({ children }) => { enqueueSnackbar('Error loading account', { variant: 'error' }); Console.error(e as string); return undefined; - } finally { - accountLoadInflightRef.current = null; } - })(); - - accountLoadInflightRef.current = pending; - return pending; - }; + }); const loadStoredAccounts = async () => { const accounts = await listAccounts(); diff --git a/nym-wallet/src/utils/dedupeInflightByKey.test.ts b/nym-wallet/src/utils/dedupeInflightByKey.test.ts new file mode 100644 index 0000000000..78c5b23800 --- /dev/null +++ b/nym-wallet/src/utils/dedupeInflightByKey.test.ts @@ -0,0 +1,43 @@ +import { dedupeInflightByKey } from './dedupeInflightByKey'; + +describe('dedupeInflightByKey', () => { + it('reuses the in-flight promise for the same key', async () => { + const inflight = new Map>(); + let calls = 0; + const load = () => { + calls += 1; + return new Promise((resolve) => { + setTimeout(() => resolve('done'), 20); + }); + }; + + const first = dedupeInflightByKey(inflight, 'MAINNET', load); + const second = dedupeInflightByKey(inflight, 'MAINNET', load); + + expect(first).toBe(second); + await first; + expect(calls).toBe(1); + }); + + it('does not reuse promises across different keys', async () => { + const inflight = new Map>(); + let calls = 0; + const load = (value: string) => () => { + calls += 1; + return Promise.resolve(value); + }; + + const mainnet = dedupeInflightByKey(inflight, 'MAINNET', load('mainnet')); + const sandbox = dedupeInflightByKey(inflight, 'SANDBOX', load('sandbox')); + + await expect(mainnet).resolves.toBe('mainnet'); + await expect(sandbox).resolves.toBe('sandbox'); + expect(calls).toBe(2); + }); + + it('clears the key after the promise settles', async () => { + const inflight = new Map>(); + await dedupeInflightByKey(inflight, 'MAINNET', async () => 'done'); + expect(inflight.has('MAINNET')).toBe(false); + }); +}); diff --git a/nym-wallet/src/utils/dedupeInflightByKey.ts b/nym-wallet/src/utils/dedupeInflightByKey.ts new file mode 100644 index 0000000000..dafec135ad --- /dev/null +++ b/nym-wallet/src/utils/dedupeInflightByKey.ts @@ -0,0 +1,17 @@ +export function dedupeInflightByKey( + inflight: Map>, + key: K, + load: () => Promise, +): Promise { + const existing = inflight.get(key); + if (existing) { + return existing; + } + + const pending = load().finally(() => { + inflight.delete(key); + }); + + inflight.set(key, pending); + return pending; +}