import type { Context, MiddlewareHandler } from 'hono' /** Hono variables used to collect Server-Timing metrics. */ export type Variables = { /** Operations that have started but not finished. */ activeTimings?: Active[] | undefined /** State used to collapse overlapping samples of one logical operation. */ timingSums?: Map | undefined /** Metrics emitted in the `Server-Timing` response header. */ serverTiming?: Metric[] | undefined } /** Hono environment shape used by timing helpers. */ export type Environment = { Variables: Variables } /** One Server-Timing metric. */ export type Metric = { /** Metric duration in milliseconds. */ duration: number /** Metric name. */ name: string } /** One operation that has started but not finished. */ export type Active = { /** Metric name. */ name: string /** Monotonic start time. */ start: number } /** Request-scoped state for one overlap-aware accumulated metric. */ export type Sum = { /** Number of currently overlapping samples. */ active: number /** Start of the current uninterrupted interval. */ start: number } /** Timings started by an outer runtime before the Hono app receives a request. */ export type Seed = { /** Operations still running when request middleware begins. */ active?: readonly Active[] | undefined /** Operations already completed before request middleware begins. */ metrics?: readonly Metric[] | undefined } const requestSeeds = new WeakMap() /** Seeds timings collected by an outer runtime onto the same request object. */ export function seed(request: Request, value: Seed): void { requestSeeds.set(request, value) } /** Adds a `Server-Timing` header with total request duration. */ export function middleware< environment extends Environment = Environment, >(): MiddlewareHandler { return async (c, next) => { const seeded = requestSeeds.get(c.req.raw) requestSeeds.delete(c.req.raw) const outerActive = [...(seeded?.active ?? [])] const active: Active[] = [...outerActive] const metrics: Metric[] = [...(seeded?.metrics ?? [])] c.set('activeTimings', active) c.set('serverTiming', metrics) c.set('timingSums', new Map()) const start = performance.now() await next() metrics.push({ duration: performance.now() - start, name: 'request' }) for (const timing of outerActive) { active.splice(active.indexOf(timing), 1) metrics.push({ duration: performance.now() - timing.start, name: timing.name }) } append(c.res, metrics) } } /** Appends metrics to a response's `Server-Timing` header and returns it. */ export function append(response: Response, metrics: readonly Metric[]): Response { if (metrics.length === 0) return response const value = format(metrics) const existing = response.headers.get('Server-Timing') response.headers.set('Server-Timing', existing ? `${existing}, ${value}` : value) return response } /** Measures one async operation and adds it to the current request timings. */ export async function time( c: Context, name: string, fn: () => Promise | value, ) { const start = performance.now() const active = { name, start } c.get('activeTimings')?.push(active) try { return await fn() } finally { const activeTimings = c.get('activeTimings') if (activeTimings) activeTimings.splice(activeTimings.indexOf(active), 1) c.get('serverTiming')?.push({ duration: performance.now() - start, name }) } } /** * Measures async work and accumulates its wall-clock union under one metric * name. Sequential samples add up while overlapping samples count only once, * keeping repeated page and parallel chain work bounded and latency-shaped. */ export async function sum( c: Context, name: string, fn: () => Promise | value, ) { const start = performance.now() const active = { name, start } c.get('activeTimings')?.push(active) const sums = c.get('timingSums') as Map | undefined let sum = sums?.get(name) if (!sum && sums) { sum = { active: 0, start } sums.set(name, sum) } if (sum) { if (sum.active === 0) sum.start = start sum.active++ } try { return await fn() } finally { const activeTimings = c.get('activeTimings') if (activeTimings) activeTimings.splice(activeTimings.indexOf(active), 1) const metrics = c.get('serverTiming') as Metric[] | undefined if (!sum || --sum.active === 0) { const duration = performance.now() - (sum?.start ?? start) const existing = metrics?.find((metric) => metric.name === name) if (existing) existing.duration += duration else metrics?.push({ duration, name }) } } } function format(metrics: readonly Metric[]) { return metrics .map((metric) => `${name(metric.name)};dur=${metric.duration.toFixed(1)}`) .join(', ') } function name(value: string) { return value.replace(/[^A-Za-z0-9_.*-]/g, '_') }