fix(mp): 修复并发请求饥饿导致开发者工具卡死

- 长轮询走独立通道(requestUnlimited),不再占用 ConcurrencyLimiter 槽位
- ConcurrencyLimiter 上限 8→12,缓解 TabBar 切换请求风暴
- 新增 safeReLaunch 去重,防止并发 401 多次触发页面跳转
- maxFailures 50→10,后端不可用时快速止损而非持续 18 分钟重试

根因:咨询页长轮询每次占用槽位 25-30s,8 个槽位被占满后
所有新请求排队等待,叠加 401 场景形成死锁。
This commit is contained in:
iven
2026-05-17 17:01:24 +08:00
parent b84becfbea
commit 9d50ef7847
4 changed files with 27 additions and 11 deletions

View File

@@ -10,7 +10,7 @@ interface LongPollOptions<T> {
enabled: boolean;
/** 成功轮询间隔 ms默认 3000 */
intervalMs?: number;
/** 连续失败上限,默认 50 */
/** 连续失败上限,默认 10 */
maxFailures?: number;
}
@@ -27,7 +27,7 @@ export function useLongPolling<T>({
onData,
enabled,
intervalMs = 3000,
maxFailures = 50,
maxFailures = 10,
}: LongPollOptions<T>) {
const generation = useRef(0);
const mountedRef = useRef(true);

View File

@@ -1,4 +1,4 @@
import { api, requestWithTimeout } from './request';
import { api, requestUnlimited } from './request';
export interface ConsultationSession {
id: string;
@@ -72,5 +72,5 @@ export async function pollMessages(sessionId: string, afterId?: string) {
params.set('timeout', '25');
const query = params.toString();
const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`;
return requestWithTimeout<ConsultationMessage[]>('GET', path, undefined, 30000);
return requestUnlimited<ConsultationMessage[]>('GET', path, undefined, 30000);
}

View File

@@ -1,4 +1,4 @@
import { api, requestWithTimeout } from '../request';
import { api, requestUnlimited } from '../request';
// ── Consultation (doctor view) ─────────────────────
@@ -71,7 +71,7 @@ export async function pollMessages(sessionId: string, afterId?: string) {
params.set('timeout', '25');
const query = params.toString();
const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`;
return requestWithTimeout<ConsultationMessage[]>('GET', path, undefined, 30000);
return requestUnlimited<ConsultationMessage[]>('GET', path, undefined, 30000);
}
export interface ConsultationStats {

View File

@@ -56,7 +56,7 @@ class ConcurrencyLimiter {
}
}
const limiter = new ConcurrencyLimiter(8);
const limiter = new ConcurrencyLimiter(12);
// --- Response cache + deduplication ---
@@ -221,13 +221,24 @@ async function doRefresh(): Promise<boolean> {
return false;
}
// --- reLaunch 去重 ---
let reLaunchPromise: Promise<void> | null = null;
function safeReLaunch(url: string): void {
if (reLaunchPromise) return;
reLaunchPromise = Taro.reLaunch({ url }).then(() => {}, () => {}).then(() => {
setTimeout(() => { reLaunchPromise = null; }, 2000);
});
}
// --- Core request ---
async function request<T>(method: string, path: string, data?: unknown, timeout?: number, signal?: AbortSignal): Promise<T> {
async function request<T>(method: string, path: string, data?: unknown, timeout?: number, signal?: AbortSignal, bypassLimiter = false): Promise<T> {
let retryCount401 = 0;
for (;;) {
if (signal?.aborted) throw new Error('请求已取消');
await limiter.acquire();
if (!bypassLimiter) await limiter.acquire();
try {
const headers = await getHeaders();
const url = `${BASE_URL}${path}`;
@@ -262,7 +273,7 @@ async function request<T>(method: string, path: string, data?: unknown, timeout?
const pages = Taro.getCurrentPages();
const currentPath = pages[pages.length - 1]?.path || '';
if (!currentPath.includes('pages/login')) {
Taro.reLaunch({ url: '/pages/login/index' });
safeReLaunch('/pages/login/index');
}
}
throw new Error('登录已过期');
@@ -287,7 +298,7 @@ async function request<T>(method: string, path: string, data?: unknown, timeout?
}
return body.data as T;
} finally {
limiter.release();
if (!bypassLimiter) limiter.release();
}
}
}
@@ -349,6 +360,11 @@ export async function requestWithTimeout<T>(method: string, path: string, data?:
return request<T>(method, path, data, timeout);
}
/** 绕过并发限制的请求,用于长轮询等长时间 hang 的请求 */
export async function requestUnlimited<T>(method: string, path: string, data?: unknown, timeout?: number): Promise<T> {
return request<T>(method, path, data, timeout, undefined, true);
}
/** 重置所有模块级状态,用于测试隔离 */
export function resetForTesting(): void {
limiter.reset();