рест и серч одновременно
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
@@ -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);
}
// Сколько строк маршрута максимум забираем за раз при дочитывании комментариев: