All files / hooks useParallelBatchFetch.js

64.58% Statements 31/48
38.23% Branches 13/34
58.82% Functions 10/17
63.63% Lines 28/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169                      27x 27x 27x       27x                     7x 7x 7x     7x   7x 7x                   7x       7x                         7x   7x       7x       7x 7x 7x                                                                                               7x 7x     7x 7x 7x     7x                               7x 7x   7x         7x                        
 
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries, useQuery } from 'react-query';
 
import { chunk } from 'lodash';
import { useOkapiKy } from '@folio/stripes/core';
import { generateKiwtQueryParams } from '../utils';
 
// A hook to do the same thing as batch fetching > 100 resources, but parallelising it.
 
// Only defining defaults to ward of "magic number" sonarlint rule -.-
const CONCURRENT_REQUESTS_DEFAULT = 5;
const MAX_BATCH_SIZE = 100;
const DEFAULT_BATCH_LIMIT = Infinity;
 
// CONCURRENT_REQUESTS and BATCH_SIZE can be tweaked here, but implementor beware
// They are formatted as if constants to discourage this
const useParallelBatchFetch = ({
  BATCH_LIMIT = DEFAULT_BATCH_LIMIT, // Number of resources to stop at, Infinity by default but can be set to a limit
  batchParams = {}, // Params object of the shape accepted by generateKiwtQueryParams
  nsValues = {},
  BATCH_SIZE = MAX_BATCH_SIZE, // Number of resources to fetch per batch
  CONCURRENT_REQUESTS = CONCURRENT_REQUESTS_DEFAULT, // Number of requests to make concurrently
  endpoint, // endpoint to hit to fetch items
  generateQueryKey, // Passed function to allow customised query keys
  queryNamespace = 'stripes-kint-components',
  queryOptions: passedQueryOptions = {}, // Options to pass to each query
}) => {
  const ky = useOkapiKy();
  const SAFE_BATCH_SIZE = Math.min(BATCH_SIZE, MAX_BATCH_SIZE);
  const [isLoading, setIsLoading] = useState(true);
 
  // Destructure passed query options to grab enabled
  const { enabled: queryEnabled = true, ...queryOptions } = passedQueryOptions;
 
  const paramsArray = useMemo(() => (
    generateKiwtQueryParams(
      {
        ...batchParams,
        perPage: SAFE_BATCH_SIZE,
        stats: true
      },
      nsValues
    )
  ), [batchParams, nsValues, SAFE_BATCH_SIZE]);
 
  const getDefaultNSArray = useCallback(
    (offset) => [queryNamespace, endpoint, offset, paramsArray, 'useChunkedBatchedFetch'],
    [queryNamespace, endpoint, paramsArray]
  );
  const namespaceArray = generateQueryKey ? generateQueryKey({
    CONCURRENT_REQUESTS,
    BATCH_LIMIT,
    SAFE_BATCH_SIZE,
    batchParams,
    endpoint,
    offset: 0,
    paramsArray,
    passedQueryOptions,
  }) : getDefaultNSArray(0);
 
 
  // Firstly fetch page 1 to get information about totals
  const firstFetchResult = useQuery(
    namespaceArray,
    () => ky.get(`${endpoint}?${[...paramsArray, 'offset=0']?.join('&')}`).json(),
    passedQueryOptions
  );
 
  const totalRecords = useMemo(() => firstFetchResult?.data?.total ?? 0, [firstFetchResult]);
 
  // WAIT for initial fetch to conclude before setting up queryArray
  // Set up query array, and only enable the first CONCURRENT_REQUESTS requests
  const getQueryArray = useCallback(() => {
    Eif (!firstFetchResult?.isFetched) {
      return [];
    }
 
    const recordsToFetch = Math.min(totalRecords, BATCH_LIMIT);
    // Have already fetched page 1
 
    const queryArray = [];
    // Offset will be i * SAFE_BATCH_SIZE
    for (let offset = SAFE_BATCH_SIZE; offset < recordsToFetch; offset += SAFE_BATCH_SIZE) {
      const queryKey = generateQueryKey ? generateQueryKey({
        CONCURRENT_REQUESTS,
        BATCH_LIMIT,
        SAFE_BATCH_SIZE,
        batchParams,
        endpoint,
        offset,
        paramsArray,
        passedQueryOptions,
      }) : getDefaultNSArray(offset);
 
      const paramString = [...paramsArray, `offset=${offset}`]?.join('&');
      queryArray.push({
        queryKey,
        queryFn: () => ky.get(`${endpoint}?${paramString}`).json(),
        // Only enable once the previous slice has all been fetched
        enabled: queryEnabled && offset / SAFE_BATCH_SIZE < CONCURRENT_REQUESTS,
        ...queryOptions
      });
    }
    return queryArray;
  }, [
    BATCH_LIMIT,
    batchParams,
    CONCURRENT_REQUESTS,
    endpoint,
    firstFetchResult,
    generateQueryKey,
    getDefaultNSArray,
    ky,
    paramsArray,
    passedQueryOptions,
    queryEnabled,
    queryOptions,
    SAFE_BATCH_SIZE,
    totalRecords
  ]);
 
  // Differentiate between chunked logic and the first return (Which includes the initial fetch)
  const itemQueries = useQueries(getQueryArray());
  const returnItemQueries = useMemo(() => [firstFetchResult, ...itemQueries], [firstFetchResult, itemQueries]);
 
  // Once chunk has finished fetching, fetch next chunk
  useEffect(() => {
    const chunkedQuery = chunk(returnItemQueries, CONCURRENT_REQUESTS);
    chunkedQuery.forEach((q, i) => {
      // Check that all previous chunk are fetched,
      // and that all of our current chunk are not fetched and not loading
      Iif (
        i !== 0 &&
        chunkedQuery[i - 1]?.every(pq => pq.isFetched === true) &&
        q.every(req => req.isFetched === false) &&
        q.every(req => req.isLoading === false)
      ) {
        // Trigger fetch for each request in the chunk
        q.forEach(req => {
          req.refetch();
        });
      }
    });
  }, [CONCURRENT_REQUESTS, returnItemQueries]);
 
  // Keep easy track of whether this hook is all loaded or not
  // (This slightly flattens the "isLoading/isFetched" distinction, but it's an ease of use prop)
  useEffect(() => {
    const newLoading = ((returnItemQueries?.length ?? 0) < 1 || returnItemQueries?.some(uq => !uq.isFetched));
 
    Iif (isLoading !== newLoading) {
      setIsLoading(newLoading);
    }
  }, [isLoading, returnItemQueries]);
 
  return {
    itemQueries: returnItemQueries,
    isLoading,
    // Offer all fetched orderLines in flattened array once ready
    items: isLoading ? [] : returnItemQueries?.reduce((acc, curr) => {
      return [...acc, ...(curr?.data?.results ?? [])];
    }, []),
    total: totalRecords
  };
};
 
export default useParallelBatchFetch;