236 lines
5.6 KiB
TypeScript
236 lines
5.6 KiB
TypeScript
import { describe, expect, test, vi } from "vitest";
|
|
|
|
import { createMarketBatchLoader } from "../src/content/market/batch-loader";
|
|
|
|
describe("market batch loader", () => {
|
|
test("puts current-page rows into loading before the request resolves", async () => {
|
|
const deferred = createDeferred();
|
|
const row = createRowRecorder("111");
|
|
const loader = createMarketBatchLoader({
|
|
apiClient: {
|
|
loadAuthorAseInfo: vi.fn(() => deferred.promise)
|
|
},
|
|
concurrency: 4
|
|
});
|
|
|
|
const loadPromise = loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [row.row]
|
|
});
|
|
|
|
expect(row.states[0]).toMatchObject({
|
|
authorId: "111",
|
|
listSeq: 1,
|
|
state: "loading"
|
|
});
|
|
|
|
deferred.resolve({
|
|
rates: {
|
|
personalVideoAfterSearchRate: "0.02% - 0.1%",
|
|
singleVideoAfterSearchRate: "<0.02%"
|
|
},
|
|
success: true
|
|
});
|
|
await loadPromise;
|
|
});
|
|
|
|
test("reuses the success cache for repeated authorIds", async () => {
|
|
const apiClient = {
|
|
loadAuthorAseInfo: vi.fn(async () => ({
|
|
rates: {
|
|
personalVideoAfterSearchRate: "0.02% - 0.1%",
|
|
singleVideoAfterSearchRate: "<0.02%"
|
|
},
|
|
success: true as const
|
|
}))
|
|
};
|
|
const firstRow = createRowRecorder("111");
|
|
const secondRow = createRowRecorder("111");
|
|
const loader = createMarketBatchLoader({
|
|
apiClient,
|
|
concurrency: 4
|
|
});
|
|
|
|
await loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [firstRow.row]
|
|
});
|
|
await loader.loadRows({
|
|
listSeq: 2,
|
|
rows: [secondRow.row]
|
|
});
|
|
|
|
expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledTimes(1);
|
|
expect(secondRow.states.at(-1)).toMatchObject({
|
|
source: "cache",
|
|
state: "success"
|
|
});
|
|
});
|
|
|
|
test("deduplicates in-flight requests for the same authorId", async () => {
|
|
const deferred = createDeferred();
|
|
const firstRow = createRowRecorder("111");
|
|
const secondRow = createRowRecorder("111");
|
|
const apiClient = {
|
|
loadAuthorAseInfo: vi.fn(() => deferred.promise)
|
|
};
|
|
const loader = createMarketBatchLoader({
|
|
apiClient,
|
|
concurrency: 4
|
|
});
|
|
|
|
const loadPromise = loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [firstRow.row, secondRow.row]
|
|
});
|
|
|
|
expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledTimes(1);
|
|
|
|
deferred.resolve({
|
|
rates: {
|
|
personalVideoAfterSearchRate: "0.02% - 0.1%",
|
|
singleVideoAfterSearchRate: "<0.02%"
|
|
},
|
|
success: true
|
|
});
|
|
await loadPromise;
|
|
});
|
|
|
|
test("honors the concurrency cap", async () => {
|
|
const deferreds = new Map([
|
|
["111", createDeferred()],
|
|
["222", createDeferred()],
|
|
["333", createDeferred()]
|
|
]);
|
|
const started: string[] = [];
|
|
const loader = createMarketBatchLoader({
|
|
apiClient: {
|
|
loadAuthorAseInfo: vi.fn((authorId: string) => {
|
|
started.push(authorId);
|
|
return deferreds.get(authorId)!.promise;
|
|
})
|
|
},
|
|
concurrency: 2
|
|
});
|
|
|
|
const loadPromise = loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [createRowRecorder("111").row, createRowRecorder("222").row, createRowRecorder("333").row]
|
|
});
|
|
|
|
expect(started).toEqual(["111", "222"]);
|
|
|
|
deferreds.get("111")!.resolve(successResult());
|
|
await tick();
|
|
expect(started).toEqual(["111", "222", "333"]);
|
|
|
|
deferreds.get("222")!.resolve(successResult());
|
|
deferreds.get("333")!.resolve(successResult());
|
|
await loadPromise;
|
|
});
|
|
|
|
test("renders failed rows as error states", async () => {
|
|
const row = createRowRecorder("111");
|
|
const loader = createMarketBatchLoader({
|
|
apiClient: {
|
|
loadAuthorAseInfo: vi.fn(async () => ({
|
|
reason: "request-failed",
|
|
success: false as const
|
|
}))
|
|
},
|
|
concurrency: 4
|
|
});
|
|
|
|
await loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [row.row]
|
|
});
|
|
|
|
expect(row.states.at(-1)).toMatchObject({
|
|
reason: "request-failed",
|
|
retryable: true,
|
|
state: "error"
|
|
});
|
|
});
|
|
|
|
test("retries the whole row when the provided retry handler is invoked", async () => {
|
|
const row = createRowRecorder("111");
|
|
const apiClient = {
|
|
loadAuthorAseInfo: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
reason: "request-failed",
|
|
success: false as const
|
|
})
|
|
.mockResolvedValueOnce(successResult())
|
|
};
|
|
const loader = createMarketBatchLoader({
|
|
apiClient,
|
|
concurrency: 4
|
|
});
|
|
|
|
await loader.loadRows({
|
|
listSeq: 1,
|
|
rows: [row.row]
|
|
});
|
|
|
|
expect(row.retry).toBeTypeOf("function");
|
|
await row.retry?.();
|
|
|
|
expect(apiClient.loadAuthorAseInfo).toHaveBeenCalledTimes(2);
|
|
expect(row.states.at(-2)).toMatchObject({
|
|
state: "loading"
|
|
});
|
|
expect(row.states.at(-1)).toMatchObject({
|
|
state: "success"
|
|
});
|
|
});
|
|
});
|
|
|
|
function createRowRecorder(authorId: string | null) {
|
|
const states: unknown[] = [];
|
|
let retry: (() => Promise<void> | void) | undefined;
|
|
|
|
return {
|
|
row: {
|
|
authorId,
|
|
render(
|
|
state: unknown,
|
|
options?: { onRetry?: () => Promise<void> | void }
|
|
) {
|
|
states.push(state);
|
|
retry = options?.onRetry;
|
|
}
|
|
},
|
|
get retry() {
|
|
return retry;
|
|
},
|
|
states
|
|
};
|
|
}
|
|
|
|
function createDeferred<T = unknown>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((nextResolve) => {
|
|
resolve = nextResolve;
|
|
});
|
|
|
|
return { promise, resolve };
|
|
}
|
|
|
|
function successResult() {
|
|
return {
|
|
rates: {
|
|
personalVideoAfterSearchRate: "0.02% - 0.1%",
|
|
singleVideoAfterSearchRate: "<0.02%"
|
|
},
|
|
success: true as const
|
|
};
|
|
}
|
|
|
|
function tick() {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, 0);
|
|
});
|
|
}
|