Skip to content

Commit 923e092

Browse files
committed
🔄 refactor(ui): remove auth user TTL cache in favor of request deduplication
1 parent 04e847e commit 923e092

File tree

2 files changed

+29
-39
lines changed

2 files changed

+29
-39
lines changed

‎ui/src/services/auth.ts‎

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,12 @@
44

55
import { errorMessage } from '../utils/error';
66

7-
export const AUTH_USER_CACHE_TTL_MS = 5_000;
8-
9-
let cachedUser: unknown = undefined;
10-
let cachedUserExpiresAt = 0;
11-
let hasCachedUser = false;
127
let pendingUserRequest: Promise<unknown> | undefined;
138

14-
function setCachedUser(user: unknown, now = Date.now()) {
15-
cachedUser = user;
16-
cachedUserExpiresAt = now + AUTH_USER_CACHE_TTL_MS;
17-
hasCachedUser = true;
18-
return user;
19-
}
20-
219
function clearCachedUser() {
22-
cachedUser = undefined;
23-
cachedUserExpiresAt = 0;
24-
hasCachedUser = false;
2510
pendingUserRequest = undefined;
2611
}
2712

28-
function hasFreshUserCache(now = Date.now()) {
29-
return hasCachedUser && cachedUserExpiresAt > now;
30-
}
31-
3213
function getPayloadErrorMessage(payload: unknown): string {
3314
if (typeof payload !== 'object' || payload === null) {
3415
return '';
@@ -61,27 +42,25 @@ async function getStrategies(): Promise<{
6142
* @returns {Promise<*>}
6243
*/
6344
async function getUser() {
64-
if (hasFreshUserCache()) {
65-
return cachedUser;
66-
}
67-
6845
if (pendingUserRequest) {
6946
return pendingUserRequest;
7047
}
7148

7249
pendingUserRequest = (async () => {
7350
try {
51+
// Only dedupe concurrent callers. Always revalidate settled auth state so
52+
// logout/session expiry in another tab is reflected on the next check.
7453
const response = await fetch('/auth/user', {
7554
redirect: 'manual',
7655
credentials: 'include',
7756
});
7857
if (response.ok) {
79-
return setCachedUser(await response.json());
58+
return await response.json();
8059
}
81-
return setCachedUser(undefined);
60+
return undefined;
8261
} catch (e: unknown) {
8362
console.debug(`Unable to fetch current user: ${errorMessage(e)}`);
84-
return setCachedUser(undefined);
63+
return undefined;
8564
} finally {
8665
pendingUserRequest = undefined;
8766
}
@@ -122,7 +101,8 @@ async function loginBasic(username: string, password: string, remember: boolean
122101

123102
throw new Error(message || 'Username or password error');
124103
}
125-
return setCachedUser(await response.json());
104+
clearCachedUser();
105+
return await response.json();
126106
}
127107

128108
/**

‎ui/tests/services/auth.spec.ts‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,20 @@ describe('Auth Service', () => {
6969
}
7070
});
7171

72-
it('reuses a fresh cached user without refetching', async () => {
73-
vi.useFakeTimers();
74-
const { AUTH_USER_CACHE_TTL_MS, getUser } = await loadAuthService();
72+
it('revalidates a settled authenticated user on the next call', async () => {
73+
const { getUser } = await loadAuthService();
7574
const mockUser = { username: 'cached-user', roles: ['admin'] };
7675
fetchMock.mockResolvedValueOnce({
7776
ok: true,
7877
json: async () => mockUser,
7978
});
80-
81-
expect(await getUser()).toEqual(mockUser);
82-
expect(await getUser()).toEqual(mockUser);
83-
84-
expect(fetchMock).toHaveBeenCalledTimes(1);
85-
86-
vi.advanceTimersByTime(AUTH_USER_CACHE_TTL_MS + 1);
8779
fetchMock.mockResolvedValueOnce({
88-
ok: true,
89-
json: async () => mockUser,
80+
ok: false,
81+
status: 401,
9082
});
9183

9284
expect(await getUser()).toEqual(mockUser);
85+
expect(await getUser()).toBeUndefined();
9386
expect(fetchMock).toHaveBeenCalledTimes(2);
9487
});
9588

@@ -115,6 +108,23 @@ describe('Auth Service', () => {
115108
await expect(first).resolves.toEqual(mockUser);
116109
await expect(second).resolves.toEqual(mockUser);
117110
});
111+
112+
it('does not keep an unauthenticated result cached after the request settles', async () => {
113+
const { getUser } = await loadAuthService();
114+
const mockUser = { username: 'fresh-user', roles: ['admin'] };
115+
fetchMock.mockResolvedValueOnce({
116+
ok: false,
117+
status: 401,
118+
});
119+
fetchMock.mockResolvedValueOnce({
120+
ok: true,
121+
json: async () => mockUser,
122+
});
123+
124+
expect(await getUser()).toBeUndefined();
125+
expect(await getUser()).toEqual(mockUser);
126+
expect(fetchMock).toHaveBeenCalledTimes(2);
127+
});
118128
});
119129

120130
describe('loginBasic', () => {

0 commit comments

Comments
 (0)