From b11180892e21e71641dff253c27dc811a62d673f Mon Sep 17 00:00:00 2001 From: Cobey Potter Date: Mon, 12 Aug 2024 00:59:55 -0400 Subject: [PATCH] fix: update abortcontroller/abortSignal for use in Reactive Native no access to React Native, instead tested for existence of AbortSignal.timeout, if there, use it; otherwise, use a general timeout on the AbortController should fix #92 --- lib/useButter.js | 19 +++++++++++++++++-- lib/utils/useFetchController.js | 19 +++++++++++++++++-- tests/lib/butter.min.js | 2 +- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/useButter.js b/lib/useButter.js index 84daba4..a452c53 100644 --- a/lib/useButter.js +++ b/lib/useButter.js @@ -22,6 +22,7 @@ export default function useButter(type, butterConfig) { const friendlyType = type.replace(type[0], type[0].toUpperCase()); const { + abortOnTimeout, applyRequestUrlForErrorMessages, cancelRequest, cleanup, @@ -77,7 +78,13 @@ export default function useButter(type, butterConfig) { try { // use static abortSignal timeout functionality to relay a cancelation // when timeout is reached - const timeoutSignal = AbortSignal.timeout(config.timeout); + // if AbortSignal.timeout doestn't exist (React Native), use a regular timeotu + const timeoutSignal = AbortSignal.timeout + ? AbortSignal.timeout(config.timeout) + : setTimeout( + () => abortOnTimeout(config.timeout), + config.timeout + ) const response = await fetch( `${apiEndpoint}?${new URLSearchParams(params)}`, @@ -87,10 +94,18 @@ export default function useButter(type, butterConfig) { headers, // use either the above timeout or // the explicit AbortController cancelRequest to cancel request - signal: AbortSignal.any([signal, timeoutSignal]) + signal: AbortSignal.timeout + ? AbortSignal.any([signal, timeoutSignal]) + : signal } ); + if (!AbortSignal.timeout) { + // if we are running on a regular timout, + // clear it after its use + clearTimeout(timeoutSignal) + } + cleanup(); if (response.status !== 200) { diff --git a/lib/utils/useFetchController.js b/lib/utils/useFetchController.js index 4e35d30..b715676 100644 --- a/lib/utils/useFetchController.js +++ b/lib/utils/useFetchController.js @@ -47,14 +47,28 @@ export default function useFetchController(type) { * @function cancelRequest * @returns {Promise} A promise that resolves when the request has been aborted. */ - async function cancelRequest() { - await controller.abort( + function cancelRequest() { + controller.abort( generateErrorMessage("Request cancelled") ); return; } + /** + * Aborts the current request due to a timeout. + * + * @param {number} timeout - The timeout duration in milliseconds after which the request was aborted. + * @returns {void} + */ + function abortOnTimeout(timeout) { + controller.abort( + generateErrorMessage(`Request timed out after ${timeout}ms.`) + ); + + return; + } + /** * Cleans up resources by nullifying the AbortController to prevent memory leaks. */ @@ -88,6 +102,7 @@ export default function useFetchController(type) { } return { + abortOnTimeout, applyRequestUrlForErrorMessages, cancelRequest, cleanup, diff --git a/tests/lib/butter.min.js b/tests/lib/butter.min.js index b3feb65..943f79e 100644 --- a/tests/lib/butter.min.js +++ b/tests/lib/butter.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Butter=t():e.Butter=t()}(this,()=>(()=>{"use strict";var n={d:(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},e={};n.d(e,{default:()=>function(e,t={}){if(e)return this instanceof r?r:new r(e,t);throw"ButterCMS API token not set"}});const O="2.0.0",s={author:"authors",category:"categories",content:"content",feed:"feeds",page:"pages",post:"posts",tag:"tags"};function P(e,t){const{onError:r,onRequest:n,onResponse:s,...a}=t.config,{auth_token:o,test:c,preview:i,...u}=t.params;r&&r(e,{options:a,params:u,type:t.type})}function x(e,t,r){t={auth_token:t};return r.testMode&&(t.test=1,t.preview=1),{...e,...t}}function a(y,q){t=y;const w=`https://api.buttercms.com/v2/${s[t]}/`,e=y.replace(y[0],y[0].toUpperCase()),{applyRequestUrlForErrorMessages:E,cancelRequest:b,cleanup:j,determineFetchError:k,signal:T}=function(t){let s=new AbortController,r="";function a(e){return r+": "+e}function o(){s=null}return{applyRequestUrlForErrorMessages:function(e){r=t+` (${e})`},cancelRequest:async function(){await s.abort(a("Request cancelled"))},cleanup:o,controller:s,determineFetchError:function(e,t){var r=s&&s.signal.reason&&s.signal.aborted,n="TimeoutError"===e.name;return r?s.signal.reason:n?a(`Request timed out after ${t}ms.`):(o(),e)},signal:s.signal}}(e);var t;async function r(t=w,r={}){const{apiToken:e,...n}=q,s=new Headers({"Content-Type":"application/json","X-Butter-Client":"JS/"+O});"undefined"==typeof window&&s.append("Accept-Encoding","gzip"),E(t);var{config:r,headers:a,params:o}=await async function(e,t,r){const{onError:n,onRequest:s,onResponse:a,...o}=r.config;var c,i;return s?({headers:e,options:c,params:i}=await s(e,{cancelRequest:r.cancelRequest,headers:r.headers,options:o,params:r.params,type:r.type}),{config:{...c,onError:n,onRequest:s,onResponse:a},headers:e,params:x(i,t,c)}):{...r,params:x(r.params,t,o)}}(t,e,{cancelRequest:b,config:n,headers:s,params:r,type:y});try{var c=AbortSignal.timeout(r.timeout),i=await fetch(t+"?"+new URLSearchParams(o),{cache:r.cache,method:"GET",headers:a,signal:AbortSignal.any([T,c])});if(j(),200!==i.status)throw{response:i,config:r,params:o};{var u=i;var p={config:r,params:o,type:y,requestHeaders:s};const{onError:l,onRequest:f,onResponse:d,...h}=p.config,{auth_token:m,test:g,preview:R,...v}=p.params;if(d){const l=u.clone();await d(l,{options:h,params:v,type:p.type})}return await{data:await u.json(),headers:Object.fromEntries(u.headers.entries()),status:u.status,statusText:u.statusText,config:{url:u.url,method:"get",headers:Object.fromEntries(p.requestHeaders.entries()),transformRequest:f?[f]:[],transformResponse:d?[d]:[],timeout:p.config.timeout}};return await void 0}}catch(e){return e.response?(t=await e.response.json(),a=Object.fromEntries(Object.entries(e.params).filter(([e])=>"auth_token"!==e)),(c=new URL(e.response.url)).searchParams.delete("auth_token"),P(t,i={data:t,headers:e.response.headers,status:e.response.status,statusText:e.response.statusText,config:e.config,params:a,type:y,url:c}),Promise.reject(new Error(`${u=t,p=Object.keys(u),1n(e,t),retrieve:async(e,t,r)=>n(e+"/"+t,r),search:r}},Post:function(e={}){var{cancelRequest:e,list:t,retrieve:r,search:n}=a("post",e);return{cancelRequest:e,list:t,retrieve:r,search:n}},Tag:function(e={}){var{cancelRequest:e,list:t,retrieve:r}=a("tag",e);return{cancelRequest:e,list:t,retrieve:r}}};function r(e,t){var r,n,{cache:t="default",onError:s=null,onRequest:a=null,onResponse:o=null,testMode:c=!1,timeout:i=3e3}=t;return{version:O,...(r=u,n={apiToken:e,cache:t,onError:s,onRequest:a,onResponse:o,testMode:c,timeout:i},Object.keys(r).reduce((e,t)=>({...e,[t.toLocaleLowerCase()]:r[t](n)}),{}))}}return e.default})()); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Butter=t():e.Butter=t()}(this,()=>(()=>{"use strict";var n={d:(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},e={};n.d(e,{default:()=>function(e,t={}){if(e)return this instanceof r?r:new r(e,t);throw"ButterCMS API token not set"}});const P="2.0.0",s={author:"authors",category:"categories",content:"content",feed:"feeds",page:"pages",post:"posts",tag:"tags"};function x(e,t){const{onError:r,onRequest:n,onResponse:s,...o}=t.config,{auth_token:a,test:c,preview:i,...u}=t.params;r&&r(e,{options:o,params:u,type:t.type})}function C(e,t,r){t={auth_token:t};return r.testMode&&(t.test=1,t.preview=1),{...e,...t}}function o(q,b){t=q;const w=`https://api.buttercms.com/v2/${s[t]}/`,e=q.replace(q[0],q[0].toUpperCase()),{abortOnTimeout:E,applyRequestUrlForErrorMessages:j,cancelRequest:T,cleanup:O,determineFetchError:k,signal:A}=function(t){let s=new AbortController,r="";function o(e){return r+": "+e}function a(){s=null}return{abortOnTimeout:function(e){s.abort(o(`Request timed out after ${e}ms.`))},applyRequestUrlForErrorMessages:function(e){r=t+` (${e})`},cancelRequest:function(){s.abort(o("Request cancelled"))},cleanup:a,controller:s,determineFetchError:function(e,t){var r=s&&s.signal.reason&&s.signal.aborted,n="TimeoutError"===e.name;return r?s.signal.reason:n?o(`Request timed out after ${t}ms.`):(a(),e)},signal:s.signal}}(e);var t;async function r(t=w,r={}){const{apiToken:e,...n}=b,s=new Headers({"Content-Type":"application/json","X-Butter-Client":"JS/"+P}),{config:o,headers:a,params:c}=("undefined"==typeof window&&s.append("Accept-Encoding","gzip"),j(t),await async function(e,t,r){const{onError:n,onRequest:s,onResponse:o,...a}=r.config;var c,i;return s?({headers:e,options:c,params:i}=await s(e,{cancelRequest:r.cancelRequest,headers:r.headers,options:a,params:r.params,type:r.type}),{config:{...c,onError:n,onRequest:s,onResponse:o},headers:e,params:C(i,t,c)}):{...r,params:C(r.params,t,a)}}(t,e,{cancelRequest:T,config:n,headers:s,params:r,type:q}));try{var i=AbortSignal.timeout?AbortSignal.timeout(o.timeout):setTimeout(()=>E(o.timeout),o.timeout),u=await fetch(t+"?"+new URLSearchParams(c),{cache:o.cache,method:"GET",headers:a,signal:AbortSignal.timeout?AbortSignal.any([A,i]):A});if(AbortSignal.timeout||clearTimeout(i),O(),200!==u.status)throw{response:u,config:o,params:c};{var p=u;var l={config:o,params:c,type:q,requestHeaders:s};const{onError:f,onRequest:m,onResponse:d,...h}=l.config,{auth_token:g,test:R,preview:v,...y}=l.params;if(d){const f=p.clone();await d(f,{options:h,params:y,type:l.type})}return await{data:await p.json(),headers:Object.fromEntries(p.headers.entries()),status:p.status,statusText:p.statusText,config:{url:p.url,method:"get",headers:Object.fromEntries(l.requestHeaders.entries()),transformRequest:m?[m]:[],transformResponse:d?[d]:[],timeout:l.config.timeout}};return await void 0}}catch(e){return e.response?(r=await e.response.json(),t=Object.fromEntries(Object.entries(e.params).filter(([e])=>"auth_token"!==e)),(i=new URL(e.response.url)).searchParams.delete("auth_token"),x(r,u={data:r,headers:e.response.headers,status:e.response.status,statusText:e.response.statusText,config:e.config,params:t,type:q,url:i}),Promise.reject(new Error(`${p=r,l=Object.keys(p),1n(e,t),retrieve:async(e,t,r)=>n(e+"/"+t,r),search:r}},Post:function(e={}){var{cancelRequest:e,list:t,retrieve:r,search:n}=o("post",e);return{cancelRequest:e,list:t,retrieve:r,search:n}},Tag:function(e={}){var{cancelRequest:e,list:t,retrieve:r}=o("tag",e);return{cancelRequest:e,list:t,retrieve:r}}};function r(e,t){var r,n,{cache:t="default",onError:s=null,onRequest:o=null,onResponse:a=null,testMode:c=!1,timeout:i=3e3}=t;return{version:P,...(r=u,n={apiToken:e,cache:t,onError:s,onRequest:o,onResponse:a,testMode:c,timeout:i},Object.keys(r).reduce((e,t)=>({...e,[t.toLocaleLowerCase()]:r[t](n)}),{}))}}return e.default})()); \ No newline at end of file