increments/request-js

View on GitHub
src/index.ts

Summary

Maintainability
B
4 hrs
Test Coverage
export interface Headers {
  [key: string]: string
}

export type Param = string | number | Date | null

export interface Params {
  [key: string]: Param | Param[]
}

export interface Config {
  data?: any // `data` is the data to be sent as the request body.
  headers?: Headers // `headers` are custom headers to be sent.
  params?: Params // `params` are the URL parameters to be sent with the request.
  timeout?: number // `timeout` specifies the number of milliseconds before the request times out.
}

const defaultHeaders: Headers = {}

export function setDefaultHeaders(headers: { [key: string]: string }) {
  Object.keys(headers).forEach(key => {
    defaultHeaders[key] = headers[key]
  })
}

export class Response<T, S> {
  public data: T
  public status: number
  public statusText: string
  public headers: Headers
  public config: Config
  public request: XMLHttpRequest
  public isSuccess: S

  constructor(xhr: XMLHttpRequest, config: Config, isSuccess: S) {
    this.data = this.parseResponse(xhr.response)
    this.status = xhr.status
    this.statusText = xhr.statusText
    this.headers = this.parseHeaders(xhr.getAllResponseHeaders().split("\n"))
    this.request = xhr
    this.config = config
    this.isSuccess = isSuccess
  }

  private parseResponse(data: any) {
    if (typeof data === "string") {
      try {
        data = JSON.parse(data)
      } catch (e) {
        /* Ignore */
      }
    }
    return data
  }

  private parseHeaders(lines: string[]): Headers {
    return lines.reduce((headers: Headers, line) => {
      const kv = line.split(":", 2)
      const key = kv[0].trim().toLocaleLowerCase()
      const value = kv[1] ? kv[1].trim() : ""
      if (key) {
        headers[key] = headers[key] ? `${headers[key]}, ${value}` : value
      }
      return headers
    }, {})
  }
}

class RequestError extends Error {
  public config: Config
  public code: string | null | undefined
  public request: XMLHttpRequest

  constructor(
    message: string,
    config: Config,
    code: string | null | undefined,
    req: XMLHttpRequest,
  ) {
    super(message)
    this.config = config
    this.code = code
    this.request = req
  }
}

// Bulid a URL by appending params to the end.
export function buildUrl(url: string, params: Params): string {
  const parts: string[] = []
  for (let key in params) {
    if (params.hasOwnProperty(key)) {
      let value = params[key]
      if (Array.isArray(value)) {
        key += "[]"
      } else {
        value = [value]
      }
      for (let v of value) {
        if (v == null) {
          continue
        } else if (v instanceof Date) {
          v = v.toISOString()
        }
        parts.push(`${key}=${encodeURIComponent(v.toString())}`)
      }
    }
  }
  return parts.length
    ? url + (url.indexOf("?") === -1 ? "?" : "&") + parts.join("&")
    : url
}

export function request<T = any, F = any>(
  method: "GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH",
  url: string,
  config: Config = {},
): Promise<Response<T, true> | Response<F, false>> {
  return new Promise((resolve, reject) => {
    let xhr: XMLHttpRequest | null = new XMLHttpRequest()

    const headers: { [key: string]: string } = {
      ...defaultHeaders,
      ...config.headers,
    }

    xhr.open(method, config.params ? buildUrl(url, config.params) : url, true)
    xhr.onload = () => {
      if (xhr) {
        resolve(new Response(
          xhr,
          config,
          200 <= xhr.status && xhr.status < 300,
        ) as any)
        xhr = null // Clean up to fix circular reference in order to avoid memory leak
      }
    }
    xhr.onerror = () => {
      if (xhr) {
        reject(new RequestError("Network Error", config, null, xhr))
        xhr = null
      }
    }
    xhr.ontimeout = () => {
      if (xhr && config.timeout) {
        reject(
          new RequestError(
            `Timeout of ${config.timeout} ms exceeded`,
            config,
            "ECONNABORTED",
            xhr,
          ),
        )
        xhr = null
      }
    }
    for (const key in headers) {
      if (headers.hasOwnProperty(key)) {
        xhr.setRequestHeader(key, headers[key])
      }
    }
    if (config.timeout) {
      xhr.timeout = config.timeout
    }

    let configData = config.data
    if (
      typeof configData === "object" &&
      headers["Content-Type"] === "application/json;charset=UTF-8"
    ) {
      configData = JSON.stringify(configData)
    }

    xhr.send(configData !== undefined ? configData : null)
  })
}