fix: stabilize market metrics hydration and sorting
This commit is contained in:
@@ -61,6 +61,7 @@ const SORTABLE_MARKET_FIELDS = [
|
||||
|
||||
type RowOrderTarget = {
|
||||
container: HTMLElement;
|
||||
mode: "css" | "dom";
|
||||
node: HTMLElement;
|
||||
};
|
||||
|
||||
@@ -106,12 +107,7 @@ export function readMarketPageSignature(root: ParentNode): string {
|
||||
document
|
||||
?.querySelector(".el-pagination .number.active, .xt-pagination .number.active")
|
||||
?.textContent?.trim() ?? "";
|
||||
const table = syncMarketTable(root);
|
||||
const authorIds =
|
||||
table?.rows
|
||||
.map((row) => row.authorId)
|
||||
.filter((authorId) => Boolean(authorId))
|
||||
.join("|") ?? "";
|
||||
const authorIds = readRawAuthorIds(root).join("|");
|
||||
|
||||
return `${explicitPageIndex || activePageIndex}::${authorIds}`;
|
||||
}
|
||||
@@ -209,6 +205,9 @@ export function applyRowOrder(
|
||||
orderedAuthorIds: string[]
|
||||
): void {
|
||||
const rowById = new Map(table.rows.map((rowDom) => [rowDom.authorId, rowDom]));
|
||||
const orderByAuthorId = new Map(
|
||||
orderedAuthorIds.map((authorId, index) => [authorId, index])
|
||||
);
|
||||
|
||||
orderedAuthorIds.forEach((authorId) => {
|
||||
const rowDom = rowById.get(authorId);
|
||||
@@ -216,7 +215,17 @@ export function applyRowOrder(
|
||||
return;
|
||||
}
|
||||
|
||||
rowDom.orderTargets.forEach(({ container, node }) => {
|
||||
rowDom.orderTargets.forEach(({ container, mode, node }) => {
|
||||
const visualOrder = orderByAuthorId.get(authorId) ?? orderedAuthorIds.length;
|
||||
if (mode === "css") {
|
||||
container.dataset.marketOrderMode = "css";
|
||||
container.style.display = "flex";
|
||||
container.style.flexDirection = "column";
|
||||
node.style.order = String(visualOrder);
|
||||
return;
|
||||
}
|
||||
|
||||
container.dataset.marketOrderMode = "dom";
|
||||
container.appendChild(node);
|
||||
});
|
||||
});
|
||||
@@ -283,6 +292,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
||||
orderTargets: [
|
||||
{
|
||||
container: body,
|
||||
mode: "dom",
|
||||
node: row
|
||||
}
|
||||
],
|
||||
@@ -324,6 +334,75 @@ function syncDivGridMarketTable(root: ParentNode): MarketTableDom | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readRawAuthorIds(root: ParentNode): string[] {
|
||||
const document = getOwnerDocument(root);
|
||||
const syntheticAuthorIds = readSyntheticAuthorIds(root);
|
||||
if (syntheticAuthorIds && syntheticAuthorIds.length > 0) {
|
||||
return syntheticAuthorIds;
|
||||
}
|
||||
|
||||
const divGridAuthorIds = readDivGridAuthorIds(root);
|
||||
if (divGridAuthorIds && divGridAuthorIds.length > 0) {
|
||||
return divGridAuthorIds;
|
||||
}
|
||||
|
||||
if (!document) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return readSerializedMarketRows(document)
|
||||
.map((row) => row.authorId)
|
||||
.filter((authorId) => Boolean(authorId));
|
||||
}
|
||||
|
||||
function readSyntheticAuthorIds(root: ParentNode): string[] | null {
|
||||
const body = root.querySelector("[data-market-body]") as HTMLElement | null;
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Array.from(body.querySelectorAll("[data-market-row]"))
|
||||
.map((row) =>
|
||||
row instanceof HTMLElement ? row.dataset.authorId ?? "" : ""
|
||||
)
|
||||
.filter((authorId) => Boolean(authorId));
|
||||
}
|
||||
|
||||
function readDivGridAuthorIds(root: ParentNode): string[] | null {
|
||||
const document = getOwnerDocument(root);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const marketRoot = document.querySelector(".base-author-list");
|
||||
if (!(marketRoot instanceof document.defaultView!.HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bodySection = Array.from(marketRoot.querySelectorAll(".section-wrapper")).find(
|
||||
(section): section is HTMLElement =>
|
||||
section instanceof document.defaultView!.HTMLElement &&
|
||||
!section.classList.contains("sticky-header")
|
||||
);
|
||||
const authorSection = bodySection
|
||||
? Array.from(bodySection.children).find(
|
||||
(child): child is HTMLElement =>
|
||||
child instanceof document.defaultView!.HTMLElement &&
|
||||
child.querySelector(".content-column .content-cell")
|
||||
) ?? null
|
||||
: null;
|
||||
const authorColumn = authorSection
|
||||
? getDirectContentColumns(authorSection)[0] ?? null
|
||||
: null;
|
||||
if (!authorColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getDirectContentCells(authorColumn)
|
||||
.map((cell) => extractAuthorId(cell))
|
||||
.filter((authorId) => Boolean(authorId));
|
||||
}
|
||||
|
||||
function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
||||
const headerSection = root.querySelector(
|
||||
".section-wrapper.sticky-header"
|
||||
@@ -520,6 +599,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
||||
|
||||
return {
|
||||
container,
|
||||
mode: "css",
|
||||
node: cell
|
||||
};
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ExportRangeControllerOptions {
|
||||
onProgress?: (state: { currentPage: number; totalPages?: number }) => void;
|
||||
prepareCurrentPageForExport(): Promise<void>;
|
||||
readCurrentPageRecords(): MarketRecord[];
|
||||
readCurrentPageRowCount(): number;
|
||||
window: Window;
|
||||
}
|
||||
|
||||
@@ -26,13 +27,10 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
||||
currentPage,
|
||||
totalPages: target.mode === "count" ? target.pageCount : undefined
|
||||
});
|
||||
const currentPageReady = await waitForCurrentPageReady(expectedMinimumRowCount);
|
||||
if (!currentPageReady) {
|
||||
const currentPageRecords = await preparePageRecords(expectedMinimumRowCount);
|
||||
if (!currentPageRecords) {
|
||||
throw new Error(`第 ${currentPage} 页加载超时,请稍后重试`);
|
||||
}
|
||||
|
||||
await options.prepareCurrentPageForExport();
|
||||
const currentPageRecords = options.readCurrentPageRecords();
|
||||
currentPageRecords.forEach((record) => {
|
||||
const existingRecord = mergedRecords.get(record.authorId);
|
||||
mergedRecords.set(record.authorId, mergeMarketRecord(existingRecord, record));
|
||||
@@ -63,6 +61,33 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
||||
}
|
||||
};
|
||||
|
||||
async function preparePageRecords(
|
||||
expectedMinimumRowCount: number | undefined
|
||||
): Promise<MarketRecord[] | null> {
|
||||
for (let attempt = 0; attempt < 4; attempt += 1) {
|
||||
const currentPageReady = await waitForCurrentPageReady();
|
||||
if (!currentPageReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await options.prepareCurrentPageForExport();
|
||||
const currentPageRecords = options.readCurrentPageRecords();
|
||||
if (
|
||||
currentPageRecords.length > 0 &&
|
||||
(
|
||||
typeof expectedMinimumRowCount !== "number" ||
|
||||
expectedMinimumRowCount <= 0 ||
|
||||
isCurrentPageTerminal() ||
|
||||
currentPageRecords.length >= expectedMinimumRowCount
|
||||
)
|
||||
) {
|
||||
return currentPageRecords;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForPageChange(previousSignature: string): Promise<boolean> {
|
||||
const previousPageState = parsePageSignature(previousSignature);
|
||||
|
||||
@@ -82,9 +107,7 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForCurrentPageReady(
|
||||
expectedMinimumRowCount: number | undefined
|
||||
): Promise<boolean> {
|
||||
async function waitForCurrentPageReady(): Promise<boolean> {
|
||||
let stableAttemptCount = 0;
|
||||
let lastReadyFingerprint = "";
|
||||
|
||||
@@ -101,17 +124,6 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof expectedMinimumRowCount === "number" &&
|
||||
expectedMinimumRowCount > 0 &&
|
||||
!pageState.isTerminalPage &&
|
||||
pageState.rowCount < expectedMinimumRowCount
|
||||
) {
|
||||
stableAttemptCount = 0;
|
||||
lastReadyFingerprint = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
const readyFingerprint = [
|
||||
pageState.pageToken,
|
||||
pageState.authorIds,
|
||||
@@ -146,9 +158,13 @@ export function createExportRangeController(options: ExportRangeControllerOption
|
||||
authorIds: pageSignature.authorIds,
|
||||
isTerminalPage: isPageControlDisabled(nextPageControl),
|
||||
pageToken: pageSignature.pageToken,
|
||||
rowCount: options.readCurrentPageRecords().length
|
||||
rowCount: options.readCurrentPageRowCount()
|
||||
};
|
||||
}
|
||||
|
||||
function isCurrentPageTerminal(): boolean {
|
||||
return isPageControlDisabled(findNextPageControl(options.document));
|
||||
}
|
||||
}
|
||||
|
||||
function parsePageSignature(signature: string): {
|
||||
|
||||
@@ -89,7 +89,7 @@ function compareRateSortRecords(
|
||||
const rightLowerBound = parseRateLowerBound(rightValue ?? null);
|
||||
|
||||
if (leftLowerBound == null && rightLowerBound == null) {
|
||||
return 0;
|
||||
return compareRecordIdentity(leftRecord, rightRecord);
|
||||
}
|
||||
|
||||
if (leftLowerBound == null) {
|
||||
@@ -107,7 +107,11 @@ function compareRateSortRecords(
|
||||
}
|
||||
|
||||
const tieBreak = compareRateValues(leftValue, rightValue);
|
||||
return sort.direction === "asc" ? tieBreak : -tieBreak;
|
||||
if (tieBreak !== 0) {
|
||||
return sort.direction === "asc" ? tieBreak : -tieBreak;
|
||||
}
|
||||
|
||||
return compareRecordIdentity(leftRecord, rightRecord);
|
||||
}
|
||||
|
||||
function compareBackendMetricRecords(
|
||||
@@ -120,7 +124,7 @@ function compareBackendMetricRecords(
|
||||
const rightValue = parseBackendMetricValue(rightRecord.backendMetrics?.[field]);
|
||||
|
||||
if (leftValue == null && rightValue == null) {
|
||||
return 0;
|
||||
return compareRecordIdentity(leftRecord, rightRecord);
|
||||
}
|
||||
|
||||
if (leftValue == null) {
|
||||
@@ -131,7 +135,11 @@ function compareBackendMetricRecords(
|
||||
return -1;
|
||||
}
|
||||
|
||||
return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
||||
if (leftValue !== rightValue) {
|
||||
return sort.direction === "asc" ? leftValue - rightValue : rightValue - leftValue;
|
||||
}
|
||||
|
||||
return compareRecordIdentity(leftRecord, rightRecord);
|
||||
}
|
||||
|
||||
function parseBackendMetricValue(value: string | null | undefined): number | null {
|
||||
@@ -156,3 +164,15 @@ function isRateSortField(
|
||||
field === "personalVideoAfterSearchRate"
|
||||
);
|
||||
}
|
||||
|
||||
function compareRecordIdentity(
|
||||
leftRecord: MarketRecord,
|
||||
rightRecord: MarketRecord
|
||||
): number {
|
||||
const authorIdCompare = leftRecord.authorId.localeCompare(rightRecord.authorId);
|
||||
if (authorIdCompare !== 0) {
|
||||
return authorIdCompare;
|
||||
}
|
||||
|
||||
return leftRecord.authorName.localeCompare(rightRecord.authorName);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
||||
import {
|
||||
applyRowOrder,
|
||||
applyRowVisibility,
|
||||
readMarketPageSignature,
|
||||
renderMarketRowState,
|
||||
syncPluginSortHeaders,
|
||||
syncMarketTable,
|
||||
@@ -97,8 +98,14 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
let activeSort: MarketSortState | undefined;
|
||||
let isSyncRunning = false;
|
||||
let isSyncScheduled = false;
|
||||
let lastKnownPageSignature = "";
|
||||
let needsResync = false;
|
||||
const observer = mutationObserverFactory(() => {
|
||||
const nextPageSignature = readMarketPageSignature(options.document);
|
||||
if (nextPageSignature === lastKnownPageSignature) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleSync();
|
||||
});
|
||||
const observationRoot = options.document.body ?? options.document.documentElement;
|
||||
@@ -374,6 +381,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
const records = getVisibleOrderedRecords(table);
|
||||
applyRowVisibility(table, new Set(records.map((record) => record.authorId)));
|
||||
applyRowOrder(table, records.map((record) => record.authorId));
|
||||
lastKnownPageSignature = readMarketPageSignature(options.document);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -719,6 +727,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
||||
try {
|
||||
await hydrateCurrentPage();
|
||||
applyCurrentView();
|
||||
lastKnownPageSignature = readMarketPageSignature(options.document);
|
||||
} finally {
|
||||
isSyncRunning = false;
|
||||
if (needsResync) {
|
||||
|
||||
Reference in New Issue
Block a user