feat: bound foreground API requests (#397)
CI / lint (pull_request) Successful in 41s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-09 11:37:57 +00:00
1 parent 055a9dbb2a
commit f0b0b703ff
10 files changed
+227 -17

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v60';
const CACHE = 'stackchain-dashboard-shell-v61';
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
const SHELL = [
+54 -3
View File
@@ -93,6 +93,7 @@
promptAuthorization = () => null,
onExpired = () => {},
onClearError = () => {},
requestTimeoutMs = 15000,
}) {
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
let expirationStarted = false;
@@ -122,6 +123,56 @@
catch (_error) { return false; }
}
function isDashboardApi(input) {
try {
const url = new URL(String(input?.url || input), origin);
return url.origin === origin && url.pathname.startsWith(base + 'api/v1/');
} catch (_error) { return false; }
}
async function fetchWithDeadline(input, options, method, phase = 'request') {
if (!isDashboardApi(input)) return fetchImpl(input, options);
const controller = new AbortController();
const callerSignal = options.signal || input?.signal;
let timeout;
let rejectCancellation;
const cancellation = new Promise((_resolve, reject) => { rejectCancellation = reject; });
const cancelFromCaller = () => {
const error = callerSignal.reason || new DOMException('The request was aborted.', 'AbortError');
error.source = 'caller';
controller.abort(error);
rejectCancellation(error);
};
if (callerSignal?.aborted) cancelFromCaller();
else callerSignal?.addEventListener?.('abort', cancelFromCaller, { once: true });
const deadline = new Promise((_resolve, reject) => {
timeout = setTimeout(() => {
const error = new Error(
SAFE_METHODS.has(method)
? 'Request timed out. Try again.'
: 'Request timed out. Refresh to verify the outcome before retrying.'
);
error.name = 'TimeoutError';
error.method = method;
error.outcome = 'unknown';
error.safeToRetry = SAFE_METHODS.has(method);
error.phase = phase;
controller.abort(error);
reject(error);
}, requestTimeoutMs);
});
try {
return await Promise.race([
Promise.resolve().then(() => fetchImpl(input, { ...options, signal: controller.signal })),
deadline,
cancellation,
]);
} finally {
clearTimeout(timeout);
callerSignal?.removeEventListener?.('abort', cancelFromCaller);
}
}
async function sessionFetch(input, options = {}, allowStepUp = true) {
const method = String(options.method || input?.method || 'GET').toUpperCase();
const requestOptions = { ...options };
@@ -131,7 +182,7 @@
if (csrf) headers.set('X-CSRF-Token', csrf);
requestOptions.headers = headers;
}
const response = await fetchImpl(input, requestOptions);
const response = await fetchWithDeadline(input, requestOptions, method);
if (response.status === 428 && isSameOrigin(input) && allowStepUp) {
const payload = await response.clone().json().catch(() => ({}));
const detail = payload?.detail || {};
@@ -147,7 +198,7 @@
});
const csrf = csrfToken();
if (csrf) authorizationHeaders.set('X-CSRF-Token', csrf);
const authorization = await fetchImpl(base + 'api/v1/fresh-authorization', {
const authorization = await fetchWithDeadline(base + 'api/v1/fresh-authorization', {
method: 'POST',
headers: authorizationHeaders,
body: JSON.stringify({
@@ -155,7 +206,7 @@
action: detail.action,
target: detail.target,
}),
});
}, 'POST', 'fresh-authorization');
if (!authorization.ok) return authorization;
const grant = await authorization.json().catch(() => ({}));
if (!grant.grant) return response;