@real-router/preload-plugin 0.2.1 → 0.3.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 +16 -4
- 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 +16 -4
- 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 +6 -4
- package/src/constants.ts +5 -0
- package/src/factory.ts +6 -1
- package/src/index.ts +9 -5
- package/src/plugin.ts +100 -60
- package/src/types.ts +20 -0
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Params,
|
|
1
|
+
import { DefaultDependencies, Params, Router } from "@real-router/types";
|
|
2
|
+
import { DefaultDependencies as DefaultDependencies$1, PluginFactory } from "@real-router/core";
|
|
2
3
|
|
|
3
4
|
//#region src/types.d.ts
|
|
4
5
|
interface PreloadPluginOptions {
|
|
@@ -7,19 +8,30 @@ interface PreloadPluginOptions {
|
|
|
7
8
|
/** Check saveData/2g and disable preloading on slow connections. @default true */
|
|
8
9
|
networkAware?: boolean;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Preload function called when navigation intent is detected (hover, touch).
|
|
13
|
+
* Fire-and-forget: return values and errors are discarded.
|
|
14
|
+
*/
|
|
15
|
+
type PreloadFn = (params: Params) => Promise<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Factory function for creating preload hooks.
|
|
18
|
+
* Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
|
|
19
|
+
* Factory runs once at first invocation; the returned function is cached per route.
|
|
20
|
+
*/
|
|
21
|
+
type PreloadFnFactory<Dependencies extends DefaultDependencies = DefaultDependencies> = (router: Router<Dependencies>, getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K]) => PreloadFn;
|
|
10
22
|
//#endregion
|
|
11
23
|
//#region src/factory.d.ts
|
|
12
24
|
declare function preloadPluginFactory(opts?: Partial<PreloadPluginOptions>): PluginFactory;
|
|
13
25
|
//#endregion
|
|
14
26
|
//#region src/index.d.ts
|
|
15
27
|
declare module "@real-router/core" {
|
|
16
|
-
interface Route {
|
|
17
|
-
preload?:
|
|
28
|
+
interface Route<Dependencies extends DefaultDependencies$1> {
|
|
29
|
+
preload?: PreloadFnFactory<Dependencies>;
|
|
18
30
|
}
|
|
19
31
|
interface Router {
|
|
20
32
|
getPreloadSettings(): PreloadPluginOptions;
|
|
21
33
|
}
|
|
22
34
|
} //# sourceMappingURL=index.d.ts.map
|
|
23
35
|
//#endregion
|
|
24
|
-
export { type PreloadPluginOptions, preloadPluginFactory };
|
|
36
|
+
export { type PreloadFn, type PreloadFnFactory, type PreloadPluginOptions, preloadPluginFactory };
|
|
25
37
|
//# 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":"
|
|
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"}
|
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};function
|
|
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;
|
|
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","#removeExtensions","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#cancelHover","#currentAnchor","#resolveAnchorPreload","#hoverTimer","#lastTouchStartEvent","#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","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 TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { Params, Plugin, Router, State } 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 #removeExtensions: () => void;\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchStartEvent: {\n target: EventTarget | null;\n timeStamp: number;\n } | null = null;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => ({ ...options }),\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\"mouseover\", this.#handleMouseOver, {\n capture: true,\n passive: true,\n });\n document.addEventListener(\"touchstart\", this.#handleTouchStart, {\n capture: true,\n passive: true,\n });\n document.addEventListener(\"touchmove\", this.#handleTouchMove, {\n capture: true,\n passive: true,\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 target = event.target as Element | null;\n\n if (!target || !(\"closest\" in target)) {\n this.#cancelHover();\n\n return;\n }\n\n const anchor = target.closest<HTMLAnchorElement>(\"a[href]\");\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.#lastTouchStartEvent = {\n target: event.target,\n timeStamp: event.timeStamp,\n };\n\n this.#cancelTouch();\n\n const target = event.target as Element | null;\n const anchor =\n target && \"closest\" in target\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\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) {\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 #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: (params: Params) => Promise<unknown>; 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: (params: Params) => Promise<unknown>; 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\n if (typeof config?.preload !== \"function\") {\n return undefined;\n }\n\n return {\n fn: config.preload as (params: Params) => Promise<unknown>,\n params: state.params,\n };\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n return (\n this.#lastTouchStartEvent !== null &&\n event.target === this.#lastTouchStartEvent.target &&\n event.timeStamp - this.#lastTouchStartEvent.timeStamp <\n GHOST_EVENT_THRESHOLD\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(\"mouseover\", this.#handleMouseOver, {\n capture: true,\n });\n document.removeEventListener(\"touchstart\", this.#handleTouchStart, {\n capture: true,\n });\n document.removeEventListener(\"touchmove\", this.#handleTouchMove, {\n capture: true,\n });\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchStartEvent = null;\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 return function preloadPlugin(routerBase) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\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","#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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"}
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Params,
|
|
1
|
+
import { DefaultDependencies, Params, Router } from "@real-router/types";
|
|
2
|
+
import { DefaultDependencies as DefaultDependencies$1, PluginFactory } from "@real-router/core";
|
|
2
3
|
|
|
3
4
|
//#region src/types.d.ts
|
|
4
5
|
interface PreloadPluginOptions {
|
|
@@ -7,19 +8,30 @@ interface PreloadPluginOptions {
|
|
|
7
8
|
/** Check saveData/2g and disable preloading on slow connections. @default true */
|
|
8
9
|
networkAware?: boolean;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Preload function called when navigation intent is detected (hover, touch).
|
|
13
|
+
* Fire-and-forget: return values and errors are discarded.
|
|
14
|
+
*/
|
|
15
|
+
type PreloadFn = (params: Params) => Promise<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Factory function for creating preload hooks.
|
|
18
|
+
* Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
|
|
19
|
+
* Factory runs once at first invocation; the returned function is cached per route.
|
|
20
|
+
*/
|
|
21
|
+
type PreloadFnFactory<Dependencies extends DefaultDependencies = DefaultDependencies> = (router: Router<Dependencies>, getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K]) => PreloadFn;
|
|
10
22
|
//#endregion
|
|
11
23
|
//#region src/factory.d.ts
|
|
12
24
|
declare function preloadPluginFactory(opts?: Partial<PreloadPluginOptions>): PluginFactory;
|
|
13
25
|
//#endregion
|
|
14
26
|
//#region src/index.d.ts
|
|
15
27
|
declare module "@real-router/core" {
|
|
16
|
-
interface Route {
|
|
17
|
-
preload?:
|
|
28
|
+
interface Route<Dependencies extends DefaultDependencies$1> {
|
|
29
|
+
preload?: PreloadFnFactory<Dependencies>;
|
|
18
30
|
}
|
|
19
31
|
interface Router {
|
|
20
32
|
getPreloadSettings(): PreloadPluginOptions;
|
|
21
33
|
}
|
|
22
34
|
} //# sourceMappingURL=index.d.ts.map
|
|
23
35
|
//#endregion
|
|
24
|
-
export { type PreloadPluginOptions, preloadPluginFactory };
|
|
36
|
+
export { type PreloadFn, type PreloadFnFactory, type PreloadPluginOptions, preloadPluginFactory };
|
|
25
37
|
//# sourceMappingURL=index.d.mts.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":"
|
|
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"}
|
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};function
|
|
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};
|
|
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","#removeExtensions","#handleMouseOver","#handleTouchStart","#handleTouchMove","#cleanup","#isGhostMouseEvent","#cancelHover","#currentAnchor","#resolveAnchorPreload","#hoverTimer","#lastTouchStartEvent","#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","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 TOUCH_PRELOAD_DELAY,\n TOUCH_SCROLL_THRESHOLD,\n} from \"./constants\";\nimport { isSlowConnection } from \"./network\";\n\nimport type { PreloadPluginOptions } from \"./types\";\nimport type { Params, Plugin, Router, State } 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 #removeExtensions: () => void;\n\n #currentAnchor: HTMLAnchorElement | null = null;\n #hoverTimer: ReturnType<typeof setTimeout> | null = null;\n #touchTimer: ReturnType<typeof setTimeout> | null = null;\n #touchStartY = 0;\n #lastTouchStartEvent: {\n target: EventTarget | null;\n timeStamp: number;\n } | null = null;\n\n constructor(\n router: Router,\n api: PluginApi,\n options: Required<PreloadPluginOptions>,\n ) {\n this.#router = router;\n this.#api = api;\n this.#options = options;\n\n this.#removeExtensions = api.extendRouter({\n getPreloadSettings: () => ({ ...options }),\n });\n }\n\n getPlugin(): Plugin {\n return {\n onStart: () => {\n document.addEventListener(\"mouseover\", this.#handleMouseOver, {\n capture: true,\n passive: true,\n });\n document.addEventListener(\"touchstart\", this.#handleTouchStart, {\n capture: true,\n passive: true,\n });\n document.addEventListener(\"touchmove\", this.#handleTouchMove, {\n capture: true,\n passive: true,\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 target = event.target as Element | null;\n\n if (!target || !(\"closest\" in target)) {\n this.#cancelHover();\n\n return;\n }\n\n const anchor = target.closest<HTMLAnchorElement>(\"a[href]\");\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.#lastTouchStartEvent = {\n target: event.target,\n timeStamp: event.timeStamp,\n };\n\n this.#cancelTouch();\n\n const target = event.target as Element | null;\n const anchor =\n target && \"closest\" in target\n ? target.closest<HTMLAnchorElement>(\"a[href]\")\n : null;\n const preload = this.#resolveAnchorPreload(anchor);\n\n if (!preload) {\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) {\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 #resolveAnchorPreload(\n anchor: HTMLAnchorElement | null | undefined,\n ): { fn: (params: Params) => Promise<unknown>; 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: (params: Params) => Promise<unknown>; 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\n if (typeof config?.preload !== \"function\") {\n return undefined;\n }\n\n return {\n fn: config.preload as (params: Params) => Promise<unknown>,\n params: state.params,\n };\n }\n\n #isGhostMouseEvent(event: MouseEvent): boolean {\n return (\n this.#lastTouchStartEvent !== null &&\n event.target === this.#lastTouchStartEvent.target &&\n event.timeStamp - this.#lastTouchStartEvent.timeStamp <\n GHOST_EVENT_THRESHOLD\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(\"mouseover\", this.#handleMouseOver, {\n capture: true,\n });\n document.removeEventListener(\"touchstart\", this.#handleTouchStart, {\n capture: true,\n });\n document.removeEventListener(\"touchmove\", this.#handleTouchMove, {\n capture: true,\n });\n\n this.#cancelHover();\n this.#cancelTouch();\n this.#lastTouchStartEvent = null;\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 return function preloadPlugin(routerBase) {\n if (typeof document === \"undefined\") {\n return {};\n }\n\n const plugin = new PreloadPlugin(\n routerBase as Router,\n getPluginApi(routerBase),\n options,\n );\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"qDAEA,MAAa,EAAiD,CAC5D,MAAO,GACP,aAAc,GACf,CCGD,SAAgB,GAA4B,CAC1C,IAAM,EAAc,UAAsC,WAY1D,OAVK,EAML,GAHI,EAAW,UAGX,EAAW,eAAe,SAAS,KAAK,EALnC,GCKX,IAAa,EAAb,KAA2B,CACzB,GACA,GACA,GACA,GAEA,GAA2C,KAC3C,GAAoD,KACpD,GAAoD,KACpD,GAAe,EACf,GAGW,KAEX,YACE,EACA,EACA,EACA,CACA,MAAA,EAAe,EACf,MAAA,EAAY,EACZ,MAAA,EAAgB,EAEhB,MAAA,EAAyB,EAAI,aAAa,CACxC,wBAA2B,CAAE,GAAG,EAAS,EAC1C,CAAC,CAGJ,WAAoB,CAClB,MAAO,CACL,YAAe,CACb,SAAS,iBAAiB,YAAa,MAAA,EAAuB,CAC5D,QAAS,GACT,QAAS,GACV,CAAC,CACF,SAAS,iBAAiB,aAAc,MAAA,EAAwB,CAC9D,QAAS,GACT,QAAS,GACV,CAAC,CACF,SAAS,iBAAiB,YAAa,MAAA,EAAuB,CAC5D,QAAS,GACT,QAAS,GACV,CAAC,EAGJ,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,EAAM,OAErB,GAAI,CAAC,GAAU,EAAE,YAAa,GAAS,CACrC,MAAA,GAAmB,CAEnB,OAGF,IAAM,EAAS,EAAO,QAA2B,UAAU,CAE3D,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,EAA4B,CAC1B,OAAQ,EAAM,OACd,UAAW,EAAM,UAClB,CAED,MAAA,GAAmB,CAEnB,IAAM,EAAS,EAAM,OACf,EACJ,GAAU,YAAa,EACnB,EAAO,QAA2B,UAAU,CAC5C,KACA,EAAU,MAAA,EAA2B,EAAO,CAE7C,IAIL,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,MAIV,KAAK,IAAI,EAAM,QAAQ,GAAG,QAAU,MAAA,EAAkB,CAAA,IAGnE,MAAA,GAAmB,EAIvB,GACE,EAC0E,CACrE,MAID,gBAAe,EAAO,UAItB,QAAA,EAAc,cAAgB,GAAkB,EAIpD,OAAO,MAAA,EAAqB,EAAO,CAGrC,GACE,EAC0E,CAC1E,IAAM,EAAQ,MAAA,EAAa,WAAW,EAAO,KAAK,CAElD,GAAI,CAAC,EACH,OAGF,IAAM,EAAS,MAAA,EAAU,eAAe,EAAM,KAAK,CAE/C,UAAO,GAAQ,SAAY,WAI/B,MAAO,CACL,GAAI,EAAO,QACX,OAAQ,EAAM,OACf,CAGH,GAAmB,EAA4B,CAC7C,OACE,MAAA,IAA8B,MAC9B,EAAM,SAAW,MAAA,EAA0B,QAC3C,EAAM,UAAY,MAAA,EAA0B,UAAA,KAKhD,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,oBAAoB,YAAa,MAAA,EAAuB,CAC/D,QAAS,GACV,CAAC,CACF,SAAS,oBAAoB,aAAc,MAAA,EAAwB,CACjE,QAAS,GACV,CAAC,CACF,SAAS,oBAAoB,YAAa,MAAA,EAAuB,CAC/D,QAAS,GACV,CAAC,CAEF,MAAA,GAAmB,CACnB,MAAA,GAAmB,CACnB,MAAA,EAA4B,OCxNhC,SAAgB,EACd,EACe,CACf,IAAM,EAA0C,CAC9C,GAAG,EACH,GAAG,EACJ,CAED,OAAO,SAAuB,EAAY,CAWxC,OAVI,OAAO,SAAa,IACf,EAAE,CAGI,IAAI,EACjB,EACA,EAAa,EAAW,CACxB,EACD,CAEa,WAAW"}
|
|
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":"qDAEA,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,EACA,EAAa,EAAW,CACxB,EACA,EACD,CAEa,WAAW"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/preload-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,18 +45,20 @@
|
|
|
45
45
|
},
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@real-router/core": "^0.
|
|
48
|
+
"@real-router/core": "^0.49.0",
|
|
49
|
+
"@real-router/types": "^0.34.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"jsdom": "28.1.0"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"test": "vitest",
|
|
55
|
-
"build": "tsdown --config-loader unrun",
|
|
56
56
|
"type-check": "tsc --noEmit",
|
|
57
57
|
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
58
58
|
"lint:package": "publint",
|
|
59
59
|
"lint:types": "attw --pack .",
|
|
60
|
-
"test:properties": "vitest --config vitest.config.properties.mts --run"
|
|
60
|
+
"test:properties": "vitest --config vitest.config.properties.mts --run",
|
|
61
|
+
"test:stress": "vitest --config vitest.config.stress.mts --run",
|
|
62
|
+
"bundle": "tsdown --config-loader unrun"
|
|
61
63
|
}
|
|
62
64
|
}
|
package/src/constants.ts
CHANGED
package/src/factory.ts
CHANGED
|
@@ -14,7 +14,11 @@ export function preloadPluginFactory(
|
|
|
14
14
|
...opts,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
if (!Number.isFinite(options.delay) || options.delay < 0) {
|
|
18
|
+
options.delay = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return function preloadPlugin(routerBase, getDependency) {
|
|
18
22
|
if (typeof document === "undefined") {
|
|
19
23
|
return {};
|
|
20
24
|
}
|
|
@@ -23,6 +27,7 @@ export function preloadPluginFactory(
|
|
|
23
27
|
routerBase as Router,
|
|
24
28
|
getPluginApi(routerBase),
|
|
25
29
|
options,
|
|
30
|
+
getDependency,
|
|
26
31
|
);
|
|
27
32
|
|
|
28
33
|
return plugin.getPlugin();
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/method-signature-style -- method syntax required for declaration merging overload (property syntax causes TS2717) */
|
|
2
|
-
import type { PreloadPluginOptions } from "./types";
|
|
3
|
-
import type {
|
|
2
|
+
import type { PreloadFnFactory, PreloadPluginOptions } from "./types";
|
|
3
|
+
import type { DefaultDependencies } from "@real-router/core";
|
|
4
4
|
|
|
5
5
|
export { preloadPluginFactory } from "./factory";
|
|
6
6
|
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
PreloadPluginOptions,
|
|
9
|
+
PreloadFn,
|
|
10
|
+
PreloadFnFactory,
|
|
11
|
+
} from "./types";
|
|
8
12
|
|
|
9
13
|
declare module "@real-router/core" {
|
|
10
|
-
interface Route {
|
|
11
|
-
preload?:
|
|
14
|
+
interface Route<Dependencies extends DefaultDependencies> {
|
|
15
|
+
preload?: PreloadFnFactory<Dependencies>;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
interface Router {
|
package/src/plugin.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GHOST_EVENT_THRESHOLD,
|
|
3
|
+
LISTENER_OPTIONS,
|
|
3
4
|
TOUCH_PRELOAD_DELAY,
|
|
4
5
|
TOUCH_SCROLL_THRESHOLD,
|
|
5
6
|
} from "./constants";
|
|
6
7
|
import { isSlowConnection } from "./network";
|
|
7
8
|
|
|
8
|
-
import type {
|
|
9
|
-
|
|
9
|
+
import type {
|
|
10
|
+
PreloadFn,
|
|
11
|
+
PreloadFnFactory,
|
|
12
|
+
PreloadPluginOptions,
|
|
13
|
+
} from "./types";
|
|
14
|
+
import type {
|
|
15
|
+
Params,
|
|
16
|
+
Plugin,
|
|
17
|
+
PluginFactory,
|
|
18
|
+
Router,
|
|
19
|
+
State,
|
|
20
|
+
} from "@real-router/core";
|
|
10
21
|
import type { PluginApi } from "@real-router/core/api";
|
|
11
22
|
|
|
12
23
|
declare module "@real-router/core" {
|
|
@@ -19,46 +30,56 @@ export class PreloadPlugin {
|
|
|
19
30
|
readonly #router: Router;
|
|
20
31
|
readonly #api: PluginApi;
|
|
21
32
|
readonly #options: Required<PreloadPluginOptions>;
|
|
33
|
+
readonly #getDependency: Parameters<PluginFactory>[1];
|
|
22
34
|
readonly #removeExtensions: () => void;
|
|
35
|
+
readonly #compiledPreloads = new Map<
|
|
36
|
+
string,
|
|
37
|
+
{ fn: PreloadFn; factory: PreloadFnFactory }
|
|
38
|
+
>();
|
|
23
39
|
|
|
24
40
|
#currentAnchor: HTMLAnchorElement | null = null;
|
|
25
41
|
#hoverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
26
42
|
#touchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
27
43
|
#touchStartY = 0;
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
timeStamp: number;
|
|
31
|
-
} | null = null;
|
|
44
|
+
#lastTouchTarget: EventTarget | null = null;
|
|
45
|
+
#lastTouchTimeStamp = Number.NaN;
|
|
32
46
|
|
|
33
47
|
constructor(
|
|
34
48
|
router: Router,
|
|
35
49
|
api: PluginApi,
|
|
36
50
|
options: Required<PreloadPluginOptions>,
|
|
51
|
+
getDependency: Parameters<PluginFactory>[1],
|
|
37
52
|
) {
|
|
38
53
|
this.#router = router;
|
|
39
54
|
this.#api = api;
|
|
40
55
|
this.#options = options;
|
|
56
|
+
this.#getDependency = getDependency;
|
|
57
|
+
|
|
58
|
+
const cachedOptions = { ...options };
|
|
41
59
|
|
|
42
60
|
this.#removeExtensions = api.extendRouter({
|
|
43
|
-
getPreloadSettings: () =>
|
|
61
|
+
getPreloadSettings: () => cachedOptions,
|
|
44
62
|
});
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
getPlugin(): Plugin {
|
|
48
66
|
return {
|
|
49
67
|
onStart: () => {
|
|
50
|
-
document.addEventListener(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
document.addEventListener(
|
|
69
|
+
"mouseover",
|
|
70
|
+
this.#handleMouseOver,
|
|
71
|
+
LISTENER_OPTIONS,
|
|
72
|
+
);
|
|
73
|
+
document.addEventListener(
|
|
74
|
+
"touchstart",
|
|
75
|
+
this.#handleTouchStart,
|
|
76
|
+
LISTENER_OPTIONS,
|
|
77
|
+
);
|
|
78
|
+
document.addEventListener(
|
|
79
|
+
"touchmove",
|
|
80
|
+
this.#handleTouchMove,
|
|
81
|
+
LISTENER_OPTIONS,
|
|
82
|
+
);
|
|
62
83
|
},
|
|
63
84
|
|
|
64
85
|
onStop: () => {
|
|
@@ -77,15 +98,7 @@ export class PreloadPlugin {
|
|
|
77
98
|
return;
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (!target || !("closest" in target)) {
|
|
83
|
-
this.#cancelHover();
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const anchor = target.closest<HTMLAnchorElement>("a[href]");
|
|
101
|
+
const anchor = this.#findAnchor(event.target);
|
|
89
102
|
|
|
90
103
|
if (anchor === this.#currentAnchor) {
|
|
91
104
|
return;
|
|
@@ -107,21 +120,15 @@ export class PreloadPlugin {
|
|
|
107
120
|
};
|
|
108
121
|
|
|
109
122
|
readonly #handleTouchStart = (event: TouchEvent): void => {
|
|
110
|
-
this.#
|
|
111
|
-
|
|
112
|
-
timeStamp: event.timeStamp,
|
|
113
|
-
};
|
|
123
|
+
this.#lastTouchTarget = event.target;
|
|
124
|
+
this.#lastTouchTimeStamp = event.timeStamp;
|
|
114
125
|
|
|
115
126
|
this.#cancelTouch();
|
|
116
127
|
|
|
117
|
-
const
|
|
118
|
-
const anchor =
|
|
119
|
-
target && "closest" in target
|
|
120
|
-
? target.closest<HTMLAnchorElement>("a[href]")
|
|
121
|
-
: null;
|
|
128
|
+
const anchor = this.#findAnchor(event.target);
|
|
122
129
|
const preload = this.#resolveAnchorPreload(anchor);
|
|
123
130
|
|
|
124
|
-
if (!preload) {
|
|
131
|
+
if (!preload || event.touches.length === 0) {
|
|
125
132
|
return;
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -134,7 +141,7 @@ export class PreloadPlugin {
|
|
|
134
141
|
};
|
|
135
142
|
|
|
136
143
|
readonly #handleTouchMove = (event: TouchEvent): void => {
|
|
137
|
-
if (this.#touchTimer === null) {
|
|
144
|
+
if (this.#touchTimer === null || event.touches.length === 0) {
|
|
138
145
|
return;
|
|
139
146
|
}
|
|
140
147
|
|
|
@@ -145,9 +152,15 @@ export class PreloadPlugin {
|
|
|
145
152
|
}
|
|
146
153
|
};
|
|
147
154
|
|
|
155
|
+
#findAnchor(target: EventTarget | null): HTMLAnchorElement | null {
|
|
156
|
+
return target instanceof Element
|
|
157
|
+
? target.closest<HTMLAnchorElement>("a[href]")
|
|
158
|
+
: null;
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
#resolveAnchorPreload(
|
|
149
162
|
anchor: HTMLAnchorElement | null | undefined,
|
|
150
|
-
): { fn:
|
|
163
|
+
): { fn: PreloadFn; params: Params } | undefined {
|
|
151
164
|
if (!anchor) {
|
|
152
165
|
return undefined;
|
|
153
166
|
}
|
|
@@ -165,7 +178,7 @@ export class PreloadPlugin {
|
|
|
165
178
|
|
|
166
179
|
#resolvePreload(
|
|
167
180
|
anchor: HTMLAnchorElement,
|
|
168
|
-
): { fn:
|
|
181
|
+
): { fn: PreloadFn; params: Params } | undefined {
|
|
169
182
|
const state = this.#router.matchUrl?.(anchor.href);
|
|
170
183
|
|
|
171
184
|
if (!state) {
|
|
@@ -173,23 +186,43 @@ export class PreloadPlugin {
|
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
const config = this.#api.getRouteConfig(state.name);
|
|
189
|
+
const factory =
|
|
190
|
+
typeof config?.preload === "function"
|
|
191
|
+
? (config.preload as PreloadFnFactory)
|
|
192
|
+
: undefined;
|
|
193
|
+
|
|
194
|
+
if (!factory) {
|
|
195
|
+
this.#compiledPreloads.delete(state.name);
|
|
176
196
|
|
|
177
|
-
if (typeof config?.preload !== "function") {
|
|
178
197
|
return undefined;
|
|
179
198
|
}
|
|
180
199
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
const cached = this.#compiledPreloads.get(state.name);
|
|
201
|
+
|
|
202
|
+
if (cached?.factory === factory) {
|
|
203
|
+
return { fn: cached.fn, params: state.params };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let fn: PreloadFn;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
fn = factory(this.#router, this.#getDependency);
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.#compiledPreloads.set(state.name, { fn, factory });
|
|
215
|
+
|
|
216
|
+
return { fn, params: state.params };
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
#isGhostMouseEvent(event: MouseEvent): boolean {
|
|
220
|
+
const delta = event.timeStamp - this.#lastTouchTimeStamp;
|
|
221
|
+
|
|
188
222
|
return (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
event.
|
|
192
|
-
GHOST_EVENT_THRESHOLD
|
|
223
|
+
delta >= 0 &&
|
|
224
|
+
delta < GHOST_EVENT_THRESHOLD &&
|
|
225
|
+
event.target === this.#lastTouchTarget
|
|
193
226
|
);
|
|
194
227
|
}
|
|
195
228
|
|
|
@@ -210,18 +243,25 @@ export class PreloadPlugin {
|
|
|
210
243
|
}
|
|
211
244
|
|
|
212
245
|
#cleanup(): void {
|
|
213
|
-
document.removeEventListener(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
document.removeEventListener(
|
|
247
|
+
"mouseover",
|
|
248
|
+
this.#handleMouseOver,
|
|
249
|
+
LISTENER_OPTIONS,
|
|
250
|
+
);
|
|
251
|
+
document.removeEventListener(
|
|
252
|
+
"touchstart",
|
|
253
|
+
this.#handleTouchStart,
|
|
254
|
+
LISTENER_OPTIONS,
|
|
255
|
+
);
|
|
256
|
+
document.removeEventListener(
|
|
257
|
+
"touchmove",
|
|
258
|
+
this.#handleTouchMove,
|
|
259
|
+
LISTENER_OPTIONS,
|
|
260
|
+
);
|
|
222
261
|
|
|
223
262
|
this.#cancelHover();
|
|
224
263
|
this.#cancelTouch();
|
|
225
|
-
this.#
|
|
264
|
+
this.#lastTouchTarget = null;
|
|
265
|
+
this.#lastTouchTimeStamp = Number.NaN;
|
|
226
266
|
}
|
|
227
267
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import type { DefaultDependencies, Params, Router } from "@real-router/types";
|
|
2
|
+
|
|
1
3
|
export interface PreloadPluginOptions {
|
|
2
4
|
/** Hover debounce delay in ms. @default 65 */
|
|
3
5
|
delay?: number;
|
|
4
6
|
/** Check saveData/2g and disable preloading on slow connections. @default true */
|
|
5
7
|
networkAware?: boolean;
|
|
6
8
|
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Preload function called when navigation intent is detected (hover, touch).
|
|
12
|
+
* Fire-and-forget: return values and errors are discarded.
|
|
13
|
+
*/
|
|
14
|
+
export type PreloadFn = (params: Params) => Promise<unknown>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Factory function for creating preload hooks.
|
|
18
|
+
* Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
|
|
19
|
+
* Factory runs once at first invocation; the returned function is cached per route.
|
|
20
|
+
*/
|
|
21
|
+
export type PreloadFnFactory<
|
|
22
|
+
Dependencies extends DefaultDependencies = DefaultDependencies,
|
|
23
|
+
> = (
|
|
24
|
+
router: Router<Dependencies>,
|
|
25
|
+
getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],
|
|
26
|
+
) => PreloadFn;
|