import { defu } from 'defu' import { createError, useFetch, useRuntimeConfig, type UseFetchOptions, } from 'nuxt/app' import { ERROR_SEPARATOR } from '#lib/constants' import { useUser } from '#lib/composables' import { getDeviceHeaders } from '#lib/utils' interface ErrorType { errors: Record } type FetchOptions = UseFetchOptions & { timeout?: number } /** * Custom composable to handle API requests with extended options and error handling. * * @param url - The API endpoint to fetch data from. Can be a string or a function returning a string. * @param userOptions - Custom options for the fetch request, including optional timeout. * @returns {Promise} A promise resolving with the fetch response or rejecting with an error. * @namespace */ export function useAPI( url: string | (() => string), userOptions: FetchOptions = {}, ) { const config = useRuntimeConfig() // Access runtime configuration. const { gpToken, token } = useUser() // Get gpToken from user composable. const controller = new AbortController() // Create a new AbortController for request timeout. // Set up a timeout to abort the request if it takes too long. const timeoutId = setTimeout(() => { controller.abort( createError({ statusCode: 408, // Request Timeout statusMessage: 'aborted', message: 'This request has been automatically aborted.', }), ) }, userOptions.timeout || 0) // Default to 0 if no timeout specified. // Default options for the fetch request. const defaultOptions: FetchOptions = { baseURL: `${config.public.API_URL}`, // Base URL from runtime config. method: 'GET', // Default HTTP method. retry: 3, // Number of retries on failure. signal: userOptions.timeout ? controller.signal : undefined, // Use signal for aborting if timeout is set. // Cache request key based on the URL. key: typeof url === 'string' ? url : url(), onRequest({ options }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(options.headers as any) = { ...options.headers, Accept: 'application/json', 'Content-type': 'application/json', ...(gpToken.value ? { gpToken: gpToken.value } : {}), // Add gpToken if available. ...(token.value ? { token: token.value } : {}), // Add gpToken if available. } const { os, device, browser } = getDeviceHeaders() // Get device headers. // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(options.headers as any) = { ...options.headers, os, device, browser, } }, onResponse({ response }) { const hasError = !response.status.toString().startsWith('2') || // Check if response status indicates an error. response._data?.status === 'error' if (hasError) { throw createError({ statusCode: response.status, statusMessage: response._data?.status?.toString(), message: response._data?.message || JSON.stringify(response._data?.errors), }) } }, onResponseError({ response }) { const statusCode = response.status || 500 // Default to 500 if status is undefined. const statusMessage = response.statusText || '' // Get status text from response. const errorsMsg = (response._data || {}) as ErrorType const errorEntries = Object.entries(errorsMsg.errors) // Construct a message from the error details. const message = errorEntries.reduce((acc: string[], [key, value]) => { return [...acc, ...value.map((item) => `${key} ${item}`)] }, []) throw createError({ statusCode, statusMessage, message: message.join(ERROR_SEPARATOR), }) }, } // Merge userOptions with defaultOptions, with userOptions taking precedence. const options = defu(userOptions, defaultOptions) as UseFetchOptions if (options.method !== 'GET') { options.watch = false // Disable re-fetching for non-GET requests. } // Perform the fetch request and handle cleanup. return useFetch(url, options).finally(() => { if (userOptions.timeout && timeoutId) { clearTimeout(timeoutId) // Clear the timeout if request completes or fails. } }) }