@real-router/preload-plugin 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { DefaultDependencies, Params, Router } from "@real-router/types";
2
- import { DefaultDependencies as DefaultDependencies$1, PluginFactory } from "@real-router/core";
2
+ import { DefaultDependencies as DefaultDependencies$1, PluginFactory, State } from "@real-router/core";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  interface PreloadPluginOptions {
@@ -29,6 +29,7 @@ declare module "@real-router/core" {
29
29
  preload?: PreloadFnFactory<Dependencies>;
30
30
  }
31
31
  interface Router {
32
+ getPreloadedState?: (href: string) => State | undefined;
32
33
  getPreloadSettings(): PreloadPluginOptions;
33
34
  }
34
35
  } //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAEiB,oBAAA;;EAEf,KAAA;EAFe;EAIf,YAAA;AAAA;;;AAOF;;KAAY,SAAA,IAAa,MAAA,EAAQ,MAAA,KAAW,OAAA;;;;;;KAOhC,gBAAA,sBACW,mBAAA,GAAsB,mBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,SAAA;;;iBCjBW,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,IACd,aAAA;;;;YCGS,KAAA,sBAA2B,qBAAA;IACnC,OAAA,GAAU,gBAAA,CAAiB,YAAA;EAAA;EAAA,UAGnB,MAAA;IACR,kBAAA,IAAsB,oBAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAEiB,oBAAA;;EAEf,KAAA;EAFe;EAIf,YAAA;AAAA;;;AAOF;;KAAY,SAAA,IAAa,MAAA,EAAQ,MAAA,KAAW,OAAA;;;;;;KAOhC,gBAAA,sBACW,mBAAA,GAAsB,mBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,SAAA;;;iBCjBW,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,IACd,aAAA;;;;YCGS,KAAA,sBAA2B,qBAAA;IACnC,OAAA,GAAU,gBAAA,CAAiB,YAAA;EAAA;EAAA,UAGnB,MAAA;IACR,iBAAA,IAAqB,IAAA,aAAiB,KAAA;IACtC,kBAAA,IAAsB,oBAAA;EAAA;AAAA"}
