import { Elysia, type MapResponse, type Context, type TraceEvent, type TraceProcess } from 'elysia' type MaybePromise = T | Promise class TimeLogger { private startTimes: Map private durations: Map private totalStartTime: number | null constructor() { this.startTimes = new Map() this.durations = new Map() this.totalStartTime = null } startTime(name: string) { this.startTimes.set(name, performance.now()) } endTime(name: string) { const startTime = this.startTimes.get(name) if (startTime !== undefined) { const duration = performance.now() - startTime this.durations.set(name, duration) this.startTimes.delete(name) } } getCompletedDurations() { return Array.from(this.durations.entries()).map(([name, duration]) => ({ name, duration })) } startTotal(): void { this.totalStartTime = performance.now() } endTotal(): number | null { if (this.totalStartTime === null) return null return performance.now() - this.totalStartTime } } export interface ServerTimingOptions { /** * Should Elysia report data back to client via 'Server-Sent-Event' */ report?: boolean /** * Allow Server Timing to log specified life-cycle events */ trace?: { /** * Capture duration from request * * @default true */ request?: boolean /** * Capture duration from parse * * @default true */ parse?: boolean /** * Capture duration from transform * * @default true */ transform?: boolean /** * Capture duration from beforeHandle * * @default true */ beforeHandle?: boolean /** * Capture duration from handle * * @default true */ handle?: boolean /** * Capture duration from afterHandle * * @default true */ afterHandle?: boolean /** * Capture duration from mapResponse * * @default true */ error?: boolean /** * Capture duration from mapResponse * * @default true */ mapResponse?: boolean /** * Capture total duration from start to finish * * @default true */ total?: boolean } /** * Determine whether or not Server Timing should be enabled * * @default NODE_ENV !== 'production' */ enabled?: boolean /** * A condition whether server timing should be log * * @default undefined */ allow?: | MaybePromise | ((context: Omit) => MaybePromise) /** * A custom mapResponse provided by user * * @default undefined */ mapResponse?: MapResponse } const getLabel = ( event: TraceEvent, listener: ( callback: (process: TraceProcess<'begin', true>) => unknown ) => unknown, write: (value: string) => void ) => { listener(async ({ onStop, onEvent, total }) => { let label = '' if (total === 0) return onEvent(({ name, index, onStop }) => { onStop(({ elapsed }) => { label += `${event}.${index}.${name || 'anon'};dur=${elapsed},` }) }) onStop(({ elapsed }) => { label += `${event};dur=${elapsed},` write(label) }) }) } export const serverTiming = ({ allow, enabled = process.env.NODE_ENV !== 'production', trace: { request: traceRequest = true, parse: traceParse = true, transform: traceTransform = true, beforeHandle: traceBeforeHandle = true, handle: traceHandle = true, afterHandle: traceAfterHandle = true, error: traceError = true, mapResponse: traceMapResponse = true, total: traceTotal = true } = {}, mapResponse }: ServerTimingOptions = {}) => { const app = new Elysia().decorate('timeLog', new TimeLogger()).trace( { as: 'global' }, async ({ onRequest, onParse, onTransform, onBeforeHandle, onHandle, onAfterHandle, onMapResponse, onError, set, context, response, context: { request: { method } } }) => { if (!enabled) return let label = '' const write = (nextValue: string) => { label += nextValue } let start: number onRequest(({ begin }) => { context.timeLog.startTotal() start = begin }) if (traceRequest) getLabel('request', onRequest, write) if (traceParse) getLabel('parse', onParse, write) if (traceTransform) getLabel('transform', onTransform, write) if (traceBeforeHandle) getLabel('beforeHandle', onBeforeHandle, write) if (traceAfterHandle) getLabel('afterHandle', onAfterHandle, write) if (traceError) getLabel('error', onError, write) if (traceMapResponse) getLabel('mapResponse', onMapResponse, write) if (traceHandle) onHandle(({ name, onStop }) => { onStop(({ elapsed }) => { label += `handle.${name};dur=${elapsed},` }) }) onMapResponse(({ onStop }) => { onStop(async ({ end }) => { const completedDurations = context.timeLog.getCompletedDurations() if (completedDurations.length > 0) { label += completedDurations .map( ({ name, duration }) => `${name};dur=${duration}` ) .join(', ') + ',' } const elapsed = context.timeLog.endTotal(); let allowed = allow if (allowed instanceof Promise) allowed = await allowed if (traceTotal) label += `total;dur=${elapsed}` else label = label.slice(0, -1) // ? Must wait until request is reported switch (typeof allowed) { case 'boolean': if (allowed === false) delete set.headers['Server-Timing'] set.headers['Server-Timing'] = label break case 'function': if ((await allowed(context)) === false) delete set.headers['Server-Timing'] set.headers['Server-Timing'] = label break default: set.headers['Server-Timing'] = label } }) }) } ) return app } export default serverTiming