src/services/dateRange.ts
Rama main · réplica de lectura del repositorio original.
export type DateRangePreset
= '30min'
| '1h'
| '6h'
| '12h'
| '24h'
| '3day'
| '7day'
| '14day'
| '30day'
| '90day'
| 'quarter'
| '6month'
| '12month'
| 'custom'
export type RollingDateRangePreset = Exclude<DateRangePreset, 'custom'>
export type DateRangePresetGroupKey = 'hours' | 'days' | 'months'
export const DEFAULT_DATE_RANGE_PRESET = '24h' as const satisfies RollingDateRangePreset
/** Default rolling window for logs table toolbars. */
export const TABLE_DATE_RANGE_DEFAULT = '30min' as const satisfies RollingDateRangePreset
/** Default rolling window for the devices table: 7 days, so most apps open with devices listed. */
export const DEVICES_TABLE_DATE_RANGE_DEFAULT = '7day' as const satisfies RollingDateRangePreset
/** Rolling window lengths for preset modes (ms before `now`). */
export const DATE_RANGE_DURATIONS_MS: Record<RollingDateRangePreset, number> = {
'30min': 30 * 60 * 1000,
'1h': 60 * 60 * 1000,
'6h': 6 * 60 * 60 * 1000,
'12h': 12 * 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
'3day': 3 * 24 * 60 * 60 * 1000,
'7day': 7 * 24 * 60 * 60 * 1000,
'14day': 14 * 24 * 60 * 60 * 1000,
'30day': 30 * 24 * 60 * 60 * 1000,
'90day': 90 * 24 * 60 * 60 * 1000,
'quarter': 90 * 24 * 60 * 60 * 1000,
'6month': 180 * 24 * 60 * 60 * 1000,
'12month': 365 * 24 * 60 * 60 * 1000,
}
/** i18n keys for each rolling preset label. */
export const DATE_RANGE_PRESET_LABEL_KEYS: Record<RollingDateRangePreset, string> = {
'30min': 'last-30-minutes',
'1h': 'last-1-hour',
'6h': 'last-6-hours',
'12h': 'last-12-hours',
'24h': 'last-24-hours',
'3day': '3-days',
'7day': '7-days',
'14day': '14-days',
'30day': '30-days',
'90day': '90-days',
'quarter': 'last-quarter',
'6month': 'last-6-months',
'12month': 'last-12-months',
}
/** Sidebar groups shown in the Cloudflare-style range picker. */
export const DATE_RANGE_PRESET_GROUPS: { key: DateRangePresetGroupKey, modes: RollingDateRangePreset[] }[] = [
{ key: 'hours', modes: ['30min', '1h', '6h', '12h', '24h'] },
{ key: 'days', modes: ['3day', '7day', '14day', '30day', '90day'] },
{ key: 'months', modes: ['quarter', '6month', '12month'] },
]
export interface DateRangeValue {
start: Date
end: Date
}
export function getDateRangeForPreset(
mode: DateRangePreset,
now = new Date(),
custom?: DateRangeValue,
): DateRangeValue {
if (mode === 'custom') {
return custom ?? {
start: new Date(now.getTime() - DATE_RANGE_DURATIONS_MS[DEFAULT_DATE_RANGE_PRESET]),
end: now,
}
}
return {
start: new Date(now.getTime() - DATE_RANGE_DURATIONS_MS[mode]),
end: now,
}
}
/** Clamp a range into optional min/max bounds. */
export function clampDateRange(
range: DateRangeValue,
minDate?: Date | null,
maxDate?: Date | null,
): DateRangeValue {
let start = range.start.getTime()
let end = range.end.getTime()
if (end < start)
[start, end] = [end, start]
if (minDate)
start = Math.max(start, minDate.getTime())
if (maxDate)
end = Math.min(end, maxDate.getTime())
if (end < start)
end = start
return { start: new Date(start), end: new Date(end) }
}
/**
* Infer a rolling preset when start/end look like "last X ending near now".
* Falls back to `custom` when no preset matches within tolerance.
*/
export function inferDateRangePreset(
start: Date,
end: Date,
now = new Date(),
toleranceMs = 2 * 60 * 1000,
): DateRangePreset {
const endDelta = Math.abs(end.getTime() - now.getTime())
if (endDelta > toleranceMs)
return 'custom'
const duration = end.getTime() - start.getTime()
if (duration <= 0)
return 'custom'
let best: RollingDateRangePreset | null = null
let bestDelta = Number.POSITIVE_INFINITY
for (const mode of Object.keys(DATE_RANGE_DURATIONS_MS) as RollingDateRangePreset[]) {
const delta = Math.abs(DATE_RANGE_DURATIONS_MS[mode] - duration)
if (delta <= toleranceMs && delta < bestDelta) {
best = mode
bestDelta = delta
}
}
return best ?? 'custom'
}
export function isRollingDateRangePreset(value: string): value is RollingDateRangePreset {
return value in DATE_RANGE_DURATIONS_MS
}
export interface DateRangeQueryParams {
range?: unknown
start?: unknown
end?: unknown
}
export type ParsedDateRangeQuery
= { mode: RollingDateRangePreset }
| { mode: 'custom', start: Date, end: Date }
/** Parse `range` (+ optional `start`/`end` for custom) from a route query. */
export function parseDateRangeQuery(query: DateRangeQueryParams): ParsedDateRangeQuery | null {
const range = typeof query.range === 'string' ? query.range : null
if (!range)
return null
if (range === 'custom') {
if (typeof query.start !== 'string' || typeof query.end !== 'string')
return null
const start = new Date(query.start)
const end = new Date(query.end)
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end.getTime() < start.getTime())
return null
return { mode: 'custom', start, end }
}
if (isRollingDateRangePreset(range))
return { mode: range }
return null
}
export interface SerializedDateRangeQuery {
range: DateRangePreset
start?: string
end?: string
}
/** Serialize a date-range mode for URL query params. */
export function serializeDateRangeQuery(
mode: DateRangePreset,
custom?: DateRangeValue,
): SerializedDateRangeQuery {
if (mode === 'custom' && custom) {
return {
range: 'custom',
start: custom.start.toISOString(),
end: custom.end.toISOString(),
}
}
if (mode === 'custom')
return { range: DEFAULT_DATE_RANGE_PRESET }
return { range: mode }
}
/**
* Stable filter identity for table reloads.
* Rolling presets must NOT embed `now`-based timestamps — those change every
* call and would reset cursor pagination back to page 1 on each next click.
*/
export function getTableDateRangeSignature(
mode: DateRangePreset,
customRange?: [Date, Date] | null,
): { mode: DateRangePreset, start?: string, end?: string } {
if (mode !== 'custom')
return { mode }
if (!customRange?.[0] || !customRange?.[1])
return { mode: 'custom' }
return {
mode: 'custom',
start: customRange[0].toISOString(),
end: customRange[1].toISOString(),
}
}
/**
* Whether a devices/logs table reload should re-run the expensive count query.
* Page-only navigation keeps the cached total so 100k+ device apps stay usable.
*/
export function shouldRecountOnTableReload(options: {
filtersChanged: boolean
previousPage: number
requestedPage: number
}): boolean {
if (options.filtersChanged)
return true
return options.previousPage === options.requestedPage
}
/**
* Time-window pagination used by logs/deployments "Load older".
* Page 1 is the selected range; page 0 / -1 / … shift one full window backward.
*/
export function getTimeWindowPageRange(
rangeStartMs: number,
rangeEndMs: number,
page: number,
): { rangeStart: number, rangeEnd: number } {
const timeDifference = rangeEndMs - rangeStartMs
const pageTimeOffset = timeDifference * (page - 1)
return {
rangeStart: rangeStartMs + pageTimeOffset,
rangeEnd: rangeEndMs + pageTimeOffset,
}
}