From 98dc078a15da74202a922f73d9d02273138432de Mon Sep 17 00:00:00 2001 From: admin123 Date: Mon, 20 Apr 2026 20:06:21 +0800 Subject: [PATCH] feat: add market result store --- src/content/market/result-store.ts | 63 ++++++++++++++++++ src/content/market/types.ts | 15 +++++ tests/result-store.test.ts | 101 +++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 src/content/market/result-store.ts create mode 100644 tests/result-store.test.ts diff --git a/src/content/market/result-store.ts b/src/content/market/result-store.ts new file mode 100644 index 0000000..6151265 --- /dev/null +++ b/src/content/market/result-store.ts @@ -0,0 +1,63 @@ +import type { + MarketApiFailureReason, + MarketRecord, + MarketRowSnapshot +} from "./types"; +import type { AfterSearchRates } from "./types"; + +export function createMarketResultStore() { + const records = new Map(); + + 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) { + 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; + } +} diff --git a/src/content/market/types.ts b/src/content/market/types.ts index c73989e..21e6529 100644 --- a/src/content/market/types.ts +++ b/src/content/market/types.ts @@ -3,6 +3,21 @@ export interface AfterSearchRates { 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; +} + export type MarketApiFailureReason = | "bad-response" | "missing-rate" diff --git a/tests/result-store.test.ts b/tests/result-store.test.ts new file mode 100644 index 0000000..c4b902a --- /dev/null +++ b/tests/result-store.test.ts @@ -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" + }); + }); +});