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
+70 -33
View File
@@ -225,11 +225,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}); });
let activeSort: MarketSortState | undefined; let activeSort: MarketSortState | undefined;
let isDisposed = false; let isDisposed = false;
let isSyncRunning = false;
let isSyncScheduled = false;
let lastKnownPageSignature = ""; let lastKnownPageSignature = "";
let needsResync = false; let needsResync = false;
let scheduledSyncTimeoutId: number | null = null; 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>(); const selectedAuthorIds = new Set<string>();
let toolbar: ReturnType<typeof ensurePluginToolbar> | undefined; let toolbar: ReturnType<typeof ensurePluginToolbar> | undefined;
let cachedFavoritesState: FavoritesStateV1 | undefined; let cachedFavoritesState: FavoritesStateV1 | undefined;
@@ -533,7 +535,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
const ready = (async () => { const ready = (async () => {
await runSyncCycle(); await runSyncCycle();
await refreshFavorites(); await refreshFavorites();
const initialScheduledSync = scheduledSyncPromise;
await waitForDomSettled(); await waitForDomSettled();
await (initialScheduledSync ?? activeSyncPromise ?? scheduledSyncPromise);
})(); })();
return { return {
@@ -544,6 +548,10 @@ export function createMarketController(options: CreateMarketControllerOptions) {
options.window.clearTimeout(scheduledSyncTimeoutId); options.window.clearTimeout(scheduledSyncTimeoutId);
scheduledSyncTimeoutId = null; scheduledSyncTimeoutId = null;
} }
resolveScheduledSync?.();
scheduledSyncPromise = null;
resolveScheduledSync = null;
rejectScheduledSync = null;
disposeFavoriteRowPickers(options.document); disposeFavoriteRowPickers(options.document);
favoritesDrawer.dispose(); favoritesDrawer.dispose();
}, },
@@ -1783,29 +1791,42 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}; };
} }
function scheduleSync(): void { function scheduleSync(): Promise<void> {
if (isDisposed) { if (isDisposed) {
return; return Promise.resolve();
} }
if (isSyncRunning) { if (activeSyncPromise) {
needsResync = true; needsResync = true;
return; return activeSyncPromise;
} }
if (isSyncScheduled) { if (scheduledSyncPromise) {
return; 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(() => { scheduledSyncTimeoutId = options.window.setTimeout(() => {
const resolve = resolveScheduledSync;
const reject = rejectScheduledSync;
scheduledSyncTimeoutId = null; scheduledSyncTimeoutId = null;
isSyncScheduled = false; scheduledSyncPromise = null;
resolveScheduledSync = null;
rejectScheduledSync = null;
if (isDisposed) { if (isDisposed) {
resolve?.();
return; return;
} }
void runSyncCycle(); const syncPromise = runSyncCycle();
void syncPromise.catch(() => undefined);
void syncPromise.then(resolve, reject);
}, 0); }, 0);
return nextScheduledSyncPromise;
} }
function runWithoutMutationSync(callback: () => void): void { function runWithoutMutationSync(callback: () => void): void {
@@ -1832,36 +1853,52 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}); });
} }
async function runSyncCycle(): Promise<void> { function runSyncCycle(): Promise<void> {
if (isDisposed) { if (isDisposed) {
return; return Promise.resolve();
} }
if (isSyncRunning) { if (activeSyncPromise) {
needsResync = true; 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; return;
} }
isSyncRunning = true; activeSyncPromise = null;
try { if (isDisposed || !needsResync) {
toolbar = ensurePluginToolbar(options.document, toolbarHandlers); return;
await hydrateCurrentPage();
if (needsFavoritesRefresh) {
needsFavoritesRefresh = false;
await refreshFavorites();
}
applyCurrentView();
lastKnownPageSignature = readMarketPageSignature(options.document);
} finally {
isSyncRunning = false;
if (isDisposed) {
return;
}
if (needsResync) {
needsResync = false;
scheduleSync();
}
} }
needsResync = false;
scheduleSync();
}
async function runSingleSyncCycle(): Promise<void> {
toolbar = ensurePluginToolbar(options.document, toolbarHandlers);
await hydrateCurrentPage();
if (needsFavoritesRefresh) {
needsFavoritesRefresh = false;
await refreshFavorites();
}
applyCurrentView();
lastKnownPageSignature = readMarketPageSignature(options.document);
} }
} }
+79
View File
@@ -4975,6 +4975,85 @@ describe("market-content-entry", () => {
expect(buildCsv).toHaveBeenCalledTimes(1); 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 () => { test("keeps the friendly drawer error when a direct folder mutation has no message", async () => {
document.body.innerHTML = buildRealMarketFixture([ document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" } { authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }