import { DeliveryReadinessError } from "./delivery-readiness.js"; const RECORDING_READINESS_TIMEOUT_MS = 10_000; const DEFAULT_CANARY_BYTES = 1_024; const MAX_CANARY_BYTES = 10_000_000; const DEFAULT_MEASUREMENT_SAMPLES = 3; export interface RecordingReadinessConfig { sessionId: string; ingestUrl: string; appId?: string; } export interface RecordingDeliveryMeasurementConfig extends RecordingReadinessConfig { /** Representative bytes produced by the enabled recorders in one workload window. */ payloadBytes: number; /** Maximum acceptable upload completion time for the representative payload. */ completionWindowMs: number; /** Number of scored uploads. Default: 3; bounded to 1..5. */ samples?: number; /** Injectable monotonic clock for deterministic tests. */ now?: () => number; } export interface RecordingDeliveryMeasurement { mode: "direct" | "segments" | "post"; payloadBytes: number; completionWindowMs: number; sampleDurationsMs: number[]; medianCompletionMs: number; } interface DirectCanary { canaryId: string; uploadId: string; url: string; expectedBytes: number; } /** Verify the server-selected recording path. No host-facing controls exist. */ export async function verifyRecordingDelivery( config: RecordingReadinessConfig, ): Promise<"direct" | "segments" | "post"> { if (typeof fetch === "undefined") { throw new DeliveryReadinessError( "upload-config-unreachable", "The recording upload configuration could not be requested.", ); } const baseUrl = originOf(config.ingestUrl); if (!baseUrl) { throw new DeliveryReadinessError( "upload-config-invalid", "The ingest URL does not provide a valid recording upload origin.", ); } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), RECORDING_READINESS_TIMEOUT_MS); let phase: "config" | "media" = "config"; try { const mode = await resolveUploadMode(baseUrl, controller.signal); phase = "media"; if (mode === "direct" || mode === "segments") { await verifyDirectPath(baseUrl, config, DEFAULT_CANARY_BYTES, controller.signal, defaultNow); } else { await verifyPostPath(baseUrl, config, DEFAULT_CANARY_BYTES, controller.signal, defaultNow); } return mode; } catch (error) { if (error instanceof DeliveryReadinessError) throw error; if (controller.signal.aborted) { throw new DeliveryReadinessError( phase === "config" ? "upload-config-timeout" : "media-storage-timeout", phase === "config" ? "Timed out while resolving the recording upload path." : "Timed out while verifying recording storage.", { cause: error }, ); } throw new DeliveryReadinessError( phase === "config" ? "upload-config-unreachable" : "media-storage-unreachable", phase === "config" ? "The recording upload configuration could not be reached." : "The recording storage path could not be reached.", { cause: error }, ); } finally { clearTimeout(timeout); } } /** * Measure whether the real recording destination can absorb the configured * media workload. A disposable warm-up is excluded, then the median of the * scored uploads is compared with the workload completion window. */ export async function measureRecordingDeliveryReadiness( config: RecordingDeliveryMeasurementConfig, ): Promise { const payloadBytes = normaliseCanaryBytes(config.payloadBytes); const completionWindowMs = normaliseCompletionWindow(config.completionWindowMs); const sampleCount = Math.min( 5, Math.max(1, Math.floor(config.samples ?? DEFAULT_MEASUREMENT_SAMPLES)), ); const now = config.now ?? defaultNow; const baseUrl = originOf(config.ingestUrl); if (!baseUrl) { throw new DeliveryReadinessError( "upload-config-invalid", "The ingest URL does not provide a valid recording upload origin.", ); } const mode = await withReadinessTimeout(RECORDING_READINESS_TIMEOUT_MS, "config", (signal) => resolveUploadMode(baseUrl, signal), ); // Warm the exact storage/CORS path, but never use a cold request to decide // candidate eligibility. A failed warm-up is also excluded; the scored // attempts below provide the repeated evidence used for the decision. await runPathProbe(baseUrl, config, mode, DEFAULT_CANARY_BYTES, now).catch(() => undefined); const durations: number[] = []; let lastError: unknown; for (let attempt = 0; attempt < sampleCount; attempt += 1) { try { durations.push(await runPathProbe(baseUrl, config, mode, payloadBytes, now)); } catch (error) { lastError = error; } } const requiredSuccesses = Math.ceil(sampleCount / 2); if (durations.length < requiredSuccesses) { const details = measurementDetails(mode, payloadBytes, completionWindowMs, durations.length); if (lastError instanceof DeliveryReadinessError) { throw new DeliveryReadinessError(lastError.code, lastError.message, { cause: lastError, ...(lastError.status !== undefined ? { status: lastError.status } : {}), details, }); } throw new DeliveryReadinessError( "media-storage-unreachable", "The recording storage path could not complete enough readiness uploads.", { cause: lastError, details }, ); } const medianCompletionMs = median(durations); if (medianCompletionMs > completionWindowMs) { throw new DeliveryReadinessError( "media-storage-too-slow", `The representative recording payload took ${Math.round(medianCompletionMs)} ms; the workload window is ${Math.round(completionWindowMs)} ms.`, { details: measurementDetails( mode, payloadBytes, completionWindowMs, durations.length, medianCompletionMs, ), }, ); } return { mode, payloadBytes, completionWindowMs, sampleDurationsMs: durations.map((duration) => Math.round(duration)), medianCompletionMs: Math.round(medianCompletionMs), }; } async function resolveUploadMode( baseUrl: string, signal: AbortSignal, ): Promise<"direct" | "segments" | "post"> { const response = await fetch(`${baseUrl}/public/upload-config`, { method: "GET", credentials: "omit", cache: "no-store", signal, }); if (!response.ok) { throw new DeliveryReadinessError( response.status >= 500 ? "upload-config-unreachable" : "upload-config-invalid", `The recording upload configuration returned HTTP ${response.status}.`, { status: response.status }, ); } let body: { recordingUpload?: unknown }; try { body = (await response.json()) as { recordingUpload?: unknown }; } catch (error) { throw new DeliveryReadinessError( "upload-config-invalid", "The recording upload configuration was not valid JSON.", { cause: error }, ); } if ( body.recordingUpload !== "direct" && body.recordingUpload !== "segments" && body.recordingUpload !== "post" ) { throw new DeliveryReadinessError( "upload-config-invalid", "The recording upload configuration did not select a supported path.", ); } return body.recordingUpload; } async function verifyDirectPath( baseUrl: string, config: RecordingReadinessConfig, canaryBytes: number, signal: AbortSignal, now: () => number, ): Promise { let canary: DirectCanary | null = null; try { canary = await postJson( `${baseUrl}/uploads/readiness/direct/start`, { sessionId: config.sessionId, bytes: canaryBytes }, config.appId, signal, ); if ( !canary.canaryId || !canary.uploadId || !canary.url || canary.expectedBytes !== canaryBytes ) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording storage canary did not return an upload target.", ); } const uploadStartedAt = now(); const put = await fetch(canary.url, { method: "PUT", body: new Blob([new Uint8Array(canaryBytes)], { type: "application/octet-stream", }), credentials: "omit", signal, }); const uploadDurationMs = Math.max(0.1, now() - uploadStartedAt); if (!put.ok) { throw mediaHttpError(put.status, "The recording storage rejected the canary upload."); } const completed = await postJson<{ ok?: unknown; receivedBytes?: unknown }>( `${baseUrl}/uploads/readiness/direct/complete`, { sessionId: config.sessionId, canaryId: canary.canaryId, uploadId: canary.uploadId, expectedBytes: canaryBytes, }, config.appId, signal, ); if (completed.ok !== true || Number(completed.receivedBytes) !== canaryBytes) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording storage did not acknowledge the complete canary.", ); } canary = null; return uploadDurationMs; } finally { if (canary) void abortDirectCanary(baseUrl, config, canary); } } async function verifyPostPath( baseUrl: string, config: RecordingReadinessConfig, canaryBytes: number, signal: AbortSignal, now: () => number, ): Promise { const headers: Record = { "content-type": "application/octet-stream", }; if (config.appId) headers["x-app-id"] = config.appId; const uploadStartedAt = now(); const response = await fetch( `${baseUrl}/uploads/readiness/post/${encodeURIComponent(config.sessionId)}`, { method: "POST", headers, body: new Blob([new Uint8Array(canaryBytes)], { type: "application/octet-stream", }), credentials: "omit", signal, }, ); const uploadDurationMs = Math.max(0.1, now() - uploadStartedAt); if (!response.ok) { throw mediaHttpError(response.status, "The recording relay rejected the canary upload."); } let body: { ok?: unknown; receivedBytes?: unknown }; try { body = (await response.json()) as typeof body; } catch (error) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording relay returned an invalid acknowledgement.", { cause: error }, ); } if (body.ok !== true || Number(body.receivedBytes) !== canaryBytes) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording relay did not acknowledge the complete canary.", ); } return uploadDurationMs; } async function postJson( url: string, body: Record, appId: string | undefined, signal: AbortSignal, ): Promise { const headers: Record = { "content-type": "application/json" }; if (appId) headers["x-app-id"] = appId; const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), credentials: "omit", signal, }); if (!response.ok) { throw mediaHttpError(response.status, `Recording storage returned HTTP ${response.status}.`); } try { return (await response.json()) as T; } catch (error) { throw new DeliveryReadinessError( "media-storage-invalid-response", "Recording storage returned an invalid acknowledgement.", { cause: error }, ); } } async function abortDirectCanary( baseUrl: string, config: RecordingReadinessConfig, canary: DirectCanary, ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 2_000); try { await postJson( `${baseUrl}/uploads/readiness/direct/abort`, { sessionId: config.sessionId, canaryId: canary.canaryId, uploadId: canary.uploadId, expectedBytes: canary.expectedBytes, }, config.appId, controller.signal, ); } catch { // Cleanup is best-effort. Bucket lifecycle rules remove abandoned parts. } finally { clearTimeout(timeout); } } async function runPathProbe( baseUrl: string, config: RecordingReadinessConfig, mode: "direct" | "segments" | "post", bytes: number, now: () => number, ): Promise { return withReadinessTimeout(RECORDING_READINESS_TIMEOUT_MS, "media", (signal) => mode === "direct" || mode === "segments" ? verifyDirectPath(baseUrl, config, bytes, signal, now) : verifyPostPath(baseUrl, config, bytes, signal, now), ); } async function withReadinessTimeout( timeoutMs: number, phase: "config" | "media", run: (signal: AbortSignal) => Promise, ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await run(controller.signal); } catch (error) { if (error instanceof DeliveryReadinessError) throw error; if (controller.signal.aborted) { throw new DeliveryReadinessError( phase === "config" ? "upload-config-timeout" : "media-storage-timeout", phase === "config" ? "Timed out while resolving the recording upload path." : "Timed out while verifying recording storage.", { cause: error }, ); } throw new DeliveryReadinessError( phase === "config" ? "upload-config-unreachable" : "media-storage-unreachable", phase === "config" ? "The recording upload configuration could not be reached." : "The recording storage path could not be reached.", { cause: error }, ); } finally { clearTimeout(timeout); } } function normaliseCanaryBytes(value: number): number { if (!Number.isFinite(value) || value < 1) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording readiness payload size must be a positive number.", ); } return Math.min(MAX_CANARY_BYTES, Math.max(1, Math.round(value))); } function normaliseCompletionWindow(value: number): number { if (!Number.isFinite(value) || value <= 0) { throw new DeliveryReadinessError( "media-storage-invalid-response", "The recording readiness completion window must be positive.", ); } return Math.max(100, Math.round(value)); } function median(values: number[]): number { const sorted = [...values].sort((a, b) => a - b); const middle = Math.floor(sorted.length / 2); if (sorted.length % 2 === 1) return sorted[middle]!; return (sorted[middle - 1]! + sorted[middle]!) / 2; } function measurementDetails( mode: "direct" | "segments" | "post", payloadBytes: number, completionWindowMs: number, successfulSamples: number, medianCompletionMs?: number, ): Readonly> { return { recordingUploadMode: mode, readinessPayloadBytes: payloadBytes, readinessWindowMs: Math.round(completionWindowMs), readinessSampleCount: successfulSamples, ...(medianCompletionMs !== undefined ? { readinessMedianMs: Math.round(medianCompletionMs) } : {}), }; } function defaultNow(): number { return typeof performance !== "undefined" ? performance.now() : Date.now(); } function mediaHttpError(status: number, message: string): DeliveryReadinessError { return new DeliveryReadinessError( status >= 400 && status < 500 ? "media-storage-rejected" : "media-storage-unreachable", message, { status }, ); } function originOf(url: string): string | null { try { return new URL(url).origin; } catch { return null; } }