UNPKG

react-native-update

Version:
172 lines (149 loc) 4.05 kB
export interface EndpointAttemptSuccess<T> { endpoint: string; value: T; duration: number; } export interface EndpointAttemptFailure { endpoint: string; error: Error; } export interface ExecuteEndpointFallbackOptions<T> { configuredEndpoints: string[]; getRemoteEndpoints?: () => Promise<string[]>; tryEndpoint: (endpoint: string) => Promise<T>; random?: () => number; now?: () => number; onFirstFailure?: (failure: EndpointAttemptFailure) => void | Promise<void>; } const normalizeError = (error: unknown) => { if (error instanceof Error) { return error; } return new Error(String(error)); }; export const dedupeEndpoints = ( endpoints: Array<string | null | undefined>, ): string[] => { const result: string[] = []; const visited = new Set<string>(); for (const endpoint of endpoints) { if (!endpoint || visited.has(endpoint)) { continue; } visited.add(endpoint); result.push(endpoint); } return result; }; export const pickRandomEndpoint = ( endpoints: string[], random: () => number = Math.random, ) => { if (!endpoints.length) { throw new Error('No endpoints configured'); } return endpoints[Math.floor(random() * endpoints.length)]; }; export async function selectFastestSuccessfulEndpoint<T>( endpoints: string[], tryEndpoint: (endpoint: string) => Promise<T>, now: () => number = Date.now, ): Promise<{ successes: EndpointAttemptSuccess<T>[]; failures: EndpointAttemptFailure[]; }> { const attempts = await Promise.all( endpoints.map(async endpoint => { const start = now(); try { const value = await tryEndpoint(endpoint); return { ok: true as const, endpoint, value, duration: now() - start, }; } catch (error) { return { ok: false as const, endpoint, error: normalizeError(error), }; } }), ); const successes: EndpointAttemptSuccess<T>[] = []; const failures: EndpointAttemptFailure[] = []; for (const attempt of attempts) { if (attempt.ok) { successes.push({ endpoint: attempt.endpoint, value: attempt.value, duration: attempt.duration, }); continue; } failures.push({ endpoint: attempt.endpoint, error: attempt.error, }); } successes.sort((left, right) => left.duration - right.duration); return { successes, failures, }; } export async function executeEndpointFallback<T>({ configuredEndpoints, getRemoteEndpoints, tryEndpoint, random = Math.random, now = Date.now, onFirstFailure, }: ExecuteEndpointFallbackOptions<T>): Promise<EndpointAttemptSuccess<T>> { const excludedEndpoints = new Set<string>(); let candidates = dedupeEndpoints(configuredEndpoints); if (!candidates.length) { throw new Error('No endpoints configured'); } const firstEndpoint = pickRandomEndpoint(candidates, random); try { return { endpoint: firstEndpoint, value: await tryEndpoint(firstEndpoint), duration: 0, }; } catch (error) { const firstFailure = { endpoint: firstEndpoint, error: normalizeError(error), }; excludedEndpoints.add(firstEndpoint); await onFirstFailure?.(firstFailure); let lastError = firstFailure.error; while (true) { const remoteEndpoints = getRemoteEndpoints ? await getRemoteEndpoints().catch(() => []) : []; candidates = dedupeEndpoints([...candidates, ...remoteEndpoints]).filter( endpoint => !excludedEndpoints.has(endpoint), ); if (!candidates.length) { throw lastError; } const { successes, failures } = await selectFastestSuccessfulEndpoint( candidates, tryEndpoint, now, ); if (successes.length) { return successes[0]; } for (const failure of failures) { excludedEndpoints.add(failure.endpoint); lastError = failure.error; } } } }