package/dist/cjs/index.js CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);const t={delay:65,networkAware:!0},n={capture:!0,passive:!0};function r(){let e=navigator.connection;return e?!!(e.saveData||e.effectiveType?.includes(`2g`)):!1}var i=class{#e;#t;#n;#r;#i;#a=new Map;#o=null;#s=null;#c=null;#l=0;#u=null;#d=NaN;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#n=n,this.#r=r;let i={...n};this.#i=t.extendRouter({getPreloadSettings:()=>i})}getPlugin(){return{onStart:()=>{document.addEventListener(`mouseover`,this.#f,n),document.addEventListener(`touchstart`,this.#p,n),document.addEventListener(`touchmove`,this.#m,n)},onStop:()=>{this.#x()},teardown:()=>{this.#x(),this.#i()}}}#f=e=>{if(this.#v(e))return;let t=this.#h(e.target);if(t===this.#o)return;this.#y(),this.#o=t;let n=this.#g(t);n&&(this.#s=setTimeout(()=>{this.#s=null,n.fn(n.params).catch(()=>{})},this.#n.delay))};#p=e=>{this.#u=e.target,this.#d=e.timeStamp,this.#b();let t=this.#h(e.target),n=this.#g(t);!n||e.touches.length===0||(this.#l=e.touches[0].clientY,this.#c=setTimeout(()=>{this.#c=null,n.fn(n.params).catch(()=>{})},100))};#m=e=>{this.#c===null||e.touches.length===0||Math.abs(e.touches[0].clientY-this.#l)>10&&this.#b()};#h(e){return e instanceof Element?e.closest(`a[href]`):null}#g(e){if(e&&!(`noPreload`in e.dataset)&&!(this.#n.networkAware&&r()))return this.#_(e)}#_(e){let t=this.#e.matchUrl?.(e.href);if(!t)return;let n=this.#t.getRouteConfig(t.name),r=typeof n?.preload==`function`?n.preload:void 0;if(!r){this.#a.delete(t.name);return}let i=this.#a.get(t.name);if(i?.factory===r)return{fn:i.fn,params:t.params};let a;try{a=r(this.#e,this.#r)}catch{return}return this.#a.set(t.name,{fn:a,factory:r}),{fn:a,params:t.params}}#v(e){let t=e.timeStamp-this.#d;return t>=0&&t<2500&&e.target===this.#u}#y(){this.#s!==null&&(clearTimeout(this.#s),this.#s=null),this.#o=null}#b(){this.#c!==null&&(clearTimeout(this.#c),this.#c=null)}#x(){document.removeEventListener(`mouseover`,this.#f,n),document.removeEventListener(`touchstart`,this.#p,n),document.removeEventListener(`touchmove`,this.#m,n),this.#y(),this.#b(),this.#u=null,this.#d=NaN}};function a(n){let r={...t,...n};return(!Number.isFinite(r.delay)||r.delay<0)&&(r.delay=0),function(t,n){return typeof document>`u`?{}:new i(t,(0,e.getPluginApi)(t),r,n).getPlugin()}}exports.preloadPluginFactory=a;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);const t={delay:65,networkAware:!0},n={capture:!0,passive:!0};function r(){let e=navigator.connection;return e?!!(e.saveData||e.effectiveType?.includes(`2g`)):!1}var i=class{#e;#t;#n;#r;#i;#a=new Map;#o=new Map;#s=null;#c=null;#l=null;#u=0;#d=null;#f=NaN;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#n=n,this.#r=r;let i={...n};this.#i=t.extendRouter({getPreloadSettings:()=>i,getPreloadedState:e=>{let t=this.#o.get(e);return t&&this.#o.delete(e),t}})}getPlugin(){return{onStart:()=>{document.addEventListener(`mouseover`,this.#p,n),document.addEventListener(`touchstart`,this.#m,n),document.addEventListener(`touchmove`,this.#h,n)},onStop:()=>{this.#C()},teardown:()=>{this.#C(),this.#i()}}}#p=e=>{if(this.#b(e))return;let t=this.#g(e.target);if(t===this.#s)return;this.#x(),this.#s=t;let n=this.#_(t);n&&(this.#c=setTimeout(()=>{this.#c=null,n.fn(n.params).catch(()=>{})},this.#n.delay))};#m=e=>{this.#d=e.target,this.#f=e.timeStamp,this.#S();let t=this.#g(e.target),n=this.#_(t);!n||e.touches.length===0||(this.#u=e.touches[0].clientY,this.#l=setTimeout(()=>{this.#l=null,n.fn(n.params).catch(()=>{})},100))};#h=e=>{this.#l===null||e.touches.length===0||Math.abs(e.touches[0].clientY-this.#u)>10&&this.#S()};#g(e){return e instanceof Element?e.closest(`a[href]`):null}#_(e){if(e&&!(`noPreload`in e.dataset)&&!(this.#n.networkAware&&r()))return this.#v(e)}#v(e){let t=this.#e.matchUrl?.(e.href);if(!t)return;this.#y(e.href,t);let n=this.#t.getRouteConfig(t.name),r=typeof n?.preload==`function`?n.preload:void 0;if(!r){this.#a.delete(t.name);return}let i=this.#a.get(t.name);if(i?.factory===r)return{fn:i.fn,params:t.params};let a;try{a=r(this.#e,this.#r)}catch{return}return this.#a.set(t.name,{fn:a,factory:r}),{fn:a,params:t.params}}#y(e,t){if(this.#o.has(e))this.#o.delete(e);else if(this.#o.size>=32)for(let e of this.#o.keys()){this.#o.delete(e);break}this.#o.set(e,t)}#b(e){let t=e.timeStamp-this.#f;return t>=0&&t<2500&&e.target===this.#d}#x(){this.#c!==null&&(clearTimeout(this.#c),this.#c=null),this.#s=null}#S(){this.#l!==null&&(clearTimeout(this.#l),this.#l=null)}#C(){document.removeEventListener(`mouseover`,this.#p,n),document.removeEventListener(`touchstart`,this.#m,n),document.removeEventListener(`touchmove`,this.#h,n),this.#x(),this.#S(),this.#d=null,this.#f=NaN,this.#o.clear()}};function a(n){let r={...t,...n};return(!Number.isFinite(r.delay)||r.delay<0)&&(r.delay=0),function(t,n){return typeof document>`u`?{}:new i(t,(0,e.getPluginApi)(t),r,n).getPlugin()}}exports.preloadPluginFactory=a;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#router","#api","#options","#getDependency","#removeExtensions","#compiledPreloads","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#findAnchor","#currentAnchor","#cancelHover","#resolveAnchorPreload","#hoverTimer","#lastTouchTarget","#lastTouchTimeStamp","#cancelTouch","#touchStartY","#touchTimer","#resolvePreload"],"sources":["../../src/constants.ts","../../src/network.ts","../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { PreloadPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<PreloadPluginOptions> = {\n delay: 65,\n networkAware: true,\n};\n\nexport const GHOST_EVENT_THRESHOLD = 2500;\n\nexport const TOUCH_SCROLL_THRESHOLD = 10;\n\nexport const TOUCH_PRELOAD_DELAY = 100;\n\nexport const LISTENER_OPTIONS: AddEventListenerOptions = {\n capture: true,\n passive: true,\n};\n","type NetworkConnection =\n | { saveData?: boolean; effectiveType?: string }\n | undefined;\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkConnection;\n}\n\nexport function isSlowConnection(): boolean {\n const connection = (navigator as NavigatorWithConnection).connection;\n\n if (!connection) {\n return false;\n }\n if (connection.saveData) {\n return true;\n }\n if (connection.effectiveType?.includes(\"2g\")) {\n return true;\n }\n\n return false;\n}\n","import {\n GHOST_EVENT_THRESHOLD,\n LISTENER_OPTIONS,\n TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type {\n PreloadFn,\n PreloadFnFactory,\n PreloadPluginOptions,\n} from \"./types\";\nimport type {\n Params,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ndeclare module \"@real-router/core\" {\n interface Router {\n matchUrl?: (url: string) => State | undefined;\n }\n}\n\nexport class PreloadPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<PreloadPluginOptions>;\n readonly #getDependency: Parameters<PluginFactory>[1];\n readonly #removeExtensions: () => void;\n readonly #compiledPreloads = new Map<\n string,\n { fn: PreloadFn; factory: PreloadFnFactory }\n >();\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchTarget: EventTarget | null = null;\n #lastTouchTimeStamp = Number.NaN;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n getDependency: Parameters<PluginFactory>[1],\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#getDependency = getDependency;\n\n const cachedOptions = { ...options };\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => cachedOptions,\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n },\n\n onStop: () => {\n this.#cleanup();\n },\n\n teardown: () => {\n this.#cleanup();\n this.#removeExtensions();\n },\n };\n }\n\n readonly #handleMouseOver = (event: MouseEvent): void => {\n if (this.#isGhostMouseEvent(event)) {\n return;\n }\n\n const anchor = this.#findAnchor(event.target);\n\n if (anchor === this.#currentAnchor) {\n return;\n }\n\n this.#cancelHover();\n this.#currentAnchor = anchor;\n\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\n return;\n }\n\n this.#hoverTimer = setTimeout(() => {\n this.#hoverTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, this.#options.delay);\n };\n\n readonly #handleTouchStart = (event: TouchEvent): void => {\n this.#lastTouchTarget = event.target;\n this.#lastTouchTimeStamp = event.timeStamp;\n\n this.#cancelTouch();\n\n const anchor = this.#findAnchor(event.target);\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload || event.touches.length === 0) {\n return;\n }\n\n this.#touchStartY = event.touches[0].clientY;\n\n this.#touchTimer = setTimeout(() => {\n this.#touchTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, TOUCH_PRELOAD_DELAY);\n };\n\n readonly #handleTouchMove = (event: TouchEvent): void => {\n if (this.#touchTimer === null || event.touches.length === 0) {\n return;\n }\n\n const deltaY = Math.abs(event.touches[0].clientY - this.#touchStartY);\n\n if (deltaY > TOUCH_SCROLL_THRESHOLD) {\n this.#cancelTouch();\n }\n };\n\n #findAnchor(target: EventTarget | null): HTMLAnchorElement | null {\n return target instanceof Element\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n }\n\n #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: PreloadFn; params: Params } | undefined {\n if (!anchor) {\n return undefined;\n }\n\n if (\"noPreload\" in anchor.dataset) {\n return undefined;\n }\n\n if (this.#options.networkAware && isSlowConnection()) {\n return undefined;\n }\n\n return this.#resolvePreload(anchor);\n }\n\n #resolvePreload(\n anchor: HTMLAnchorElement,\n ): { fn: PreloadFn; params: Params } | undefined {\n const state = this.#router.matchUrl?.(anchor.href);\n\n if (!state) {\n return undefined;\n }\n\n const config = this.#api.getRouteConfig(state.name);\n const factory =\n typeof config?.preload === \"function\"\n ? (config.preload as PreloadFnFactory)\n : undefined;\n\n if (!factory) {\n this.#compiledPreloads.delete(state.name);\n\n return undefined;\n }\n\n const cached = this.#compiledPreloads.get(state.name);\n\n if (cached?.factory === factory) {\n return { fn: cached.fn, params: state.params };\n }\n\n let fn: PreloadFn;\n\n try {\n fn = factory(this.#router, this.#getDependency);\n } catch {\n return undefined;\n }\n\n this.#compiledPreloads.set(state.name, { fn, factory });\n\n return { fn, params: state.params };\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n const delta = event.timeStamp - this.#lastTouchTimeStamp;\n\n return (\n delta >= 0 &&\n delta < GHOST_EVENT_THRESHOLD &&\n event.target === this.#lastTouchTarget\n );\n }\n\n #cancelHover(): void {\n if (this.#hoverTimer !== null) {\n clearTimeout(this.#hoverTimer);\n this.#hoverTimer = null;\n }\n\n this.#currentAnchor = null;\n }\n\n #cancelTouch(): void {\n if (this.#touchTimer !== null) {\n clearTimeout(this.#touchTimer);\n this.#touchTimer = null;\n }\n }\n\n #cleanup(): void {\n document.removeEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchTarget = null;\n this.#lastTouchTimeStamp = Number.NaN;\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { defaultOptions } from \"./constants\";\nimport { PreloadPlugin } from \"./plugin\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function preloadPluginFactory(\n opts?: Partial<PreloadPluginOptions>,\n): PluginFactory {\n const options: Required<PreloadPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n if (!Number.isFinite(options.delay) || options.delay < 0) {\n options.delay = 0;\n }\n\n return function preloadPlugin(routerBase, getDependency) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n getDependency,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#router","#api","#options","#getDependency","#removeExtensions","#compiledPreloads","#stateCache","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#findAnchor","#currentAnchor","#cancelHover","#resolveAnchorPreload","#hoverTimer","#lastTouchTarget","#lastTouchTimeStamp","#cancelTouch","#touchStartY","#touchTimer","#resolvePreload","#cacheState"],"sources":["../../src/constants.ts","../../src/network.ts","../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { PreloadPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<PreloadPluginOptions> = {\n delay: 65,\n networkAware: true,\n};\n\nexport const GHOST_EVENT_THRESHOLD = 2500;\n\nexport const TOUCH_SCROLL_THRESHOLD = 10;\n\nexport const TOUCH_PRELOAD_DELAY = 100;\n\nexport const LISTENER_OPTIONS: AddEventListenerOptions = {\n capture: true,\n passive: true,\n};\n","type NetworkConnection =\n | { saveData?: boolean; effectiveType?: string }\n | undefined;\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkConnection;\n}\n\nexport function isSlowConnection(): boolean {\n const connection = (navigator as NavigatorWithConnection).connection;\n\n if (!connection) {\n return false;\n }\n if (connection.saveData) {\n return true;\n }\n if (connection.effectiveType?.includes(\"2g\")) {\n return true;\n }\n\n return false;\n}\n","import {\n GHOST_EVENT_THRESHOLD,\n LISTENER_OPTIONS,\n TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type {\n PreloadFn,\n PreloadFnFactory,\n PreloadPluginOptions,\n} from \"./types\";\nimport type {\n Params,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ndeclare module \"@real-router/core\" {\n interface Router {\n matchUrl?: (url: string) => State | undefined;\n }\n}\n\nconst STATE_CACHE_LIMIT = 32;\n\nexport class PreloadPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<PreloadPluginOptions>;\n readonly #getDependency: Parameters<PluginFactory>[1];\n readonly #removeExtensions: () => void;\n readonly #compiledPreloads = new Map<\n string,\n { fn: PreloadFn; factory: PreloadFnFactory }\n >();\n // Pre-resolved State cache keyed by anchor href. Populated when a hover/touch\n // resolves a route via router.matchUrl, consumed once via\n // router.getPreloadedState(href). Single-use semantics (delete-on-read) keep\n // the cache from drifting out of sync with current world state — once the\n // consumer commits the snapshot via api.navigateToState, the entry is gone.\n // Bounded with insertion-order eviction (#562).\n readonly #stateCache = new Map<string, State>();\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchTarget: EventTarget | null = null;\n #lastTouchTimeStamp = Number.NaN;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n getDependency: Parameters<PluginFactory>[1],\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#getDependency = getDependency;\n\n const cachedOptions = { ...options };\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => cachedOptions,\n getPreloadedState: (href: string): State | undefined => {\n const state = this.#stateCache.get(href);\n\n if (state) {\n this.#stateCache.delete(href);\n }\n\n return state;\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n },\n\n onStop: () => {\n this.#cleanup();\n },\n\n teardown: () => {\n this.#cleanup();\n this.#removeExtensions();\n },\n };\n }\n\n readonly #handleMouseOver = (event: MouseEvent): void => {\n if (this.#isGhostMouseEvent(event)) {\n return;\n }\n\n const anchor = this.#findAnchor(event.target);\n\n if (anchor === this.#currentAnchor) {\n return;\n }\n\n this.#cancelHover();\n this.#currentAnchor = anchor;\n\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\n return;\n }\n\n this.#hoverTimer = setTimeout(() => {\n this.#hoverTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, this.#options.delay);\n };\n\n readonly #handleTouchStart = (event: TouchEvent): void => {\n this.#lastTouchTarget = event.target;\n this.#lastTouchTimeStamp = event.timeStamp;\n\n this.#cancelTouch();\n\n const anchor = this.#findAnchor(event.target);\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload || event.touches.length === 0) {\n return;\n }\n\n this.#touchStartY = event.touches[0].clientY;\n\n this.#touchTimer = setTimeout(() => {\n this.#touchTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, TOUCH_PRELOAD_DELAY);\n };\n\n readonly #handleTouchMove = (event: TouchEvent): void => {\n if (this.#touchTimer === null || event.touches.length === 0) {\n return;\n }\n\n const deltaY = Math.abs(event.touches[0].clientY - this.#touchStartY);\n\n if (deltaY > TOUCH_SCROLL_THRESHOLD) {\n this.#cancelTouch();\n }\n };\n\n #findAnchor(target: EventTarget | null): HTMLAnchorElement | null {\n return target instanceof Element\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n }\n\n #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: PreloadFn; params: Params } | undefined {\n if (!anchor) {\n return undefined;\n }\n\n if (\"noPreload\" in anchor.dataset) {\n return undefined;\n }\n\n if (this.#options.networkAware && isSlowConnection()) {\n return undefined;\n }\n\n return this.#resolvePreload(anchor);\n }\n\n #resolvePreload(\n anchor: HTMLAnchorElement,\n ): { fn: PreloadFn; params: Params } | undefined {\n const state = this.#router.matchUrl?.(anchor.href);\n\n if (!state) {\n return undefined;\n }\n\n this.#cacheState(anchor.href, state);\n\n const config = this.#api.getRouteConfig(state.name);\n const factory =\n typeof config?.preload === \"function\"\n ? (config.preload as PreloadFnFactory)\n : undefined;\n\n if (!factory) {\n this.#compiledPreloads.delete(state.name);\n\n return undefined;\n }\n\n const cached = this.#compiledPreloads.get(state.name);\n\n if (cached?.factory === factory) {\n return { fn: cached.fn, params: state.params };\n }\n\n let fn: PreloadFn;\n\n try {\n fn = factory(this.#router, this.#getDependency);\n } catch {\n return undefined;\n }\n\n this.#compiledPreloads.set(state.name, { fn, factory });\n\n return { fn, params: state.params };\n }\n\n #cacheState(href: string, state: State): void {\n // Re-insert to refresh recency ordering (Map iteration is insertion order).\n if (this.#stateCache.has(href)) {\n this.#stateCache.delete(href);\n } else if (this.#stateCache.size >= STATE_CACHE_LIMIT) {\n // size >= LIMIT > 0 → iterator has at least one key.\n for (const oldest of this.#stateCache.keys()) {\n this.#stateCache.delete(oldest);\n\n break;\n }\n }\n\n this.#stateCache.set(href, state);\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n const delta = event.timeStamp - this.#lastTouchTimeStamp;\n\n return (\n delta >= 0 &&\n delta < GHOST_EVENT_THRESHOLD &&\n event.target === this.#lastTouchTarget\n );\n }\n\n #cancelHover(): void {\n if (this.#hoverTimer !== null) {\n clearTimeout(this.#hoverTimer);\n this.#hoverTimer = null;\n }\n\n this.#currentAnchor = null;\n }\n\n #cancelTouch(): void {\n if (this.#touchTimer !== null) {\n clearTimeout(this.#touchTimer);\n this.#touchTimer = null;\n }\n }\n\n #cleanup(): void {\n document.removeEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchTarget = null;\n this.#lastTouchTimeStamp = Number.NaN;\n this.#stateCache.clear();\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { defaultOptions } from \"./constants\";\nimport { PreloadPlugin } from \"./plugin\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function preloadPluginFactory(\n opts?: Partial<PreloadPluginOptions>,\n): PluginFactory {\n const options: Required<PreloadPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n if (!Number.isFinite(options.delay) || options.delay < 0) {\n options.delay = 0;\n }\n\n return function preloadPlugin(routerBase, getDependency) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n getDependency,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { DefaultDependencies, Params, Router } from "@real-router/types";
2
- import { DefaultDependencies as DefaultDependencies$1, PluginFactory } from "@real-router/core";
2
+ import { DefaultDependencies as DefaultDependencies$1, PluginFactory, State } from "@real-router/core";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  interface PreloadPluginOptions {
@@ -29,6 +29,7 @@ declare module "@real-router/core" {
29
29
  preload?: PreloadFnFactory<Dependencies>;
30
30
  }
31
31
  interface Router {
32
+ getPreloadedState?: (href: string) => State | undefined;
32
33
  getPreloadSettings(): PreloadPluginOptions;
33
34
  }
34
35
  } //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAEiB,oBAAA;;EAEf,KAAA;EAFe;EAIf,YAAA;AAAA;;;AAOF;;KAAY,SAAA,IAAa,MAAA,EAAQ,MAAA,KAAW,OAAA;;;;;;KAOhC,gBAAA,sBACW,mBAAA,GAAsB,mBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,SAAA;;;iBCjBW,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,IACd,aAAA;;;;YCGS,KAAA,sBAA2B,qBAAA;IACnC,OAAA,GAAU,gBAAA,CAAiB,YAAA;EAAA;EAAA,UAGnB,MAAA;IACR,kBAAA,IAAsB,oBAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;UAEiB,oBAAA;;EAEf,KAAA;EAFe;EAIf,YAAA;AAAA;;;AAOF;;KAAY,SAAA,IAAa,MAAA,EAAQ,MAAA,KAAW,OAAA;;;;;;KAOhC,gBAAA,sBACW,mBAAA,GAAsB,mBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,SAAA;;;iBCjBW,oBAAA,CACd,IAAA,GAAO,OAAA,CAAQ,oBAAA,IACd,aAAA;;;;YCGS,KAAA,sBAA2B,qBAAA;IACnC,OAAA,GAAU,gBAAA,CAAiB,YAAA;EAAA;EAAA,UAGnB,MAAA;IACR,iBAAA,IAAqB,IAAA,aAAiB,KAAA;IACtC,kBAAA,IAAsB,oBAAA;EAAA;AAAA"}
@@ -1,2 +1,2 @@
1
- import{getPluginApi as e}from"@real-router/core/api";const t={delay:65,networkAware:!0},n={capture:!0,passive:!0};function r(){let e=navigator.connection;return e?!!(e.saveData||e.effectiveType?.includes(`2g`)):!1}var i=class{#e;#t;#n;#r;#i;#a=new Map;#o=null;#s=null;#c=null;#l=0;#u=null;#d=NaN;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#n=n,this.#r=r;let i={...n};this.#i=t.extendRouter({getPreloadSettings:()=>i})}getPlugin(){return{onStart:()=>{document.addEventListener(`mouseover`,this.#f,n),document.addEventListener(`touchstart`,this.#p,n),document.addEventListener(`touchmove`,this.#m,n)},onStop:()=>{this.#x()},teardown:()=>{this.#x(),this.#i()}}}#f=e=>{if(this.#v(e))return;let t=this.#h(e.target);if(t===this.#o)return;this.#y(),this.#o=t;let n=this.#g(t);n&&(this.#s=setTimeout(()=>{this.#s=null,n.fn(n.params).catch(()=>{})},this.#n.delay))};#p=e=>{this.#u=e.target,this.#d=e.timeStamp,this.#b();let t=this.#h(e.target),n=this.#g(t);!n||e.touches.length===0||(this.#l=e.touches[0].clientY,this.#c=setTimeout(()=>{this.#c=null,n.fn(n.params).catch(()=>{})},100))};#m=e=>{this.#c===null||e.touches.length===0||Math.abs(e.touches[0].clientY-this.#l)>10&&this.#b()};#h(e){return e instanceof Element?e.closest(`a[href]`):null}#g(e){if(e&&!(`noPreload`in e.dataset)&&!(this.#n.networkAware&&r()))return this.#_(e)}#_(e){let t=this.#e.matchUrl?.(e.href);if(!t)return;let n=this.#t.getRouteConfig(t.name),r=typeof n?.preload==`function`?n.preload:void 0;if(!r){this.#a.delete(t.name);return}let i=this.#a.get(t.name);if(i?.factory===r)return{fn:i.fn,params:t.params};let a;try{a=r(this.#e,this.#r)}catch{return}return this.#a.set(t.name,{fn:a,factory:r}),{fn:a,params:t.params}}#v(e){let t=e.timeStamp-this.#d;return t>=0&&t<2500&&e.target===this.#u}#y(){this.#s!==null&&(clearTimeout(this.#s),this.#s=null),this.#o=null}#b(){this.#c!==null&&(clearTimeout(this.#c),this.#c=null)}#x(){document.removeEventListener(`mouseover`,this.#f,n),document.removeEventListener(`touchstart`,this.#p,n),document.removeEventListener(`touchmove`,this.#m,n),this.#y(),this.#b(),this.#u=null,this.#d=NaN}};function a(n){let r={...t,...n};return(!Number.isFinite(r.delay)||r.delay<0)&&(r.delay=0),function(t,n){return typeof document>`u`?{}:new i(t,e(t),r,n).getPlugin()}}export{a as preloadPluginFactory};
1
+ import{getPluginApi as e}from"@real-router/core/api";const t={delay:65,networkAware:!0},n={capture:!0,passive:!0};function r(){let e=navigator.connection;return e?!!(e.saveData||e.effectiveType?.includes(`2g`)):!1}var i=class{#e;#t;#n;#r;#i;#a=new Map;#o=new Map;#s=null;#c=null;#l=null;#u=0;#d=null;#f=NaN;constructor(e,t,n,r){this.#e=e,this.#t=t,this.#n=n,this.#r=r;let i={...n};this.#i=t.extendRouter({getPreloadSettings:()=>i,getPreloadedState:e=>{let t=this.#o.get(e);return t&&this.#o.delete(e),t}})}getPlugin(){return{onStart:()=>{document.addEventListener(`mouseover`,this.#p,n),document.addEventListener(`touchstart`,this.#m,n),document.addEventListener(`touchmove`,this.#h,n)},onStop:()=>{this.#C()},teardown:()=>{this.#C(),this.#i()}}}#p=e=>{if(this.#b(e))return;let t=this.#g(e.target);if(t===this.#s)return;this.#x(),this.#s=t;let n=this.#_(t);n&&(this.#c=setTimeout(()=>{this.#c=null,n.fn(n.params).catch(()=>{})},this.#n.delay))};#m=e=>{this.#d=e.target,this.#f=e.timeStamp,this.#S();let t=this.#g(e.target),n=this.#_(t);!n||e.touches.length===0||(this.#u=e.touches[0].clientY,this.#l=setTimeout(()=>{this.#l=null,n.fn(n.params).catch(()=>{})},100))};#h=e=>{this.#l===null||e.touches.length===0||Math.abs(e.touches[0].clientY-this.#u)>10&&this.#S()};#g(e){return e instanceof Element?e.closest(`a[href]`):null}#_(e){if(e&&!(`noPreload`in e.dataset)&&!(this.#n.networkAware&&r()))return this.#v(e)}#v(e){let t=this.#e.matchUrl?.(e.href);if(!t)return;this.#y(e.href,t);let n=this.#t.getRouteConfig(t.name),r=typeof n?.preload==`function`?n.preload:void 0;if(!r){this.#a.delete(t.name);return}let i=this.#a.get(t.name);if(i?.factory===r)return{fn:i.fn,params:t.params};let a;try{a=r(this.#e,this.#r)}catch{return}return this.#a.set(t.name,{fn:a,factory:r}),{fn:a,params:t.params}}#y(e,t){if(this.#o.has(e))this.#o.delete(e);else if(this.#o.size>=32)for(let e of this.#o.keys()){this.#o.delete(e);break}this.#o.set(e,t)}#b(e){let t=e.timeStamp-this.#f;return t>=0&&t<2500&&e.target===this.#d}#x(){this.#c!==null&&(clearTimeout(this.#c),this.#c=null),this.#s=null}#S(){this.#l!==null&&(clearTimeout(this.#l),this.#l=null)}#C(){document.removeEventListener(`mouseover`,this.#p,n),document.removeEventListener(`touchstart`,this.#m,n),document.removeEventListener(`touchmove`,this.#h,n),this.#x(),this.#S(),this.#d=null,this.#f=NaN,this.#o.clear()}};function a(n){let r={...t,...n};return(!Number.isFinite(r.delay)||r.delay<0)&&(r.delay=0),function(t,n){return typeof document>`u`?{}:new i(t,e(t),r,n).getPlugin()}}export{a as preloadPluginFactory};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#router","#api","#options","#getDependency","#removeExtensions","#compiledPreloads","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#findAnchor","#currentAnchor","#cancelHover","#resolveAnchorPreload","#hoverTimer","#lastTouchTarget","#lastTouchTimeStamp","#cancelTouch","#touchStartY","#touchTimer","#resolvePreload"],"sources":["../../src/constants.ts","../../src/network.ts","../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { PreloadPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<PreloadPluginOptions> = {\n delay: 65,\n networkAware: true,\n};\n\nexport const GHOST_EVENT_THRESHOLD = 2500;\n\nexport const TOUCH_SCROLL_THRESHOLD = 10;\n\nexport const TOUCH_PRELOAD_DELAY = 100;\n\nexport const LISTENER_OPTIONS: AddEventListenerOptions = {\n capture: true,\n passive: true,\n};\n","type NetworkConnection =\n | { saveData?: boolean; effectiveType?: string }\n | undefined;\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkConnection;\n}\n\nexport function isSlowConnection(): boolean {\n const connection = (navigator as NavigatorWithConnection).connection;\n\n if (!connection) {\n return false;\n }\n if (connection.saveData) {\n return true;\n }\n if (connection.effectiveType?.includes(\"2g\")) {\n return true;\n }\n\n return false;\n}\n","import {\n GHOST_EVENT_THRESHOLD,\n LISTENER_OPTIONS,\n TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type {\n PreloadFn,\n PreloadFnFactory,\n PreloadPluginOptions,\n} from \"./types\";\nimport type {\n Params,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ndeclare module \"@real-router/core\" {\n interface Router {\n matchUrl?: (url: string) => State | undefined;\n }\n}\n\nexport class PreloadPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<PreloadPluginOptions>;\n readonly #getDependency: Parameters<PluginFactory>[1];\n readonly #removeExtensions: () => void;\n readonly #compiledPreloads = new Map<\n string,\n { fn: PreloadFn; factory: PreloadFnFactory }\n >();\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchTarget: EventTarget | null = null;\n #lastTouchTimeStamp = Number.NaN;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n getDependency: Parameters<PluginFactory>[1],\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#getDependency = getDependency;\n\n const cachedOptions = { ...options };\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => cachedOptions,\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n },\n\n onStop: () => {\n this.#cleanup();\n },\n\n teardown: () => {\n this.#cleanup();\n this.#removeExtensions();\n },\n };\n }\n\n readonly #handleMouseOver = (event: MouseEvent): void => {\n if (this.#isGhostMouseEvent(event)) {\n return;\n }\n\n const anchor = this.#findAnchor(event.target);\n\n if (anchor === this.#currentAnchor) {\n return;\n }\n\n this.#cancelHover();\n this.#currentAnchor = anchor;\n\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\n return;\n }\n\n this.#hoverTimer = setTimeout(() => {\n this.#hoverTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, this.#options.delay);\n };\n\n readonly #handleTouchStart = (event: TouchEvent): void => {\n this.#lastTouchTarget = event.target;\n this.#lastTouchTimeStamp = event.timeStamp;\n\n this.#cancelTouch();\n\n const anchor = this.#findAnchor(event.target);\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload || event.touches.length === 0) {\n return;\n }\n\n this.#touchStartY = event.touches[0].clientY;\n\n this.#touchTimer = setTimeout(() => {\n this.#touchTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, TOUCH_PRELOAD_DELAY);\n };\n\n readonly #handleTouchMove = (event: TouchEvent): void => {\n if (this.#touchTimer === null || event.touches.length === 0) {\n return;\n }\n\n const deltaY = Math.abs(event.touches[0].clientY - this.#touchStartY);\n\n if (deltaY > TOUCH_SCROLL_THRESHOLD) {\n this.#cancelTouch();\n }\n };\n\n #findAnchor(target: EventTarget | null): HTMLAnchorElement | null {\n return target instanceof Element\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n }\n\n #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: PreloadFn; params: Params } | undefined {\n if (!anchor) {\n return undefined;\n }\n\n if (\"noPreload\" in anchor.dataset) {\n return undefined;\n }\n\n if (this.#options.networkAware && isSlowConnection()) {\n return undefined;\n }\n\n return this.#resolvePreload(anchor);\n }\n\n #resolvePreload(\n anchor: HTMLAnchorElement,\n ): { fn: PreloadFn; params: Params } | undefined {\n const state = this.#router.matchUrl?.(anchor.href);\n\n if (!state) {\n return undefined;\n }\n\n const config = this.#api.getRouteConfig(state.name);\n const factory =\n typeof config?.preload === \"function\"\n ? (config.preload as PreloadFnFactory)\n : undefined;\n\n if (!factory) {\n this.#compiledPreloads.delete(state.name);\n\n return undefined;\n }\n\n const cached = this.#compiledPreloads.get(state.name);\n\n if (cached?.factory === factory) {\n return { fn: cached.fn, params: state.params };\n }\n\n let fn: PreloadFn;\n\n try {\n fn = factory(this.#router, this.#getDependency);\n } catch {\n return undefined;\n }\n\n this.#compiledPreloads.set(state.name, { fn, factory });\n\n return { fn, params: state.params };\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n const delta = event.timeStamp - this.#lastTouchTimeStamp;\n\n return (\n delta >= 0 &&\n delta < GHOST_EVENT_THRESHOLD &&\n event.target === this.#lastTouchTarget\n );\n }\n\n #cancelHover(): void {\n if (this.#hoverTimer !== null) {\n clearTimeout(this.#hoverTimer);\n this.#hoverTimer = null;\n }\n\n this.#currentAnchor = null;\n }\n\n #cancelTouch(): void {\n if (this.#touchTimer !== null) {\n clearTimeout(this.#touchTimer);\n this.#touchTimer = null;\n }\n }\n\n #cleanup(): void {\n document.removeEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchTarget = null;\n this.#lastTouchTimeStamp = Number.NaN;\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { defaultOptions } from \"./constants\";\nimport { PreloadPlugin } from \"./plugin\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function preloadPluginFactory(\n opts?: Partial<PreloadPluginOptions>,\n): PluginFactory {\n const options: Required<PreloadPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n if (!Number.isFinite(options.delay) || options.delay < 0) {\n options.delay = 0;\n }\n\n return function preloadPlugin(routerBase, getDependency) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n getDependency,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["#router","#api","#options","#getDependency","#removeExtensions","#compiledPreloads","#stateCache","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#findAnchor","#currentAnchor","#cancelHover","#resolveAnchorPreload","#hoverTimer","#lastTouchTarget","#lastTouchTimeStamp","#cancelTouch","#touchStartY","#touchTimer","#resolvePreload","#cacheState"],"sources":["../../src/constants.ts","../../src/network.ts","../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { PreloadPluginOptions } from \"./types\";\n\nexport const defaultOptions: Required<PreloadPluginOptions> = {\n delay: 65,\n networkAware: true,\n};\n\nexport const GHOST_EVENT_THRESHOLD = 2500;\n\nexport const TOUCH_SCROLL_THRESHOLD = 10;\n\nexport const TOUCH_PRELOAD_DELAY = 100;\n\nexport const LISTENER_OPTIONS: AddEventListenerOptions = {\n capture: true,\n passive: true,\n};\n","type NetworkConnection =\n | { saveData?: boolean; effectiveType?: string }\n | undefined;\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkConnection;\n}\n\nexport function isSlowConnection(): boolean {\n const connection = (navigator as NavigatorWithConnection).connection;\n\n if (!connection) {\n return false;\n }\n if (connection.saveData) {\n return true;\n }\n if (connection.effectiveType?.includes(\"2g\")) {\n return true;\n }\n\n return false;\n}\n","import {\n GHOST_EVENT_THRESHOLD,\n LISTENER_OPTIONS,\n TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type {\n PreloadFn,\n PreloadFnFactory,\n PreloadPluginOptions,\n} from \"./types\";\nimport type {\n Params,\n Plugin,\n PluginFactory,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\ndeclare module \"@real-router/core\" {\n interface Router {\n matchUrl?: (url: string) => State | undefined;\n }\n}\n\nconst STATE_CACHE_LIMIT = 32;\n\nexport class PreloadPlugin {\n readonly #router: Router;\n readonly #api: PluginApi;\n readonly #options: Required<PreloadPluginOptions>;\n readonly #getDependency: Parameters<PluginFactory>[1];\n readonly #removeExtensions: () => void;\n readonly #compiledPreloads = new Map<\n string,\n { fn: PreloadFn; factory: PreloadFnFactory }\n >();\n // Pre-resolved State cache keyed by anchor href. Populated when a hover/touch\n // resolves a route via router.matchUrl, consumed once via\n // router.getPreloadedState(href). Single-use semantics (delete-on-read) keep\n // the cache from drifting out of sync with current world state — once the\n // consumer commits the snapshot via api.navigateToState, the entry is gone.\n // Bounded with insertion-order eviction (#562).\n readonly #stateCache = new Map<string, State>();\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchTarget: EventTarget | null = null;\n #lastTouchTimeStamp = Number.NaN;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n getDependency: Parameters<PluginFactory>[1],\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n this.#getDependency = getDependency;\n\n const cachedOptions = { ...options };\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => cachedOptions,\n getPreloadedState: (href: string): State | undefined => {\n const state = this.#stateCache.get(href);\n\n if (state) {\n this.#stateCache.delete(href);\n }\n\n return state;\n },\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.addEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n },\n\n onStop: () => {\n this.#cleanup();\n },\n\n teardown: () => {\n this.#cleanup();\n this.#removeExtensions();\n },\n };\n }\n\n readonly #handleMouseOver = (event: MouseEvent): void => {\n if (this.#isGhostMouseEvent(event)) {\n return;\n }\n\n const anchor = this.#findAnchor(event.target);\n\n if (anchor === this.#currentAnchor) {\n return;\n }\n\n this.#cancelHover();\n this.#currentAnchor = anchor;\n\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\n return;\n }\n\n this.#hoverTimer = setTimeout(() => {\n this.#hoverTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, this.#options.delay);\n };\n\n readonly #handleTouchStart = (event: TouchEvent): void => {\n this.#lastTouchTarget = event.target;\n this.#lastTouchTimeStamp = event.timeStamp;\n\n this.#cancelTouch();\n\n const anchor = this.#findAnchor(event.target);\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload || event.touches.length === 0) {\n return;\n }\n\n this.#touchStartY = event.touches[0].clientY;\n\n this.#touchTimer = setTimeout(() => {\n this.#touchTimer = null;\n preload.fn(preload.params).catch(() => {});\n }, TOUCH_PRELOAD_DELAY);\n };\n\n readonly #handleTouchMove = (event: TouchEvent): void => {\n if (this.#touchTimer === null || event.touches.length === 0) {\n return;\n }\n\n const deltaY = Math.abs(event.touches[0].clientY - this.#touchStartY);\n\n if (deltaY > TOUCH_SCROLL_THRESHOLD) {\n this.#cancelTouch();\n }\n };\n\n #findAnchor(target: EventTarget | null): HTMLAnchorElement | null {\n return target instanceof Element\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n }\n\n #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: PreloadFn; params: Params } | undefined {\n if (!anchor) {\n return undefined;\n }\n\n if (\"noPreload\" in anchor.dataset) {\n return undefined;\n }\n\n if (this.#options.networkAware && isSlowConnection()) {\n return undefined;\n }\n\n return this.#resolvePreload(anchor);\n }\n\n #resolvePreload(\n anchor: HTMLAnchorElement,\n ): { fn: PreloadFn; params: Params } | undefined {\n const state = this.#router.matchUrl?.(anchor.href);\n\n if (!state) {\n return undefined;\n }\n\n this.#cacheState(anchor.href, state);\n\n const config = this.#api.getRouteConfig(state.name);\n const factory =\n typeof config?.preload === \"function\"\n ? (config.preload as PreloadFnFactory)\n : undefined;\n\n if (!factory) {\n this.#compiledPreloads.delete(state.name);\n\n return undefined;\n }\n\n const cached = this.#compiledPreloads.get(state.name);\n\n if (cached?.factory === factory) {\n return { fn: cached.fn, params: state.params };\n }\n\n let fn: PreloadFn;\n\n try {\n fn = factory(this.#router, this.#getDependency);\n } catch {\n return undefined;\n }\n\n this.#compiledPreloads.set(state.name, { fn, factory });\n\n return { fn, params: state.params };\n }\n\n #cacheState(href: string, state: State): void {\n // Re-insert to refresh recency ordering (Map iteration is insertion order).\n if (this.#stateCache.has(href)) {\n this.#stateCache.delete(href);\n } else if (this.#stateCache.size >= STATE_CACHE_LIMIT) {\n // size >= LIMIT > 0 → iterator has at least one key.\n for (const oldest of this.#stateCache.keys()) {\n this.#stateCache.delete(oldest);\n\n break;\n }\n }\n\n this.#stateCache.set(href, state);\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n const delta = event.timeStamp - this.#lastTouchTimeStamp;\n\n return (\n delta >= 0 &&\n delta < GHOST_EVENT_THRESHOLD &&\n event.target === this.#lastTouchTarget\n );\n }\n\n #cancelHover(): void {\n if (this.#hoverTimer !== null) {\n clearTimeout(this.#hoverTimer);\n this.#hoverTimer = null;\n }\n\n this.#currentAnchor = null;\n }\n\n #cancelTouch(): void {\n if (this.#touchTimer !== null) {\n clearTimeout(this.#touchTimer);\n this.#touchTimer = null;\n }\n }\n\n #cleanup(): void {\n document.removeEventListener(\n \"mouseover\",\n this.#handleMouseOver,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchstart\",\n this.#handleTouchStart,\n LISTENER_OPTIONS,\n );\n document.removeEventListener(\n \"touchmove\",\n this.#handleTouchMove,\n LISTENER_OPTIONS,\n );\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchTarget = null;\n this.#lastTouchTimeStamp = Number.NaN;\n this.#stateCache.clear();\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { defaultOptions } from \"./constants\";\nimport { PreloadPlugin } from \"./plugin\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { PluginFactory, Router } from \"@real-router/core\";\n\nexport function preloadPluginFactory(\n opts?: Partial<PreloadPluginOptions>,\n): PluginFactory {\n const options: Required<PreloadPluginOptions> = {\n ...defaultOptions,\n ...opts,\n };\n\n if (!Number.isFinite(options.delay) || options.delay < 0) {\n options.delay = 0;\n }\n\n return function preloadPlugin(routerBase, getDependency) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n getDependency,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/preload-plugin",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "commonjs",
5
5
  "description": "Preload plugin — trigger data preloading on navigation intent (hover, touch)",
6
6
  "main": "./dist/cjs/index.js",
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "sideEffects": false,
47
47
  "dependencies": {
48
- "@real-router/core": "^0.50.0",
49
- "@real-router/types": "^0.34.0"
48
+ "@real-router/core": "^0.52.0",
49
+ "@real-router/types": "^0.35.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "jsdom": "28.1.0"
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/method-signature-style -- method syntax required for declaration merging overload (property syntax causes TS2717) */
2
2
  import type { PreloadFnFactory, PreloadPluginOptions } from "./types";
3
- import type { DefaultDependencies } from "@real-router/core";
3
+ import type { DefaultDependencies, State } from "@real-router/core";
4
4
 
5
5
  export { preloadPluginFactory } from "./factory";
6
6
 
@@ -16,6 +16,7 @@ declare module "@real-router/core" {
16
16
  }
17
17
 
18
18
  interface Router {
19
+ getPreloadedState?: (href: string) => State | undefined;
19
20
  getPreloadSettings(): PreloadPluginOptions;
20
21
  }
21
22
  }
package/src/plugin.ts CHANGED
@@ -26,6 +26,8 @@ declare module "@real-router/core" {
26
26
  }
27
27
  }
28
28
 
29
+ const STATE_CACHE_LIMIT = 32;
30
+
29
31
  export class PreloadPlugin {
30
32
  readonly #router: Router;
31
33
  readonly #api: PluginApi;
@@ -36,6 +38,13 @@ export class PreloadPlugin {
36
38
  string,
37
39
  { fn: PreloadFn; factory: PreloadFnFactory }
38
40
  >();
41
+ // Pre-resolved State cache keyed by anchor href. Populated when a hover/touch
42
+ // resolves a route via router.matchUrl, consumed once via
43
+ // router.getPreloadedState(href). Single-use semantics (delete-on-read) keep
44
+ // the cache from drifting out of sync with current world state — once the
45
+ // consumer commits the snapshot via api.navigateToState, the entry is gone.
46
+ // Bounded with insertion-order eviction (#562).
47
+ readonly #stateCache = new Map<string, State>();
39
48
 
40
49
  #currentAnchor: HTMLAnchorElement | null = null;
41
50
  #hoverTimer: ReturnType<typeof setTimeout> | null = null;
@@ -59,6 +68,15 @@ export class PreloadPlugin {
59
68
 
60
69
  this.#removeExtensions = api.extendRouter({
61
70
  getPreloadSettings: () => cachedOptions,
71
+ getPreloadedState: (href: string): State | undefined => {
72
+ const state = this.#stateCache.get(href);
73
+
74
+ if (state) {
75
+ this.#stateCache.delete(href);
76
+ }
77
+
78
+ return state;
79
+ },
62
80
  });
63
81
  }
64
82
 
@@ -185,6 +203,8 @@ export class PreloadPlugin {
185
203
  return undefined;
186
204
  }
187
205
 
206
+ this.#cacheState(anchor.href, state);
207
+
188
208
  const config = this.#api.getRouteConfig(state.name);
189
209
  const factory =
190
210
  typeof config?.preload === "function"
@@ -216,6 +236,22 @@ export class PreloadPlugin {
216
236
  return { fn, params: state.params };
217
237
  }
218
238
 
239
+ #cacheState(href: string, state: State): void {
240
+ // Re-insert to refresh recency ordering (Map iteration is insertion order).
241
+ if (this.#stateCache.has(href)) {
242
+ this.#stateCache.delete(href);
243
+ } else if (this.#stateCache.size >= STATE_CACHE_LIMIT) {
244
+ // size >= LIMIT > 0 → iterator has at least one key.
245
+ for (const oldest of this.#stateCache.keys()) {
246
+ this.#stateCache.delete(oldest);
247
+
248
+ break;
249
+ }
250
+ }
251
+
252
+ this.#stateCache.set(href, state);
253
+ }
254
+
219
255
  #isGhostMouseEvent(event: MouseEvent): boolean {
220
256
  const delta = event.timeStamp - this.#lastTouchTimeStamp;
221
257
 
@@ -263,5 +299,6 @@ export class PreloadPlugin {
263
299
  this.#cancelTouch();
264
300
  this.#lastTouchTarget = null;
265
301
  this.#lastTouchTimeStamp = Number.NaN;
302
+ this.#stateCache.clear();
266
303
  }
267
304
  }