import type { JourneyState } from './types'; /** Per-endpoint request policy (app-wide or per-journey). Longest matching prefix wins. */ export interface EndpointPolicy { /** * Path/host prefix against the request key (`"/path"` same-origin, `"host/path"` cross-domain). * Match requires an exact hit or a `/` boundary after the prefix — `/api/user` matches * `/api/user` and `/api/user/42`, but not `/api/user-preferences`. A trailing `/` on * `match` already supplies the boundary. */ match: string; /** Custom slow-request threshold in ms; omit to fall through. `0` disables latency scoring for this prefix. */ slowRequestMs?: number; /** When true, this endpoint never changes the journey verdict (errors, latency, aborts, 4xx). */ ignore?: boolean; } /** App-wide slow-request threshold fallback (not an axios abort). */ export interface SlowRequestConfig { /** Fallback threshold when no endpoint or journey override applies. */ defaultMs: number; /** App-wide endpoint overrides; journey-specific ones belong on JourneyDef.endpoints. */ endpoints?: readonly EndpointPolicy[]; } export interface ResolvedEndpointPolicy { ignore: boolean; slowRequestMs: number; } /** Copy sorted longest-match-first so lookup can return on the first hit. */ export function sortEndpointsByLongestMatch( endpoints: readonly EndpointPolicy[] | undefined ): EndpointPolicy[] | undefined { if (endpoints == null) { return undefined; } if (endpoints.length <= 1) { return endpoints.length === 0 ? [] : [...endpoints]; } return [...endpoints].sort((a, b) => b.match.length - a.match.length); } /** * True when `key` equals `match` or continues past a path boundary. * `/api/user` matches `/api/user` and `/api/user/42`, not `/api/user-preferences`. */ function matchesEndpoint(key: string, match: string): boolean { if (!key.startsWith(match)) { return false; } if (match.endsWith('/')) { return true; } const next = key[match.length]; return next === undefined || next === '/'; } /** * Longest-prefix match of an endpoint list against a request key. * Expects `endpoints` sorted longest-match-first (see `sortEndpointsByLongestMatch`). */ export function longestEndpointMatch( endpoints: readonly EndpointPolicy[] | undefined, key: string ): EndpointPolicy | undefined { for (const e of endpoints ?? []) { if (matchesEndpoint(key, e.match)) { return e; } } return undefined; } /** * Resolve ignore + latency threshold with longest-prefix matching. * Journey overrides win over app defaults for slow thresholds. */ export function resolveEndpointPolicy( journey: JourneyState, appSlow: SlowRequestConfig, requestKey: string ): ResolvedEndpointPolicy { const own = longestEndpointMatch(journey.endpoints, requestKey); if (own?.ignore === true) { return { ignore: true, slowRequestMs: 0 }; } const app = longestEndpointMatch(appSlow.endpoints, requestKey); if (own) { return { ignore: false, slowRequestMs: own.slowRequestMs ?? app?.slowRequestMs ?? journey.slowRequestMs ?? appSlow.defaultMs, }; } if (app?.ignore === true) { return { ignore: true, slowRequestMs: 0 }; } return { ignore: false, slowRequestMs: app?.slowRequestMs ?? journey.slowRequestMs ?? appSlow.defaultMs, }; }