fix: await initial market synchronization

This commit is contained in:
wxs
2026-07-17 16:06:51 +08:00
parent 01efa9f847
commit 8ca9f95583
2 changed files with 149 additions and 33 deletions
+63 -26
View File
@@ -225,11 +225,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
});
let activeSort: MarketSortState | undefined;
let isDisposed = false;
let isSyncRunning = false;
let isSyncScheduled = false;
let lastKnownPageSignature = "";
let needsResync = false;
let scheduledSyncTimeoutId: number | null = null;
let activeSyncPromise: Promise<void> | null = null;
let scheduledSyncPromise: Promise<void> | null = null;
let resolveScheduledSync: (() => void) | null = null;
let rejectScheduledSync: ((reason?: unknown) => void) | null = null;
const selectedAuthorIds = new Set<string>();
let toolbar: ReturnType<typeof ensurePluginToolbar> | undefined;
let cachedFavoritesState: FavoritesStateV1 | undefined;
@@ -533,7 +535,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
const ready = (async () => {
await runSyncCycle();
await refreshFavorites();
const initialScheduledSync = scheduledSyncPromise;
await waitForDomSettled();
await (initialScheduledSync ?? activeSyncPromise ?? scheduledSyncPromise);
})();
return {
@@ -544,6 +548,10 @@ export function createMarketController(options: CreateMarketControllerOptions) {
options.window.clearTimeout(scheduledSyncTimeoutId);
scheduledSyncTimeoutId = null;
}
resolveScheduledSync?.();
scheduledSyncPromise = null;
resolveScheduledSync = null;
rejectScheduledSync = null;
disposeFavoriteRowPickers(options.document);
favoritesDrawer.dispose();
},
@@ -1783,29 +1791,42 @@ export function createMarketController(options: CreateMarketControllerOptions) {
};
}
function scheduleSync(): void {
function scheduleSync(): Promise<void> {
if (isDisposed) {
return;
return Promise.resolve();
}
if (isSyncRunning) {
if (activeSyncPromise) {
needsResync = true;
return;
return activeSyncPromise;
}
if (isSyncScheduled) {
return;
if (scheduledSyncPromise) {
return scheduledSyncPromise;
}
isSyncScheduled = true;
const nextScheduledSyncPromise = new Promise<void>((resolve, reject) => {
resolveScheduledSync = resolve;
rejectScheduledSync = reject;
});
scheduledSyncPromise = nextScheduledSyncPromise;
void nextScheduledSyncPromise.catch(() => undefined);
scheduledSyncTimeoutId = options.window.setTimeout(() => {
const resolve = resolveScheduledSync;
const reject = rejectScheduledSync;
scheduledSyncTimeoutId = null;
isSyncScheduled = false;
scheduledSyncPromise = null;
resolveScheduledSync = null;
rejectScheduledSync = null;
if (isDisposed) {
resolve?.();
return;
}
void runSyncCycle();
const syncPromise = runSyncCycle();
void syncPromise.catch(() => undefined);
void syncPromise.then(resolve, reject);
}, 0);
return nextScheduledSyncPromise;
}
function runWithoutMutationSync(callback: () => void): void {
@@ -1832,18 +1853,44 @@ export function createMarketController(options: CreateMarketControllerOptions) {
});
}
async function runSyncCycle(): Promise<void> {
function runSyncCycle(): Promise<void> {
if (isDisposed) {
return;
return Promise.resolve();
}
if (isSyncRunning) {
if (activeSyncPromise) {
needsResync = true;
return activeSyncPromise;
}
const syncPromise = runSingleSyncCycle();
activeSyncPromise = syncPromise;
void syncPromise.then(
() => {
completeSyncCycle(syncPromise);
},
() => {
completeSyncCycle(syncPromise);
}
);
return syncPromise;
}
function completeSyncCycle(syncPromise: Promise<void>): void {
if (activeSyncPromise !== syncPromise) {
return;
}
isSyncRunning = true;
try {
activeSyncPromise = null;
if (isDisposed || !needsResync) {
return;
}
needsResync = false;
scheduleSync();
}
async function runSingleSyncCycle(): Promise<void> {
toolbar = ensurePluginToolbar(options.document, toolbarHandlers);
await hydrateCurrentPage();
if (needsFavoritesRefresh) {
@@ -1852,16 +1899,6 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}
applyCurrentView();
lastKnownPageSignature = readMarketPageSignature(options.document);
} finally {
isSyncRunning = false;
if (isDisposed) {
return;
}
if (needsResync) {
needsResync = false;
scheduleSync();
}
}
}
}
+79
View File
@@ -4975,6 +4975,85 @@ describe("market-content-entry", () => {
expect(buildCsv).toHaveBeenCalledTimes(1);
});
test("awaits a scheduled initial hydration before ready enables an immediate export", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
const repository = createTestFavoritesRepository();
const originalRead = repository.read.bind(repository);
const observer = createMutationObserverFactory();
const successfulMetrics = {
rates: {
personalVideoAfterSearchRate: "0.2%",
singleVideoAfterSearchRate: "0.1%"
},
success: true as const
};
const deferredMetrics = createDeferred<typeof successfulMetrics>();
let shouldReplaceInitialPage = true;
let activeMetricLoads = 0;
let maxActiveMetricLoads = 0;
repository.read = async () => {
const state = await originalRead();
if (shouldReplaceInitialPage) {
shouldReplaceInitialPage = false;
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
]);
observer.trigger();
}
return state;
};
const loadAuthorMetrics = vi.fn(async (authorId: string) => {
activeMetricLoads += 1;
maxActiveMetricLoads = Math.max(maxActiveMetricLoads, activeMetricLoads);
try {
if (authorId === "222") {
return await deferredMetrics.promise;
}
return successfulMetrics;
} finally {
activeMetricLoads -= 1;
}
});
const buildCsv = vi.fn(() => "csv-output");
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildCsv,
document,
favoritesRepository: repository,
loadAuthorMetrics,
mutationObserverFactory: observer.factory,
onCsvReady: vi.fn(),
window
}));
let readySettled = false;
void controller.ready.then(() => {
readySettled = true;
});
await waitForCondition(() =>
loadAuthorMetrics.mock.calls.some(([authorId]) => authorId === "222")
);
await Promise.resolve();
expect(readySettled).toBe(false);
deferredMetrics.resolve(successfulMetrics);
await controller.ready;
setSelectValue('[data-plugin-export-range="select"]', "current");
dispatchChange('[data-plugin-export-range="select"]');
removeDefaultSpreadMetricFilter();
click('[data-plugin-export="button"]');
await waitForMockCall(buildCsv, 40, 50);
expect(buildCsv).toHaveBeenCalledTimes(1);
expect(loadAuthorMetrics.mock.calls.map(([authorId]) => authorId)).toEqual(["111", "222"]);
expect(maxActiveMetricLoads).toBe(1);
});
test("keeps the friendly drawer error when a direct folder mutation has no message", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }