perf(web): свод 2 — состояние вкладки с поколениями запросов

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 22:21:04 +04:00
parent 900a3c6535
commit 78e06583f4
2 changed files with 152 additions and 0 deletions

View File

@ -110,3 +110,51 @@ export function getSvod2RowId(row) {
if (isPlaceholderRow(row)) return `ph-${row.index}`;
return `${row.data?.header?.source_form}-${row.data?.header?.source_id}-${row.data?.line_id}`;
}
export function createSheetPagingState() {
return {
generation: 0,
dataGeneration: 0,
pendingQueryKey: '',
loadedQueryKey: '',
total: 0,
facets: null,
pages: new Map(),
loading: false,
error: '',
};
}
export function svod2PagingReducer(state, action) {
if (action.generation !== state.generation && action.type !== 'begin') return state;
switch (action.type) {
case 'begin':
return {
...state,
generation: action.generation,
pendingQueryKey: action.queryKey,
error: '',
loading: action.mode === 'load' ? true : state.loading,
...(action.mode === 'load' ? { pages: new Map(), total: 0, facets: null } : {}),
};
case 'page': {
const first = state.dataGeneration !== action.generation;
return {
...state,
pages: first ? new Map([[action.pageIndex, action.rows]]) : touchPage(state.pages, action.pageIndex, action.rows, action.keepPages),
dataGeneration: action.generation,
loadedQueryKey: first ? state.pendingQueryKey : state.loadedQueryKey,
total: action.total,
};
}
case 'facets':
return { ...state, facets: action.facets };
case 'fail':
if (action.mode === 'scroll') return state;
return { ...state, error: action.message, pages: new Map(), total: 0 };
case 'settled':
return { ...state, loading: false };
default:
return state;
}
}

View File

@ -6,6 +6,7 @@ import {
buildSvod2PageParams,
buildSvod2QueryParams,
buildWindowRows,
createSheetPagingState,
facetsToMaps,
getMissingPages,
getPageCount,
@ -16,6 +17,7 @@ import {
getWindowRange,
isPlaceholderRow,
shouldShowFound,
svod2PagingReducer,
touchPage,
} from './svod2Paging.js';
@ -152,3 +154,105 @@ test('getSvod2RowId', () => {
assert.equal(isPlaceholderRow({ data: {} }), false);
assert.equal(isPlaceholderRow(undefined), false);
});
const begin = (s, generation, mode, queryKey = 'k') => svod2PagingReducer(s, { type: 'begin', generation, mode, queryKey });
const page = (s, generation, pageIndex, rows, total) =>
svod2PagingReducer(s, { type: 'page', generation, pageIndex, rows, total, keepPages: [pageIndex] });
test('начальное состояние вкладки', () => {
const s = createSheetPagingState();
assert.equal(s.generation, 0);
assert.equal(s.dataGeneration, 0);
assert.equal(s.total, 0);
assert.equal(s.facets, null);
assert.equal(s.loading, false);
assert.equal(s.error, '');
assert.equal(s.pages.size, 0);
});
test('устаревшие page/facets/fail игнорируются', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = begin(s, 2, 'refine');
for (const a of [
{ type: 'page', generation: 1, pageIndex: 0, rows: [], total: 9 },
{ type: 'facets', generation: 1, facets: {} },
{ type: 'fail', generation: 1, mode: 'load', message: 'x' },
{ type: 'settled', generation: 1 },
])
assert.equal(svod2PagingReducer(s, a), s);
});
test('load ставит loading и pendingQueryKey, сбрасывает error', () => {
const s = begin(createSheetPagingState(), 1, 'load', 'q1');
assert.equal(s.loading, true);
assert.equal(s.generation, 1);
assert.equal(s.pendingQueryKey, 'q1');
assert.equal(s.error, '');
});
test('refine оставляет прежние строки до первого ответа нового поколения', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a', 'b'], 2);
s = svod2PagingReducer(s, { type: 'facets', generation: 1, facets: { total: 2 } });
s = svod2PagingReducer(s, { type: 'settled', generation: 1 });
s = begin(s, 2, 'refine', 'k2');
assert.deepEqual(s.pages.get(0), ['a', 'b']);
assert.equal(s.total, 2);
assert.deepEqual(s.facets, { total: 2 });
assert.equal(s.loading, false);
assert.equal(s.pendingQueryKey, 'k2');
s = page(s, 2, 0, ['c'], 1);
assert.deepEqual([...s.pages.keys()], [0]);
assert.deepEqual(s.pages.get(0), ['c']);
assert.equal(s.dataGeneration, 2);
});
test('первый ответ поколения ставит loadedQueryKey', () => {
let s = begin(createSheetPagingState(), 1, 'load', 'k');
assert.equal(s.loadedQueryKey, '');
s = page(s, 1, 0, ['a'], 1);
assert.equal(s.loadedQueryKey, 'k');
assert.equal(s.dataGeneration, 1);
});
test('ошибка догрузки (scroll) не меняет состояние, страница снова среди недостающих', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 600);
assert.equal(svod2PagingReducer(s, { type: 'fail', generation: s.generation, mode: 'scroll', message: 'x' }), s);
assert.deepEqual(getMissingPages([0, 1], s.pages, new Set()), [1]);
});
test('ошибка load очищает строки и ставит error', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5);
s = svod2PagingReducer(s, { type: 'fail', generation: 1, mode: 'load', message: 'x' });
assert.equal(s.error, 'x');
assert.equal(s.pages.size, 0);
assert.equal(s.total, 0);
});
test('ошибка refine очищает строки и ставит error', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5);
s = begin(s, 2, 'refine');
s = svod2PagingReducer(s, { type: 'fail', generation: 2, mode: 'refine', message: 'y' });
assert.equal(s.error, 'y');
assert.equal(s.pages.size, 0);
assert.equal(s.total, 0);
});
test('последний total поколения побеждает', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = page(s, 1, 0, ['a'], 5563);
s = page(s, 1, 1, ['b'], 5560);
assert.equal(s.total, 5560);
assert.equal(s.pages.size, 2);
});
test('facets и settled текущего поколения', () => {
let s = begin(createSheetPagingState(), 1, 'load');
s = svod2PagingReducer(s, { type: 'facets', generation: 1, facets: { total: 3 } });
assert.deepEqual(s.facets, { total: 3 });
s = svod2PagingReducer(s, { type: 'settled', generation: 1 });
assert.equal(s.loading, false);
});