功能(桌面端):增加在线辅助本地缓存
This commit is contained in:
@@ -34,10 +34,14 @@ vi.mock("../session/sessionManager", () => ({
|
||||
describe("api axios retry", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1_000);
|
||||
errorMessage.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
const { clearApiResponseCache, setApiResponseCacheScope } = await import("./axios");
|
||||
setApiResponseCacheScope(null);
|
||||
clearApiResponseCache();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -67,4 +71,152 @@ describe("api axios retry", () => {
|
||||
await expect(request).resolves.toMatchObject({ message: "Network Error" });
|
||||
expect(errorMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("deduplicates concurrent apiGet calls for the same request", async () => {
|
||||
const { apiGet } = await import("./axios");
|
||||
const adapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { ok: true },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
apiGet("/api/v1/studies/", { adapter }),
|
||||
apiGet("/api/v1/studies/", { adapter }),
|
||||
]);
|
||||
|
||||
expect(adapter).toHaveBeenCalledTimes(1);
|
||||
expect(first.data).toEqual({ ok: true });
|
||||
expect(second.data).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("does not deduplicate requests when disabled by the caller", async () => {
|
||||
const { apiGet } = await import("./axios");
|
||||
const adapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { ok: true },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
|
||||
await Promise.all([
|
||||
apiGet("/api/v1/auth/login-key", { adapter, disableRequestDedupe: true }),
|
||||
apiGet("/api/v1/auth/login-key", { adapter, disableRequestDedupe: true }),
|
||||
]);
|
||||
|
||||
expect(adapter).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps request headers in the dedupe key when they can affect the response", async () => {
|
||||
const { apiGet } = await import("./axios");
|
||||
const adapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { accept: (config.headers as any)?.Accept },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
|
||||
const [json, csv] = await Promise.all([
|
||||
apiGet("/api/v1/reports", { adapter, headers: { Accept: "application/json" } }),
|
||||
apiGet("/api/v1/reports", { adapter, headers: { Accept: "text/csv" } }),
|
||||
]);
|
||||
|
||||
expect(adapter).toHaveBeenCalledTimes(2);
|
||||
expect(json.data).toEqual({ accept: "application/json" });
|
||||
expect(csv.data).toEqual({ accept: "text/csv" });
|
||||
});
|
||||
|
||||
it("serves configured apiGet calls from memory cache until ttl expires", async () => {
|
||||
const { apiGet } = await import("./axios");
|
||||
let sequence = 0;
|
||||
const adapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { sequence: (sequence += 1) },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { etag: "v1" },
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
|
||||
const first = await apiGet("/api/v1/studies/", { adapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
const second = await apiGet("/api/v1/studies/", { adapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
expect(first.data).toEqual({ sequence: 1 });
|
||||
expect(second.data).toEqual({ sequence: 1 });
|
||||
expect(adapter).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
const third = await apiGet("/api/v1/studies/", { adapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
expect(third.data).toEqual({ sequence: 2 });
|
||||
expect(adapter).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("clears cached GET responses after successful mutations", async () => {
|
||||
const { apiGet, apiPost } = await import("./axios");
|
||||
let sequence = 0;
|
||||
const getAdapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { sequence: (sequence += 1) },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
const postAdapter = vi.fn<AxiosAdapter>(async (config) => ({
|
||||
data: { id: "study-1" },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
}));
|
||||
|
||||
await apiGet("/api/v1/studies/", { adapter: getAdapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
await apiGet("/api/v1/studies/", { adapter: getAdapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
expect(getAdapter).toHaveBeenCalledTimes(1);
|
||||
|
||||
await apiPost("/api/v1/studies/", { name: "Study" }, { adapter: postAdapter });
|
||||
const afterMutation = await apiGet("/api/v1/studies/", {
|
||||
adapter: getAdapter,
|
||||
cache: { ttlMs: 1000, tags: ["studies"] },
|
||||
});
|
||||
|
||||
expect(afterMutation.data).toEqual({ sequence: 2 });
|
||||
expect(getAdapter).toHaveBeenCalledTimes(2);
|
||||
expect(postAdapter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not write an in-flight GET response into cache after the cache generation changes", async () => {
|
||||
const { apiGet, clearApiResponseCache } = await import("./axios");
|
||||
let sequence = 0;
|
||||
let resolveFirst: (() => void) | undefined;
|
||||
const adapter = vi.fn<AxiosAdapter>((config) => {
|
||||
sequence += 1;
|
||||
const response = {
|
||||
data: { sequence },
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config: config as InternalAxiosRequestConfig,
|
||||
};
|
||||
if (sequence === 1) {
|
||||
return new Promise((resolve) => {
|
||||
resolveFirst = () => resolve(response);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
});
|
||||
|
||||
const firstRequest = apiGet("/api/v1/studies/", { adapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
await vi.dynamicImportSettled();
|
||||
expect(adapter).toHaveBeenCalledTimes(1);
|
||||
|
||||
clearApiResponseCache();
|
||||
resolveFirst?.();
|
||||
await expect(firstRequest).resolves.toMatchObject({ data: { sequence: 1 } });
|
||||
|
||||
const afterClear = await apiGet("/api/v1/studies/", { adapter, cache: { ttlMs: 1000, tags: ["studies"] } });
|
||||
expect(afterClear.data).toEqual({ sequence: 2 });
|
||||
expect(adapter).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user