feat: add market result store
This commit is contained in:
parent
649de1608c
commit
98dc078a15
63
src/content/market/result-store.ts
Normal file
63
src/content/market/result-store.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import type {
|
||||||
|
MarketApiFailureReason,
|
||||||
|
MarketRecord,
|
||||||
|
MarketRowSnapshot
|
||||||
|
} from "./types";
|
||||||
|
import type { AfterSearchRates } from "./types";
|
||||||
|
|
||||||
|
export function createMarketResultStore() {
|
||||||
|
const records = new Map<string, MarketRecord>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRecord(authorId: string) {
|
||||||
|
return records.get(authorId) ?? null;
|
||||||
|
},
|
||||||
|
listRecords() {
|
||||||
|
return Array.from(records.values());
|
||||||
|
},
|
||||||
|
setAuthorFailed(authorId: string, reason: MarketApiFailureReason) {
|
||||||
|
const existingRecord = ensureRecord(authorId);
|
||||||
|
existingRecord.status = "failed";
|
||||||
|
existingRecord.failureReason = reason;
|
||||||
|
},
|
||||||
|
setAuthorLoading(authorId: string) {
|
||||||
|
const existingRecord = ensureRecord(authorId);
|
||||||
|
existingRecord.status = "loading";
|
||||||
|
delete existingRecord.failureReason;
|
||||||
|
},
|
||||||
|
setAuthorSuccess(authorId: string, rates: Required<AfterSearchRates>) {
|
||||||
|
const existingRecord = ensureRecord(authorId);
|
||||||
|
existingRecord.status = "success";
|
||||||
|
existingRecord.rates = rates;
|
||||||
|
delete existingRecord.failureReason;
|
||||||
|
},
|
||||||
|
upsertMarketRow(row: MarketRowSnapshot) {
|
||||||
|
const existingRecord = records.get(row.authorId);
|
||||||
|
if (existingRecord) {
|
||||||
|
return existingRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRecord: MarketRecord = {
|
||||||
|
...row,
|
||||||
|
status: "idle"
|
||||||
|
};
|
||||||
|
records.set(row.authorId, nextRecord);
|
||||||
|
return nextRecord;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureRecord(authorId: string): MarketRecord {
|
||||||
|
const existingRecord = records.get(authorId);
|
||||||
|
if (existingRecord) {
|
||||||
|
return existingRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRecord: MarketRecord = {
|
||||||
|
authorId,
|
||||||
|
authorName: authorId,
|
||||||
|
status: "idle"
|
||||||
|
};
|
||||||
|
records.set(authorId, nextRecord);
|
||||||
|
return nextRecord;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,21 @@ export interface AfterSearchRates {
|
|||||||
singleVideoAfterSearchRate?: string;
|
singleVideoAfterSearchRate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MarketRecordStatus = "idle" | "loading" | "success" | "failed" | "missing";
|
||||||
|
|
||||||
|
export interface MarketRowSnapshot {
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
location?: string;
|
||||||
|
price21To60s?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketRecord extends MarketRowSnapshot {
|
||||||
|
status: MarketRecordStatus;
|
||||||
|
failureReason?: MarketApiFailureReason;
|
||||||
|
rates?: Required<AfterSearchRates>;
|
||||||
|
}
|
||||||
|
|
||||||
export type MarketApiFailureReason =
|
export type MarketApiFailureReason =
|
||||||
| "bad-response"
|
| "bad-response"
|
||||||
| "missing-rate"
|
| "missing-rate"
|
||||||
|
|||||||
101
tests/result-store.test.ts
Normal file
101
tests/result-store.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { createMarketResultStore } from "../src/content/market/result-store";
|
||||||
|
|
||||||
|
describe("result-store", () => {
|
||||||
|
test("creates loading records from current-page rows", () => {
|
||||||
|
const store = createMarketResultStore();
|
||||||
|
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
price21To60s: "450000"
|
||||||
|
});
|
||||||
|
store.setAuthorLoading("123");
|
||||||
|
|
||||||
|
expect(store.getRecord("123")).toMatchObject({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
price21To60s: "450000",
|
||||||
|
status: "loading"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates one author to success", () => {
|
||||||
|
const store = createMarketResultStore();
|
||||||
|
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice"
|
||||||
|
});
|
||||||
|
store.setAuthorSuccess("123", {
|
||||||
|
singleVideoAfterSearchRate: "<0.02%",
|
||||||
|
personalVideoAfterSearchRate: "0.02% - 0.1%"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getRecord("123")).toMatchObject({
|
||||||
|
status: "success",
|
||||||
|
rates: {
|
||||||
|
singleVideoAfterSearchRate: "<0.02%",
|
||||||
|
personalVideoAfterSearchRate: "0.02% - 0.1%"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves failed authors instead of dropping them", () => {
|
||||||
|
const store = createMarketResultStore();
|
||||||
|
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice"
|
||||||
|
});
|
||||||
|
store.setAuthorFailed("123", "request-failed");
|
||||||
|
|
||||||
|
expect(store.listRecords()).toHaveLength(1);
|
||||||
|
expect(store.getRecord("123")).toMatchObject({
|
||||||
|
status: "failed",
|
||||||
|
failureReason: "request-failed"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dedupes the same author across repeated page writes", () => {
|
||||||
|
const store = createMarketResultStore();
|
||||||
|
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
price21To60s: "450000"
|
||||||
|
});
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice v2",
|
||||||
|
price21To60s: "470000"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.listRecords()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the original major fields stable after repeated writes", () => {
|
||||||
|
const store = createMarketResultStore();
|
||||||
|
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
location: "Hangzhou",
|
||||||
|
price21To60s: "450000"
|
||||||
|
});
|
||||||
|
store.upsertMarketRow({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice v2",
|
||||||
|
location: "Shanghai",
|
||||||
|
price21To60s: "470000"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getRecord("123")).toMatchObject({
|
||||||
|
authorId: "123",
|
||||||
|
authorName: "Alice",
|
||||||
|
location: "Hangzhou",
|
||||||
|
price21To60s: "450000"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user