Files
iistwin/server/utils/task-pagination.ts
Ильяс Султанов a27766a38b perf(tasks): лимиты/пагинация /api/tasks и дельта-синк в SQL
Шаги 0.10 и 0.9 плана production-готовности:
- дефолт limit 50, кап 200; списковые ответы без description
- курсорная пагинация (updated_at, id) + nextCursor
- since-фильтр дельта-синка перенесён в SQL (updated_at >= since)
- embedding pre-count через COUNT(*) вместо выгрузки миллиона строк
- 11 новых юнит-тестов (56/56 зелёные)

Инвариант поиска сохранён: search-ветка /api/tasks?search= не изменена
2026-09-07 22:01:25 +03:00

45 lines
2.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Хелперы пагинации спискового endpoint'а GET /api/tasks.
* Дефолтный лимит 50, жёсткий максимум 200 — защита от выгрузки
* всей таблицы задач одним запросом без параметров.
*/
export const TASKS_LIST_DEFAULT_LIMIT = 50;
export const TASKS_LIST_MAX_LIMIT = 200;
/** Курсорная пагинация по (updatedAt, id): ключевая пара последней строки выдачи */
export interface TasksCursor {
updatedAt: Date;
id: number;
}
/** Приводит запрошенный лимит к диапазону 1..200; без значения — дефолт 50 */
export function clampTasksLimit(requested?: number | null): number {
if (requested == null || Number.isNaN(requested) || requested <= 0) {
return TASKS_LIST_DEFAULT_LIMIT;
}
return Math.min(Math.floor(requested), TASKS_LIST_MAX_LIMIT);
}
/**
* Парсит курсор формата `<updatedAt ISO>_<id>`.
* Невалидный курсор возвращает null — запрос обрабатывается как первая страница.
*/
export function parseTasksCursor(raw?: string | null): TasksCursor | null {
if (!raw) return null;
const idx = raw.lastIndexOf('_');
if (idx <= 0 || idx === raw.length - 1) return null;
const ts = new Date(raw.slice(0, idx)).getTime();
const id = Number(raw.slice(idx + 1));
if (Number.isNaN(ts) || !Number.isInteger(id) || id <= 0) return null;
return { updatedAt: new Date(ts), id };
}
/** Собирает nextCursor из последней строки выдачи; null, если у строки нет updatedAt */
export function buildTasksNextCursor(task: { id: number; updatedAt: Date | string | null }): string | null {
if (!task.updatedAt) return null;
const ts = new Date(task.updatedAt).getTime();
if (Number.isNaN(ts)) return null;
return `${new Date(ts).toISOString()}_${task.id}`;
}