Skip to content

Commit fdb51b9

Browse files
OhYeeclaude
andcommitted
fix(api): coalesce concurrent isS3Enabled probes
Two related bugs in the previous boolean-only cache: 1. Stampede: `isS3Enabled()` was called from every `DrawingCard` that mounts. With several cards mounting in the same render pass, each call hit the cache as `null`, fired its own GET /files/config, and raced. Now we cache the in-flight Promise so concurrent callers coalesce onto a single request. 2. Sticky-false on transient failures: a 401 during the auth-status bootstrap or a one-off network blip permanently latched the cache to `false`, disabling S3 uploads for the rest of the page lifetime even after auth recovered. Only a successful response is cached now; failures resolve `false` for that caller but leave the cache open for retry. Change-Id: Ib38aae45e2724057f12b9a8d03a3603381ecef6a Co-developed-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 98e407e commit fdb51b9

1 file changed

Lines changed: 29 additions & 9 deletions

File tree

frontend/src/api/index.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -686,22 +686,42 @@ export const updateLibrary = async (items: LibraryItem[]): Promise<LibraryItem[]
686686
// S3 file upload helpers
687687
// ---------------------------------------------------------------------------
688688

689-
/** Cache the result of the /files/config probe so we only call it once. */
689+
/** Cached resolved result, settled successfully at least once. */
690690
let s3EnabledCache: boolean | null = null;
691+
/**
692+
* In-flight promise so concurrent callers (e.g. many `DrawingCard`s
693+
* mounting at once) coalesce onto a single `/files/config` request
694+
* instead of stampeding the backend, and so a transient failure on
695+
* one caller doesn't poison the cache for everyone.
696+
*/
697+
let s3EnabledInFlight: Promise<boolean> | null = null;
691698

692699
/**
693700
* Returns true when the backend has S3 configured.
694-
* The result is cached for the lifetime of the page.
701+
* The successful result is cached for the lifetime of the page; on a
702+
* transient request failure we let the next caller retry rather than
703+
* permanently latching to false.
695704
*/
696705
export const isS3Enabled = async (): Promise<boolean> => {
697706
if (s3EnabledCache !== null) return s3EnabledCache;
698-
try {
699-
const response = await api.get<{ s3Enabled: boolean }>("/files/config");
700-
s3EnabledCache = response.data.s3Enabled === true;
701-
} catch {
702-
s3EnabledCache = false;
703-
}
704-
return s3EnabledCache;
707+
if (s3EnabledInFlight) return s3EnabledInFlight;
708+
709+
s3EnabledInFlight = (async () => {
710+
try {
711+
const response = await api.get<{ s3Enabled: boolean }>("/files/config");
712+
s3EnabledCache = response.data.s3Enabled === true;
713+
return s3EnabledCache;
714+
} catch {
715+
// Don't cache failures — a transient 401 during auth bootstrap or
716+
// a network blip would otherwise permanently disable S3 for the
717+
// rest of the page lifetime.
718+
return false;
719+
} finally {
720+
s3EnabledInFlight = null;
721+
}
722+
})();
723+
724+
return s3EnabledInFlight;
705725
};
706726

707727
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)