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;
|