3axap4eHko/apidly

View on GitHub
src/createClient.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import createEvent from 'evnty';
import {
  ClientOptions,
  RequestOptions,
  ApidlyRequest,
  ApidlyResponse,
  RequestMiddleware,
  ResponseMiddleware,
  Events,
  Middlewares,
  MiddleWired,
  EventWired,
  RequestType,
  ResponseType,
  RetryStrategy,
  Callbable,
  Compile,
} from './types';
import { Endpoint } from './createEndpoint';
import { jsonRequest, jsonResponse } from './dataTypes';
import { pickFirstOption, sanitize, defaultRetryStrategy, retry, compile } from './utils';

interface RequestInitOptions<Output, Params, Data> extends ClientOptions<Params>, Middlewares<Output, Params, Data>, Events<Output, Params, Data> {
  compileBase: ReturnType<Compile>;
}

const request = async <Output, Params, Data>(
  init: RequestInitOptions<Output, Params, Data>,
  endpoint: Endpoint<Output, Params, Data>,
  options: RequestOptions<Output, Params, Data> = {}
) => {
  type RequestOptionsType = RequestOptions<Output, Partial<Params>, Partial<Data>>;
  const requestType = pickFirstOption<RequestOptionsType, RequestType<Params, Data>>('requestType', jsonRequest, options, endpoint.options, init as any);
  const responseType = pickFirstOption<RequestOptionsType, ResponseType<Output, Params, Data>>(
    'responseType',
    jsonResponse,
    options,
    endpoint.options,
    init as any
  );
  const maxRetries = pickFirstOption<RequestOptionsType, number>('maxRetries', 0, options, endpoint.options, init as any);
  const retryStrategy = pickFirstOption<RequestOptionsType, RetryStrategy>('retryStrategy', defaultRetryStrategy, options, endpoint.options, init as any);
  const cache = pickFirstOption<RequestOptionsType, RequestCache>('cache', void 0, options, endpoint.options, init as any);
  const credentials = pickFirstOption<RequestOptionsType, RequestCredentials>('credentials', void 0, options, endpoint.options, init as any);
  const integrity = pickFirstOption<RequestOptionsType, string>('integrity', void 0, options, endpoint.options, init as any);
  const keepalive = pickFirstOption<RequestOptionsType, boolean>('keepalive', void 0, options, endpoint.options, init as any);
  const method = pickFirstOption<RequestOptionsType, string>('method', 'get', options, endpoint.options, init as any);
  const mode = pickFirstOption<RequestOptionsType, RequestMode>('mode', void 0, options, endpoint.options, init as any);
  const redirect = pickFirstOption<RequestOptionsType, RequestRedirect>('redirect', void 0, options, endpoint.options, init as any);
  const referrer = pickFirstOption<RequestOptionsType, string>('referrer', void 0, options, endpoint.options, init as any);
  const referrerPolicy = pickFirstOption<RequestOptionsType, ReferrerPolicy>('referrerPolicy', void 0, options, endpoint.options, init as any);
  const signal = pickFirstOption<RequestOptionsType, AbortSignal | null>('signal', void 0, options, endpoint.options, init as any);
  const window = pickFirstOption<RequestOptionsType, any>('window', void 0, options, endpoint.options, init as any);

  const headers = new Headers();
  for (const headersInit of [init.headers, endpoint.options.headers, options.headers].filter(Boolean)) {
    new Headers(headersInit).forEach((value, key) => {
      headers.set(key, value);
    });
  }

  const dataList = [endpoint.options.data, options.data];
  const data = dataList.some((d) => typeof d !== 'undefined') ? ({ ...endpoint.options.data, ...options.data } as Data) : void 0;

  const params = { ...init.params, ...endpoint.options.params, ...options.params } as Params;
  const pathname = endpoint.compilePath(params);
  const base = init.compileBase(params);

  const url = new URL(pathname, base);
  url.pathname = url.pathname.replace('//', '/');
  for (const [key, value] of url.searchParams) {
    if (value === '') {
      url.searchParams.delete(key);
    }
  }

  const request = sanitize<ApidlyRequest<Params, Data>>({
    requestType,
    responseType,
    maxRetries,
    retryStrategy,
    data,
    params,
    cache,
    credentials,
    headers,
    integrity,
    keepalive,
    method,
    mode,
    redirect,
    referrer,
    referrerPolicy,
    signal,
    window,
  });

  if (request.data) {
    await requestType(url, request);
  }

  await init.start(url, request);

  try {
    return await retry(
      async () => {
        for (const requestMiddleware of [].concat(init.request, endpoint.middlewares.request)) {
          await requestMiddleware(url, request);
        }
        const response = (await fetch(url.href, request)) as ApidlyResponse<Output>;
        response.data = await responseType(response, url, request);
        for (const responseMiddleware of [].concat(init.response, endpoint.middlewares.response)) {
          await responseMiddleware(response, url, request);
        }
        await init.success(response, url, request);

        return response.data;
      },
      retryStrategy,
      maxRetries
    );
  } catch (e: any) {
    await init.error(e, url, request);
    throw e;
  } finally {
    await init.done(url, request);
  }
};

export interface Client<ClientParams> extends MiddleWired<unknown, ClientParams, unknown>, EventWired<unknown, ClientParams, unknown> {
  <Output, Params, Data>(endpoint: Endpoint<Output, Params, Data>, options?: RequestOptions<Output, Params & Partial<ClientParams>, Data>): Promise<Output>;
}
export class Client<ClientParams = unknown> extends Callbable {
  private requestInit: RequestInitOptions<unknown, ClientParams, unknown>;

  constructor(clientOptions: ClientOptions<ClientParams>) {
    const requestInit: RequestInitOptions<unknown, ClientParams, unknown> = {
      ...clientOptions,
      start: createEvent(),
      success: createEvent(),
      error: createEvent(),
      done: createEvent(),
      request: [] as RequestMiddleware<ClientParams, unknown>[],
      response: [] as ResponseMiddleware<unknown, ClientParams, unknown>[],
      compileBase: compile(clientOptions.base),
    };

    super(request.bind(null, requestInit));

    this.requestInit = requestInit;
  }
  request(middleware: RequestMiddleware<ClientParams, unknown>) {
    this.requestInit.request.push(middleware);

    return this;
  }
  response(middleware: ResponseMiddleware<unknown, ClientParams, unknown>) {
    this.requestInit.response.push(middleware);

    return this;
  }
  onStart(listener: (url: URL, request: ApidlyRequest<ClientParams, unknown>) => any) {
    return this.requestInit.start.on(listener);
  }
  onSuccess(listener: (response: ApidlyResponse<unknown>, url: URL, request: ApidlyRequest<ClientParams, unknown>) => any) {
    return this.requestInit.success.on(listener);
  }
  onDone(listener: (url: URL, request: ApidlyRequest<ClientParams, unknown>) => any) {
    return this.requestInit.done.on(listener);
  }
  onError(listener: (error: Error, url: URL, request: ApidlyRequest<ClientParams, unknown>) => any) {
    return this.requestInit.error.on(listener);
  }
}

export default <ClientParams>(clientOptions: ClientOptions<ClientParams>) => {
  return new Client<ClientParams>(clientOptions);
};