fix(workbench): share sync reads and retry failed invalidations

This commit is contained in:
2026-10-08 15:41:50 +08:00
parent 64ef058e0d
commit 7c6d06cb43
8 changed files with 274 additions and 74 deletions
+28 -13
View File
@@ -69,22 +69,39 @@ export type WorkbenchProjectPresentation = {
};
const SERVER_API_BASE_URL = env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, "");
const projectContextPromises = new WeakMap<AccessTokenProvider, Promise<WorkbenchProject>>();
const PROJECT_CACHE_TTL_MS = 5 * 60_000;
const projectContextPromises = new WeakMap<AccessTokenProvider, {
promise: Promise<WorkbenchProject>;
expiresAt: number;
}>();
function awaitProject(promise: Promise<WorkbenchProject>, signal?: AbortSignal) {
if (!signal) return promise;
return new Promise<WorkbenchProject>((resolve, reject) => {
const abort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
if (signal.aborted) { abort(); return; }
signal.addEventListener("abort", abort, { once: true });
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
});
}
export async function findOwnedWorkbenchProject(
getAccessToken?: AccessTokenProvider,
signal?: AbortSignal
): Promise<{ project: WorkbenchProject; accessToken: string | null | undefined }> {
const accessToken = await getAccessToken?.();
const cacheable = Boolean(getAccessToken && !signal);
const existing = cacheable && getAccessToken
signal?.throwIfAborted();
const existing = getAccessToken
? projectContextPromises.get(getAccessToken)
: undefined;
if (existing) return { project: await existing, accessToken };
if (existing && existing.expiresAt > Date.now()) {
return { project: await awaitProject(existing.promise, signal), accessToken };
}
const loadProject = async () => {
const response = await fetch(`${SERVER_API_BASE_URL}/api/v1/projects?limit=1000&offset=0`, {
headers: createWorkbenchHeaders(accessToken),
signal
// One caller cancelling must not cancel other consumers of this lookup.
signal: getAccessToken ? AbortSignal.timeout(15_000) : signal
});
const payload: unknown = await response.json().catch(() => null);
if (!response.ok) throw new Error(getServerError(payload, response.status, "项目查询"));
@@ -96,15 +113,13 @@ export async function findOwnedWorkbenchProject(
return project;
};
const pending = loadProject();
if (cacheable && getAccessToken) projectContextPromises.set(getAccessToken, pending);
try {
return { project: await pending, accessToken };
} catch (error) {
if (cacheable && getAccessToken && projectContextPromises.get(getAccessToken) === pending) {
projectContextPromises.delete(getAccessToken);
}
throw error;
if (getAccessToken) {
projectContextPromises.set(getAccessToken, { promise: pending, expiresAt: Date.now() + PROJECT_CACHE_TTL_MS });
void pending.catch(() => {
if (projectContextPromises.get(getAccessToken)?.promise === pending) projectContextPromises.delete(getAccessToken);
});
}
return { project: await awaitProject(pending, signal), accessToken };
}
export async function fetchWorkbenchProjectPresentation(