fix: await initial market synchronization
This commit is contained in:
+70
-33
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user