specnav-core 0.2.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/dist/adaptive.cjs +108 -0
  4. package/dist/adaptive.cjs.map +1 -0
  5. package/dist/adaptive.d.cts +24 -0
  6. package/dist/adaptive.d.ts +24 -0
  7. package/dist/adaptive.js +105 -0
  8. package/dist/adaptive.js.map +1 -0
  9. package/dist/cache.cjs +224 -0
  10. package/dist/cache.cjs.map +1 -0
  11. package/dist/cache.d.cts +33 -0
  12. package/dist/cache.d.ts +33 -0
  13. package/dist/cache.js +221 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/graph.cjs +132 -0
  16. package/dist/graph.cjs.map +1 -0
  17. package/dist/graph.d.cts +19 -0
  18. package/dist/graph.d.ts +19 -0
  19. package/dist/graph.js +128 -0
  20. package/dist/graph.js.map +1 -0
  21. package/dist/index.cjs +843 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +8 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +826 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/morpher.cjs +158 -0
  28. package/dist/morpher.cjs.map +1 -0
  29. package/dist/morpher.d.cts +19 -0
  30. package/dist/morpher.d.ts +19 -0
  31. package/dist/morpher.js +154 -0
  32. package/dist/morpher.js.map +1 -0
  33. package/dist/performance.cjs +40 -0
  34. package/dist/performance.cjs.map +1 -0
  35. package/dist/performance.d.cts +21 -0
  36. package/dist/performance.d.ts +21 -0
  37. package/dist/performance.js +37 -0
  38. package/dist/performance.js.map +1 -0
  39. package/dist/speculator.cjs +59 -0
  40. package/dist/speculator.cjs.map +1 -0
  41. package/dist/speculator.d.cts +14 -0
  42. package/dist/speculator.d.ts +14 -0
  43. package/dist/speculator.js +56 -0
  44. package/dist/speculator.js.map +1 -0
  45. package/dist/trajectory.cjs +146 -0
  46. package/dist/trajectory.cjs.map +1 -0
  47. package/dist/trajectory.d.cts +34 -0
  48. package/dist/trajectory.d.ts +34 -0
  49. package/dist/trajectory.js +143 -0
  50. package/dist/trajectory.js.map +1 -0
  51. package/dist/types-DnUtmOfQ.d.cts +88 -0
  52. package/dist/types-DnUtmOfQ.d.ts +88 -0
  53. package/package.json +95 -0
