/** * Хелперы пагинации спискового 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); } /** * Парсит курсор формата `_`. * Невалидный курсор возвращает 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}`; }