diff --git a/package.json b/package.json index 7b2ca37..a11ada3 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "dist" ], "dependencies": { - "axios": "^0.21.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-xml-parser": "^1.1.6" diff --git a/src/dymo_utils.js b/src/dymo_utils.js index 9bcf237..acba9e2 100644 --- a/src/dymo_utils.js +++ b/src/dymo_utils.js @@ -1,5 +1,5 @@ -import axios from "axios"; import XMLParser from "react-xml-parser"; +import { createFetchInstance, isAbortError } from "./fetchUtils"; import { WS_PROTOCOL, @@ -26,10 +26,10 @@ async function storeDymoRequestParams() { continue loop2; } try { - const response = await axios.get( - dymoUrlBuilder(WS_PROTOCOL, hostList[currentHostIndex], currentPort, WS_SVC_PATH, "status") - ); - const [successRequestHost, successRequestPort] = response.config.url.split("/")[2].split(":"); + const url = dymoUrlBuilder(WS_PROTOCOL, hostList[currentHostIndex], currentPort, WS_SVC_PATH, "status"); + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + const [successRequestHost, successRequestPort] = url.split("/")[2].split(":"); localStore("dymo-ws-request-params", {activeHost: successRequestHost, activePort: successRequestPort}); break loop1; } catch (error) {} @@ -42,48 +42,69 @@ export async function dymoRequestBuilder({ wsPath = WS_SVC_PATH, wsAction, method, - cancelToken, - axiosOtherParams = {}, + signal, + data, + headers = {}, }) { if (!localRetrieve("dymo-ws-request-params")) { await storeDymoRequestParams(); } const {activeHost, activePort} = localRetrieve("dymo-ws-request-params"); - const dymoAxiosInstance = axios.create(); - dymoAxiosInstance.interceptors.response.use( - function (response) { - return response; - }, - async function (error) { - if (axios.isCancel(error) || error?.response?.status === 500) { - return Promise.reject(error); + const dymoFetchInstance = createFetchInstance(); + + // Add response interceptor for retry logic + dymoFetchInstance.interceptors.response.push({ + onFulfilled: (response) => response, + onRejected: async (error) => { + if (isAbortError(error) || (error.response && error.response.status === 500)) { + throw error; } + await storeDymoRequestParams(); if (!localRetrieve("dymo-ws-request-params")) { - return Promise.reject(error); + throw error; } + try { const {activeHost, activePort} = localRetrieve("dymo-ws-request-params"); - const response = await axios.request({ - url: dymoUrlBuilder(wsProtocol, activeHost, activePort, wsPath, wsAction), - method, - cancelToken, - ...axiosOtherParams, - }); - return Promise.resolve(response); - } catch (error) { - return Promise.reject(error); + const response = await fetch( + dymoUrlBuilder(wsProtocol, activeHost, activePort, wsPath, wsAction), + { + method, + signal, + body: data, + headers + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response; + } catch (retryError) { + throw retryError; } } - ); - const request = await dymoAxiosInstance.request({ + }); + const response = await dymoFetchInstance.request({ url: dymoUrlBuilder(wsProtocol, activeHost, activePort, wsPath, wsAction), method, - cancelToken, - ...axiosOtherParams, + signal, + data, + headers }); - return request; + + // Parse response based on content type + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + response.data = await response.json(); + } else { + response.data = await response.text(); + } + + return response; } export function dymoUrlBuilder(wsProtocol, wsHost, wsPort, wsPath, wsAction) { @@ -126,10 +147,8 @@ export function printLabel(printerName, labelXml, labelSetXml) { return dymoRequestBuilder({ method: "POST", wsAction: "printLabel", - axiosOtherParams: { - data: `printerName=${encodeURIComponent(printerName)}&printParamsXml=&labelXml=${encodeURIComponent( - labelXml - )}&labelSetXml=${labelSetXml || ""}`, - }, + data: `printerName=${encodeURIComponent(printerName)}&printParamsXml=&labelXml=${encodeURIComponent( + labelXml + )}&labelSetXml=${labelSetXml || ""}`, }); } diff --git a/src/fetchUtils.js b/src/fetchUtils.js new file mode 100644 index 0000000..ce92902 --- /dev/null +++ b/src/fetchUtils.js @@ -0,0 +1,108 @@ +class FetchError extends Error { + constructor(message, response, isAborted = false) { + super(message); + this.name = 'FetchError'; + this.response = response; + this.isAborted = isAborted; + } +} + +export function isAbortError(error) { + return error instanceof FetchError && error.isAborted; +} + +export async function fetchWithRetry(url, options = {}, retryConfig = {}) { + const { maxRetries = 0, retryCondition, onRetry } = retryConfig; + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(url, options); + + if (!response.ok && (!retryCondition || retryCondition(response))) { + throw new FetchError(`HTTP error! status: ${response.status}`, response); + } + + return response; + } catch (error) { + if (error.name === 'AbortError') { + throw new FetchError('Request aborted', null, true); + } + + lastError = error; + + if (attempt < maxRetries && (!retryCondition || retryCondition(error.response))) { + if (onRetry) { + await onRetry(error, attempt); + } + continue; + } + + throw error; + } + } + + throw lastError; +} + +export function createFetchInstance() { + const interceptors = { + response: [] + }; + + return { + interceptors, + + async request(config) { + const { url, method = 'GET', data, headers = {}, signal, ...otherOptions } = config; + + const options = { + method, + headers: { ...headers }, + signal, + ...otherOptions + }; + + if (data) { + if (typeof data === 'string') { + options.body = data; + if (!options.headers['Content-Type']) { + options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + } else if (data instanceof FormData) { + options.body = data; + } else { + options.body = JSON.stringify(data); + if (!options.headers['Content-Type']) { + options.headers['Content-Type'] = 'application/json'; + } + } + } + + let response; + try { + response = await fetch(url, options); + } catch (error) { + if (error.name === 'AbortError') { + throw new FetchError('Request aborted', null, true); + } + throw error; + } + + // Run response interceptors + for (const interceptor of this.interceptors.response) { + try { + response = await interceptor.onFulfilled(response, config); + } catch (error) { + if (interceptor.onRejected) { + response = await interceptor.onRejected(error, config); + } else { + throw error; + } + } + } + + return response; + } + }; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index c4a9947..a39cd2c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import {useState, useEffect, useRef} from "react"; -import axios from "axios"; +import { isAbortError } from "./fetchUtils"; import {useData} from "./hooks"; import { @@ -16,27 +16,27 @@ export const printLabel = innerPrintSingleLabel; export function useDymoCheckService(port) { const [status, setStatus] = useState("initial"); - const tokenSource = useRef(); + const abortControllerRef = useRef(); useEffect(() => { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } - tokenSource.current = axios.CancelToken.source(); + abortControllerRef.current = new AbortController(); setStatus("loading"); - dymoRequestBuilder({method: "GET", wsAction: "status", cancelToken: tokenSource.current.token}) + dymoRequestBuilder({method: "GET", wsAction: "status", signal: abortControllerRef.current.signal}) .then(() => { - tokenSource.current = null; + abortControllerRef.current = null; setStatus("success"); }) .catch((error) => { - if (!axios.isCancel(error)) { + if (!isAbortError(error)) { setStatus("error"); } }); return () => { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } }; }, [port]); @@ -46,19 +46,19 @@ export function useDymoCheckService(port) { export function useDymoFetchPrinters(statusDymoService, modelPrinter = "LabelWriterPrinter", port) { const [data, setData] = useData({statusFetchPrinters: "initial", printers: [], error: null}); - const tokenSource = useRef(); + const abortControllerRef = useRef(); useEffect(() => { if (statusDymoService === "success") { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } - tokenSource.current = axios.CancelToken.source(); + abortControllerRef.current = new AbortController(); setData({statusFetchPrinters: "loading"}); - dymoRequestBuilder({method: "GET", wsAction: "getPrinters", cancelToken: tokenSource.current.token}) + dymoRequestBuilder({method: "GET", wsAction: "getPrinters", signal: abortControllerRef.current.signal}) .then((response) => { - tokenSource.current = null; + abortControllerRef.current = null; setData({ statusFetchPrinters: "success", printers: getDymoPrintersFromXml(response.data, modelPrinter), @@ -66,14 +66,14 @@ export function useDymoFetchPrinters(statusDymoService, modelPrinter = "LabelWri }); }) .catch((error) => { - if (!axios.isCancel(error)) { + if (!isAbortError(error)) { setData({statusFetchPrinters: "error", printers: [], error: error}); } }); } return () => { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } }; }, [modelPrinter, port, setData, statusDymoService]); @@ -83,35 +83,35 @@ export function useDymoFetchPrinters(statusDymoService, modelPrinter = "LabelWri export function useDymoOpenLabel(statusDymoService, labelXML, port) { const [data, setData] = useData({statusOpenLabel: "initial", label: null, error: null}); - const tokenSource = useRef(); + const abortControllerRef = useRef(); useEffect(() => { if (statusDymoService === "success") { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } - tokenSource.current = axios.CancelToken.source(); + abortControllerRef.current = new AbortController(); setData({statusOpenLabel: "loading"}); dymoRequestBuilder({ method: "POST", wsAction: "renderLabel", - cancelToken: tokenSource.current.token, - axiosOtherParams: {data: `labelXml=${encodeURIComponent(labelXML)}&renderParamsXml=&printerName=`}, + signal: abortControllerRef.current.signal, + data: `labelXml=${encodeURIComponent(labelXML)}&renderParamsXml=&printerName=`, headers: {"Access-Control-Request-Private-Network": true, "Access-Control-Allow-Origin": "*"}, }) .then((response) => { - tokenSource.current = null; + abortControllerRef.current = null; setData({statusOpenLabel: "success", label: response.data, error: null}); }) .catch((error) => { - if (!axios.isCancel(error)) { + if (!isAbortError(error)) { setData({statusOpenLabel: "error", label: null, error: error}); } }); } return () => { - if (tokenSource.current) { - tokenSource.current.cancel(); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } }; }, [statusDymoService, labelXML, setData, port]); diff --git a/src/storage.js b/src/storage.js index 0f55c05..7424a09 100644 --- a/src/storage.js +++ b/src/storage.js @@ -1,24 +1,18 @@ -function store(storage, key, data, timeout = null) { - const expiration = timeout && moment().add(timeout, "seconds").format(); - storage.setItem(key, JSON.stringify({data, expiration})); +function store(storage, key, data) { + storage.setItem(key, JSON.stringify({data})); } function retrieve(storage, key, data = null) { try { - const item = JSON.parse(storage.getItem(key)), - expired = !!item.expiration && moment(item.expiration) < moment(); - if (expired) { - storage.removeItem(key); - return data; - } + const item = JSON.parse(storage.getItem(key)); return item.data || data; } catch (err) { return data; } } -export function localStore(key, data, timeout = null) { - store(localStorage, key, data, timeout); +export function localStore(key, data) { + store(localStorage, key, data); } export function localRetrieve(key, data = null) { diff --git a/yarn.lock b/yarn.lock index d01a9ff..c6de62a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2215,13 +2215,6 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== -axios@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" - integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== - dependencies: - follow-redirects "^1.10.0" - axobject-query@^2.0.2: version "2.1.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.1.2.tgz#2bdffc0371e643e5f03ba99065d5179b9ca79799" @@ -4856,7 +4849,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.10.0: +follow-redirects@^1.0.0: version "1.13.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==