perf: deduplicate concurrent API reads
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
_clearApiClientInflightForTests,
|
||||
fetchJson,
|
||||
setTokenGetter,
|
||||
} from './api-client.js';
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
test('deduplicates concurrent GET requests for the same URL and token', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
let resolveFetch: ((response: Response) => void) | undefined;
|
||||
globalThis.fetch = ((_: string | URL | Request, init?: RequestInit) => {
|
||||
calls += 1;
|
||||
assert.equal(new Headers(init?.headers).get('authorization'), 'Bearer test-token');
|
||||
return new Promise<Response>(resolve => { resolveFetch = resolve; });
|
||||
}) as typeof fetch;
|
||||
setTokenGetter(() => 'test-token');
|
||||
_clearApiClientInflightForTests();
|
||||
|
||||
try {
|
||||
const first = fetchJson<{ value: number }>('/api/test');
|
||||
const second = fetchJson<{ value: number }>('/api/test');
|
||||
assert.equal(calls, 1);
|
||||
resolveFetch?.(jsonResponse({ value: 42 }));
|
||||
assert.deepEqual(await first, { value: 42 });
|
||||
assert.deepEqual(await second, { value: 42 });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
setTokenGetter(() => null);
|
||||
_clearApiClientInflightForTests();
|
||||
}
|
||||
});
|
||||
|
||||
test('does not retain a failed GET request', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) throw new Error('network down');
|
||||
return jsonResponse({ ok: true });
|
||||
}) as typeof fetch;
|
||||
setTokenGetter(() => null);
|
||||
_clearApiClientInflightForTests();
|
||||
|
||||
try {
|
||||
await assert.rejects(fetchJson('/api/retry'), /network down/);
|
||||
assert.deepEqual(await fetchJson('/api/retry'), { ok: true });
|
||||
assert.equal(calls, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
_clearApiClientInflightForTests();
|
||||
}
|
||||
});
|
||||
+24
-2
@@ -1,13 +1,13 @@
|
||||
/** 全局认证 fetch 客户端 */
|
||||
|
||||
let tokenGetter: () => string | null = () => null;
|
||||
const inflightGetRequests = new Map<string, Promise<unknown>>();
|
||||
|
||||
export function setTokenGetter(fn: () => string | null) {
|
||||
tokenGetter = fn;
|
||||
}
|
||||
|
||||
export async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const token = tokenGetter();
|
||||
async function executeJsonRequest<T>(url: string, options: RequestInit | undefined, token: string | null): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
@@ -22,3 +22,25 @@ export async function fetchJson<T>(url: string, options?: RequestInit): Promise<
|
||||
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const token = tokenGetter();
|
||||
const method = (options?.method || 'GET').toUpperCase();
|
||||
const canDedupe = method === 'GET' && options?.body == null && options?.signal == null;
|
||||
if (!canDedupe) return executeJsonRequest<T>(url, options, token);
|
||||
|
||||
const key = `${token || 'anonymous'}:${url}`;
|
||||
const existing = inflightGetRequests.get(key) as Promise<T> | undefined;
|
||||
if (existing) return existing;
|
||||
|
||||
const request = executeJsonRequest<T>(url, options, token)
|
||||
.finally(() => {
|
||||
if (inflightGetRequests.get(key) === request) inflightGetRequests.delete(key);
|
||||
});
|
||||
inflightGetRequests.set(key, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
export function _clearApiClientInflightForTests(): void {
|
||||
inflightGetRequests.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user