различные кэши

This commit is contained in:
gorbenko.pavel
2026-08-06 14:55:46 +05:00
parent a86c8d5464
commit bd3689cf14
4 changed files with 115 additions and 43 deletions
@@ -3,7 +3,7 @@ import styles from './RequestPortal.module.scss';
import type { IRequestPortalProps } from './IRequestPortalProps'; import type { IRequestPortalProps } from './IRequestPortalProps';
import { IconSprite } from '../shared/ui/Icon/IconSprite'; import { IconSprite } from '../shared/ui/Icon/IconSprite';
import { navigate, useRoute, NavSection } from '../shared/routing'; import { navigate, useRoute, NavSection } from '../shared/routing';
import { onRequestsChanged } from '../shared/requestEvents'; import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
import { SideNav } from './SideNav/SideNav'; import { SideNav } from './SideNav/SideNav';
import { Drawer } from './Drawer/Drawer'; import { Drawer } from './Drawer/Drawer';
import { CategoryTabs } from './CategoryTabs/CategoryTabs'; import { CategoryTabs } from './CategoryTabs/CategoryTabs';
@@ -75,7 +75,14 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
} }
}, [route.mode, route.formKey]); }, [route.mode, route.formKey]);
const goto = (nav: NavSection): void => navigate({ mode: 'submit', nav }); const goto = (nav: NavSection): void => {
// Клик по УЖЕ активному пункту меню не меняет маршрут, значит эффект
// загрузки в таблице не перезапустится сам. Шлём событие обновления —
// таблица на него подписана и перечитает данные фоном, не убирая с экрана
// текущие строки (см. RequestsTableView/SearchView).
if (nav === route.nav && route.mode === 'submit') emitRequestsChanged();
navigate({ mode: 'submit', nav });
};
const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav }); const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav });
return ( return (
@@ -13,6 +13,7 @@ import { IMyRequest } from './models/IMyRequest';
import { REQUEST_STATUS, IRequestType } from '../../shared/approval'; import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
import { onRequestsChanged } from '../../shared/requestEvents'; import { onRequestsChanged } from '../../shared/requestEvents';
import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery'; import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery';
import { IPage, pageKey, pageSignature, readPage, writePage } from './pageCache';
import { Pager } from './Pager'; import { Pager } from './Pager';
import styles from './RequestsTableView.module.scss'; import styles from './RequestsTableView.module.scss';
@@ -177,49 +178,58 @@ export const RequestsTableView: React.FC<IRequestsTableViewProps> = ({
React.useEffect(() => { React.useEffect(() => {
if (types === undefined) return; if (types === undefined) return;
let cancelled = false; let cancelled = false;
const cacheKey = pageKey('table', [kind, debouncedQuery.trim(), page]);
// Сразу показываем то, что уже загружали для ЭТОЙ вкладки (и этого запроса
// и страницы), а обновление идёт фоном. Компонент один на обе вкладки —
// без кеша при переключении на экране оставались бы строки предыдущей
// вкладки, пока не придёт ответ. Если для этой вкладки данных ещё нет —
// rows становится undefined, и показывается «Загрузка…».
const cached = readPage(cacheKey);
setRows(cached ? cached.rows : undefined);
setTotalRows(cached ? cached.totalRows : 0);
const fetchPage = async (): Promise<IPage> => {
if (kind === 'own') {
return debouncedQuery.trim().length > 0
? searchOwnRequests(context, types, debouncedQuery, page)
: getOwnRequests(context, types, page);
}
if (debouncedQuery.trim().length === 0) {
// Без поиска — страница уже нарезана и дочитана сервером.
const r = await getPendingApproval(context, 'myTurn', page);
return { rows: r.rows, totalRows: r.totalRows };
}
// С поиском — сначала полный список «где я есть в маршруте» (без
// автора), фильтруем текстом, и только потом режем на страницы и
// дочитываем автора — только для видимой страницы.
const idx = await getPendingApprovalIndex(context, 'involved');
const filtered = await filterPendingByQuery(context, idx.types, idx.rows, debouncedQuery);
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
await hydratePendingAuthors(context, idx.types, pageRows);
return { rows: pageRows, totalRows: filtered.length };
};
const load = async (): Promise<void> => { const load = async (): Promise<void> => {
try { try {
if (kind === 'own') { const fresh = await fetchPage();
const hasOwnQuery = debouncedQuery.trim().length > 0; if (cancelled) return;
const r = hasOwnQuery
? await searchOwnRequests(context, types, debouncedQuery, page) // Перерисовываем, только если данные реально изменились — иначе при
: await getOwnRequests(context, types, page); // каждом возврате на вкладку таблица моргала бы, перерисовываясь тем же
if (cancelled) return; // самым содержимым.
setRows(r.rows); const prev = readPage(cacheKey);
setTotalRows(r.totalRows); if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
setError(''); writePage(cacheKey, fresh);
return; setRows(fresh.rows);
setTotalRows(fresh.totalRows);
} }
const hasQuery = debouncedQuery.trim().length > 0;
if (!hasQuery) {
// Без поиска — страница уже нарезана и дочитана сервером.
const r = await getPendingApproval(context, 'myTurn', page);
if (cancelled) return;
console.table(r.debug);
setRows(r.rows);
setTotalRows(r.totalRows);
setError('');
if (onMyTurnCountChange) onMyTurnCountChange(r.totalRows);
return;
}
// С поиском — сначала полный список «где я есть в маршруте» (без
// автора), фильтруем текстом, и только потом режем на страницы и
// дочитываем автора — только для видимой страницы.
const idx = await getPendingApprovalIndex(context, 'involved');
if (cancelled) return;
console.table(idx.debug);
const filtered = await filterPendingByQuery(context, idx.types, idx.rows, debouncedQuery);
if (cancelled) return;
const pageRows = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
await hydratePendingAuthors(context, idx.types, pageRows);
if (cancelled) return;
setRows(pageRows);
setTotalRows(filtered.length);
setError(''); setError('');
if (onMyTurnCountChange && kind === 'approvals' && debouncedQuery.trim().length === 0) {
onMyTurnCountChange(fresh.totalRows);
}
} catch (e) { } catch (e) {
if (!cancelled) setError((e as Error).message); if (!cancelled) setError((e as Error).message);
} }
@@ -5,6 +5,7 @@ import { IMyRequest } from './models/IMyRequest';
import { REQUEST_STATUS, IRequestType } from '../../shared/approval'; import { REQUEST_STATUS, IRequestType } from '../../shared/approval';
import { onRequestsChanged } from '../../shared/requestEvents'; import { onRequestsChanged } from '../../shared/requestEvents';
import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery'; import { usePersistedQuery, useDebouncedQuery } from '../../shared/usePersistedQuery';
import { pageKey, pageSignature, readPage, writePage } from './pageCache';
import { Pager } from './Pager'; import { Pager } from './Pager';
import styles from './SearchView.module.scss'; import styles from './SearchView.module.scss';
@@ -140,13 +141,24 @@ export const SearchView: React.FC<ISearchViewProps> = ({ context, onOpen }) => {
return; return;
} }
let cancelled = false; let cancelled = false;
const cacheKey = pageKey('search', [debouncedQuery.trim(), page]);
// Уже искали это раньше — показываем прежний результат сразу, обновление
// идёт фоном (см. RequestsTableView, там та же схема и те же причины).
const cached = readPage(cacheKey);
setRows(cached ? cached.rows : undefined);
setTotalRows(cached ? cached.totalRows : 0);
const load = async (): Promise<void> => { const load = async (): Promise<void> => {
try { try {
const r = await searchMyRequests(context, types, debouncedQuery, page); const fresh = await searchMyRequests(context, types, debouncedQuery, page);
if (cancelled) return; if (cancelled) return;
setRows(r.rows); const prev = readPage(cacheKey);
setTotalRows(r.totalRows); if (!prev || pageSignature(prev) !== pageSignature(fresh)) {
writePage(cacheKey, fresh);
setRows(fresh.rows);
setTotalRows(fresh.totalRows);
}
setError(''); setError('');
} catch (e) { } catch (e) {
if (!cancelled) setError((e as Error).message); if (!cancelled) setError((e as Error).message);
@@ -0,0 +1,43 @@
import { IMyRequest } from './models/IMyRequest';
// Загруженная страница таблицы — то, что кладётся в кеш вкладки
// (см. RequestsTableView/SearchView).
export interface IPage {
rows: IMyRequest[];
totalRows: number;
}
// Хранилище на уровне МОДУЛЯ, а не внутри компонента (useRef), потому что
// компоненты таблиц размонтируются при переключении разделов: RequestPortal
// рендерит либо SubmitFeature, либо SearchView, либо RequestsTableView — уход
// на «Поиск» или «Новая заявка» уничтожает экземпляр таблицы вместе со всем его
// состоянием. С useRef кеш переживал только переключение «Мои заявки» ↔ «На
// согласовании» (там экземпляр один, меняется лишь проп kind), а после захода
// на «Поиск» терялся. Здесь он живёт до перезагрузки страницы.
//
// Ключ формирует вызывающая сторона и обязательно начинает с префикса раздела
// (см. pageKey), иначе ключи двух таблиц пересеклись бы.
const store = new Map<string, IPage>();
// Ключ кеша: раздел + всё, от чего зависит содержимое страницы.
export function pageKey(view: string, parts: Array<string | number>): string {
return [view, ...parts].join('|');
}
export function readPage(key: string): IPage | undefined {
return store.get(key);
}
export function writePage(key: string, page: IPage): void {
store.set(key, page);
}
// Слепок страницы для сравнения «изменилось ли что-нибудь». Сравниваем поля,
// которые видны в таблице: если они совпали, перерисовывать нечего — иначе при
// каждом возврате на вкладку и при каждом фоновом обновлении таблица моргала бы,
// перерисовываясь тем же самым содержимым.
export function pageSignature(page: IPage): string {
return `${page.totalRows}|${page.rows
.map((r) => [r.requestTypeId, r.id, r.title, r.status, r.created, r.authorName].join('~'))
.join(';')}`;
}