From 78e06583f43864b54078ffaee1fa2e928be83129 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 22:21:04 +0400 Subject: [PATCH] =?UTF-8?q?perf(web):=20=D1=81=D0=B2=D0=BE=D0=B4=202=20?= =?UTF-8?q?=E2=80=94=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20=D1=81=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=BE=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/pages/Svod2Page/svod2Paging.js | 48 +++++++++ web/src/pages/Svod2Page/svod2Paging.test.js | 104 ++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/web/src/pages/Svod2Page/svod2Paging.js b/web/src/pages/Svod2Page/svod2Paging.js index f2c29d0..da917f3 100644 --- a/web/src/pages/Svod2Page/svod2Paging.js +++ b/web/src/pages/Svod2Page/svod2Paging.js @@ -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; + } +} diff --git a/web/src/pages/Svod2Page/svod2Paging.test.js b/web/src/pages/Svod2Page/svod2Paging.test.js index 98e2fdb..02b585e 100644 --- a/web/src/pages/Svod2Page/svod2Paging.test.js +++ b/web/src/pages/Svod2Page/svod2Paging.test.js @@ -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); +});