@@ -0,0 +1,37 @@
1
+ // src/performance.ts
2
+ var PerformanceMonitor = class {
3
+ metrics = [];
4
+ maxMetrics = 100;
5
+ recordNavigation(metrics) {
6
+ this.metrics.push(metrics);
7
+ if (this.metrics.length > this.maxMetrics) {
8
+ this.metrics.shift();
9
+ }
10
+ }
11
+ getAverageNavigationTime() {
12
+ if (this.metrics.length === 0) return 0;
13
+ const sum = this.metrics.reduce((acc, m) => acc + m.navigationTime, 0);
14
+ return sum / this.metrics.length;
15
+ }
16
+ getCacheHitRate() {
17
+ if (this.metrics.length === 0) return 0;
18
+ const hits = this.metrics.filter((m) => m.cacheHit).length;
19
+ return hits / this.metrics.length;
20
+ }
21
+ getSpeculativeHitRate() {
22
+ if (this.metrics.length === 0) return 0;
23
+ const hits = this.metrics.filter((m) => m.speculativeHit).length;
24
+ return hits / this.metrics.length;
25
+ }
26
+ getMetrics() {
27
+ return [...this.metrics];
28
+ }
29
+ clear() {
30
+ this.metrics = [];
31
+ }
32
+ };
33
+ var performanceMonitor = new PerformanceMonitor();
34
+
35
+ export { PerformanceMonitor, performanceMonitor };
36
+ //# sourceMappingURL=performance.js.map
37
+ //# sourceMappingURL=performance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/performance.ts"],"names":[],"mappings":";AAWO,IAAM,qBAAN,MAAyB;AAAA,EACtB,UAAgC,EAAC;AAAA,EACjC,UAAA,GAAa,GAAA;AAAA,EAErB,iBAAiB,OAAA,EAAmC;AAClD,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,OAAO,CAAA;AAGzB,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,GAAS,IAAA,CAAK,UAAA,EAAY;AACzC,MAAA,IAAA,CAAK,QAAQ,KAAA,EAAM;AAAA,IACrB;AAAA,EACF;AAAA,EAEA,wBAAA,GAAmC;AACjC,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AACtC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,CAAC,KAAK,CAAA,KAAM,GAAA,GAAM,CAAA,CAAE,cAAA,EAAgB,CAAC,CAAA;AACrE,IAAA,OAAO,GAAA,GAAM,KAAK,OAAA,CAAQ,MAAA;AAAA,EAC5B;AAAA,EAEA,eAAA,GAA0B;AACxB,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AACtC,IAAA,MAAM,OAAO,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,CAAA,KAAK,CAAA,CAAE,QAAQ,CAAA,CAAE,MAAA;AAClD,IAAA,OAAO,IAAA,GAAO,KAAK,OAAA,CAAQ,MAAA;AAAA,EAC7B;AAAA,EAEA,qBAAA,GAAgC;AAC9B,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AACtC,IAAA,MAAM,OAAO,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,CAAA,KAAK,CAAA,CAAE,cAAc,CAAA,CAAE,MAAA;AACxD,IAAA,OAAO,IAAA,GAAO,KAAK,OAAA,CAAQ,MAAA;AAAA,EAC7B;AAAA,EAEA,UAAA,GAAmC;AACjC,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,OAAO,CAAA;AAAA,EACzB;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,UAAU,EAAC;AAAA,EAClB;AACF;AAEO,IAAM,kBAAA,GAAqB,IAAI,kBAAA","file":"performance.js","sourcesContent":["// Performance monitoring utilities\n\nexport interface PerformanceMetrics {\n navigationTime: number;\n cacheHit: boolean;\n cacheLayer?: 1 | 2 | 3;\n speculativeHit: boolean;\n fetchTime?: number;\n morphTime?: number;\n}\n\nexport class PerformanceMonitor {\n private metrics: PerformanceMetrics[] = [];\n private maxMetrics = 100;\n\n recordNavigation(metrics: PerformanceMetrics): void {\n this.metrics.push(metrics);\n \n // Keep only last N metrics\n if (this.metrics.length > this.maxMetrics) {\n this.metrics.shift();\n }\n }\n\n getAverageNavigationTime(): number {\n if (this.metrics.length === 0) return 0;\n const sum = this.metrics.reduce((acc, m) => acc + m.navigationTime, 0);\n return sum / this.metrics.length;\n }\n\n getCacheHitRate(): number {\n if (this.metrics.length === 0) return 0;\n const hits = this.metrics.filter(m => m.cacheHit).length;\n return hits / this.metrics.length;\n }\n\n getSpeculativeHitRate(): number {\n if (this.metrics.length === 0) return 0;\n const hits = this.metrics.filter(m => m.speculativeHit).length;\n return hits / this.metrics.length;\n }\n\n getMetrics(): PerformanceMetrics[] {\n return [...this.metrics];\n }\n\n clear(): void {\n this.metrics = [];\n }\n}\n\nexport const performanceMonitor = new PerformanceMonitor();\n"]}
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ // src/speculator.ts
4
+ var SpeculativeRenderer = class {
5
+ speculations = /* @__PURE__ */ new Map();
6
+ maxSpeculations;
7
+ constructor(maxSpeculations = 3) {
8
+ this.maxSpeculations = maxSpeculations;
9
+ }
10
+ async speculate(href, html) {
11
+ if (this.speculations.has(href)) return;
12
+ if (this.speculations.size >= this.maxSpeculations) {
13
+ const oldest = Array.from(this.speculations.values()).sort(
14
+ (a, b) => a.timestamp - b.timestamp
15
+ )[0];
16
+ if (oldest) this.cancel(oldest.href);
17
+ }
18
+ const container = this.createDetachedContainer();
19
+ container.innerHTML = html;
20
+ this.speculations.set(href, {
21
+ href,
22
+ html,
23
+ container,
24
+ timestamp: Date.now()
25
+ });
26
+ }
27
+ get(href) {
28
+ const speculation = this.speculations.get(href);
29
+ return speculation?.container ?? null;
30
+ }
31
+ cancel(href) {
32
+ const speculation = this.speculations.get(href);
33
+ if (speculation) {
34
+ speculation.container.remove();
35
+ this.speculations.delete(href);
36
+ }
37
+ }
38
+ cancelAll() {
39
+ this.speculations.forEach((spec) => spec.container.remove());
40
+ this.speculations.clear();
41
+ }
42
+ has(href) {
43
+ return this.speculations.has(href);
44
+ }
45
+ createDetachedContainer() {
46
+ const container = document.createElement("div");
47
+ container.style.display = "none";
48
+ container.setAttribute("data-specnav-speculation", "true");
49
+ return container;
50
+ }
51
+ };
52
+ function createSpeculativeRenderer(maxSpeculations) {
53
+ return new SpeculativeRenderer(maxSpeculations);
54
+ }
55
+
56
+ exports.SpeculativeRenderer = SpeculativeRenderer;
57
+ exports.createSpeculativeRenderer = createSpeculativeRenderer;
58
+ //# sourceMappingURL=speculator.cjs.map
59
+ //# sourceMappingURL=speculator.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/speculator.ts"],"names":[],"mappings":";;;AAkBO,IAAM,sBAAN,MAA0B;AAAA,EACvB,YAAA,uBAAgD,GAAA,EAAI;AAAA,EACpD,eAAA;AAAA,EAER,WAAA,CAAY,kBAAkB,CAAA,EAAG;AAC/B,IAAA,IAAA,CAAK,eAAA,GAAkB,eAAA;AAAA,EACzB;AAAA,EAEA,MAAM,SAAA,CAAU,IAAA,EAAc,IAAA,EAA6B;AACzD,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG;AAGjC,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,IAAA,IAAQ,IAAA,CAAK,eAAA,EAAiB;AAClD,MAAA,MAAM,SAAS,KAAA,CAAM,IAAA,CAAK,KAAK,YAAA,CAAa,MAAA,EAAQ,CAAA,CAAE,IAAA;AAAA,QACpD,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,YAAY,CAAA,CAAE;AAAA,QAC1B,CAAC,CAAA;AACH,MAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAI,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,SAAA,GAAY,KAAK,uBAAA,EAAwB;AAC/C,IAAA,SAAA,CAAU,SAAA,GAAY,IAAA;AAEtB,IAAA,IAAA,CAAK,YAAA,CAAa,IAAI,IAAA,EAAM;AAAA,MAC1B,IAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA,EAAW,KAAK,GAAA;AAAI,KACrB,CAAA;AAAA,EACH;AAAA,EAEA,IAAI,IAAA,EAAkC;AACpC,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAC9C,IAAA,OAAO,aAAa,SAAA,IAAa,IAAA;AAAA,EACnC;AAAA,EAEA,OAAO,IAAA,EAAoB;AACzB,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAC9C,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,WAAA,CAAY,UAAU,MAAA,EAAO;AAC7B,MAAA,IAAA,CAAK,YAAA,CAAa,OAAO,IAAI,CAAA;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,CAAC,SAAS,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AAC3D,IAAA,IAAA,CAAK,aAAa,KAAA,EAAM;AAAA,EAC1B;AAAA,EAEA,IAAI,IAAA,EAAuB;AACzB,IAAA,OAAO,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAAA,EACnC;AAAA,EAEQ,uBAAA,GAAuC;AAC7C,IAAA,MAAM,SAAA,GAAY,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC9C,IAAA,SAAA,CAAU,MAAM,OAAA,GAAU,MAAA;AAC1B,IAAA,SAAA,CAAU,YAAA,CAAa,4BAA4B,MAAM,CAAA;AACzD,IAAA,OAAO,SAAA;AAAA,EACT;AACF;AAEO,SAAS,0BACd,eAAA,EACqB;AACrB,EAAA,OAAO,IAAI,oBAAoB,eAAe,CAAA;AAChD","file":"speculator.cjs","sourcesContent":["interface SpeculatedPage {\n href: string;\n html: string;\n container: HTMLElement;\n timestamp: number;\n}\n\n/**\n * SpeculativeRenderer pre-renders pages in detached DOM containers.\n * \n * LIMITATIONS:\n * - This implementation uses innerHTML, NOT React rendering. The speculated content\n * is raw HTML set on a disconnected DOM node, not a fully rendered React component tree.\n * - For true React speculative rendering, the container must be appended to the document\n * (hidden via CSS) and ReactDOM.createRoot(container).render(...) must be called.\n * - Current implementation is suitable for static HTML morphing but may produce different\n * results than a real React render for dynamic components.\n */\nexport class SpeculativeRenderer {\n private speculations: Map<string, SpeculatedPage> = new Map();\n private maxSpeculations: number;\n\n constructor(maxSpeculations = 3) {\n this.maxSpeculations = maxSpeculations;\n }\n\n async speculate(href: string, html: string): Promise<void> {\n if (this.speculations.has(href)) return;\n\n // Evict oldest if at capacity\n if (this.speculations.size >= this.maxSpeculations) {\n const oldest = Array.from(this.speculations.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n )[0];\n if (oldest) this.cancel(oldest.href);\n }\n\n const container = this.createDetachedContainer();\n container.innerHTML = html;\n\n this.speculations.set(href, {\n href,\n html,\n container,\n timestamp: Date.now(),\n });\n }\n\n get(href: string): HTMLElement | null {\n const speculation = this.speculations.get(href);\n return speculation?.container ?? null;\n }\n\n cancel(href: string): void {\n const speculation = this.speculations.get(href);\n if (speculation) {\n speculation.container.remove();\n this.speculations.delete(href);\n }\n }\n\n cancelAll(): void {\n this.speculations.forEach((spec) => spec.container.remove());\n this.speculations.clear();\n }\n\n has(href: string): boolean {\n return this.speculations.has(href);\n }\n\n private createDetachedContainer(): HTMLElement {\n const container = document.createElement(\"div\");\n container.style.display = \"none\";\n container.setAttribute(\"data-specnav-speculation\", \"true\");\n return container;\n }\n}\n\nexport function createSpeculativeRenderer(\n maxSpeculations?: number\n): SpeculativeRenderer {\n return new SpeculativeRenderer(maxSpeculations);\n}\n"]}
@@ -0,0 +1,14 @@
1
+ declare class SpeculativeRenderer {
2
+ private speculations;
3
+ private maxSpeculations;
4
+ constructor(maxSpeculations?: number);
5
+ speculate(href: string, html: string): Promise<void>;
6
+ get(href: string): HTMLElement | null;
7
+ cancel(href: string): void;
8
+ cancelAll(): void;
9
+ has(href: string): boolean;
10
+ private createDetachedContainer;
11
+ }
12
+ declare function createSpeculativeRenderer(maxSpeculations?: number): SpeculativeRenderer;
13
+
14
+ export { SpeculativeRenderer, createSpeculativeRenderer };
@@ -0,0 +1,14 @@
1
+ declare class SpeculativeRenderer {
2
+ private speculations;
3
+ private maxSpeculations;
4
+ constructor(maxSpeculations?: number);
5
+ speculate(href: string, html: string): Promise<void>;
6
+ get(href: string): HTMLElement | null;
7
+ cancel(href: string): void;
8
+ cancelAll(): void;
9
+ has(href: string): boolean;
10
+ private createDetachedContainer;
11
+ }
12
+ declare function createSpeculativeRenderer(maxSpeculations?: number): SpeculativeRenderer;
13
+
14
+ export { SpeculativeRenderer, createSpeculativeRenderer };
@@ -0,0 +1,56 @@
1
+ // src/speculator.ts
2
+ var SpeculativeRenderer = class {
3
+ speculations = /* @__PURE__ */ new Map();
4
+ maxSpeculations;
5
+ constructor(maxSpeculations = 3) {
6
+ this.maxSpeculations = maxSpeculations;
7
+ }
8
+ async speculate(href, html) {
9
+ if (this.speculations.has(href)) return;
10
+ if (this.speculations.size >= this.maxSpeculations) {
11
+ const oldest = Array.from(this.speculations.values()).sort(
12
+ (a, b) => a.timestamp - b.timestamp
13
+ )[0];
14
+ if (oldest) this.cancel(oldest.href);
15
+ }
16
+ const container = this.createDetachedContainer();
17
+ container.innerHTML = html;
18
+ this.speculations.set(href, {
19
+ href,
20
+ html,
21
+ container,
22
+ timestamp: Date.now()
23
+ });
24
+ }
25
+ get(href) {
26
+ const speculation = this.speculations.get(href);
27
+ return speculation?.container ?? null;
28
+ }
29
+ cancel(href) {
30
+ const speculation = this.speculations.get(href);
31
+ if (speculation) {
32
+ speculation.container.remove();
33
+ this.speculations.delete(href);
34
+ }
35
+ }
36
+ cancelAll() {
37
+ this.speculations.forEach((spec) => spec.container.remove());
38
+ this.speculations.clear();
39
+ }
40
+ has(href) {
41
+ return this.speculations.has(href);
42
+ }
43
+ createDetachedContainer() {
44
+ const container = document.createElement("div");
45
+ container.style.display = "none";
46
+ container.setAttribute("data-specnav-speculation", "true");
47
+ return container;
48
+ }
49
+ };
50
+ function createSpeculativeRenderer(maxSpeculations) {
51
+ return new SpeculativeRenderer(maxSpeculations);
52
+ }
53
+
54
+ export { SpeculativeRenderer, createSpeculativeRenderer };
55
+ //# sourceMappingURL=speculator.js.map
56
+ //# sourceMappingURL=speculator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/speculator.ts"],"names":[],"mappings":";AAkBO,IAAM,sBAAN,MAA0B;AAAA,EACvB,YAAA,uBAAgD,GAAA,EAAI;AAAA,EACpD,eAAA;AAAA,EAER,WAAA,CAAY,kBAAkB,CAAA,EAAG;AAC/B,IAAA,IAAA,CAAK,eAAA,GAAkB,eAAA;AAAA,EACzB;AAAA,EAEA,MAAM,SAAA,CAAU,IAAA,EAAc,IAAA,EAA6B;AACzD,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA,EAAG;AAGjC,IAAA,IAAI,IAAA,CAAK,YAAA,CAAa,IAAA,IAAQ,IAAA,CAAK,eAAA,EAAiB;AAClD,MAAA,MAAM,SAAS,KAAA,CAAM,IAAA,CAAK,KAAK,YAAA,CAAa,MAAA,EAAQ,CAAA,CAAE,IAAA;AAAA,QACpD,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,YAAY,CAAA,CAAE;AAAA,QAC1B,CAAC,CAAA;AACH,MAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAI,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,SAAA,GAAY,KAAK,uBAAA,EAAwB;AAC/C,IAAA,SAAA,CAAU,SAAA,GAAY,IAAA;AAEtB,IAAA,IAAA,CAAK,YAAA,CAAa,IAAI,IAAA,EAAM;AAAA,MAC1B,IAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA,EAAW,KAAK,GAAA;AAAI,KACrB,CAAA;AAAA,EACH;AAAA,EAEA,IAAI,IAAA,EAAkC;AACpC,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAC9C,IAAA,OAAO,aAAa,SAAA,IAAa,IAAA;AAAA,EACnC;AAAA,EAEA,OAAO,IAAA,EAAoB;AACzB,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAC9C,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,WAAA,CAAY,UAAU,MAAA,EAAO;AAC7B,MAAA,IAAA,CAAK,YAAA,CAAa,OAAO,IAAI,CAAA;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,SAAA,GAAkB;AAChB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,CAAC,SAAS,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AAC3D,IAAA,IAAA,CAAK,aAAa,KAAA,EAAM;AAAA,EAC1B;AAAA,EAEA,IAAI,IAAA,EAAuB;AACzB,IAAA,OAAO,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,IAAI,CAAA;AAAA,EACnC;AAAA,EAEQ,uBAAA,GAAuC;AAC7C,IAAA,MAAM,SAAA,GAAY,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AAC9C,IAAA,SAAA,CAAU,MAAM,OAAA,GAAU,MAAA;AAC1B,IAAA,SAAA,CAAU,YAAA,CAAa,4BAA4B,MAAM,CAAA;AACzD,IAAA,OAAO,SAAA;AAAA,EACT;AACF;AAEO,SAAS,0BACd,eAAA,EACqB;AACrB,EAAA,OAAO,IAAI,oBAAoB,eAAe,CAAA;AAChD","file":"speculator.js","sourcesContent":["interface SpeculatedPage {\n href: string;\n html: string;\n container: HTMLElement;\n timestamp: number;\n}\n\n/**\n * SpeculativeRenderer pre-renders pages in detached DOM containers.\n * \n * LIMITATIONS:\n * - This implementation uses innerHTML, NOT React rendering. The speculated content\n * is raw HTML set on a disconnected DOM node, not a fully rendered React component tree.\n * - For true React speculative rendering, the container must be appended to the document\n * (hidden via CSS) and ReactDOM.createRoot(container).render(...) must be called.\n * - Current implementation is suitable for static HTML morphing but may produce different\n * results than a real React render for dynamic components.\n */\nexport class SpeculativeRenderer {\n private speculations: Map<string, SpeculatedPage> = new Map();\n private maxSpeculations: number;\n\n constructor(maxSpeculations = 3) {\n this.maxSpeculations = maxSpeculations;\n }\n\n async speculate(href: string, html: string): Promise<void> {\n if (this.speculations.has(href)) return;\n\n // Evict oldest if at capacity\n if (this.speculations.size >= this.maxSpeculations) {\n const oldest = Array.from(this.speculations.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n )[0];\n if (oldest) this.cancel(oldest.href);\n }\n\n const container = this.createDetachedContainer();\n container.innerHTML = html;\n\n this.speculations.set(href, {\n href,\n html,\n container,\n timestamp: Date.now(),\n });\n }\n\n get(href: string): HTMLElement | null {\n const speculation = this.speculations.get(href);\n return speculation?.container ?? null;\n }\n\n cancel(href: string): void {\n const speculation = this.speculations.get(href);\n if (speculation) {\n speculation.container.remove();\n this.speculations.delete(href);\n }\n }\n\n cancelAll(): void {\n this.speculations.forEach((spec) => spec.container.remove());\n this.speculations.clear();\n }\n\n has(href: string): boolean {\n return this.speculations.has(href);\n }\n\n private createDetachedContainer(): HTMLElement {\n const container = document.createElement(\"div\");\n container.style.display = \"none\";\n container.setAttribute(\"data-specnav-speculation\", \"true\");\n return container;\n }\n}\n\nexport function createSpeculativeRenderer(\n maxSpeculations?: number\n): SpeculativeRenderer {\n return new SpeculativeRenderer(maxSpeculations);\n}\n"]}
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ // src/trajectory.ts
4
+ var DEFAULT_CONFIG = {
5
+ lookaheadMs: 120,
6
+ minVelocity: 0.2,
7
+ sampleRate: 50,
8
+ cancelOnDeviation: true
9
+ };
10
+ var TrajectoryEngine = class {
11
+ samples = [];
12
+ links = /* @__PURE__ */ new Map();
13
+ config;
14
+ lastPrediction = null;
15
+ sampleInterval = null;
16
+ onPrediction;
17
+ onCancel;
18
+ cooldownMap = /* @__PURE__ */ new Map();
19
+ updateDebounceTimer = null;
20
+ constructor(config = {}, callbacks) {
21
+ this.config = { ...DEFAULT_CONFIG, ...config };
22
+ this.onPrediction = callbacks?.onPrediction;
23
+ this.onCancel = callbacks?.onCancel;
24
+ }
25
+ start() {
26
+ if (typeof window === "undefined") return;
27
+ window.addEventListener("pointermove", this.handlePointerMove);
28
+ this.sampleInterval = window.setInterval(
29
+ () => this.processSamples(),
30
+ this.config.sampleRate
31
+ );
32
+ }
33
+ stop() {
34
+ if (typeof window === "undefined") return;
35
+ window.removeEventListener("pointermove", this.handlePointerMove);
36
+ if (this.sampleInterval) {
37
+ clearInterval(this.sampleInterval);
38
+ this.sampleInterval = null;
39
+ }
40
+ if (this.updateDebounceTimer) {
41
+ clearTimeout(this.updateDebounceTimer);
42
+ this.updateDebounceTimer = null;
43
+ }
44
+ this.samples = [];
45
+ this.links.clear();
46
+ this.cooldownMap.clear();
47
+ this.lastPrediction = null;
48
+ }
49
+ registerLink(href, element) {
50
+ const rect = element.getBoundingClientRect();
51
+ this.links.set(element, { href, rect, element });
52
+ }
53
+ unregisterLink(element) {
54
+ this.links.delete(element);
55
+ }
56
+ updateLinkPositions() {
57
+ if (this.updateDebounceTimer) {
58
+ clearTimeout(this.updateDebounceTimer);
59
+ }
60
+ this.updateDebounceTimer = window.setTimeout(() => {
61
+ this.links.forEach((link, element) => {
62
+ const rect = element.getBoundingClientRect();
63
+ this.links.set(element, { ...link, rect });
64
+ });
65
+ this.updateDebounceTimer = null;
66
+ }, 100);
67
+ }
68
+ handlePointerMove = (e) => {
69
+ this.samples.push({
70
+ x: e.clientX,
71
+ y: e.clientY,
72
+ t: e.timeStamp
73
+ });
74
+ if (this.samples.length > 10) {
75
+ this.samples.shift();
76
+ }
77
+ };
78
+ processSamples() {
79
+ if (this.samples.length < 4) return;
80
+ const velocity = this.computeVelocity();
81
+ const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);
82
+ if (speed < this.config.minVelocity) {
83
+ if (this.lastPrediction && this.config.cancelOnDeviation) {
84
+ this.onCancel?.(this.lastPrediction);
85
+ this.lastPrediction = null;
86
+ }
87
+ return;
88
+ }
89
+ const target = this.predictTarget(velocity);
90
+ if (target && target.href !== this.lastPrediction) {
91
+ const now = Date.now();
92
+ const lastTime = this.cooldownMap.get(target.href) ?? 0;
93
+ const cooldown = 500;
94
+ if (now - lastTime < cooldown) {
95
+ return;
96
+ }
97
+ if (this.lastPrediction && this.config.cancelOnDeviation) {
98
+ this.onCancel?.(this.lastPrediction);
99
+ }
100
+ this.lastPrediction = target.href;
101
+ this.cooldownMap.set(target.href, now);
102
+ this.onPrediction?.(target.href);
103
+ } else if (!target && this.lastPrediction && this.config.cancelOnDeviation) {
104
+ this.onCancel?.(this.lastPrediction);
105
+ this.lastPrediction = null;
106
+ }
107
+ }
108
+ computeVelocity() {
109
+ const recent = this.samples.slice(-4);
110
+ const dt = recent[recent.length - 1].t - recent[0].t;
111
+ if (dt === 0) return { vx: 0, vy: 0 };
112
+ const dx = recent[recent.length - 1].x - recent[0].x;
113
+ const dy = recent[recent.length - 1].y - recent[0].y;
114
+ return {
115
+ vx: dx / dt,
116
+ vy: dy / dt
117
+ };
118
+ }
119
+ projectRay(velocity) {
120
+ const origin = this.samples[this.samples.length - 1];
121
+ return {
122
+ x: origin.x + velocity.vx * this.config.lookaheadMs,
123
+ y: origin.y + velocity.vy * this.config.lookaheadMs
124
+ };
125
+ }
126
+ predictTarget(velocity) {
127
+ const tip = this.projectRay(velocity);
128
+ for (const link of this.links.values()) {
129
+ if (this.rectContains(link.rect, tip)) {
130
+ return link;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ rectContains(rect, point) {
136
+ return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
137
+ }
138
+ };
139
+ function createTrajectoryEngine(config, callbacks) {
140
+ return new TrajectoryEngine(config, callbacks);
141
+ }
142
+
143
+ exports.TrajectoryEngine = TrajectoryEngine;
144
+ exports.createTrajectoryEngine = createTrajectoryEngine;
145
+ //# sourceMappingURL=trajectory.cjs.map
146
+ //# sourceMappingURL=trajectory.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/trajectory.ts"],"names":[],"mappings":"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","file":"trajectory.cjs","sourcesContent":["import type {\n PointerSample,\n Velocity,\n LinkRect,\n TrajectoryConfig,\n} from \"./types\";\n\nconst DEFAULT_CONFIG: TrajectoryConfig = {\n lookaheadMs: 120,\n minVelocity: 0.2,\n sampleRate: 50,\n cancelOnDeviation: true,\n};\n\nexport class TrajectoryEngine {\n private samples: PointerSample[] = [];\n private links: Map<HTMLAnchorElement, LinkRect> = new Map();\n private config: TrajectoryConfig;\n private lastPrediction: string | null = null;\n private sampleInterval: number | null = null;\n private onPrediction?: (href: string) => void;\n private onCancel?: (href: string) => void;\n private cooldownMap = new Map<string, number>();\n private updateDebounceTimer: number | null = null;\n\n constructor(\n config: Partial<TrajectoryConfig> = {},\n callbacks?: {\n onPrediction?: (href: string) => void;\n onCancel?: (href: string) => void;\n }\n ) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.onPrediction = callbacks?.onPrediction;\n this.onCancel = callbacks?.onCancel;\n }\n\n start(): void {\n if (typeof window === \"undefined\") return;\n\n window.addEventListener(\"pointermove\", this.handlePointerMove);\n this.sampleInterval = window.setInterval(\n () => this.processSamples(),\n this.config.sampleRate\n );\n }\n\n stop(): void {\n if (typeof window === \"undefined\") return;\n\n window.removeEventListener(\"pointermove\", this.handlePointerMove);\n if (this.sampleInterval) {\n clearInterval(this.sampleInterval);\n this.sampleInterval = null;\n }\n if (this.updateDebounceTimer) {\n clearTimeout(this.updateDebounceTimer);\n this.updateDebounceTimer = null;\n }\n \n // Clean up memory\n this.samples = [];\n this.links.clear();\n this.cooldownMap.clear();\n this.lastPrediction = null;\n }\n\n registerLink(href: string, element: HTMLAnchorElement): void {\n const rect = element.getBoundingClientRect();\n this.links.set(element, { href, rect, element });\n }\n\n unregisterLink(element: HTMLAnchorElement): void {\n this.links.delete(element);\n }\n\n updateLinkPositions(): void {\n // Debounce to prevent excessive reflows\n if (this.updateDebounceTimer) {\n clearTimeout(this.updateDebounceTimer);\n }\n\n this.updateDebounceTimer = window.setTimeout(() => {\n this.links.forEach((link, element) => {\n const rect = element.getBoundingClientRect();\n this.links.set(element, { ...link, rect });\n });\n this.updateDebounceTimer = null;\n }, 100);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n this.samples.push({\n x: e.clientX,\n y: e.clientY,\n t: e.timeStamp,\n });\n\n // Keep only last 10 samples for smoothing\n if (this.samples.length > 10) {\n this.samples.shift();\n }\n };\n\n private processSamples(): void {\n if (this.samples.length < 4) return;\n\n const velocity = this.computeVelocity();\n const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);\n\n if (speed < this.config.minVelocity) {\n if (this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n this.lastPrediction = null;\n }\n return;\n }\n\n const target = this.predictTarget(velocity);\n\n if (target && target.href !== this.lastPrediction) {\n // Check cooldown\n const now = Date.now();\n const lastTime = this.cooldownMap.get(target.href) ?? 0;\n const cooldown = 500; // 500ms cooldown per href\n \n if (now - lastTime < cooldown) {\n return; // Still in cooldown\n }\n\n if (this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n }\n \n this.lastPrediction = target.href;\n this.cooldownMap.set(target.href, now);\n this.onPrediction?.(target.href);\n } else if (!target && this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n this.lastPrediction = null;\n }\n }\n\n private computeVelocity(): Velocity {\n const recent = this.samples.slice(-4);\n const dt = recent[recent.length - 1]!.t - recent[0]!.t;\n\n if (dt === 0) return { vx: 0, vy: 0 };\n\n const dx = recent[recent.length - 1]!.x - recent[0]!.x;\n const dy = recent[recent.length - 1]!.y - recent[0]!.y;\n\n return {\n vx: dx / dt,\n vy: dy / dt,\n };\n }\n\n private projectRay(velocity: Velocity): { x: number; y: number } {\n const origin = this.samples[this.samples.length - 1]!;\n return {\n x: origin.x + velocity.vx * this.config.lookaheadMs,\n y: origin.y + velocity.vy * this.config.lookaheadMs,\n };\n }\n\n private predictTarget(velocity: Velocity): LinkRect | null {\n const tip = this.projectRay(velocity);\n\n for (const link of this.links.values()) {\n if (this.rectContains(link.rect, tip)) {\n return link;\n }\n }\n\n return null;\n }\n\n private rectContains(rect: DOMRect, point: { x: number; y: number }): boolean {\n return (\n point.x >= rect.left &&\n point.x <= rect.right &&\n point.y >= rect.top &&\n point.y <= rect.bottom\n );\n }\n}\n\nexport function createTrajectoryEngine(\n config?: Partial<TrajectoryConfig>,\n callbacks?: {\n onPrediction?: (href: string) => void;\n onCancel?: (href: string) => void;\n }\n): TrajectoryEngine {\n return new TrajectoryEngine(config, callbacks);\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import { T as TrajectoryConfig } from './types-DnUtmOfQ.cjs';
2
+
3
+ declare class TrajectoryEngine {
4
+ private samples;
5
+ private links;
6
+ private config;
7
+ private lastPrediction;
8
+ private sampleInterval;
9
+ private onPrediction?;
10
+ private onCancel?;
11
+ private cooldownMap;
12
+ private updateDebounceTimer;
13
+ constructor(config?: Partial<TrajectoryConfig>, callbacks?: {
14
+ onPrediction?: (href: string) => void;
15
+ onCancel?: (href: string) => void;
16
+ });
17
+ start(): void;
18
+ stop(): void;
19
+ registerLink(href: string, element: HTMLAnchorElement): void;
20
+ unregisterLink(element: HTMLAnchorElement): void;
21
+ updateLinkPositions(): void;
22
+ private handlePointerMove;
23
+ private processSamples;
24
+ private computeVelocity;
25
+ private projectRay;
26
+ private predictTarget;
27
+ private rectContains;
28
+ }
29
+ declare function createTrajectoryEngine(config?: Partial<TrajectoryConfig>, callbacks?: {
30
+ onPrediction?: (href: string) => void;
31
+ onCancel?: (href: string) => void;
32
+ }): TrajectoryEngine;
33
+
34
+ export { TrajectoryEngine, createTrajectoryEngine };
@@ -0,0 +1,34 @@
1
+ import { T as TrajectoryConfig } from './types-DnUtmOfQ.js';
2
+
3
+ declare class TrajectoryEngine {
4
+ private samples;
5
+ private links;
6
+ private config;
7
+ private lastPrediction;
8
+ private sampleInterval;
9
+ private onPrediction?;
10
+ private onCancel?;
11
+ private cooldownMap;
12
+ private updateDebounceTimer;
13
+ constructor(config?: Partial<TrajectoryConfig>, callbacks?: {
14
+ onPrediction?: (href: string) => void;
15
+ onCancel?: (href: string) => void;
16
+ });
17
+ start(): void;
18
+ stop(): void;
19
+ registerLink(href: string, element: HTMLAnchorElement): void;
20
+ unregisterLink(element: HTMLAnchorElement): void;
21
+ updateLinkPositions(): void;
22
+ private handlePointerMove;
23
+ private processSamples;
24
+ private computeVelocity;
25
+ private projectRay;
26
+ private predictTarget;
27
+ private rectContains;
28
+ }
29
+ declare function createTrajectoryEngine(config?: Partial<TrajectoryConfig>, callbacks?: {
30
+ onPrediction?: (href: string) => void;
31
+ onCancel?: (href: string) => void;
32
+ }): TrajectoryEngine;
33
+
34
+ export { TrajectoryEngine, createTrajectoryEngine };
@@ -0,0 +1,143 @@
1
+ // src/trajectory.ts
2
+ var DEFAULT_CONFIG = {
3
+ lookaheadMs: 120,
4
+ minVelocity: 0.2,
5
+ sampleRate: 50,
6
+ cancelOnDeviation: true
7
+ };
8
+ var TrajectoryEngine = class {
9
+ samples = [];
10
+ links = /* @__PURE__ */ new Map();
11
+ config;
12
+ lastPrediction = null;
13
+ sampleInterval = null;
14
+ onPrediction;
15
+ onCancel;
16
+ cooldownMap = /* @__PURE__ */ new Map();
17
+ updateDebounceTimer = null;
18
+ constructor(config = {}, callbacks) {
19
+ this.config = { ...DEFAULT_CONFIG, ...config };
20
+ this.onPrediction = callbacks?.onPrediction;
21
+ this.onCancel = callbacks?.onCancel;
22
+ }
23
+ start() {
24
+ if (typeof window === "undefined") return;
25
+ window.addEventListener("pointermove", this.handlePointerMove);
26
+ this.sampleInterval = window.setInterval(
27
+ () => this.processSamples(),
28
+ this.config.sampleRate
29
+ );
30
+ }
31
+ stop() {
32
+ if (typeof window === "undefined") return;
33
+ window.removeEventListener("pointermove", this.handlePointerMove);
34
+ if (this.sampleInterval) {
35
+ clearInterval(this.sampleInterval);
36
+ this.sampleInterval = null;
37
+ }
38
+ if (this.updateDebounceTimer) {
39
+ clearTimeout(this.updateDebounceTimer);
40
+ this.updateDebounceTimer = null;
41
+ }
42
+ this.samples = [];
43
+ this.links.clear();
44
+ this.cooldownMap.clear();
45
+ this.lastPrediction = null;
46
+ }
47
+ registerLink(href, element) {
48
+ const rect = element.getBoundingClientRect();
49
+ this.links.set(element, { href, rect, element });
50
+ }
51
+ unregisterLink(element) {
52
+ this.links.delete(element);
53
+ }
54
+ updateLinkPositions() {
55
+ if (this.updateDebounceTimer) {
56
+ clearTimeout(this.updateDebounceTimer);
57
+ }
58
+ this.updateDebounceTimer = window.setTimeout(() => {
59
+ this.links.forEach((link, element) => {
60
+ const rect = element.getBoundingClientRect();
61
+ this.links.set(element, { ...link, rect });
62
+ });
63
+ this.updateDebounceTimer = null;
64
+ }, 100);
65
+ }
66
+ handlePointerMove = (e) => {
67
+ this.samples.push({
68
+ x: e.clientX,
69
+ y: e.clientY,
70
+ t: e.timeStamp
71
+ });
72
+ if (this.samples.length > 10) {
73
+ this.samples.shift();
74
+ }
75
+ };
76
+ processSamples() {
77
+ if (this.samples.length < 4) return;
78
+ const velocity = this.computeVelocity();
79
+ const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);
80
+ if (speed < this.config.minVelocity) {
81
+ if (this.lastPrediction && this.config.cancelOnDeviation) {
82
+ this.onCancel?.(this.lastPrediction);
83
+ this.lastPrediction = null;
84
+ }
85
+ return;
86
+ }
87
+ const target = this.predictTarget(velocity);
88
+ if (target && target.href !== this.lastPrediction) {
89
+ const now = Date.now();
90
+ const lastTime = this.cooldownMap.get(target.href) ?? 0;
91
+ const cooldown = 500;
92
+ if (now - lastTime < cooldown) {
93
+ return;
94
+ }
95
+ if (this.lastPrediction && this.config.cancelOnDeviation) {
96
+ this.onCancel?.(this.lastPrediction);
97
+ }
98
+ this.lastPrediction = target.href;
99
+ this.cooldownMap.set(target.href, now);
100
+ this.onPrediction?.(target.href);
101
+ } else if (!target && this.lastPrediction && this.config.cancelOnDeviation) {
102
+ this.onCancel?.(this.lastPrediction);
103
+ this.lastPrediction = null;
104
+ }
105
+ }
106
+ computeVelocity() {
107
+ const recent = this.samples.slice(-4);
108
+ const dt = recent[recent.length - 1].t - recent[0].t;
109
+ if (dt === 0) return { vx: 0, vy: 0 };
110
+ const dx = recent[recent.length - 1].x - recent[0].x;
111
+ const dy = recent[recent.length - 1].y - recent[0].y;
112
+ return {
113
+ vx: dx / dt,
114
+ vy: dy / dt
115
+ };
116
+ }
117
+ projectRay(velocity) {
118
+ const origin = this.samples[this.samples.length - 1];
119
+ return {
120
+ x: origin.x + velocity.vx * this.config.lookaheadMs,
121
+ y: origin.y + velocity.vy * this.config.lookaheadMs
122
+ };
123
+ }
124
+ predictTarget(velocity) {
125
+ const tip = this.projectRay(velocity);
126
+ for (const link of this.links.values()) {
127
+ if (this.rectContains(link.rect, tip)) {
128
+ return link;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ rectContains(rect, point) {
134
+ return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
135
+ }
136
+ };
137
+ function createTrajectoryEngine(config, callbacks) {
138
+ return new TrajectoryEngine(config, callbacks);
139
+ }
140
+
141
+ export { TrajectoryEngine, createTrajectoryEngine };
142
+ //# sourceMappingURL=trajectory.js.map
143
+ //# sourceMappingURL=trajectory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/trajectory.ts"],"names":[],"mappings":"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","file":"trajectory.js","sourcesContent":["import type {\n PointerSample,\n Velocity,\n LinkRect,\n TrajectoryConfig,\n} from \"./types\";\n\nconst DEFAULT_CONFIG: TrajectoryConfig = {\n lookaheadMs: 120,\n minVelocity: 0.2,\n sampleRate: 50,\n cancelOnDeviation: true,\n};\n\nexport class TrajectoryEngine {\n private samples: PointerSample[] = [];\n private links: Map<HTMLAnchorElement, LinkRect> = new Map();\n private config: TrajectoryConfig;\n private lastPrediction: string | null = null;\n private sampleInterval: number | null = null;\n private onPrediction?: (href: string) => void;\n private onCancel?: (href: string) => void;\n private cooldownMap = new Map<string, number>();\n private updateDebounceTimer: number | null = null;\n\n constructor(\n config: Partial<TrajectoryConfig> = {},\n callbacks?: {\n onPrediction?: (href: string) => void;\n onCancel?: (href: string) => void;\n }\n ) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.onPrediction = callbacks?.onPrediction;\n this.onCancel = callbacks?.onCancel;\n }\n\n start(): void {\n if (typeof window === \"undefined\") return;\n\n window.addEventListener(\"pointermove\", this.handlePointerMove);\n this.sampleInterval = window.setInterval(\n () => this.processSamples(),\n this.config.sampleRate\n );\n }\n\n stop(): void {\n if (typeof window === \"undefined\") return;\n\n window.removeEventListener(\"pointermove\", this.handlePointerMove);\n if (this.sampleInterval) {\n clearInterval(this.sampleInterval);\n this.sampleInterval = null;\n }\n if (this.updateDebounceTimer) {\n clearTimeout(this.updateDebounceTimer);\n this.updateDebounceTimer = null;\n }\n \n // Clean up memory\n this.samples = [];\n this.links.clear();\n this.cooldownMap.clear();\n this.lastPrediction = null;\n }\n\n registerLink(href: string, element: HTMLAnchorElement): void {\n const rect = element.getBoundingClientRect();\n this.links.set(element, { href, rect, element });\n }\n\n unregisterLink(element: HTMLAnchorElement): void {\n this.links.delete(element);\n }\n\n updateLinkPositions(): void {\n // Debounce to prevent excessive reflows\n if (this.updateDebounceTimer) {\n clearTimeout(this.updateDebounceTimer);\n }\n\n this.updateDebounceTimer = window.setTimeout(() => {\n this.links.forEach((link, element) => {\n const rect = element.getBoundingClientRect();\n this.links.set(element, { ...link, rect });\n });\n this.updateDebounceTimer = null;\n }, 100);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n this.samples.push({\n x: e.clientX,\n y: e.clientY,\n t: e.timeStamp,\n });\n\n // Keep only last 10 samples for smoothing\n if (this.samples.length > 10) {\n this.samples.shift();\n }\n };\n\n private processSamples(): void {\n if (this.samples.length < 4) return;\n\n const velocity = this.computeVelocity();\n const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);\n\n if (speed < this.config.minVelocity) {\n if (this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n this.lastPrediction = null;\n }\n return;\n }\n\n const target = this.predictTarget(velocity);\n\n if (target && target.href !== this.lastPrediction) {\n // Check cooldown\n const now = Date.now();\n const lastTime = this.cooldownMap.get(target.href) ?? 0;\n const cooldown = 500; // 500ms cooldown per href\n \n if (now - lastTime < cooldown) {\n return; // Still in cooldown\n }\n\n if (this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n }\n \n this.lastPrediction = target.href;\n this.cooldownMap.set(target.href, now);\n this.onPrediction?.(target.href);\n } else if (!target && this.lastPrediction && this.config.cancelOnDeviation) {\n this.onCancel?.(this.lastPrediction);\n this.lastPrediction = null;\n }\n }\n\n private computeVelocity(): Velocity {\n const recent = this.samples.slice(-4);\n const dt = recent[recent.length - 1]!.t - recent[0]!.t;\n\n if (dt === 0) return { vx: 0, vy: 0 };\n\n const dx = recent[recent.length - 1]!.x - recent[0]!.x;\n const dy = recent[recent.length - 1]!.y - recent[0]!.y;\n\n return {\n vx: dx / dt,\n vy: dy / dt,\n };\n }\n\n private projectRay(velocity: Velocity): { x: number; y: number } {\n const origin = this.samples[this.samples.length - 1]!;\n return {\n x: origin.x + velocity.vx * this.config.lookaheadMs,\n y: origin.y + velocity.vy * this.config.lookaheadMs,\n };\n }\n\n private predictTarget(velocity: Velocity): LinkRect | null {\n const tip = this.projectRay(velocity);\n\n for (const link of this.links.values()) {\n if (this.rectContains(link.rect, tip)) {\n return link;\n }\n }\n\n return null;\n }\n\n private rectContains(rect: DOMRect, point: { x: number; y: number }): boolean {\n return (\n point.x >= rect.left &&\n point.x <= rect.right &&\n point.y >= rect.top &&\n point.y <= rect.bottom\n );\n }\n}\n\nexport function createTrajectoryEngine(\n config?: Partial<TrajectoryConfig>,\n callbacks?: {\n onPrediction?: (href: string) => void;\n onCancel?: (href: string) => void;\n }\n): TrajectoryEngine {\n return new TrajectoryEngine(config, callbacks);\n}\n"]}