1
0
cvsa/packages/backend/middlewares/timing.ts

262 lines
5.6 KiB
TypeScript

import { Elysia, type MapResponse, type Context, type TraceEvent, type TraceProcess } from "elysia";
type MaybePromise<T> = T | Promise<T>;
class TimeLogger {
private startTimes: Map<string, number>;
private durations: Map<string, number>;
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<boolean> | ((context: Omit<Context, "path">) => MaybePromise<boolean>);
/**
* 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;