UNPKG

@lucidclient/speculate

Version:

A lightweight library to handle speculate rules for prefetching and prerendering documents, with added support for data fetching on intent.

2 lines 6.22 kB
var E=()=>{if(!navigator.onLine)return console.warn("The device is offline, speculate library not initialised."),!1;if("connection"in navigator){let i=navigator.connection;if(i?.saveData)return console.warn("Save-Data is enabled, speculate library not initialised."),!1;if(/(2|3)g/.test(i?.effectiveType))return console.warn("2G or 3G connection is detected, speculate library not initialised."),!1}return!0},c=E;var s={moderateEvents:["mouseenter","touchstart","focus"],fallbackTriggerSupport:["visible","moderate"],fallbackTrigger:"moderate",fallbackAction:"prefetch",validActions:["prefetch","prerender"],validTriggers:["visible","immediate","eager","moderate","conservative"],genSpeculate:"gen-speculate"},f=!1,m=new Set,d="supports"in HTMLScriptElement&&HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules"),A=document.createElement("link").relList?.supports?.("prefetch"),o,a=new AbortController,h=i=>{o.unobserve(i),C(i)},k=i=>{for(let e of s.moderateEvents)i.addEventListener(e,y,{passive:!0,signal:a.signal})},C=i=>{for(let e of s.moderateEvents)i.removeEventListener(e,y)},y=i=>{let e=i.target;e&&p(e,g(e.rel))},p=(i,e)=>{if(!I({href:i.href,target:i.target}))return h(i);d?L(i.href,e):A?w(i.href):fetch(i.href,{priority:"low",signal:a.signal}),m.add(i.href),h(i)},L=(i,e)=>{try{let t=document.createElement("script");t.type="speculationrules",t.setAttribute(s.genSpeculate,"");let r=[{source:"list",urls:[i],eagerness:e[1]==="visible"?void 0:e[1]}];t.textContent=e[0]==="prefetch"?JSON.stringify({prefetch:r}):JSON.stringify({prerender:r,prefetch:r}),document.head.appendChild(t)}catch(t){console.error(t)}},w=i=>{let e=document.createElement("link");e.rel="prefetch",e.setAttribute(s.genSpeculate,""),e.href=i,e.as="document",document.head.appendChild(e)},g=i=>{let e,t;for(let r of i.split(" "))if(r.startsWith("prefetch:")||r.startsWith("prerender:")){[e,t]=r.split(":");break}return e||(e=s.fallbackAction),t||(t=s.fallbackTrigger),d||(e=s.fallbackAction,s.fallbackTriggerSupport.includes(t)||(t=s.fallbackTrigger)),s.validActions.includes(e)||(e=s.fallbackAction),s.validTriggers.includes(t)||(t=s.fallbackTrigger),[e,t]},I=i=>{try{if(i.href=i.href.replace(/#.*/,""),i.target==="_blank"||i.href.includes("mailto:")||i.href.includes("tel:"))return!1;let e=new URL(i.href);return!(e.origin!==window.location.origin||e.pathname===window.location.pathname||m.has(i.href))}catch{return!1}},T=()=>{if(f||(f=!0,!c()))return;o=o||new IntersectionObserver(t=>{for(let r of t)if(r.isIntersecting&&r.target instanceof HTMLAnchorElement){let n=r.target,[S,b]=g(n.rel);b==="visible"?p(n,[S,b]):h(n)}});let e=document.querySelectorAll('a[rel*="prefetch:"], a[rel*="prerender:"]');for(let t of e){let[r,n]=g(t.rel);n==="visible"?o.observe(t):n==="moderate"&&!d?k(t):p(t,[r,n])}},D=()=>{a.abort(),a=new AbortController,o?.disconnect(),m.clear(),f=!1;let i=document.querySelectorAll(`link[${s.genSpeculate}], script[${s.genSpeculate}]`);for(let e of i)e.remove()},O=()=>(typeof requestIdleCallback<"u"?requestIdleCallback(T):setTimeout(T,0),D),H=O;var v=class{constructor(e){this.config=e;this.registerEvents(),this.handleOptimisticPrefetch()}cache=new Map;abortController=new AbortController;intentDebounce=null;registerEvents(){for(let e of this.normaliseTargets(this.config.elements))e.addEventListener("mouseover",this.mouseOverEventHandler,{signal:this.abortController.signal}),e.addEventListener("click",this.clickEventHandler,{signal:this.abortController.signal})}mouseOverEventHandler=e=>{if(!c())return;let t=e.target;this.getCacheKey(t)&&(this.intentDebounce&&this.intentDebounce.target!==t&&(clearTimeout(this.intentDebounce.timeout),this.intentDebounce=null),this.intentDebounce||(this.intentDebounce={target:t,timeout:setTimeout(()=>{this.prefetch(t),this.intentDebounce=null},this.hoverDelay)}))};clickEventHandler=async e=>{let t=await this.prefetch(e.target);this.config.onClick&&this.config.onClick(e,t)};getCacheKey(e){return this.config.getCacheKey?this.config.getCacheKey(e)??null:e.id||null}isStale(e){return Date.now()-e>this.cacheConfig.staleTime}manageCacheSize(){for(;this.cache.size>=this.cacheConfig.maxSize;){let e=null,t=Number.POSITIVE_INFINITY;for(let[r,n]of this.cache.entries())n.timestamp<t&&(t=n.timestamp,e=r);e&&this.cache.delete(e)}}handleOptimisticPrefetch(){let e=[...this.optimisticStrategies].sort((t,r)=>(t.priority??Number.POSITIVE_INFINITY)-(r.priority??Number.POSITIVE_INFINITY));for(let t of e){if(t.condition&&!t.condition())continue;let r=this.normaliseTargets(t.elements);for(let n of r)this.schedulePrefetch(n)}}async executeFetchCallback(e){try{return await this.config.fetch(e)}catch(t){return{error:{message:t instanceof Error?t.message:"An unknown error was caught while executing the fetch callback.",exception:t},data:void 0}}}schedulePrefetch(e){let t=()=>this.prefetch(e);typeof requestIdleCallback<"u"?requestIdleCallback(t):setTimeout(t,0)}normaliseTargets(e){return e?e instanceof NodeList?Array.from(e):Array.isArray(e)?e:[e]:[]}get cacheConfig(){return{maxSize:this.config.cache?.maxSize??5,staleTime:this.config.cache?.staleTime??12e4}}get optimisticStrategies(){return this.config.optimistic?Array.isArray(this.config.optimistic)?this.config.optimistic:[this.config.optimistic]:[]}get hoverDelay(){return this.config.hoverDelay??100}destroy(){this.abortController.abort(),this.clearCache(),this.intentDebounce&&(clearTimeout(this.intentDebounce.timeout),this.intentDebounce=null)}refresh(e){e&&(this.config={...this.config,...e.elements&&{elements:e.elements},...e.optimistic&&{optimistic:e.optimistic}}),this.destroy(),this.abortController=new AbortController,this.registerEvents(),this.handleOptimisticPrefetch()}async prefetch(e){let t=this.getCacheKey(e);if(t){let n=this.cache.get(t);if(n&&!this.isStale(n.timestamp))return n.promise}let r=this.executeFetchCallback(e);return t&&(this.manageCacheSize(),this.cache.set(t,{timestamp:Date.now(),promise:r})),r}async prefetchAll(e){let t=Array.from(e);return Promise.all(t.map(r=>this.prefetch(r)))}clearCache(e){if(!e){this.cache.clear();return}let t=this.getCacheKey(e);t&&this.cache.delete(t)}},P=v;export{P as Speculator,H as speculateLinks}; //# sourceMappingURL=index.js.map