@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.
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +2 -1
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -1
- package/src/plugin.ts +37 -0
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
package/dist/cjs/index.d.ts.map
CHANGED
|
@@ -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=
|
|
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
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -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":"0GAEA,MAAa,EAAiD,CAC5D,MAAO,GACP,aAAc,GACf,CAQY,EAA4C,CACvD,QAAS,GACT,QAAS,GACV,CCRD,SAAgB,GAA4B,CAC1C,IAAM,EAAc,UAAsC,WAY1D,OAVK,EAML,GAHI,EAAW,UAGX,EAAW,eAAe,SAAS,KAAK,EALnC,GCgBX,IAAa,EAAb,KAA2B,CACzB,GACA,GACA,GACA,GACA,GACA,GAA6B,IAAI,IAKjC,GAA2C,KAC3C,GAAoD,KACpD,GAAoD,KACpD,GAAe,EACf,GAAuC,KACvC,GAAsB,IAEtB,YACE,EACA,EACA,EACA,EACA,CACA,MAAA,EAAe,EACf,MAAA,EAAY,EACZ,MAAA,EAAgB,EAChB,MAAA,EAAsB,EAEtB,IAAM,EAAgB,CAAE,GAAG,EAAS,CAEpC,MAAA,EAAyB,EAAI,aAAa,CACxC,uBAA0B,EAC3B,CAAC,CAGJ,WAAoB,CAClB,MAAO,CACL,YAAe,CACb,SAAS,iBACP,YACA,MAAA,EACA,EACD,CACD,SAAS,iBACP,aACA,MAAA,EACA,EACD,CACD,SAAS,iBACP,YACA,MAAA,EACA,EACD,EAGH,WAAc,CACZ,MAAA,GAAe,EAGjB,aAAgB,CACd,MAAA,GAAe,CACf,MAAA,GAAwB,EAE3B,CAGH,GAA6B,GAA4B,CACvD,GAAI,MAAA,EAAwB,EAAM,CAChC,OAGF,IAAM,EAAS,MAAA,EAAiB,EAAM,OAAO,CAE7C,GAAI,IAAW,MAAA,EACb,OAGF,MAAA,GAAmB,CACnB,MAAA,EAAsB,EAEtB,IAAM,EAAU,MAAA,EAA2B,EAAO,CAE7C,IAIL,MAAA,EAAmB,eAAiB,CAClC,MAAA,EAAmB,KACnB,EAAQ,GAAG,EAAQ,OAAO,CAAC,UAAY,GAAG,EACzC,MAAA,EAAc,MAAM,GAGzB,GAA8B,GAA4B,CACxD,MAAA,EAAwB,EAAM,OAC9B,MAAA,EAA2B,EAAM,UAEjC,MAAA,GAAmB,CAEnB,IAAM,EAAS,MAAA,EAAiB,EAAM,OAAO,CACvC,EAAU,MAAA,EAA2B,EAAO,CAE9C,CAAC,GAAW,EAAM,QAAQ,SAAW,IAIzC,MAAA,EAAoB,EAAM,QAAQ,GAAG,QAErC,MAAA,EAAmB,eAAiB,CAClC,MAAA,EAAmB,KACnB,EAAQ,GAAG,EAAQ,OAAO,CAAC,UAAY,GAAG,MACrB,GAGzB,GAA6B,GAA4B,CACnD,MAAA,IAAqB,MAAQ,EAAM,QAAQ,SAAW,GAI3C,KAAK,IAAI,EAAM,QAAQ,GAAG,QAAU,MAAA,EAAkB,CAAA,IAGnE,MAAA,GAAmB,EAIvB,GAAY,EAAsD,CAChE,OAAO,aAAkB,QACrB,EAAO,QAA2B,UAAU,CAC5C,KAGN,GACE,EAC+C,CAC1C,MAID,gBAAe,EAAO,UAItB,QAAA,EAAc,cAAgB,GAAkB,EAIpD,OAAO,MAAA,EAAqB,EAAO,CAGrC,GACE,EAC+C,CAC/C,IAAM,EAAQ,MAAA,EAAa,WAAW,EAAO,KAAK,CAElD,GAAI,CAAC,EACH,OAGF,IAAM,EAAS,MAAA,EAAU,eAAe,EAAM,KAAK,CAC7C,EACJ,OAAO,GAAQ,SAAY,WACtB,EAAO,QACR,IAAA,GAEN,GAAI,CAAC,EAAS,CACZ,MAAA,EAAuB,OAAO,EAAM,KAAK,CAEzC,OAGF,IAAM,EAAS,MAAA,EAAuB,IAAI,EAAM,KAAK,CAErD,GAAI,GAAQ,UAAY,EACtB,MAAO,CAAE,GAAI,EAAO,GAAI,OAAQ,EAAM,OAAQ,CAGhD,IAAI,EAEJ,GAAI,CACF,EAAK,EAAQ,MAAA,EAAc,MAAA,EAAoB,MACzC,CACN,OAKF,OAFA,MAAA,EAAuB,IAAI,EAAM,KAAM,CAAE,KAAI,UAAS,CAAC,CAEhD,CAAE,KAAI,OAAQ,EAAM,OAAQ,CAGrC,GAAmB,EAA4B,CAC7C,IAAM,EAAQ,EAAM,UAAY,MAAA,EAEhC,OACE,GAAS,GACT,EAAA,MACA,EAAM,SAAW,MAAA,EAIrB,IAAqB,CACf,MAAA,IAAqB,OACvB,aAAa,MAAA,EAAiB,CAC9B,MAAA,EAAmB,MAGrB,MAAA,EAAsB,KAGxB,IAAqB,CACf,MAAA,IAAqB,OACvB,aAAa,MAAA,EAAiB,CAC9B,MAAA,EAAmB,MAIvB,IAAiB,CACf,SAAS,oBACP,YACA,MAAA,EACA,EACD,CACD,SAAS,oBACP,aACA,MAAA,EACA,EACD,CACD,SAAS,oBACP,YACA,MAAA,EACA,EACD,CAED,MAAA,GAAmB,CACnB,MAAA,GAAmB,CACnB,MAAA,EAAwB,KACxB,MAAA,EAA2B,MChQ/B,SAAgB,EACd,EACe,CACf,IAAM,EAA0C,CAC9C,GAAG,EACH,GAAG,EACJ,CAMD,OAJI,CAAC,OAAO,SAAS,EAAQ,MAAM,EAAI,EAAQ,MAAQ,KACrD,EAAQ,MAAQ,GAGX,SAAuB,EAAY,EAAe,CAYvD,OAXI,OAAO,SAAa,IACf,EAAE,CAGI,IAAI,EACjB,GAAA,EAAA,EAAA,cACa,EAAW,CACxB,EACA,EACD,CAEa,WAAW"}
|
|
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":"0GAEA,MAAa,EAAiD,CAC5D,MAAO,GACP,aAAc,GACf,CAQY,EAA4C,CACvD,QAAS,GACT,QAAS,GACV,CCRD,SAAgB,GAA4B,CAC1C,IAAM,EAAc,UAAsC,WAY1D,OAVK,EAML,GAHI,EAAW,UAGX,EAAW,eAAe,SAAS,KAAK,EALnC,GCkBX,IAAa,EAAb,KAA2B,CACzB,GACA,GACA,GACA,GACA,GACA,GAA6B,IAAI,IAUjC,GAAuB,IAAI,IAE3B,GAA2C,KAC3C,GAAoD,KACpD,GAAoD,KACpD,GAAe,EACf,GAAuC,KACvC,GAAsB,IAEtB,YACE,EACA,EACA,EACA,EACA,CACA,MAAA,EAAe,EACf,MAAA,EAAY,EACZ,MAAA,EAAgB,EAChB,MAAA,EAAsB,EAEtB,IAAM,EAAgB,CAAE,GAAG,EAAS,CAEpC,MAAA,EAAyB,EAAI,aAAa,CACxC,uBAA0B,EAC1B,kBAAoB,GAAoC,CACtD,IAAM,EAAQ,MAAA,EAAiB,IAAI,EAAK,CAMxC,OAJI,GACF,MAAA,EAAiB,OAAO,EAAK,CAGxB,GAEV,CAAC,CAGJ,WAAoB,CAClB,MAAO,CACL,YAAe,CACb,SAAS,iBACP,YACA,MAAA,EACA,EACD,CACD,SAAS,iBACP,aACA,MAAA,EACA,EACD,CACD,SAAS,iBACP,YACA,MAAA,EACA,EACD,EAGH,WAAc,CACZ,MAAA,GAAe,EAGjB,aAAgB,CACd,MAAA,GAAe,CACf,MAAA,GAAwB,EAE3B,CAGH,GAA6B,GAA4B,CACvD,GAAI,MAAA,EAAwB,EAAM,CAChC,OAGF,IAAM,EAAS,MAAA,EAAiB,EAAM,OAAO,CAE7C,GAAI,IAAW,MAAA,EACb,OAGF,MAAA,GAAmB,CACnB,MAAA,EAAsB,EAEtB,IAAM,EAAU,MAAA,EAA2B,EAAO,CAE7C,IAIL,MAAA,EAAmB,eAAiB,CAClC,MAAA,EAAmB,KACnB,EAAQ,GAAG,EAAQ,OAAO,CAAC,UAAY,GAAG,EACzC,MAAA,EAAc,MAAM,GAGzB,GAA8B,GAA4B,CACxD,MAAA,EAAwB,EAAM,OAC9B,MAAA,EAA2B,EAAM,UAEjC,MAAA,GAAmB,CAEnB,IAAM,EAAS,MAAA,EAAiB,EAAM,OAAO,CACvC,EAAU,MAAA,EAA2B,EAAO,CAE9C,CAAC,GAAW,EAAM,QAAQ,SAAW,IAIzC,MAAA,EAAoB,EAAM,QAAQ,GAAG,QAErC,MAAA,EAAmB,eAAiB,CAClC,MAAA,EAAmB,KACnB,EAAQ,GAAG,EAAQ,OAAO,CAAC,UAAY,GAAG,MACrB,GAGzB,GAA6B,GAA4B,CACnD,MAAA,IAAqB,MAAQ,EAAM,QAAQ,SAAW,GAI3C,KAAK,IAAI,EAAM,QAAQ,GAAG,QAAU,MAAA,EAEzC,CAAA,IACR,MAAA,GAAmB,EAIvB,GAAY,EAAsD,CAChE,OAAO,aAAkB,QACrB,EAAO,QAA2B,UAAU,CAC5C,KAGN,GACE,EAC+C,CAC1C,MAID,gBAAe,EAAO,UAItB,QAAA,EAAc,cAAgB,GAAkB,EAIpD,OAAO,MAAA,EAAqB,EAAO,CAGrC,GACE,EAC+C,CAC/C,IAAM,EAAQ,MAAA,EAAa,WAAW,EAAO,KAAK,CAElD,GAAI,CAAC,EACH,OAGF,MAAA,EAAiB,EAAO,KAAM,EAAM,CAEpC,IAAM,EAAS,MAAA,EAAU,eAAe,EAAM,KAAK,CAC7C,EACJ,OAAO,GAAQ,SAAY,WACtB,EAAO,QACR,IAAA,GAEN,GAAI,CAAC,EAAS,CACZ,MAAA,EAAuB,OAAO,EAAM,KAAK,CAEzC,OAGF,IAAM,EAAS,MAAA,EAAuB,IAAI,EAAM,KAAK,CAErD,GAAI,GAAQ,UAAY,EACtB,MAAO,CAAE,GAAI,EAAO,GAAI,OAAQ,EAAM,OAAQ,CAGhD,IAAI,EAEJ,GAAI,CACF,EAAK,EAAQ,MAAA,EAAc,MAAA,EAAoB,MACzC,CACN,OAKF,OAFA,MAAA,EAAuB,IAAI,EAAM,KAAM,CAAE,KAAI,UAAS,CAAC,CAEhD,CAAE,KAAI,OAAQ,EAAM,OAAQ,CAGrC,GAAY,EAAc,EAAoB,CAE5C,GAAI,MAAA,EAAiB,IAAI,EAAK,CAC5B,MAAA,EAAiB,OAAO,EAAK,SACpB,MAAA,EAAiB,MAAQ,GAElC,IAAK,IAAM,KAAU,MAAA,EAAiB,MAAM,CAAE,CAC5C,MAAA,EAAiB,OAAO,EAAO,CAE/B,MAIJ,MAAA,EAAiB,IAAI,EAAM,EAAM,CAGnC,GAAmB,EAA4B,CAC7C,IAAM,EAAQ,EAAM,UAAY,MAAA,EAEhC,OACE,GAAS,GACT,EAAA,MACA,EAAM,SAAW,MAAA,EAIrB,IAAqB,CACf,MAAA,IAAqB,OACvB,aAAa,MAAA,EAAiB,CAC9B,MAAA,EAAmB,MAGrB,MAAA,EAAsB,KAGxB,IAAqB,CACf,MAAA,IAAqB,OACvB,aAAa,MAAA,EAAiB,CAC9B,MAAA,EAAmB,MAIvB,IAAiB,CACf,SAAS,oBACP,YACA,MAAA,EACA,EACD,CACD,SAAS,oBACP,aACA,MAAA,EACA,EACD,CACD,SAAS,oBACP,YACA,MAAA,EACA,EACD,CAED,MAAA,GAAmB,CACnB,MAAA,GAAmB,CACnB,MAAA,EAAwB,KACxB,MAAA,EAA2B,IAC3B,MAAA,EAAiB,OAAO,GCrS5B,SAAgB,EACd,EACe,CACf,IAAM,EAA0C,CAC9C,GAAG,EACH,GAAG,EACJ,CAMD,OAJI,CAAC,OAAO,SAAS,EAAQ,MAAM,EAAI,EAAQ,MAAQ,KACrD,EAAQ,MAAQ,GAGX,SAAuB,EAAY,EAAe,CAYvD,OAXI,OAAO,SAAa,IACf,EAAE,CAUJ,IAPY,EACjB,GAAA,EAAA,EAAA,cACa,EAAW,CACxB,EACA,EAGW,CAAC,WAAW"}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -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
|
package/dist/esm/index.d.mts.map
CHANGED
|
@@ -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"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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=
|
|
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
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -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
|
+
"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.
|
|
49
|
-
"@real-router/types": "^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
|
}
|