fanos
Version:
Fanos is a lightweight JavaScript library for reliable data transmission via the Beacon API, with auto-retry, Fetch fallback, batching, and flexible triggers.
3 lines (2 loc) • 6.35 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fanos=t()}(this,(function(){"use strict";const e=Object.freeze({url:null,headers:{"Content-Type":"application/json"},storeKey:"__FANOS__",autoFlush:!0,storeFailed:!0,debug:!1,retryInterval:5e3,maxPayloadSize:64e3,maxAttemptsPerRequest:3,maxRetryCycles:10,maxRetryDelay:3e5,fallbackToFetch:!1}),t=(e,t)=>e instanceof Blob||e instanceof FormData||e instanceof URLSearchParams?e:new Blob([JSON.stringify(e)],{type:t["Content-Type"]}),i=(e,i)=>t(e,i);const s=(e,...t)=>{!0!==e&&"info"!==e||console.debug("[Fanos]",...t)},n=e=>{try{if(crypto&&crypto.randomUUID)return crypto.randomUUID()}catch(t){s(e,"crypto.randomUUID() failed, falling back to alternative method",t)}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(e=>{const t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)}))};class r{constructor(e,t){this._config=e,this._queue=t}sendRequest(e){const t=i(e.data,e.headers),n=(e=>e instanceof Blob?e.size:e instanceof FormData?[...e.entries()].reduce(((e,[t,i])=>e+t.length+("string"==typeof i?i.length:0)),0):e instanceof URLSearchParams?(new TextEncoder).encode(e.toString()).length:(new TextEncoder).encode(JSON.stringify(e)).length)(t);if(n>this._config.maxPayloadSize)return s(this._config.debug,"Warning: Payload too large, splitting..."),this.splitAndSend(e);return navigator.sendBeacon(e.url,t)?(s(this._config.debug,"Beacon succeeded:",e.id),this._queue.delete(e),!0):!!this._config.fallbackToFetch&&this.fetchFallback(e)}createRequest(e,t){return{id:n(this._config.debug),url:t.url||this._config.url,headers:{...this._config.headers,...t.headers},data:e,attempts:0,timestamp:Date.now()}}async splitAndSend(e){s(this._config.debug,`Splitting large request: ${e.id}`);const i=t(e.data,e.headers),n=i instanceof Blob?await i.text():JSON.stringify(e.data),r=this._config.maxPayloadSize-1e3,o=[];for(let e=0;e<n.length;e+=r)o.push(n.slice(e,e+r));let a=!0;return o.forEach(((t,i)=>{const n=new Blob([t],{type:e.headers["Content-Type"]});navigator.sendBeacon(e.url,n)||(s(this._config.debug,`Chunk ${i+1} failed to send`),a=!1)})),a&&this._queue.delete(e),a}async fetchFallback(e){try{const t=await fetch(e.url,{method:"POST",headers:e.headers,body:i(e.data,e.headers),keepalive:!0});if(!t.ok){const e=await t.text();throw new Error(`HTTP ${t.status}: ${e}`)}return s(this._config.debug,"Fetch fallback succeeded:",e.id),this._queue.delete(e),!0}catch(e){return s(this._config.debug,"Fetch fallback failed:",e),"TypeError"===e.name?s(this._config.debug,"Network error detected."):s(this._config.debug,"HTTP error detected."),!1}}}class o{static defaults=e;static instance=new o;constructor(e={}){this.config={...o.defaults,...e},this.queue=new Set,this.abortController=new AbortController,this.requestHandler=new r(this.config,this.queue),this.retryTimer=null,this._initialized=!1}static send(e,t){return o.instance.send(e,t)}static configure(e){return Object.assign(o.instance.config,e),o.instance}static flush(){o.instance?.flush()}static destroy(){o.instance?.destroy()}send(e,t={}){return new Promise(((i,n)=>{this._initialized||this._initialize();const r=this.requestHandler.createRequest(e,t);s(this.config.debug,"Sending request:",{request:r});try{this.requestHandler.sendRequest(r)?i():this._handleFailure(r,n)}catch(e){this._handleFailure(r,n,e)}}))}flush(){if(0===this.queue.size)return void s(this.config.debug,"Queue is empty, skipping flush.");s(this.config.debug,`Flushing queue with ${this.queue.size} items`);const e=new Set,t=this.config.maxAttemptsPerRequest;for(const i of this.queue)if(!(i.attempts>=t))try{this.requestHandler.sendRequest(i)&&e.add(i.id)}catch(e){s(this.config.debug,"Flush error:",e)}this.queue.forEach((i=>{(e.has(i.id)||i.attempts>=t)&&this.queue.delete(i)})),this.queue.size>0?this._persistQueue():this.config.debug&&s(this.config.debug,"Queue is empty, skipping persistence.")}destroy(){s(this.config.debug,"Destroying instance"),this.abortController.abort(),clearTimeout(this.retryTimer),this.queue.clear(),this._persistQueue(),this._initialized=!1}_initialize(){s(this.config.debug,"Initializing..."),this._loadQueue(),this._eventHandler(),this._scheduleRetries(),this._initialized=!0}_handleFailure(e,t,i=new Error("Beacon failed")){e.attempts++,s(this.config.debug,`Request ${e.id} failed (attempt ${e.attempts})`),this.config.storeFailed&&(this.queue.add(e),this._persistQueue()),t(i)}_scheduleRetries(e=1){if(this.retryTimer&&clearTimeout(this.retryTimer),0===this.queue.size)return void s(this.config.debug,"Queue is empty, stopping retries.");const t=this.config.maxRetryCycles||10;if(e>t)return void s(this.config.debug,"Max retry attempts reached, stopping retries.");const i=this.config.maxRetryDelay||3e5,n=Math.min(this.config.retryInterval*2**(e-1)*(1+.2*Math.random()),i);this.retryTimer=setTimeout((()=>{this.flush(),this._scheduleRetries(e+1)}),n)}_eventHandler(){if(!this.config.autoFlush)return;this.abortController.abort(),this.abortController=new AbortController;const e=()=>{this.queue.size>0?(s(this.config.debug,"Auto-flushing queue due to unload event."),this.flush()):this.config.debug&&s(this.config.debug,"Queue is empty, skipping auto-flush.")},{signal:t}=this.abortController;["pagehide","beforeunload","visibilitychange"].forEach((i=>{window.addEventListener(i,e,{signal:t})}))}_loadQueue(){try{const e=localStorage.getItem(this.config.storeKey);if(e){const t=JSON.parse(e);Array.isArray(t)&&(this.queue=new Set(t),s(this.config.debug,`Loaded queue from storage: ${t.length} items`))}}catch(e){s(this.config.debug,"Error loading queue:",e),this.queue.clear(),localStorage.removeItem(this.config.storeKey)}}_persistQueue=function(e,t){let i;return(...s)=>{clearTimeout(i),i=setTimeout((()=>e.apply(this,s)),t)}}((()=>{if(this.config.storeFailed&&0!==this.queue.size)try{const e=JSON.stringify([...Array.from(this.queue)].slice(0,100));localStorage.setItem(this.config.storeKey,e),s(this.config.debug,`Persisted queue: ${this.queue.size} items`)}catch(e){s(this.config.debug,"Persist error:",e)}else this.config.debug&&s(this.config.debug,"Skipping persistence: queue is empty or storeFailed is false.")}),500)}return o}));
//# sourceMappingURL=fanos.umd.min.js.map