Emdash source with visual editor image upload fix

Fixes:
1. media.ts: wrap placeholder generation in try-catch
2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
2026-05-03 10:44:54 +07:00
parent 78f81bebb6
commit 2d1be52177
2352 changed files with 662964 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
/**
* Perf probe Worker -- deployed per-region with placement hints.
* Receives measurement requests via service binding fetch(),
* runs the measurements from its placed location, returns results.
*/
import { measureRoutes } from "./measure.js";
import type { MeasureRequest, MeasureResponse } from "./measure.js";
export default {
async fetch(request: Request): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
try {
const body = await request.json<MeasureRequest & { region?: string }>();
const results = await measureRoutes(body);
const response: MeasureResponse = {
results,
probeRegion: body.region ?? "unknown",
};
return Response.json(response);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return Response.json({ error: message }, { status: 500 });
}
},
} satisfies ExportedHandler;

View File

@@ -0,0 +1,212 @@
/** Measurement logic -- runs inside the placed probe Worker. */
export interface MeasureRequest {
targetUrl: string;
routes: Array<{ path: string; label: string }>;
warmRequests: number;
}
/**
* Parsed Server-Timing header. Keyed by timing name. `desc` is optional.
* Example: { render: { dur: 42, desc: "Page render" }, mw: { dur: 58 } }
*/
export type ServerTimings = Record<string, { dur: number; desc?: string }>;
export interface RouteResult {
path: string;
label: string;
coldTtfbMs: number;
/**
* Median warm-request TTFB. Null if warmRequests was 0 and no warm
* samples were taken — caller should fall back to coldTtfbMs in that case.
*/
warmTtfbMs: number | null;
/** p95 warm-request TTFB. Null when no warm samples were taken. */
p95TtfbMs: number | null;
statusCode: number;
cfColo: string | null;
cfPlacement: string | null;
/** Parsed from the cold response. Null if header absent or unparseable. */
coldServerTimings: ServerTimings | null;
/**
* Median of each Server-Timing metric across all warm requests.
* Null if no warm responses carried the header or no warm requests
* were issued. Use this to isolate steady-state render/middleware/
* runtime cost, independent of cold-start.
*/
warmServerTimings: ServerTimings | null;
}
export interface MeasureResponse {
results: RouteResult[];
probeRegion: string;
}
/**
* Parse the Server-Timing response header.
*
* Grammar (RFC 8673 §2):
* Server-Timing: metric[;param]*[, metric[;param]*]*
* param = dur=<number> | desc="<string>" | desc=<token>
*
* We only extract `dur` and `desc` and silently skip malformed entries.
* Unknown params are ignored rather than rejected so future additions
* upstream don't cause us to drop data.
*/
export function parseServerTiming(header: string | null): ServerTimings | null {
if (!header) return null;
const out: ServerTimings = {};
for (const rawEntry of header.split(",")) {
const parts = rawEntry.split(";").map((p) => p.trim());
const name = parts[0];
if (!name) continue;
const entry: { dur: number; desc?: string } = { dur: 0 };
let sawDur = false;
for (const param of parts.slice(1)) {
const eq = param.indexOf("=");
if (eq === -1) continue;
const key = param.slice(0, eq).trim();
let value = param.slice(eq + 1).trim();
// desc may be quoted
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
if (key === "dur") {
const n = Number(value);
if (Number.isFinite(n)) {
entry.dur = n;
sawDur = true;
}
} else if (key === "desc") {
entry.desc = value;
}
}
if (sawDur) out[name] = entry;
}
return Object.keys(out).length > 0 ? out : null;
}
/**
* Measure TTFB for a single URL.
* Returns wall-clock time from fetch start to first byte (headers received).
*/
async function measureTtfb(url: string): Promise<{
ttfbMs: number;
statusCode: number;
cfColo: string | null;
cfPlacement: string | null;
serverTimings: ServerTimings | null;
}> {
const start = performance.now();
const response = await fetch(url, {
method: "GET",
headers: {
"User-Agent": "emdash-perf-probe/1.0",
// Bust any edge cache
"Cache-Control": "no-cache",
},
redirect: "follow",
});
const ttfbMs = performance.now() - start;
// Consume the body so the connection is properly released
await response.arrayBuffer();
// Extract cf-ray colo: format is "<ray-id>-<COLO>"
const cfRay = response.headers.get("cf-ray");
const cfColo = cfRay?.split("-").pop() ?? null;
const cfPlacement = response.headers.get("cf-placement");
const serverTimings = parseServerTiming(response.headers.get("server-timing"));
return { ttfbMs, statusCode: response.status, cfColo, cfPlacement, serverTimings };
}
/** Compute the median of an array. */
function median(values: number[]): number {
const sorted = values.toSorted((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[mid - 1]! + sorted[mid]!) / 2;
}
return sorted[mid]!;
}
/** Compute p95 of an array. */
function p95(values: number[]): number {
const sorted = values.toSorted((a, b) => a - b);
const idx = Math.ceil(sorted.length * 0.95) - 1;
return sorted[Math.max(0, idx)]!;
}
/**
* Run measurements for all routes.
* For each route: 1 cold request (cache-busted with unique query param),
* then N warm requests. Returns structured results.
*/
export async function measureRoutes(req: MeasureRequest): Promise<RouteResult[]> {
const results: RouteResult[] = [];
for (const route of req.routes) {
const url = `${req.targetUrl}${route.path}`;
// Cold request -- add a unique query param to avoid any isolate reuse
const coldUrl = url + (url.includes("?") ? "&" : "?") + `_perf_cold=${Date.now()}`;
const cold = await measureTtfb(coldUrl);
// Warm requests — keep per-metric samples so we can median each one.
const warmTimings: number[] = [];
const warmMetricSamples: Record<string, { durs: number[]; desc?: string }> = {};
let lastStatusCode = cold.statusCode;
for (let i = 0; i < req.warmRequests; i++) {
const warm = await measureTtfb(url);
warmTimings.push(warm.ttfbMs);
lastStatusCode = warm.statusCode;
if (warm.serverTimings) {
for (const [name, entry] of Object.entries(warm.serverTimings)) {
const acc = warmMetricSamples[name] ?? { durs: [], desc: entry.desc };
acc.durs.push(entry.dur);
if (!acc.desc && entry.desc) acc.desc = entry.desc;
warmMetricSamples[name] = acc;
}
}
}
// Collapse per-metric samples into medians so the stored shape
// mirrors coldServerTimings.
const warmServerTimings: ServerTimings | null = Object.keys(warmMetricSamples).length
? Object.fromEntries(
Object.entries(warmMetricSamples).map(([name, { durs, desc }]) => {
const entry: { dur: number; desc?: string } = {
dur: Math.round(median(durs) * 100) / 100,
};
if (desc) entry.desc = desc;
return [name, entry];
}),
)
: null;
// Handle the (uncommon) warmRequests=0 case: without warm samples,
// median/p95 would compute against an empty array and produce NaN.
// Report the cold TTFB in both slots so the row remains valid;
// warm timings are reported as null so downstream code knows there's
// no warm breakdown to render.
const hasWarm = warmTimings.length > 0;
const warmTtfbMs = hasWarm ? Math.round(median(warmTimings) * 100) / 100 : null;
const p95TtfbMs = hasWarm ? Math.round(p95(warmTimings) * 100) / 100 : null;
results.push({
path: route.path,
label: route.label,
coldTtfbMs: Math.round(cold.ttfbMs * 100) / 100,
warmTtfbMs,
p95TtfbMs,
statusCode: lastStatusCode,
cfColo: cold.cfColo,
cfPlacement: cold.cfPlacement,
coldServerTimings: cold.serverTimings,
warmServerTimings,
});
}
return results;
}