рест и серч одновременно
Tests / frontend-test (pull_request) Successful in 1m18s

This commit is contained in:
gorbenko.pavel
2026-08-21 15:00:33 +05:00
parent 7f08a819a9
commit a19444eb28
2 changed files with 117 additions and 51 deletions
@@ -353,18 +353,26 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
const q = debouncedQuery.trim();
const fetchRows = async (): Promise<IPage> => {
// onPartial — Search и REST-довесок по свежести («Мои заявки»/«Доработка»,
// см. getOwnRequests/getOwnRequestsByStatus в myRequests.service.ts) теперь
// идут параллельно, и заранее не угадать, какой из двух ответит первым —
// сеть шумит то в одну, то в другую сторону. onPartial зовётся с тем, что
// пришло раньше, чтобы не держать список пустым ради заведомо более
// медленного источника. Остальные ветки (approvals/execution/поиск) его
// просто не принимают — там нет пары «Search + довесок» с независимо
// валидным промежуточным результатом.
const fetchRows = async (onPartial?: (p: IPage) => void): Promise<IPage> => {
// «Доработка» — тот же список, что «Инициированные», просто со статусом,
// зашитым в раздел, а не выбранным в фильтре.
if (view === 'rework' || (view === 'own:created' && statusFilter)) {
const status = view === 'rework' ? REQUEST_STATUS.returned : statusFilter;
return getOwnRequestsByStatus(context, types, status, page, debouncedQuery);
return getOwnRequestsByStatus(context, types, status, page, debouncedQuery, onPartial);
}
if (view === 'own:created') {
return q.length > 0
? searchOwnRequests(context, types, debouncedQuery, page)
: getOwnRequests(context, types, page);
: getOwnRequests(context, types, page, onPartial);
}
const queue = QUEUE_BY_VIEW[view];
@@ -387,9 +395,11 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
// Комментарий к решению дочитываем ОДНИМ местом на все ветки: «Доработка» и
// «Отклонённые мной» приходят разными путями (по автору и по строкам
// маршрута соответственно), а колонка у них одинаковая по смыслу. И только
// для видимой страницы — как и автор в hydratePendingAuthors.
const fetchPage = async (): Promise<IPage> => {
const result = await fetchRows();
// для видимой страницы — как и автор в hydratePendingAuthors. Промежуточный
// показ (onPartial) комментарий ещё не несёт — он дочитывается только для
// ИТОГОВОГО результата, полный показ его донесёт следом.
const fetchPage = async (onPartial?: (p: IPage) => void): Promise<IPage> => {
const result = await fetchRows(onPartial);
const decisionStatus = decisionStatusFor(view, statusFilter);
if (decisionStatus) {
await hydrateDecisionComments(context, types, result.rows, decisionStatus);
@@ -399,7 +409,23 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
const load = async (forceRender = false): Promise<void> => {
try {
const fresh = await perfScope(`таблица ${view} стр.${page}`, fetchPage);
// Промежуточный показ — ТОЛЬКО когда для этой страницы прямо СЕЙЧАС
// ничего не закешировано (первый заход, экран пуст). Проверяем на
// каждый вызов load(), а не один раз на весь эффект: тот же load()
// переиспользуется автообновлением по таймеру и событием
// rp-requests-changed все 10 минут жизни вкладки, и ко второму вызову
// кеш почти наверняка уже есть — без повторной проверки список бы на
// мгновение "усыхал" при КАЖДОМ фоновом обновлении, а не только при
// самой первой загрузке.
const onPartial = readPage(cacheKey)
? undefined
: (partial: IPage): void => {
if (cancelled) return;
setRows(partial.rows);
setTotalRows(partial.totalRows);
};
const fresh = await perfScope(`таблица ${view} стр.${page}`, () => fetchPage(onPartial));
if (cancelled) return;
// Перерисовываем, только если данные реально изменились — иначе при
@@ -1252,37 +1252,85 @@ async function getRecentlyChangedOwnRowsForType(context: WebPartContext, type: I
}
}
// Итог слияния Search-страницы с REST-довеском по свежести — общая логика для
// getOwnRequests и ownRequestsByStatus. Довесок ДОПОЛНЯЕТ Search, а не заменяет:
// то, что Search уже нашёл, в приоритете (там могло появиться более точное
// совпадение по совместному запросу — статус/текст), довесок добавляет только
// то, чего в найденном ещё нет.
function mergeSearchAndFresh(searchPage: IOwnRequestsPage, freshRows: IMyRequest[]): IOwnRequestsPage {
const searchKeys = new Set(searchPage.rows.map((r) => rowKey(r)));
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
const merged = onlyFresh
.concat(searchPage.rows)
.sort((a, b) => b.created.localeCompare(a.created))
.slice(0, PAGE_SIZE);
return { rows: merged, totalRows: searchPage.totalRows + onlyFresh.length };
}
/**
* Гонит Search-страницу и REST-довесок по свежести ПАРАЛЛЕЛЬНО — то один
* отвечает быстрее, то другой, заранее не угадать, и ждать заведомо более
* медленный, держа экран пустым, незачем.
*
* Если задан onPartial — зовёт его РАЗ, как только придёт ПЕРВЫЙ из двух
* источников (та половина, что уже есть, без дедупа со вторым — его мы ещё не
* видели). Возвращает функция в любом случае ИТОГОВЫЙ слитый результат;
* onPartial — только для промежуточного показа, на итог не влияет.
*
* Без onPartial ведёт себя как раньше: ждёт оба источника и сразу отдаёт слитый
* результат — второй показ (полный) экономить незачем, если промежуточный
* всё равно никто не увидит.
*/
async function raceThenMerge(
searchPromise: Promise<IOwnRequestsPage>,
freshPromise: Promise<IMyRequest[]>,
onPartial: ((page: IOwnRequestsPage) => void) | undefined
): Promise<IOwnRequestsPage> {
if (onPartial) {
let searchDone = false;
let freshDone = false;
// Ошибку здесь не обрабатываем — её поймает Promise.all ниже; этот
// отдельный .then только подсматривает за тем, что пришло раньше.
searchPromise.then((r) => {
searchDone = true;
if (!freshDone) onPartial(r);
}, () => undefined);
freshPromise.then((r) => {
freshDone = true;
if (!searchDone) onPartial({ rows: r, totalRows: r.length });
}, () => undefined);
}
const [searchPage, freshRows] = await Promise.all([searchPromise, freshPromise]);
return mergeSearchAndFresh(searchPage, freshRows);
}
// «Мои заявки» БЕЗ поиска — гибрид для масштаба: Search с серверной пагинацией
// (RowLimit/StartRow) находит страницу без сканирования всего архива, REST по
// «последним N минутам» добирает то, что Search мог не успеть проиндексировать
// (см. RECENT_MS). Довесок применяется только на первой странице — свежее по
// дате там и оказалось бы, будь оно проиндексировано.
//
// Search и довесок идут ПАРАЛЛЕЛЬНО (см. raceThenMerge) — раньше довесок
// стартовал только ПОСЛЕ ответа Search, хотя они независимы друг от друга и
// читают разные источники; тем самым просто складывалось время обоих вместо
// времени более медленного из двух.
export async function getOwnRequests(
context: WebPartContext,
types: IRequestType[],
page: number
page: number,
onPartial?: (page: IOwnRequestsPage) => void
): Promise<IOwnRequestsPage> {
if (types.length === 0) return { rows: [], totalRows: 0 };
const searchPage = await searchOwnRequests(context, types, '', page);
const searchPromise = searchOwnRequests(context, types, '', page);
if (page !== 1) return searchPromise;
if (page !== 1) return searchPage;
const freshPromise = Promise.all(types.map((t) => getFreshOwnRowsForType(context, t))).then((r) =>
r.reduce((all, x) => all.concat(x), [] as IMyRequest[])
);
const freshRows = (
await Promise.all(
types.map((t) => getFreshOwnRowsForType(context, t))
)
).reduce((all, r) => all.concat(r), [] as IMyRequest[]);
const searchKeys = new Set(searchPage.rows.map((r) => rowKey(r)));
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
const merged = onlyFresh
.concat(searchPage.rows)
.sort((a, b) => b.created.localeCompare(a.created))
.slice(0, PAGE_SIZE);
return { rows: merged, totalRows: searchPage.totalRows + onlyFresh.length };
return raceThenMerge(searchPromise, freshPromise, onPartial);
}
// «Мои заявки» С поиском — только Search, без REST-подстраховки на свежесть:
@@ -1339,9 +1387,10 @@ export async function getOwnRequestsByStatus(
types: IRequestType[],
status: string,
page: number,
query: string = ''
query: string = '',
onPartial?: (page: IOwnRequestsPage) => void
): Promise<IOwnRequestsPage> {
return ownRequestsByStatus(context, types, status, page, query);
return ownRequestsByStatus(context, types, status, page, query, onPartial);
}
/**
@@ -1370,7 +1419,8 @@ async function ownRequestsByStatus(
types: IRequestType[],
status: string,
page: number,
query: string
query: string,
onPartial?: (page: IOwnRequestsPage) => void
): Promise<IOwnRequestsPage> {
if (types.length === 0) return { rows: [], totalRows: 0 };
@@ -1385,7 +1435,7 @@ async function ownRequestsByStatus(
if (text) clauses.push(text);
const kql = clauses.join(' AND ');
const results = await traced('Search: свои заявки', () =>
const searchPromise: Promise<IOwnRequestsPage> = traced('Search: свои заявки', () =>
sp.search({
Querytext: kql,
RowLimit: PAGE_SIZE,
@@ -1395,34 +1445,24 @@ async function ownRequestsByStatus(
// Свежие сверху — как в searchOwnRequests.
SortList: [{ Property: 'Created', Direction: 1 }],
})
);
const totalRows = results.TotalRows || 0;
const entries = hitsToEntries(results, types);
const searchRows = entries.map(toMyRequestFromSearch);
if (page !== 1 || query.trim()) return { rows: searchRows, totalRows };
).then((results) => ({
rows: hitsToEntries(results, types).map(toMyRequestFromSearch),
totalRows: results.TotalRows || 0,
}));
// Довесок по недавним изменениям — только на первой странице и только без
// текстового поиска (тот же принцип, что в getOwnRequests): заявку могли
// вернуть на доработку только что, индекс мог не успеть её подхватить.
const freshRows = (
await Promise.all(
types.map((t) => getRecentlyChangedOwnRowsForType(context, t))
)
)
.reduce((acc, r) => acc.concat(r), [] as IMyRequest[])
.filter((r) => r.status === status);
// Запускаем СРАЗУ, не дожидаясь Search: условие включения известно из
// параметров вызова, ждать ради него ответ Search незачем — раньше довесок
// стартовал только ПОСЛЕ Search, хотя независим от его результата.
if (page !== 1 || query.trim()) return searchPromise;
const searchKeys = new Set(entries.map((e) => requestKey(e.target.listId, e.id)));
const onlyFresh = freshRows.filter((r) => !searchKeys.has(rowKey(r)));
const freshPromise = Promise.all(types.map((t) => getRecentlyChangedOwnRowsForType(context, t))).then((r) =>
r.reduce((acc, x) => acc.concat(x), [] as IMyRequest[]).filter((x) => x.status === status)
);
const merged = onlyFresh
.concat(searchRows)
.sort((a, b) => b.created.localeCompare(a.created))
.slice(0, PAGE_SIZE);
return { rows: merged, totalRows: totalRows + onlyFresh.length };
return raceThenMerge(searchPromise, freshPromise, onPartial);
}
// Сколько строк маршрута максимум забираем за раз при дочитывании комментариев: