release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+29
View File
@@ -0,0 +1,29 @@
export interface WebVitalsClientOptions {
endpoint?: string;
sampleRate?: number;
}
export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
const endpoint = JSON.stringify(options.endpoint ?? "/__wrnexus/metrics/vitals");
const sampleRate = Math.max(0, Math.min(1, options.sampleRate ?? 0.1));
return `(function(){
if (!window.PerformanceObserver || Math.random() > ${sampleRate}) return;
var endpoint = ${endpoint};
var cls = 0;
function rating(name, value) {
if (name === "LCP") return value <= 2500 ? "good" : value <= 4000 ? "needs-improvement" : "poor";
if (name === "INP") return value <= 200 ? "good" : value <= 500 ? "needs-improvement" : "poor";
if (name === "CLS") return value <= 0.1 ? "good" : value <= 0.25 ? "needs-improvement" : "poor";
return "unknown";
}
function send(name, value) {
var body = JSON.stringify({ name: name, value: value, rating: rating(name, value), route: location.pathname, navigationType: performance.getEntriesByType("navigation")[0]?.type });
if (navigator.sendBeacon) navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(){});
}
try { new PerformanceObserver(function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) send("LCP", last.startTime); }).observe({type:"largest-contentful-paint",buffered:true}); } catch(_) {}
try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }).observe({type:"layout-shift",buffered:true}); } catch(_) {}
try { new PerformanceObserver(function(list){ var max=0; list.getEntries().forEach(function(e){ max=Math.max(max,e.duration||0); }); if(max) send("INP",max); }).observe({type:"event",durationThreshold:40,buffered:true}); } catch(_) {}
addEventListener("visibilitychange", function(){ if(document.visibilityState === "hidden" && cls) send("CLS", cls); }, { once: true });
})();`;
}
+16
View File
@@ -0,0 +1,16 @@
export { MetricsRegistry } from "./metrics.ts";
export type { MetricLabels, MetricPoint } from "./metrics.ts";
export {
createHttpMetricExporter,
createWebVitalsHandler,
defaultMetrics,
metricsMiddleware,
} from "./server.ts";
export type {
MetricExporter,
MetricsMiddlewareOptions,
WebVitalRecord,
WebVitalsHandlerOptions,
} from "./server.ts";
export { webVitalsClient } from "./client.ts";
export type { WebVitalsClientOptions } from "./client.ts";
+81
View File
@@ -0,0 +1,81 @@
export type MetricLabels = Record<string, string | number | boolean>;
export interface MetricPoint {
name: string;
type: "counter" | "gauge" | "histogram";
value: number;
count?: number;
sum?: number;
min?: number;
max?: number;
labels: MetricLabels;
timestamp: number;
}
function labelKey(labels: MetricLabels): string {
return Object.entries(labels)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([key, value]) => `${key}=${String(value)}`)
.join(",");
}
interface StoredMetric extends MetricPoint {
key: string;
}
export class MetricsRegistry {
private points = new Map<string, StoredMetric>();
constructor(private readonly clock: () => number = Date.now) {}
increment(name: string, value = 1, labels: MetricLabels = {}): void {
const key = `counter:${name}:${labelKey(labels)}`;
const current = this.points.get(key);
this.points.set(key, {
key,
name,
type: "counter",
value: (current?.value ?? 0) + value,
labels: { ...labels },
timestamp: this.clock(),
});
}
gauge(name: string, value: number, labels: MetricLabels = {}): void {
const key = `gauge:${name}:${labelKey(labels)}`;
this.points.set(key, {
key,
name,
type: "gauge",
value,
labels: { ...labels },
timestamp: this.clock(),
});
}
observe(name: string, value: number, labels: MetricLabels = {}): void {
const key = `histogram:${name}:${labelKey(labels)}`;
const current = this.points.get(key);
const count = (current?.count ?? 0) + 1;
const sum = (current?.sum ?? 0) + value;
this.points.set(key, {
key,
name,
type: "histogram",
value: sum / count,
count,
sum,
min: current?.min === undefined ? value : Math.min(current.min, value),
max: current?.max === undefined ? value : Math.max(current.max, value),
labels: { ...labels },
timestamp: this.clock(),
});
}
snapshot(): MetricPoint[] {
return [...this.points.values()].map(({ key: _key, ...point }) => ({ ...point }));
}
reset(): void {
this.points.clear();
}
}
+132
View File
@@ -0,0 +1,132 @@
import type { Context, Middleware } from "@wrnexus/core";
import { MetricsRegistry, type MetricPoint } from "./metrics.ts";
export interface MetricsMiddlewareOptions {
registry?: MetricsRegistry;
routeLabel?: (ctx: Context) => string;
includePath?: boolean;
}
export function metricsMiddleware(options: MetricsMiddlewareOptions = {}): Middleware {
const registry = options.registry ?? defaultMetrics;
let active = 0;
return async (ctx, next) => {
const started = performance.now();
active++;
registry.gauge("http.server.active_requests", active);
try {
const response = await next();
const route =
options.routeLabel?.(ctx) ?? (options.includePath ? ctx.url.pathname : "unknown");
const labels = { method: ctx.req.method, status: response.status, route };
registry.increment("http.server.requests", 1, labels);
registry.observe("http.server.duration_ms", performance.now() - started, labels);
return response;
} catch (error) {
registry.increment("http.server.errors", 1, { method: ctx.req.method });
throw error;
} finally {
active--;
registry.gauge("http.server.active_requests", active);
}
};
}
export interface WebVitalRecord {
name: "LCP" | "INP" | "CLS" | "FCP" | "TTFB";
value: number;
rating?: "good" | "needs-improvement" | "poor";
route?: string;
navigationType?: string;
}
export interface WebVitalsHandlerOptions {
registry?: MetricsRegistry;
onRecord?: (record: WebVitalRecord, request: Request) => void | Promise<void>;
maxBodyBytes?: number;
}
const VITAL_NAMES = new Set(["LCP", "INP", "CLS", "FCP", "TTFB"]);
export function createWebVitalsHandler(options: WebVitalsHandlerOptions = {}) {
const registry = options.registry ?? defaultMetrics;
const maxBodyBytes = options.maxBodyBytes ?? 8 * 1024;
return async (request: Request): Promise<Response> => {
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
if (request.headers.get("sec-fetch-site") === "cross-site") {
return new Response("Forbidden", { status: 403 });
}
const origin = request.headers.get("origin");
if (origin) {
try {
if (new URL(origin).origin !== new URL(request.url).origin) {
return new Response("Forbidden", { status: 403 });
}
} catch {
return new Response("Forbidden", { status: 403 });
}
}
const length = Number(request.headers.get("content-length") ?? "0");
if (length > maxBodyBytes) return new Response("Payload Too Large", { status: 413 });
let body: unknown;
try {
const text = await request.text();
if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {
return new Response("Payload Too Large", { status: 413 });
}
body = JSON.parse(text);
} catch {
return new Response("Invalid JSON", { status: 400 });
}
if (!body || typeof body !== "object") return new Response("Invalid metric", { status: 400 });
const raw = body as Record<string, unknown>;
if (
!VITAL_NAMES.has(String(raw.name)) ||
typeof raw.value !== "number" ||
!Number.isFinite(raw.value)
) {
return new Response("Invalid metric", { status: 400 });
}
const record: WebVitalRecord = {
name: raw.name as WebVitalRecord["name"],
value: raw.value,
...(typeof raw.rating === "string" ? { rating: raw.rating as WebVitalRecord["rating"] } : {}),
...(typeof raw.route === "string" ? { route: raw.route.slice(0, 256) } : {}),
...(typeof raw.navigationType === "string"
? { navigationType: raw.navigationType.slice(0, 64) }
: {}),
};
registry.observe(`web.vitals.${record.name.toLowerCase()}`, record.value, {
rating: record.rating ?? "unknown",
route: record.route ?? "unknown",
});
await options.onRecord?.(record, request);
return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
};
}
export interface MetricExporter {
export(points: readonly MetricPoint[]): Promise<void> | void;
}
export function createHttpMetricExporter(
endpoint: string,
options: { headers?: HeadersInit; fetch?: typeof fetch } = {},
): MetricExporter {
const send = options.fetch ?? fetch;
return {
async export(points) {
const response = await send(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({ resourceMetrics: points }),
});
if (!response.ok) throw new Error(`Metric export failed with ${response.status}.`);
},
};
}
export const defaultMetrics = new MetricsRegistry();