{"version":3,"sources":["../src/sleep.ts","../src/http.ts"],"sourcesContent":["/**\n * Sleep for a given number of milliseconds. Note that this method is async,\n * so please remember to call it with await, like `await sleep(1000);`.\n */\nexport function sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","// ─────────────────────────────────────────────────────────────────────────────\n// Simplified HTTP fetch interface\n//\n// Inspired by axios, but having just the basic functionality, which seems\n// sufficient for the vast majority of fetch use cases.\n// ─────────────────────────────────────────────────────────────────────────────\n\nimport { sleep } from \"./sleep\";\n\nexport class HttpError extends Error {\n  constructor(\n    public status: number,\n    message?: string,\n  ) {\n    super(message ?? `HTTP ${status} error`);\n  }\n}\n\n/** Consumes the response body. */\nexport async function safeGetJson(\n  response: Response,\n): Promise<unknown | undefined> {\n  try {\n    return await response.json();\n  } catch {\n    return undefined;\n  }\n}\n\n/** Consumes the response text. */\nexport async function safeGetText(\n  response: Response,\n): Promise<string | undefined> {\n  try {\n    return await response.text();\n  } catch {\n    return undefined;\n  }\n}\n\n/** Consumes the response body. */\nexport async function safeGetErrorMessage(response: Response): Promise<string> {\n  const s = `HTTP ${response.status}: ${response.statusText}`;\n  const text = await safeGetText(response);\n\n  return s + (text ? `: text=${text}` : \"\");\n}\n\nexport async function throwIfError(response: Response): Promise<Response> {\n  if (!response.ok) {\n    throw new HttpError(response.status, await safeGetErrorMessage(response));\n  }\n\n  return response;\n}\n\nexport type HttpBodyInit = BodyInit | object | null;\n\nexport type RequestInitNoBody = Omit<RequestInit, \"body\">;\n\nexport type HttpRequestInit = RequestInitNoBody & {\n  body?: HttpBodyInit;\n};\n\nexport function normalizeBody(body: HttpBodyInit | undefined): {\n  body?: BodyInit | null;\n  headers?: Record<string, string>;\n} {\n  if (\n    body &&\n    typeof body === \"object\" &&\n    !(\n      body instanceof Blob ||\n      body instanceof ArrayBuffer ||\n      body instanceof FormData ||\n      body instanceof URLSearchParams ||\n      body instanceof ReadableStream ||\n      ArrayBuffer.isView(body)\n    )\n  ) {\n    return {\n      body: JSON.stringify(body),\n      headers: { \"Content-Type\": \"application/json\" },\n    };\n  }\n\n  return { body: body as BodyInit | null | undefined };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type HttpMiddleware = (\n  request: Request,\n  next: (req: Request) => Promise<Response>,\n) => Promise<Response>;\n\n/** Throw on non-OK responses. */\nexport const throwOnError: HttpMiddleware = async (request, next) => {\n  const response = await next(request);\n  return await throwIfError(response);\n};\n\n/**\n * Add retries. If you also use throwOnError, the sequence should be\n * `http.use(throwOnError).use(retries())` so that throwOnError runs outside of\n * the retry loops.\n */\nexport const retries: (options?: {\n  maxTries?: number;\n  delayMs?: number;\n  backoffMultiplier?: number;\n}) => HttpMiddleware = (options?: {\n  maxTries?: number;\n  delayMs?: number;\n  backoffMultiplier?: number;\n}) => {\n  const maxTries = options?.maxTries ?? 4;\n  const delayMs = options?.delayMs ?? 1000;\n  const backoffMultiplier = options?.backoffMultiplier ?? 1.5;\n\n  if (maxTries < 1) {\n    throw new Error(`Invalid maxTries=${maxTries}`);\n  }\n\n  return async (\n    request: Request,\n    next: (req: Request) => Promise<Response>,\n  ) => {\n    let ms = delayMs;\n\n    for (let i = 1; i <= maxTries; i++, ms *= backoffMultiplier) {\n      // Don't catch errors. Once thrown, the retry loop will end.\n      const response = await next(request.clone());\n\n      if (i < maxTries && response.status >= 500 && response.status <= 599) {\n        const jitterMs = (Math.random() - 0.5) * 200;\n        await sleep(ms + jitterMs);\n        continue;\n      }\n\n      return response;\n    }\n\n    throw new Error(`Unreachable code in retries`);\n  };\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class HttpFetch {\n  public constructor(private readonly middlewares: HttpMiddleware[] = []) {}\n\n  /** Add a middleware to the chain. */\n  public use(middleware: HttpMiddleware): HttpFetch {\n    return new HttpFetch([...this.middlewares, middleware]);\n  }\n\n  /**\n   * Main fetch method with middleware support. Supports auto-conversion of\n   * JSON bodies to JSON strings, and adding the JSON Content-Type.\n   */\n  public async fetch(\n    input: RequestInfo | URL,\n    init?: HttpRequestInit,\n  ): Promise<Response> {\n    // This applies to every request and hence is not built as a middleware.\n    const { body, headers } = normalizeBody(init?.body);\n\n    // The caller-specified headers will take priority.\n    const newHeaders = new Headers(headers);\n    const callerHeaders = new Headers(init?.headers);\n\n    for (const [k, v] of callerHeaders.entries()) {\n      newHeaders.set(k, v);\n    }\n\n    const newInit: RequestInit = {\n      ...init,\n      body,\n      headers: newHeaders,\n    };\n\n    const request = new Request(input, newInit);\n\n    // Build middleware chain from last to first (so first added runs first)\n    let handler = async (req: Request): Promise<Response> => {\n      return fetch(req);\n    };\n\n    // Apply middlewares in reverse order\n    for (let i = this.middlewares.length - 1; i >= 0; i--) {\n      const currentMiddleware = this.middlewares[i];\n      const nextHandler = handler;\n      handler = (req) => currentMiddleware(req, nextHandler);\n    }\n\n    return handler(request);\n  }\n\n  // Convenience methods\n  public async get(\n    input: RequestInfo | URL,\n    init?: HttpRequestInit,\n  ): Promise<Response> {\n    return this.fetch(input, init);\n  }\n\n  public async post(\n    input: RequestInfo | URL,\n    // A POST request should almost always have a body.\n    body?: HttpBodyInit,\n    // Don't allow specifying the body in the RequestInit.\n    init?: RequestInitNoBody,\n  ): Promise<Response> {\n    return this.fetch(input, { ...init, method: \"POST\", body });\n  }\n\n  public async put(\n    input: RequestInfo | URL,\n    // A PUT request should almost always have a body.\n    body?: HttpBodyInit,\n    // Don't allow specifying the body in the RequestInit.\n    init?: RequestInitNoBody,\n  ): Promise<Response> {\n    return this.fetch(input, { ...init, method: \"PUT\", body });\n  }\n\n  public async patch(\n    input: RequestInfo | URL,\n    // A PATCH request should almost always have a body.\n    body?: HttpBodyInit,\n    // Don't allow specifying the body in the RequestInit.\n    init?: RequestInitNoBody,\n  ): Promise<Response> {\n    return this.fetch(input, { ...init, method: \"PATCH\", body });\n  }\n\n  public async delete(\n    input: RequestInfo | URL,\n    init?: HttpRequestInit,\n  ): Promise<Response> {\n    return this.fetch(input, { ...init, method: \"DELETE\" });\n  }\n\n  public async head(\n    input: RequestInfo | URL,\n    init?: HttpRequestInit,\n  ): Promise<Response> {\n    return this.fetch(input, { ...init, method: \"HEAD\" });\n  }\n}\n\n/**\n * Standard http instance that can be used for most purposes.\n * - To add retries, use `http.use(retries())`\n * - To throw on errors, use `http.use(throwOnError)`\n * - To do both, use `http.use(throwOnError).use(retries())`\n */\nexport const http = new HttpFetch();\n"],"mappings":"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","names":["sleep","ms","resolve","HttpError","status","message","safeGetJson","response","safeGetText","safeGetErrorMessage","s","text","throwIfError","normalizeBody","body","throwOnError","request","next","retries","options","maxTries","delayMs","backoffMultiplier","ms","i","jitterMs","sleep","HttpFetch","_HttpFetch","middlewares","middleware","input","init","headers","newHeaders","callerHeaders","k","v","newInit","handler","req","currentMiddleware","nextHandler","http"]}