diff --git a/xmcl b/xmcl index e7303a155..8457cb4b4 160000 --- a/xmcl +++ b/xmcl @@ -1 +1 @@ -Subproject commit e7303a15566614dac3fbdb1ed9116cb798fec0f2 +Subproject commit 8457cb4b429831f540a58f21599480a81e1131dc diff --git a/xmcl-runtime/network/pluginNetworkInterface.ts b/xmcl-runtime/network/pluginNetworkInterface.ts index 43603e3b5..215ab12e1 100644 --- a/xmcl-runtime/network/pluginNetworkInterface.ts +++ b/xmcl-runtime/network/pluginNetworkInterface.ts @@ -102,9 +102,100 @@ export const pluginNetworkInterface: LauncherAppPlugin = (app) => { }), new JsonCacheStorage(cache)) setGlobalDispatcher(apiDispatcher) + function calculateRetryAfterHeader(retryAfter: number) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff + } + const downloadAgentOptions = getDefaultAgentOptions({ maxTimeout: 60_000, maxRetries: 30, + // @ts-ignore + retry: (err, { state, opts }, cb) => { + const { statusCode, code, headers } = err as any + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods, + } = retryOptions! as any + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes!.includes(code) + ) { + if (code !== 'UND_ERR_CONNECT_TIMEOUT') { + cb(err) + return + } + // Check if there are connection with the same origin + // If there are, this error is usually due to high traffic, and we should retry + // @ts-ignore + const clients = downloadProxy.agent[kClients] as Map + if (!opts.origin) { + cb(err) + return + } + const pool = clients.get(typeof opts.origin === 'string' ? opts.origin : opts.origin.origin) + const stats = pool?.stats + if (!stats?.connected && !stats?.pending && !stats?.running && !stats?.queued && !stats?.free) { + cb(err) + return + } + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > (maxRetries ?? 5)) { + cb(err) + return + } + + let retryAfterHeader = headers?.['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = Number.isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, (maxTimeout!)) + : Math.min(currentTimeout * timeoutFactor! ** counter, maxTimeout!) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + }, }) const downloadProxy = new ProxyAgent({ controller: proxy,