Home Manual Reference Source Repository

src/utils/helpers.js

/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2020, Mapcreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * Get all the pages from a {@link PaginatedResourceListing} or a range
 * @param {Promise<PaginatedResourceListing>|PaginatedResourceListing} page - Promise that returns a {@link PaginatedResourceListing}
 * @param {?Number} [start=1] - Start page
 * @param {?Number} [stop] - Stop page, defaults to the page count if not filled in.
 * @returns {Promise<Array<ResourceBase>>} - multiple pages
 * @throws {ApiError} - If the api returns errors
 * @example
 * import { helpers } from "@mapcreator/api";
 *
 * const promise = api.users.list(1, 50); // 50 per page is more efficient
 *
 * helpers.getPaginatedRange(promise).then(data => {
 *    data.map(row => `[${row.id}] ${row.name}`) // We just want the names
 *        .forEach(console.log) // Log the names and ids of every user
 * })
 */
export async function getPaginatedRange (page, start = 1, stop) {
  // Resolve promise if any
  if (page instanceof Promise) {
    page = await page;
  }

  const out = page.data;
  const promises = [];

  // Handle defaults
  start = start || page.page;
  stop = stop || page.pageCount;

  if (start === page.page) {
    start++;
  }

  // Get all pages
  for (let i = start; i <= stop; i++) {
    promises.push(page.get(i));
  }

  // Resolve
  const rows = await Promise.all(promises);

  return out.concat(...rows.map(x => x.data));
}

/**
 * Async delay
 * @private
 * @param {number} ms - milliseconds
 * @returns {Promise}
 */
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

/**
 * Wraps around ky to make it return cancelable requests
 * @param {function(*=, *=): Response} fn - ky instance
 * @returns {function(*=, *=): Response}
 * @private
 */
export function wrapKyCancelable (fn) {
  return (input, options = {}) => {
    if (typeof options === 'object' && options.hasOwnProperty('signal')) {
      return fn(input, options);
    }

    const controller = new AbortController();
    const promise = fn(input, { signal: controller.signal, ...options });

    promise.cancel = () => controller.abort();

    return promise;
  };
}

/**
 * @typedef {Promise} CancelablePromise
 * @property {function(): void} cancel - Cancel the promise
 */

/**
 * Makes a promise cancelable by passing it a signal
 * @param {function} fn - async method
 * @returns {CancelablePromise}
 * @private
 */
export function makeCancelable (fn) {
  const controller = new AbortController();

  const promise = fn(controller.signal);

  if (promise instanceof Promise) {
    promise.cancel = () => controller.abort();
  }

  return promise;
}

/**
 * Convert Date into server format
 * @param {Date} date - Target
 * @returns {String} - Formatted date
 * @private
 */
export function serializeUTCDate (date) {
  if (!(date instanceof Date)) {
    throw new TypeError('Expected date to be of type Date');
  }

  const pad = num => `00${num}`.slice(-Math.max(String(num).length, 2));

  let out = [
    date.getUTCFullYear(),
    date.getUTCMonth() + 1,
    date.getUTCDate(),
  ].map(pad).join('-');

  out += ` ${[
    date.getUTCHours(),
    date.getUTCMinutes(),
    date.getUTCSeconds(),
  ].map(pad).join(':')}`;

  return out;
}

export function clone (input, clonePrivate = true) {
  const _clone = value => clone(value, clonePrivate);

  if (typeof input !== 'object' || input === null) {
    return input;
  } else if (Array.isArray(input)) {
    return input.map(_clone);
  }

  const output = {};

  for (const key of Object.keys(input)) {
    if (!clonePrivate && key.startsWith('_')) {
      continue;
    }

    output[key] = _clone(input[key]);
  }

  return output;
}