feat: subscription timeout settings & subscription update logic efficiency (#1562)

1. Add updateTimeout property to IProfileItem interface
2. Add dedicated timeout config option for subscriptions
3. Improve subscription update logic efficiency
4. Use subscriptionTimeout as the smart fallback time
5. Localize
profiles.editInfo.updateTimeout and profiles.editInfo.updateTimeoutPlaceholder for en-US/fa-IR/ru-RU/zh-CN/zh-TW
This commit is contained in:
julong 2026-01-23 20:42:45 +08:00 committed by GitHub
parent 767cdfeef3
commit d3a23a0601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 87 additions and 73 deletions

View File

@ -218,10 +218,10 @@ async function fetchAndValidateSubscription(options: FetchOptions): Promise<Fetc
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
const id = item.id || new Date().getTime().toString(16)
const newItem = {
const newItem: IProfileItem = {
id,
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
type: item.type,
type: item.type!,
url: item.url,
substore: item.substore || false,
interval: item.interval || 0,
@ -230,14 +230,22 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
allowFixedInterval: item.allowFixedInterval || false,
autoUpdate: item.autoUpdate ?? false,
authToken: item.authToken,
updated: new Date().getTime()
} as IProfileItem
updated: new Date().getTime(),
updateTimeout: item.updateTimeout || 5
}
// Local
if (newItem.type === 'local') {
await setProfileStr(id, item.file || '')
return newItem
}
// Remote
if (!item.url) throw new Error('Empty URL')
switch (newItem.type) {
case 'remote': {
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
if (!item.url) throw new Error('Empty URL')
const userItemTimeoutMs = (newItem.updateTimeout || 5) * 1000
const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = {
url: item.url,
@ -247,46 +255,25 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
substore: newItem.substore || false
}
let result: FetchResult
let finalUseProxy = newItem.useProxy
const fetchSub = (useProxy: boolean, timeout: number) =>
fetchAndValidateSubscription({ ...baseOptions, useProxy, timeout })
if (newItem.useProxy) {
result = await fetchAndValidateSubscription({
...baseOptions,
useProxy: true,
timeout: subscriptionTimeout
})
} else if (newItem.substore) {
// SubStore requests (especially collections) need more time as they fetch and merge multiple subscriptions
// Use the full subscriptionTimeout since SubStore is a local server and doesn't need smart fallback
result = await fetchAndValidateSubscription({
...baseOptions,
useProxy: false,
timeout: subscriptionTimeout
})
let result: FetchResult
if (newItem.useProxy || newItem.substore) {
result = await fetchSub(newItem.useProxy!, userItemTimeoutMs)
} else {
const smartTimeout = 5000
try {
result = await fetchAndValidateSubscription({
...baseOptions,
useProxy: false,
timeout: smartTimeout
})
result = await fetchSub(false, userItemTimeoutMs)
} catch (directError) {
try {
result = await fetchAndValidateSubscription({
...baseOptions,
useProxy: true,
timeout: smartTimeout
})
finalUseProxy = true
// smart fallback
result = await fetchSub(true, subscriptionTimeout)
} catch {
throw directError
}
}
}
newItem.useProxy = finalUseProxy
const { data, headers } = result
if (headers['content-disposition'] && newItem.name === 'Remote File') {
@ -301,14 +288,8 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
if (headers['subscription-userinfo']) {
newItem.extra = parseSubinfo(headers['subscription-userinfo'])
}
await setProfileStr(id, data)
break
}
case 'local': {
await setProfileStr(id, item.file || '')
break
}
}
return newItem
}

View File

@ -32,7 +32,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
const { item, updateProfileItem, onClose } = props
const { overrideConfig } = useOverrideConfig()
const { items: overrideItems = [] } = overrideConfig || {}
const [values, setValues] = useState(item)
const [values, setValues] = useState({
...item,
updateTimeout: item.updateTimeout ?? 5
})
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
const { t } = useTranslation()
@ -40,6 +43,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
try {
const updatedItem = {
...values,
updateTimeout: values.updateTimeout ?? 5,
override: values.override?.filter(
(i) =>
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
@ -192,6 +196,24 @@ const EditInfoModal: React.FC<Props> = (props) => {
)}
</>
)}
<SettingItem title={t('profiles.editInfo.updateTimeout')}>
<Input
size="sm"
type="text"
className={cn(inputWidth)}
value={values.updateTimeout?.toString() ?? ''}
onValueChange={(v) => {
if (v === '') {
setValues({ ...values, updateTimeout: undefined as unknown as number })
return
}
if (/^\d+$/.test(v)) {
setValues({ ...values, updateTimeout: parseInt(v, 10) })
}
}}
placeholder={t('profiles.editInfo.updateTimeoutPlaceholder')}
/>
</SettingItem>
<SettingItem title={t('profiles.editInfo.override.title')}>
<div>
{overrideItems

View File

@ -484,6 +484,8 @@
"profiles.editInfo.override.global": "Global",
"profiles.editInfo.override.noAvailable": "No available overrides",
"profiles.editInfo.override.add": "Add Override",
"profiles.editInfo.updateTimeout": "Update Timeout",
"profiles.editInfo.updateTimeoutPlaceholder": "Timeout in seconds",
"profiles.editFile.title": "Edit Profile",
"profiles.editFile.notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use",
"profiles.editFile.override": "Override",

View File

@ -454,6 +454,8 @@
"profiles.editInfo.override.global": "جهانی",
"profiles.editInfo.override.noAvailable": "جایگزینی‌ای در دسترس نیست",
"profiles.editInfo.override.add": "افزودن جایگزینی",
"profiles.editInfo.updateTimeout": "زمان انتظار به‌روزرسانی",
"profiles.editInfo.updateTimeoutPlaceholder": "زمان پایان دادن به ثانیه",
"profiles.editInfo.intervalPlaceholder": "مثال: 30 یا '0 * * * *'",
"profiles.editInfo.intervalInvalid": "نامعتبر",
"profiles.editInfo.intervalMinutes": "فاصله زمانی ثابت به دقیقه",

View File

@ -454,6 +454,8 @@
"profiles.editInfo.override.global": "Глобальный",
"profiles.editInfo.override.noAvailable": "Нет доступных переопределений",
"profiles.editInfo.override.add": "Добавить переопределение",
"profiles.editInfo.updateTimeout": "Таймаут обновления",
"profiles.editInfo.updateTimeoutPlaceholder": "Время ожидания в секундах",
"profiles.editInfo.intervalPlaceholder": "например: 30 или '0 * * * *'",
"profiles.editInfo.intervalInvalid": "Недействительно",
"profiles.editInfo.intervalMinutes": "Фиксированный интервал в минутах",

View File

@ -489,6 +489,8 @@
"profiles.editInfo.override.global": "全局",
"profiles.editInfo.override.noAvailable": "没有可用的覆写",
"profiles.editInfo.override.add": "添加覆写",
"profiles.editInfo.updateTimeout": "更新超时时间",
"profiles.editInfo.updateTimeoutPlaceholder": "以秒为单位的超时时间",
"profiles.editFile.title": "编辑订阅",
"profiles.editFile.notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用",
"profiles.editFile.override": "覆写",

View File

@ -489,6 +489,8 @@
"profiles.editInfo.override.global": "全局",
"profiles.editInfo.override.noAvailable": "沒有可用的覆寫",
"profiles.editInfo.override.add": "添加覆寫",
"profiles.editInfo.updateTimeout": "更新逾時時間",
"profiles.editInfo.updateTimeoutPlaceholder": "以秒為單位的超時時間",
"profiles.editFile.title": "編輯訂閱",
"profiles.editFile.notice": "注意:此處編輯配置更新訂閱後會還原,如需要自定義配置請使用",
"profiles.editFile.override": "覆寫",

View File

@ -497,6 +497,7 @@ interface IProfileItem {
allowFixedInterval?: boolean
autoUpdate?: boolean
authToken?: string
updateTimeout?: number
}
interface ISubStoreSub {