Skip to content

Commit

Permalink
fix: enhanced garbage collection on fetch aborts & error messaging
Browse files Browse the repository at this point in the history
  • Loading branch information
courcelan committed Jun 12, 2024
1 parent d84a9bf commit 14e97e3
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 143 deletions.
146 changes: 69 additions & 77 deletions lib/useButter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
BUTTER_CLIENT_VERSION
} from './constants.js';

import useCancelFetch from './utils/useCancelFetch.js';
import useFetchTimeout from './utils/useFetchTimeout.js';
import useFetchController from "./utils/useFetchController.js";
import useOnError from "./utils/useOnError.js"
import useOnRequest from './utils/useOnRequest.js';
import useOnResponse from './utils/useOnResponse.js';
Expand All @@ -17,54 +16,48 @@ import useOnResponse from './utils/useOnResponse.js';
* @param {Object} butterConfig - Configuration object for ButterCMS API requests.
* @returns {Object} An object containing methods to interact with the specified content type.
*/

export default function useButter (type, butterConfig) {
export default function useButter(type, butterConfig) {
const BASE_PATH = getBasePath(type);

const friendlyType = type.replace(type[0], type[0].toUpperCase());

/**
* Use the custom hook useCancelFetch to create a cancellation controller for fetch requests.
*
* @const {Object} cancelFetch - An object containing methods to cancel fetch requests.
* @property {Function} cancelRequest - A function to cancel the fetch request.
* @property {AbortController} fetchController - An instance of AbortController to signal cancellation of fetch requests.
*/

const {
applyRequestUrlForErrorMessages,
cancelRequest,
controller: fetchController,
} = useCancelFetch(friendlyType);

cleanup,
determineFetchError,
signal
} = useFetchController(friendlyType);

/**
* Fetches data from the ButterCMS API at the specified endpoint using provided parameters.
*
* @async
* @function get
* @function
* @param {string} [apiEndpoint=BASE_PATH] - The API endpoint to send the request to.
* @param {Object} [localParams={}] - Additional parameters to include in the request.
* @returns {Promise<Object>} An object containing the data, meta, and any errors from the API response.
* @throws Will throw an error object containing errors, config, and params if the API response includes errors.
*/
async function get (apiEndpoint = BASE_PATH, localParams = {}) {
// Extract apiToken and separate it from the rest of the user's configuration
async function get(apiEndpoint = BASE_PATH, localParams = {}) {
const {
apiToken: auth_token,
...userConfig
} = butterConfig;

// Initialize headers for the request

const butterHeaders = new Headers({
"Content-Type": "application/json",
"X-Butter-Client": `JS/${ BUTTER_CLIENT_VERSION }`
"X-Butter-Client": `JS/${BUTTER_CLIENT_VERSION}`
});

// Append additional header for server-side environments

if (typeof window === "undefined") {
butterHeaders.append("Accept-Encoding", "gzip");
}

// Prepare the request configuration using the onRequest hook

// Sets the error message prefix based on the API endpoint
// It is crucial for generating meaningful error messages for debugging.
applyRequestUrlForErrorMessages(apiEndpoint);

const {
config,
headers,
Expand All @@ -80,84 +73,75 @@ export default function useButter (type, butterConfig) {
type,
}
);

try {
// Set up fetch timeout and signal
const {
cleanup,
signal
} = useFetchTimeout(fetchController, friendlyType, config.timeout);
// use static abortSignal timeout functionality to relay a cancelation
// when timeout is reached
const timeoutSignal = AbortSignal.timeout(config.timeout);

// Perform the API request
const response = await fetch(
`${ apiEndpoint }?${ new URLSearchParams(params) }`,
`${apiEndpoint}?${new URLSearchParams(params)}`,
{
cache: config.cache,
method: "GET",
headers,
signal
// use either the above timeout o
// r the explicity AbortController cancelRequest to cancel request
signal: AbortSignal.any([signal, timeoutSignal])
}
);

// Clean up after the fetch timeout

cleanup();

// Check for errors in the response and throw if present

if (response.status !== 200) {
throw { response, config, params };
}
// Process the response using the onResponse hook
else {
// Return the successful response data and metadata
} else {
return await useOnResponse(
response,
{
config,
// pass the request as user won't see our
// changes for auth_token or testMode
// remove testMode params and auth_token in onResponse
params,
type
}
);
}
} catch (errorPayload) {

// Handle errors caught from the API request
}
/**
* Handles errors that occur during the fetch operation.
* If the error contains a response, it processes the JSON to extract the error detail,
* logs the error using `useOnError`, and rejects the promise with the extracted error detail.
* If the error does not contain a response, it determines the fetch error, logs it,
* performs cleanup, and rejects the promise with the determined error.
*
* @param {Object} errorPayload - The error object caught from the fetch operation.
* @returns {Promise<never>} A promise that always rejects with an error.
*/
catch (errorPayload) {
if (errorPayload.response) {
// Process errors using the onError hook with the attached config
const {
detail: errorDetail
} = await errorPayload.response.json();
useOnError(
} = await errorPayload.response.json();

useOnError(
errorDetail,
{
config: errorPayload.config,
params: errorPayload.params,
type
}
);

// Return null data and the errors

return Promise.reject(
new Error(
errorDetail
)
);
} else {
// Handle fetch abort errors
// this catches the AbortController, which has a reason of the cancellation
// if we do not throw/catch the error ourself,
// such timing out results in early exception
// if aborted, we get an early throw
// and update the error accordingly
const isFetchAbort = fetchController.signal.reason && fetchController.signal.aborted;
const reportableError = isFetchAbort
? fetchController.signal.reason
: errorPayload;

// Process the abort error using the onError hook
const reportableError = determineFetchError(
errorPayload,
config.timeout
);

useOnError(
reportableError,
{
Expand All @@ -166,7 +150,9 @@ export default function useButter (type, butterConfig) {
type
}
);


cleanup();

return Promise.reject(
new Error(
reportableError
Expand All @@ -180,26 +166,26 @@ export default function useButter (type, butterConfig) {
* Retrieves a list of items based on the provided options.
*
* @async
* @function list
* @function
* @param {Object} [options={}] - Additional options for the request.
* @returns {Promise<Object>} The result of the list operation.
*/
async function list (options = {}) {
async function list(options = {}) {
return await get(BASE_PATH, options);
}

/**
* Retrieves a specific item based on the provided slug and options.
*
* @async
* @function retrieve
* @function
* @param {string} slug - The unique identifier for the item to retrieve.
* @param {Object} [options={}] - Additional options for the request.
* @returns {Promise<Object>} The result of the retrieve operation.
*/
async function retrieve (slug, options) {
async function retrieve(slug, options) {
return await get(
`${ BASE_PATH }${ slug }/`,
`${BASE_PATH}${slug}/`,
options
);
}
Expand All @@ -208,16 +194,16 @@ export default function useButter (type, butterConfig) {
* Performs a search with the given query and options.
*
* @async
* @function search
* @function
* @param {string} [query=""] - The search query string.
* @param {Object} [options={}] - Additional options for the request.
* @returns {Promise<Object>} The result of the search operation.
*/
async function search (query = "", options = {}) {
async function search(query = "", options = {}) {
options.query = query;

return await get(
`${ BASE_PATH }search/`,
`${BASE_PATH}search/`,
options
);
}
Expand All @@ -230,6 +216,12 @@ export default function useButter (type, butterConfig) {
};
}

function getBasePath (type) {
return `${ BASE_URL }/${ BASE_PATHS[type] }/`;
/**
* Constructs the base path for the specified content type.
*
* @param {string} type - The type of content to interact with.
* @returns {string} The base path for the specified content type.
*/
function getBasePath(type) {
return `${BASE_URL}/${BASE_PATHS[type]}/`;
}
34 changes: 0 additions & 34 deletions lib/utils/useCancelFetch.js

This file was deleted.

Loading

0 comments on commit 14e97e3

Please sign in to comment.