feat: bound foreground API requests (#397)
This commit is contained in:
1 parent
055a9dbb2a
commit
f0b0b703ff
10 files changed
+227
-17
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user