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.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/dist/adaptive.cjs +108 -0
- package/dist/adaptive.cjs.map +1 -0
- package/dist/adaptive.d.cts +24 -0
- package/dist/adaptive.d.ts +24 -0
- package/dist/adaptive.js +105 -0
- package/dist/adaptive.js.map +1 -0
- package/dist/cache.cjs +224 -0
- package/dist/cache.cjs.map +1 -0
- package/dist/cache.d.cts +33 -0
- package/dist/cache.d.ts +33 -0
- package/dist/cache.js +221 -0
- package/dist/cache.js.map +1 -0
- package/dist/graph.cjs +132 -0
- package/dist/graph.cjs.map +1 -0
- package/dist/graph.d.cts +19 -0
- package/dist/graph.d.ts +19 -0
- package/dist/graph.js +128 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.cjs +843 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +826 -0
- package/dist/index.js.map +1 -0
- package/dist/morpher.cjs +158 -0
- package/dist/morpher.cjs.map +1 -0
- package/dist/morpher.d.cts +19 -0
- package/dist/morpher.d.ts +19 -0
- package/dist/morpher.js +154 -0
- package/dist/morpher.js.map +1 -0
- package/dist/performance.cjs +40 -0
- package/dist/performance.cjs.map +1 -0
- package/dist/performance.d.cts +21 -0
- package/dist/performance.d.ts +21 -0
- package/dist/performance.js +37 -0
- package/dist/performance.js.map +1 -0
- package/dist/speculator.cjs +59 -0
- package/dist/speculator.cjs.map +1 -0
- package/dist/speculator.d.cts +14 -0
- package/dist/speculator.d.ts +14 -0
- package/dist/speculator.js +56 -0
- package/dist/speculator.js.map +1 -0
- package/dist/trajectory.cjs +146 -0
- package/dist/trajectory.cjs.map +1 -0
- package/dist/trajectory.d.cts +34 -0
- package/dist/trajectory.d.ts +34 -0
- package/dist/trajectory.js +143 -0
- package/dist/trajectory.js.map +1 -0
- package/dist/types-DnUtmOfQ.d.cts +88 -0
- package/dist/types-DnUtmOfQ.d.ts +88 -0
- package/package.json +95 -0
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { C as CacheConfig, a as CacheLayer } from './types-DnUtmOfQ.js';
|
|
2
|
+
|
|
3
|
+
declare class CacheManager {
|
|
4
|
+
private config;
|
|
5
|
+
private memoryCache;
|
|
6
|
+
private accessOrder;
|
|
7
|
+
private broadcastChannel?;
|
|
8
|
+
private onCacheHit?;
|
|
9
|
+
constructor(config?: Partial<CacheConfig>, callbacks?: {
|
|
10
|
+
onCacheHit?: (href: string, layer: CacheLayer) => void;
|
|
11
|
+
});
|
|
12
|
+
get(href: string): Promise<string | null>;
|
|
13
|
+
set(href: string, html: string): Promise<void>;
|
|
14
|
+
private containsDangerousContent;
|
|
15
|
+
private sanitizeHref;
|
|
16
|
+
clear(href?: string): void;
|
|
17
|
+
getSize(): number;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
private getFromMemory;
|
|
20
|
+
private setInMemory;
|
|
21
|
+
private getFromServiceWorker;
|
|
22
|
+
private setInServiceWorker;
|
|
23
|
+
private clearFromServiceWorker;
|
|
24
|
+
private clearAllServiceWorker;
|
|
25
|
+
private isExcluded;
|
|
26
|
+
private broadcastSet;
|
|
27
|
+
private handleBroadcast;
|
|
28
|
+
}
|
|
29
|
+
declare function createCacheManager(config?: Partial<CacheConfig>, callbacks?: {
|
|
30
|
+
onCacheHit?: (href: string, layer: CacheLayer) => void;
|
|
31
|
+
}): CacheManager;
|
|
32
|
+
|
|
33
|
+
export { CacheManager, createCacheManager };
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
memory: {
|
|
4
|
+
enabled: true,
|
|
5
|
+
maxPages: 10
|
|
6
|
+
},
|
|
7
|
+
serviceWorker: {
|
|
8
|
+
enabled: true,
|
|
9
|
+
ttl: 300,
|
|
10
|
+
cacheName: "specnav-v1",
|
|
11
|
+
crossTabSync: true,
|
|
12
|
+
broadcastChannel: "specnav-sync"
|
|
13
|
+
},
|
|
14
|
+
edge: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
maxAge: 60,
|
|
17
|
+
staleWhileRevalidate: 300
|
|
18
|
+
},
|
|
19
|
+
exclude: []
|
|
20
|
+
};
|
|
21
|
+
var CacheManager = class {
|
|
22
|
+
config;
|
|
23
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
24
|
+
accessOrder = [];
|
|
25
|
+
broadcastChannel;
|
|
26
|
+
onCacheHit;
|
|
27
|
+
constructor(config = {}, callbacks) {
|
|
28
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
29
|
+
this.onCacheHit = callbacks?.onCacheHit;
|
|
30
|
+
if (this.config.serviceWorker.enabled && this.config.serviceWorker.crossTabSync && typeof BroadcastChannel !== "undefined") {
|
|
31
|
+
this.broadcastChannel = new BroadcastChannel(
|
|
32
|
+
this.config.serviceWorker.broadcastChannel
|
|
33
|
+
);
|
|
34
|
+
this.broadcastChannel.onmessage = this.handleBroadcast;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async get(href) {
|
|
38
|
+
if (this.isExcluded(href)) return null;
|
|
39
|
+
const memoryHit = this.getFromMemory(href);
|
|
40
|
+
if (memoryHit) {
|
|
41
|
+
this.onCacheHit?.(href, 1);
|
|
42
|
+
return memoryHit;
|
|
43
|
+
}
|
|
44
|
+
if (this.config.serviceWorker.enabled) {
|
|
45
|
+
const swHit = await this.getFromServiceWorker(href);
|
|
46
|
+
if (swHit) {
|
|
47
|
+
this.onCacheHit?.(href, 2);
|
|
48
|
+
this.setInMemory(href, swHit);
|
|
49
|
+
return swHit;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async set(href, html) {
|
|
55
|
+
if (this.isExcluded(href)) return;
|
|
56
|
+
const sanitizedHref = this.sanitizeHref(href);
|
|
57
|
+
if (!sanitizedHref) return;
|
|
58
|
+
if (this.containsDangerousContent(html)) {
|
|
59
|
+
console.warn(`Refusing to cache potentially dangerous content from ${sanitizedHref}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const page = {
|
|
63
|
+
href: sanitizedHref,
|
|
64
|
+
html,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
ttl: this.config.serviceWorker.ttl
|
|
67
|
+
};
|
|
68
|
+
if (this.config.memory.enabled) {
|
|
69
|
+
this.setInMemory(sanitizedHref, html);
|
|
70
|
+
}
|
|
71
|
+
if (this.config.serviceWorker.enabled) {
|
|
72
|
+
await this.setInServiceWorker(page);
|
|
73
|
+
this.broadcastSet(sanitizedHref);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
containsDangerousContent(html) {
|
|
77
|
+
const dangerousPatterns = [
|
|
78
|
+
/on\w+\s*=\s*["'][^"']*["']/i,
|
|
79
|
+
// Inline event handlers
|
|
80
|
+
/javascript:/i
|
|
81
|
+
// javascript: URLs
|
|
82
|
+
];
|
|
83
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
86
|
+
const attrs = match[1] || "";
|
|
87
|
+
if (/src\s*=/.test(attrs)) continue;
|
|
88
|
+
if (/type\s*=\s*["']?(application\/json|application\/ld\+json|text\/template)["']?/i.test(attrs)) continue;
|
|
89
|
+
if (/id\s*=\s*["']__NEXT_DATA__["']/i.test(attrs)) continue;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return dangerousPatterns.some((pattern) => pattern.test(html));
|
|
93
|
+
}
|
|
94
|
+
sanitizeHref(href) {
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(href, window.location.origin);
|
|
97
|
+
if (url.origin !== window.location.origin) return null;
|
|
98
|
+
return url.pathname + url.search + url.hash;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
clear(href) {
|
|
104
|
+
if (href) {
|
|
105
|
+
this.memoryCache.delete(href);
|
|
106
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
107
|
+
this.clearFromServiceWorker(href);
|
|
108
|
+
} else {
|
|
109
|
+
this.memoryCache.clear();
|
|
110
|
+
this.accessOrder = [];
|
|
111
|
+
this.clearAllServiceWorker();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
getSize() {
|
|
115
|
+
return this.memoryCache.size;
|
|
116
|
+
}
|
|
117
|
+
destroy() {
|
|
118
|
+
this.memoryCache.clear();
|
|
119
|
+
this.accessOrder = [];
|
|
120
|
+
this.broadcastChannel?.close();
|
|
121
|
+
}
|
|
122
|
+
getFromMemory(href) {
|
|
123
|
+
const page = this.memoryCache.get(href);
|
|
124
|
+
if (!page) return null;
|
|
125
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
126
|
+
this.accessOrder.push(href);
|
|
127
|
+
return page.html;
|
|
128
|
+
}
|
|
129
|
+
setInMemory(href, html) {
|
|
130
|
+
const maxPageSize = 5 * 1024 * 1024;
|
|
131
|
+
if (html.length > maxPageSize) {
|
|
132
|
+
console.warn(`Page ${href} exceeds max size, not caching`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.memoryCache.set(href, {
|
|
136
|
+
href,
|
|
137
|
+
html,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
ttl: this.config.serviceWorker.ttl
|
|
140
|
+
});
|
|
141
|
+
const existingIndex = this.accessOrder.indexOf(href);
|
|
142
|
+
if (existingIndex !== -1) {
|
|
143
|
+
this.accessOrder.splice(existingIndex, 1);
|
|
144
|
+
}
|
|
145
|
+
this.accessOrder.push(href);
|
|
146
|
+
if (this.memoryCache.size > this.config.memory.maxPages) {
|
|
147
|
+
const oldest = this.accessOrder.shift();
|
|
148
|
+
if (oldest) this.memoryCache.delete(oldest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async getFromServiceWorker(href) {
|
|
152
|
+
if (typeof caches === "undefined") return null;
|
|
153
|
+
try {
|
|
154
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
155
|
+
const response = await cache.match(href);
|
|
156
|
+
if (!response) return null;
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
const page = data;
|
|
159
|
+
const age = Date.now() - page.timestamp;
|
|
160
|
+
if (age > page.ttl * 1e3) {
|
|
161
|
+
await cache.delete(href);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return page.html;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async setInServiceWorker(page) {
|
|
170
|
+
if (typeof caches === "undefined") return;
|
|
171
|
+
try {
|
|
172
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
173
|
+
const response = new Response(JSON.stringify(page), {
|
|
174
|
+
headers: {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"Cache-Control": `max-age=${this.config.edge.maxAge}, stale-while-revalidate=${this.config.edge.staleWhileRevalidate}`
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
await cache.put(page.href, response);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async clearFromServiceWorker(href) {
|
|
184
|
+
if (typeof caches === "undefined") return;
|
|
185
|
+
try {
|
|
186
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
187
|
+
await cache.delete(href);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async clearAllServiceWorker() {
|
|
192
|
+
if (typeof caches === "undefined") return;
|
|
193
|
+
try {
|
|
194
|
+
await caches.delete(this.config.serviceWorker.cacheName);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
isExcluded(href) {
|
|
199
|
+
return this.config.exclude.some((pattern) => {
|
|
200
|
+
if (typeof pattern === "string") {
|
|
201
|
+
return pattern.includes("*") ? new RegExp(pattern.replace(/\*/g, ".*")).test(href) : href === pattern;
|
|
202
|
+
}
|
|
203
|
+
return pattern.test(href);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
broadcastSet(href) {
|
|
207
|
+
this.broadcastChannel?.postMessage({ type: "set", href });
|
|
208
|
+
}
|
|
209
|
+
handleBroadcast = (event) => {
|
|
210
|
+
if (event.data.type === "set") {
|
|
211
|
+
this.memoryCache.delete(event.data.href);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
function createCacheManager(config, callbacks) {
|
|
216
|
+
return new CacheManager(config, callbacks);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { CacheManager, createCacheManager };
|
|
220
|
+
//# sourceMappingURL=cache.js.map
|
|
221
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts"],"names":[],"mappings":"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","file":"cache.js","sourcesContent":["import type { CacheConfig, CachedPage, CacheLayer } from \"./types\";\n\nconst DEFAULT_CONFIG: CacheConfig = {\n memory: {\n enabled: true,\n maxPages: 10,\n },\n serviceWorker: {\n enabled: true,\n ttl: 300,\n cacheName: \"specnav-v1\",\n crossTabSync: true,\n broadcastChannel: \"specnav-sync\",\n },\n edge: {\n enabled: true,\n maxAge: 60,\n staleWhileRevalidate: 300,\n },\n exclude: [],\n};\n\nexport class CacheManager {\n private config: CacheConfig;\n private memoryCache: Map<string, CachedPage> = new Map();\n private accessOrder: string[] = [];\n private broadcastChannel?: BroadcastChannel;\n private onCacheHit?: (href: string, layer: CacheLayer) => void;\n\n constructor(\n config: Partial<CacheConfig> = {},\n callbacks?: { onCacheHit?: (href: string, layer: CacheLayer) => void }\n ) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.onCacheHit = callbacks?.onCacheHit;\n\n if (\n this.config.serviceWorker.enabled &&\n this.config.serviceWorker.crossTabSync &&\n typeof BroadcastChannel !== \"undefined\"\n ) {\n this.broadcastChannel = new BroadcastChannel(\n this.config.serviceWorker.broadcastChannel\n );\n this.broadcastChannel.onmessage = this.handleBroadcast;\n }\n }\n\n async get(href: string): Promise<string | null> {\n if (this.isExcluded(href)) return null;\n\n // L1: Memory cache\n const memoryHit = this.getFromMemory(href);\n if (memoryHit) {\n this.onCacheHit?.(href, 1);\n return memoryHit;\n }\n\n // L2: Service Worker cache\n if (this.config.serviceWorker.enabled) {\n const swHit = await this.getFromServiceWorker(href);\n if (swHit) {\n this.onCacheHit?.(href, 2);\n this.setInMemory(href, swHit);\n return swHit;\n }\n }\n\n return null;\n }\n\n async set(href: string, html: string): Promise<void> {\n if (this.isExcluded(href)) return;\n\n // Sanitize href to prevent cache poisoning\n const sanitizedHref = this.sanitizeHref(href);\n if (!sanitizedHref) return;\n\n // Validate HTML doesn't contain dangerous inline scripts\n if (this.containsDangerousContent(html)) {\n console.warn(`Refusing to cache potentially dangerous content from ${sanitizedHref}`);\n return;\n }\n\n const page: CachedPage = {\n href: sanitizedHref,\n html,\n timestamp: Date.now(),\n ttl: this.config.serviceWorker.ttl,\n };\n\n // L1: Memory\n if (this.config.memory.enabled) {\n this.setInMemory(sanitizedHref, html);\n }\n\n // L2: Service Worker\n if (this.config.serviceWorker.enabled) {\n await this.setInServiceWorker(page);\n this.broadcastSet(sanitizedHref);\n }\n }\n\n private containsDangerousContent(html: string): boolean {\n // Check for inline event handlers and javascript: URLs\n const dangerousPatterns = [\n /on\\w+\\s*=\\s*[\"'][^\"']*[\"']/i, // Inline event handlers\n /javascript:/i, // javascript: URLs\n ];\n\n // Check for inline scripts, but exclude safe types\n const scriptRegex = /<script([^>]*)>([\\s\\S]*?)<\\/script>/gi;\n let match;\n while ((match = scriptRegex.exec(html)) !== null) {\n const attrs = match[1] || \"\";\n \n // Allow scripts with src attribute\n if (/src\\s*=/.test(attrs)) continue;\n \n // Allow safe script types (JSON data, templates)\n if (/type\\s*=\\s*[\"']?(application\\/json|application\\/ld\\+json|text\\/template)[\"']?/i.test(attrs)) continue;\n \n // Allow Next.js data script\n if (/id\\s*=\\s*[\"']__NEXT_DATA__[\"']/i.test(attrs)) continue;\n \n // If we get here, it's an inline script without safe type\n return true;\n }\n\n return dangerousPatterns.some(pattern => pattern.test(html));\n }\n\n private sanitizeHref(href: string): string | null {\n try {\n const url = new URL(href, window.location.origin);\n // Only allow same-origin URLs\n if (url.origin !== window.location.origin) return null;\n return url.pathname + url.search + url.hash;\n } catch {\n return null;\n }\n }\n\n clear(href?: string): void {\n if (href) {\n this.memoryCache.delete(href);\n this.accessOrder = this.accessOrder.filter((h) => h !== href);\n this.clearFromServiceWorker(href);\n } else {\n this.memoryCache.clear();\n this.accessOrder = [];\n this.clearAllServiceWorker();\n }\n }\n\n getSize(): number {\n return this.memoryCache.size;\n }\n\n destroy(): void {\n this.memoryCache.clear();\n this.accessOrder = [];\n this.broadcastChannel?.close();\n }\n\n private getFromMemory(href: string): string | null {\n const page = this.memoryCache.get(href);\n if (!page) return null;\n\n // Update LRU order\n this.accessOrder = this.accessOrder.filter((h) => h !== href);\n this.accessOrder.push(href);\n\n return page.html;\n }\n\n private setInMemory(href: string, html: string): void {\n // Prevent memory exhaustion - limit individual page size\n const maxPageSize = 5 * 1024 * 1024; // 5MB per page\n if (html.length > maxPageSize) {\n console.warn(`Page ${href} exceeds max size, not caching`);\n return;\n }\n\n this.memoryCache.set(href, {\n href,\n html,\n timestamp: Date.now(),\n ttl: this.config.serviceWorker.ttl,\n });\n\n // Deduplicate before adding to accessOrder\n const existingIndex = this.accessOrder.indexOf(href);\n if (existingIndex !== -1) {\n this.accessOrder.splice(existingIndex, 1);\n }\n this.accessOrder.push(href);\n\n // LRU eviction\n if (this.memoryCache.size > this.config.memory.maxPages) {\n const oldest = this.accessOrder.shift();\n if (oldest) this.memoryCache.delete(oldest);\n }\n }\n\n private async getFromServiceWorker(href: string): Promise<string | null> {\n if (typeof caches === \"undefined\") return null;\n\n try {\n const cache = await caches.open(this.config.serviceWorker.cacheName);\n const response = await cache.match(href);\n\n if (!response) return null;\n\n const data = await response.json();\n const page = data as CachedPage;\n\n // Check TTL\n const age = Date.now() - page.timestamp;\n if (age > page.ttl * 1000) {\n await cache.delete(href);\n return null;\n }\n\n return page.html;\n } catch {\n return null;\n }\n }\n\n private async setInServiceWorker(page: CachedPage): Promise<void> {\n if (typeof caches === \"undefined\") return;\n\n try {\n const cache = await caches.open(this.config.serviceWorker.cacheName);\n const response = new Response(JSON.stringify(page), {\n headers: {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": `max-age=${this.config.edge.maxAge}, stale-while-revalidate=${this.config.edge.staleWhileRevalidate}`,\n },\n });\n await cache.put(page.href, response);\n } catch {\n // Silently fail\n }\n }\n\n private async clearFromServiceWorker(href: string): Promise<void> {\n if (typeof caches === \"undefined\") return;\n\n try {\n const cache = await caches.open(this.config.serviceWorker.cacheName);\n await cache.delete(href);\n } catch {\n // Silently fail\n }\n }\n\n private async clearAllServiceWorker(): Promise<void> {\n if (typeof caches === \"undefined\") return;\n\n try {\n await caches.delete(this.config.serviceWorker.cacheName);\n } catch {\n // Silently fail\n }\n }\n\n private isExcluded(href: string): boolean {\n return this.config.exclude.some((pattern) => {\n if (typeof pattern === \"string\") {\n return pattern.includes(\"*\")\n ? new RegExp(pattern.replace(/\\*/g, \".*\")).test(href)\n : href === pattern;\n }\n return pattern.test(href);\n });\n }\n\n private broadcastSet(href: string): void {\n this.broadcastChannel?.postMessage({ type: \"set\", href });\n }\n\n private handleBroadcast = (event: MessageEvent): void => {\n if (event.data.type === \"set\") {\n // Invalidate memory cache to force fetch from SW\n this.memoryCache.delete(event.data.href);\n }\n };\n}\n\nexport function createCacheManager(\n config?: Partial<CacheConfig>,\n callbacks?: { onCacheHit?: (href: string, layer: CacheLayer) => void }\n): CacheManager {\n return new CacheManager(config, callbacks);\n}\n"]}
|
package/dist/graph.cjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/graph.ts
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
minConfidence: 0.6,
|
|
7
|
+
sessionWeight: 0.7,
|
|
8
|
+
storageKey: "specnav-graph",
|
|
9
|
+
maxNodes: 50
|
|
10
|
+
};
|
|
11
|
+
var NavigationGraphLearner = class {
|
|
12
|
+
config;
|
|
13
|
+
graph = {};
|
|
14
|
+
sessionGraph = {};
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
17
|
+
if (this.config.enabled) {
|
|
18
|
+
this.loadGraph();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
recordNavigation(fromHref, toHref) {
|
|
22
|
+
if (!this.config.enabled) return;
|
|
23
|
+
if (!this.sessionGraph[fromHref]) {
|
|
24
|
+
this.sessionGraph[fromHref] = {};
|
|
25
|
+
}
|
|
26
|
+
const current = this.sessionGraph[fromHref][toHref] ?? 0;
|
|
27
|
+
this.sessionGraph[fromHref][toHref] = current + 1;
|
|
28
|
+
if (!this.graph[fromHref]) {
|
|
29
|
+
this.graph[fromHref] = {};
|
|
30
|
+
}
|
|
31
|
+
const persistentCurrent = this.graph[fromHref][toHref] ?? 0;
|
|
32
|
+
this.graph[fromHref][toHref] = persistentCurrent + 1;
|
|
33
|
+
this.saveGraph();
|
|
34
|
+
}
|
|
35
|
+
getPredictions(fromHref) {
|
|
36
|
+
if (!this.config.enabled) return [];
|
|
37
|
+
const sessionEdges = this.sessionGraph[fromHref] ?? {};
|
|
38
|
+
const persistentEdges = this.graph[fromHref] ?? {};
|
|
39
|
+
const merged = {};
|
|
40
|
+
Object.entries(persistentEdges).forEach(([href, count]) => {
|
|
41
|
+
merged[href] = count * (1 - this.config.sessionWeight);
|
|
42
|
+
});
|
|
43
|
+
Object.entries(sessionEdges).forEach(([href, count]) => {
|
|
44
|
+
merged[href] = (merged[href] ?? 0) + count * this.config.sessionWeight;
|
|
45
|
+
});
|
|
46
|
+
const total = Object.values(merged).reduce((sum, val) => sum + val, 0);
|
|
47
|
+
if (total === 0) return [];
|
|
48
|
+
const predictions = Object.entries(merged).map(([href, count]) => ({
|
|
49
|
+
href,
|
|
50
|
+
confidence: count / total
|
|
51
|
+
})).filter((p) => p.confidence >= this.config.minConfidence).sort((a, b) => b.confidence - a.confidence).map((p) => p.href);
|
|
52
|
+
return predictions;
|
|
53
|
+
}
|
|
54
|
+
getGraph() {
|
|
55
|
+
return { ...this.graph };
|
|
56
|
+
}
|
|
57
|
+
clearGraph() {
|
|
58
|
+
this.graph = {};
|
|
59
|
+
this.sessionGraph = {};
|
|
60
|
+
this.saveGraph();
|
|
61
|
+
}
|
|
62
|
+
loadGraph() {
|
|
63
|
+
if (typeof localStorage === "undefined") return;
|
|
64
|
+
try {
|
|
65
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
66
|
+
if (stored) {
|
|
67
|
+
this.graph = JSON.parse(stored);
|
|
68
|
+
this.pruneGraph();
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
saveGraph() {
|
|
74
|
+
if (typeof localStorage === "undefined") return;
|
|
75
|
+
try {
|
|
76
|
+
this.pruneGraph();
|
|
77
|
+
const serialized = JSON.stringify(this.graph);
|
|
78
|
+
if (serialized.length > 4 * 1024 * 1024) {
|
|
79
|
+
console.warn("Navigation graph too large, pruning more aggressively");
|
|
80
|
+
const reducedMaxNodes = Math.floor(this.config.maxNodes / 2);
|
|
81
|
+
const originalMaxNodes = this.config.maxNodes;
|
|
82
|
+
this.config.maxNodes = reducedMaxNodes;
|
|
83
|
+
this.pruneGraph();
|
|
84
|
+
this.config.maxNodes = originalMaxNodes;
|
|
85
|
+
}
|
|
86
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(this.graph));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof Error && error.name === "QuotaExceededError") {
|
|
89
|
+
console.warn("localStorage quota exceeded, clearing navigation graph");
|
|
90
|
+
this.clearGraph();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
pruneGraph() {
|
|
95
|
+
const nodes = Object.keys(this.graph);
|
|
96
|
+
if (nodes.length <= this.config.maxNodes) return;
|
|
97
|
+
const nodeCounts = nodes.map((node) => ({
|
|
98
|
+
node,
|
|
99
|
+
count: Object.values(this.graph[node]).reduce(
|
|
100
|
+
(sum, val) => sum + val,
|
|
101
|
+
0
|
|
102
|
+
)
|
|
103
|
+
}));
|
|
104
|
+
nodeCounts.sort((a, b) => b.count - a.count);
|
|
105
|
+
const toKeep = new Set(
|
|
106
|
+
nodeCounts.slice(0, this.config.maxNodes).map((n) => n.node)
|
|
107
|
+
);
|
|
108
|
+
Object.keys(this.graph).forEach((node) => {
|
|
109
|
+
if (!toKeep.has(node)) {
|
|
110
|
+
delete this.graph[node];
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function createNavigationGraphLearner(config) {
|
|
116
|
+
return new NavigationGraphLearner(config);
|
|
117
|
+
}
|
|
118
|
+
function getNavigationGraph(storageKey = "specnav-graph") {
|
|
119
|
+
if (typeof localStorage === "undefined") return {};
|
|
120
|
+
try {
|
|
121
|
+
const stored = localStorage.getItem(storageKey);
|
|
122
|
+
return stored ? JSON.parse(stored) : {};
|
|
123
|
+
} catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
exports.NavigationGraphLearner = NavigationGraphLearner;
|
|
129
|
+
exports.createNavigationGraphLearner = createNavigationGraphLearner;
|
|
130
|
+
exports.getNavigationGraph = getNavigationGraph;
|
|
131
|
+
//# sourceMappingURL=graph.cjs.map
|
|
132
|
+
//# sourceMappingURL=graph.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph.ts"],"names":[],"mappings":"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","file":"graph.cjs","sourcesContent":["import type { GraphConfig, NavigationGraph } from \"./types\";\n\nconst DEFAULT_CONFIG: GraphConfig = {\n enabled: true,\n minConfidence: 0.6,\n sessionWeight: 0.7,\n storageKey: \"specnav-graph\",\n maxNodes: 50,\n};\n\nexport class NavigationGraphLearner {\n private config: GraphConfig;\n private graph: NavigationGraph = {};\n private sessionGraph: NavigationGraph = {};\n\n constructor(config: Partial<GraphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n if (this.config.enabled) {\n this.loadGraph();\n }\n }\n\n recordNavigation(fromHref: string, toHref: string): void {\n if (!this.config.enabled) return;\n\n // Update session graph\n if (!this.sessionGraph[fromHref]) {\n this.sessionGraph[fromHref] = {};\n }\n const current = this.sessionGraph[fromHref]![toHref] ?? 0;\n this.sessionGraph[fromHref]![toHref] = current + 1;\n\n // Update persistent graph\n if (!this.graph[fromHref]) {\n this.graph[fromHref] = {};\n }\n const persistentCurrent = this.graph[fromHref]![toHref] ?? 0;\n this.graph[fromHref]![toHref] = persistentCurrent + 1;\n\n this.saveGraph();\n }\n\n getPredictions(fromHref: string): string[] {\n if (!this.config.enabled) return [];\n\n const sessionEdges = this.sessionGraph[fromHref] ?? {};\n const persistentEdges = this.graph[fromHref] ?? {};\n\n // Merge session and persistent with weighting\n const merged: Record<string, number> = {};\n\n Object.entries(persistentEdges).forEach(([href, count]) => {\n merged[href] = count * (1 - this.config.sessionWeight);\n });\n\n Object.entries(sessionEdges).forEach(([href, count]) => {\n merged[href] = (merged[href] ?? 0) + count * this.config.sessionWeight;\n });\n\n // Calculate total for normalization\n const total = Object.values(merged).reduce((sum, val) => sum + val, 0);\n if (total === 0) return [];\n\n // Convert to confidence scores and filter\n const predictions = Object.entries(merged)\n .map(([href, count]) => ({\n href,\n confidence: count / total,\n }))\n .filter((p) => p.confidence >= this.config.minConfidence)\n .sort((a, b) => b.confidence - a.confidence)\n .map((p) => p.href);\n\n return predictions;\n }\n\n getGraph(): NavigationGraph {\n return { ...this.graph };\n }\n\n clearGraph(): void {\n this.graph = {};\n this.sessionGraph = {};\n this.saveGraph();\n }\n\n private loadGraph(): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n const stored = localStorage.getItem(this.config.storageKey);\n if (stored) {\n this.graph = JSON.parse(stored);\n this.pruneGraph();\n }\n } catch {\n // Silently fail\n }\n }\n\n private saveGraph(): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n this.pruneGraph();\n const serialized = JSON.stringify(this.graph);\n \n // Check size before saving (localStorage typically has 5-10MB limit)\n if (serialized.length > 4 * 1024 * 1024) { // 4MB safety limit\n console.warn(\"Navigation graph too large, pruning more aggressively\");\n // Prune to half the max nodes (use local variable)\n const reducedMaxNodes = Math.floor(this.config.maxNodes / 2);\n const originalMaxNodes = this.config.maxNodes;\n this.config.maxNodes = reducedMaxNodes;\n this.pruneGraph();\n this.config.maxNodes = originalMaxNodes; // Restore original\n }\n \n localStorage.setItem(this.config.storageKey, JSON.stringify(this.graph));\n } catch (error) {\n // Handle quota exceeded error\n if (error instanceof Error && error.name === \"QuotaExceededError\") {\n console.warn(\"localStorage quota exceeded, clearing navigation graph\");\n this.clearGraph();\n }\n }\n }\n\n private pruneGraph(): void {\n const nodes = Object.keys(this.graph);\n if (nodes.length <= this.config.maxNodes) return;\n\n // Calculate total transitions per node\n const nodeCounts = nodes.map((node) => ({\n node,\n count: Object.values(this.graph[node]!).reduce(\n (sum, val) => sum + val,\n 0\n ),\n }));\n\n // Sort by count and keep top maxNodes\n nodeCounts.sort((a, b) => b.count - a.count);\n const toKeep = new Set(\n nodeCounts.slice(0, this.config.maxNodes).map((n) => n.node)\n );\n\n // Remove nodes not in top set\n Object.keys(this.graph).forEach((node) => {\n if (!toKeep.has(node)) {\n delete this.graph[node];\n }\n });\n }\n}\n\nexport function createNavigationGraphLearner(\n config?: Partial<GraphConfig>\n): NavigationGraphLearner {\n return new NavigationGraphLearner(config);\n}\n\nexport function getNavigationGraph(\n storageKey = \"specnav-graph\"\n): NavigationGraph {\n if (typeof localStorage === \"undefined\") return {};\n\n try {\n const stored = localStorage.getItem(storageKey);\n return stored ? JSON.parse(stored) : {};\n } catch {\n return {};\n }\n}\n"]}
|
package/dist/graph.d.cts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { G as GraphConfig, N as NavigationGraph } from './types-DnUtmOfQ.cjs';
|
|
2
|
+
|
|
3
|
+
declare class NavigationGraphLearner {
|
|
4
|
+
private config;
|
|
5
|
+
private graph;
|
|
6
|
+
private sessionGraph;
|
|
7
|
+
constructor(config?: Partial<GraphConfig>);
|
|
8
|
+
recordNavigation(fromHref: string, toHref: string): void;
|
|
9
|
+
getPredictions(fromHref: string): string[];
|
|
10
|
+
getGraph(): NavigationGraph;
|
|
11
|
+
clearGraph(): void;
|
|
12
|
+
private loadGraph;
|
|
13
|
+
private saveGraph;
|
|
14
|
+
private pruneGraph;
|
|
15
|
+
}
|
|
16
|
+
declare function createNavigationGraphLearner(config?: Partial<GraphConfig>): NavigationGraphLearner;
|
|
17
|
+
declare function getNavigationGraph(storageKey?: string): NavigationGraph;
|
|
18
|
+
|
|
19
|
+
export { NavigationGraphLearner, createNavigationGraphLearner, getNavigationGraph };
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { G as GraphConfig, N as NavigationGraph } from './types-DnUtmOfQ.js';
|
|
2
|
+
|
|
3
|
+
declare class NavigationGraphLearner {
|
|
4
|
+
private config;
|
|
5
|
+
private graph;
|
|
6
|
+
private sessionGraph;
|
|
7
|
+
constructor(config?: Partial<GraphConfig>);
|
|
8
|
+
recordNavigation(fromHref: string, toHref: string): void;
|
|
9
|
+
getPredictions(fromHref: string): string[];
|
|
10
|
+
getGraph(): NavigationGraph;
|
|
11
|
+
clearGraph(): void;
|
|
12
|
+
private loadGraph;
|
|
13
|
+
private saveGraph;
|
|
14
|
+
private pruneGraph;
|
|
15
|
+
}
|
|
16
|
+
declare function createNavigationGraphLearner(config?: Partial<GraphConfig>): NavigationGraphLearner;
|
|
17
|
+
declare function getNavigationGraph(storageKey?: string): NavigationGraph;
|
|
18
|
+
|
|
19
|
+
export { NavigationGraphLearner, createNavigationGraphLearner, getNavigationGraph };
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/graph.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
enabled: true,
|
|
4
|
+
minConfidence: 0.6,
|
|
5
|
+
sessionWeight: 0.7,
|
|
6
|
+
storageKey: "specnav-graph",
|
|
7
|
+
maxNodes: 50
|
|
8
|
+
};
|
|
9
|
+
var NavigationGraphLearner = class {
|
|
10
|
+
config;
|
|
11
|
+
graph = {};
|
|
12
|
+
sessionGraph = {};
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
15
|
+
if (this.config.enabled) {
|
|
16
|
+
this.loadGraph();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
recordNavigation(fromHref, toHref) {
|
|
20
|
+
if (!this.config.enabled) return;
|
|
21
|
+
if (!this.sessionGraph[fromHref]) {
|
|
22
|
+
this.sessionGraph[fromHref] = {};
|
|
23
|
+
}
|
|
24
|
+
const current = this.sessionGraph[fromHref][toHref] ?? 0;
|
|
25
|
+
this.sessionGraph[fromHref][toHref] = current + 1;
|
|
26
|
+
if (!this.graph[fromHref]) {
|
|
27
|
+
this.graph[fromHref] = {};
|
|
28
|
+
}
|
|
29
|
+
const persistentCurrent = this.graph[fromHref][toHref] ?? 0;
|
|
30
|
+
this.graph[fromHref][toHref] = persistentCurrent + 1;
|
|
31
|
+
this.saveGraph();
|
|
32
|
+
}
|
|
33
|
+
getPredictions(fromHref) {
|
|
34
|
+
if (!this.config.enabled) return [];
|
|
35
|
+
const sessionEdges = this.sessionGraph[fromHref] ?? {};
|
|
36
|
+
const persistentEdges = this.graph[fromHref] ?? {};
|
|
37
|
+
const merged = {};
|
|
38
|
+
Object.entries(persistentEdges).forEach(([href, count]) => {
|
|
39
|
+
merged[href] = count * (1 - this.config.sessionWeight);
|
|
40
|
+
});
|
|
41
|
+
Object.entries(sessionEdges).forEach(([href, count]) => {
|
|
42
|
+
merged[href] = (merged[href] ?? 0) + count * this.config.sessionWeight;
|
|
43
|
+
});
|
|
44
|
+
const total = Object.values(merged).reduce((sum, val) => sum + val, 0);
|
|
45
|
+
if (total === 0) return [];
|
|
46
|
+
const predictions = Object.entries(merged).map(([href, count]) => ({
|
|
47
|
+
href,
|
|
48
|
+
confidence: count / total
|
|
49
|
+
})).filter((p) => p.confidence >= this.config.minConfidence).sort((a, b) => b.confidence - a.confidence).map((p) => p.href);
|
|
50
|
+
return predictions;
|
|
51
|
+
}
|
|
52
|
+
getGraph() {
|
|
53
|
+
return { ...this.graph };
|
|
54
|
+
}
|
|
55
|
+
clearGraph() {
|
|
56
|
+
this.graph = {};
|
|
57
|
+
this.sessionGraph = {};
|
|
58
|
+
this.saveGraph();
|
|
59
|
+
}
|
|
60
|
+
loadGraph() {
|
|
61
|
+
if (typeof localStorage === "undefined") return;
|
|
62
|
+
try {
|
|
63
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
64
|
+
if (stored) {
|
|
65
|
+
this.graph = JSON.parse(stored);
|
|
66
|
+
this.pruneGraph();
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
saveGraph() {
|
|
72
|
+
if (typeof localStorage === "undefined") return;
|
|
73
|
+
try {
|
|
74
|
+
this.pruneGraph();
|
|
75
|
+
const serialized = JSON.stringify(this.graph);
|
|
76
|
+
if (serialized.length > 4 * 1024 * 1024) {
|
|
77
|
+
console.warn("Navigation graph too large, pruning more aggressively");
|
|
78
|
+
const reducedMaxNodes = Math.floor(this.config.maxNodes / 2);
|
|
79
|
+
const originalMaxNodes = this.config.maxNodes;
|
|
80
|
+
this.config.maxNodes = reducedMaxNodes;
|
|
81
|
+
this.pruneGraph();
|
|
82
|
+
this.config.maxNodes = originalMaxNodes;
|
|
83
|
+
}
|
|
84
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(this.graph));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error && error.name === "QuotaExceededError") {
|
|
87
|
+
console.warn("localStorage quota exceeded, clearing navigation graph");
|
|
88
|
+
this.clearGraph();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
pruneGraph() {
|
|
93
|
+
const nodes = Object.keys(this.graph);
|
|
94
|
+
if (nodes.length <= this.config.maxNodes) return;
|
|
95
|
+
const nodeCounts = nodes.map((node) => ({
|
|
96
|
+
node,
|
|
97
|
+
count: Object.values(this.graph[node]).reduce(
|
|
98
|
+
(sum, val) => sum + val,
|
|
99
|
+
0
|
|
100
|
+
)
|
|
101
|
+
}));
|
|
102
|
+
nodeCounts.sort((a, b) => b.count - a.count);
|
|
103
|
+
const toKeep = new Set(
|
|
104
|
+
nodeCounts.slice(0, this.config.maxNodes).map((n) => n.node)
|
|
105
|
+
);
|
|
106
|
+
Object.keys(this.graph).forEach((node) => {
|
|
107
|
+
if (!toKeep.has(node)) {
|
|
108
|
+
delete this.graph[node];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function createNavigationGraphLearner(config) {
|
|
114
|
+
return new NavigationGraphLearner(config);
|
|
115
|
+
}
|
|
116
|
+
function getNavigationGraph(storageKey = "specnav-graph") {
|
|
117
|
+
if (typeof localStorage === "undefined") return {};
|
|
118
|
+
try {
|
|
119
|
+
const stored = localStorage.getItem(storageKey);
|
|
120
|
+
return stored ? JSON.parse(stored) : {};
|
|
121
|
+
} catch {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { NavigationGraphLearner, createNavigationGraphLearner, getNavigationGraph };
|
|
127
|
+
//# sourceMappingURL=graph.js.map
|
|
128
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph.ts"],"names":[],"mappings":"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","file":"graph.js","sourcesContent":["import type { GraphConfig, NavigationGraph } from \"./types\";\n\nconst DEFAULT_CONFIG: GraphConfig = {\n enabled: true,\n minConfidence: 0.6,\n sessionWeight: 0.7,\n storageKey: \"specnav-graph\",\n maxNodes: 50,\n};\n\nexport class NavigationGraphLearner {\n private config: GraphConfig;\n private graph: NavigationGraph = {};\n private sessionGraph: NavigationGraph = {};\n\n constructor(config: Partial<GraphConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n if (this.config.enabled) {\n this.loadGraph();\n }\n }\n\n recordNavigation(fromHref: string, toHref: string): void {\n if (!this.config.enabled) return;\n\n // Update session graph\n if (!this.sessionGraph[fromHref]) {\n this.sessionGraph[fromHref] = {};\n }\n const current = this.sessionGraph[fromHref]![toHref] ?? 0;\n this.sessionGraph[fromHref]![toHref] = current + 1;\n\n // Update persistent graph\n if (!this.graph[fromHref]) {\n this.graph[fromHref] = {};\n }\n const persistentCurrent = this.graph[fromHref]![toHref] ?? 0;\n this.graph[fromHref]![toHref] = persistentCurrent + 1;\n\n this.saveGraph();\n }\n\n getPredictions(fromHref: string): string[] {\n if (!this.config.enabled) return [];\n\n const sessionEdges = this.sessionGraph[fromHref] ?? {};\n const persistentEdges = this.graph[fromHref] ?? {};\n\n // Merge session and persistent with weighting\n const merged: Record<string, number> = {};\n\n Object.entries(persistentEdges).forEach(([href, count]) => {\n merged[href] = count * (1 - this.config.sessionWeight);\n });\n\n Object.entries(sessionEdges).forEach(([href, count]) => {\n merged[href] = (merged[href] ?? 0) + count * this.config.sessionWeight;\n });\n\n // Calculate total for normalization\n const total = Object.values(merged).reduce((sum, val) => sum + val, 0);\n if (total === 0) return [];\n\n // Convert to confidence scores and filter\n const predictions = Object.entries(merged)\n .map(([href, count]) => ({\n href,\n confidence: count / total,\n }))\n .filter((p) => p.confidence >= this.config.minConfidence)\n .sort((a, b) => b.confidence - a.confidence)\n .map((p) => p.href);\n\n return predictions;\n }\n\n getGraph(): NavigationGraph {\n return { ...this.graph };\n }\n\n clearGraph(): void {\n this.graph = {};\n this.sessionGraph = {};\n this.saveGraph();\n }\n\n private loadGraph(): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n const stored = localStorage.getItem(this.config.storageKey);\n if (stored) {\n this.graph = JSON.parse(stored);\n this.pruneGraph();\n }\n } catch {\n // Silently fail\n }\n }\n\n private saveGraph(): void {\n if (typeof localStorage === \"undefined\") return;\n\n try {\n this.pruneGraph();\n const serialized = JSON.stringify(this.graph);\n \n // Check size before saving (localStorage typically has 5-10MB limit)\n if (serialized.length > 4 * 1024 * 1024) { // 4MB safety limit\n console.warn(\"Navigation graph too large, pruning more aggressively\");\n // Prune to half the max nodes (use local variable)\n const reducedMaxNodes = Math.floor(this.config.maxNodes / 2);\n const originalMaxNodes = this.config.maxNodes;\n this.config.maxNodes = reducedMaxNodes;\n this.pruneGraph();\n this.config.maxNodes = originalMaxNodes; // Restore original\n }\n \n localStorage.setItem(this.config.storageKey, JSON.stringify(this.graph));\n } catch (error) {\n // Handle quota exceeded error\n if (error instanceof Error && error.name === \"QuotaExceededError\") {\n console.warn(\"localStorage quota exceeded, clearing navigation graph\");\n this.clearGraph();\n }\n }\n }\n\n private pruneGraph(): void {\n const nodes = Object.keys(this.graph);\n if (nodes.length <= this.config.maxNodes) return;\n\n // Calculate total transitions per node\n const nodeCounts = nodes.map((node) => ({\n node,\n count: Object.values(this.graph[node]!).reduce(\n (sum, val) => sum + val,\n 0\n ),\n }));\n\n // Sort by count and keep top maxNodes\n nodeCounts.sort((a, b) => b.count - a.count);\n const toKeep = new Set(\n nodeCounts.slice(0, this.config.maxNodes).map((n) => n.node)\n );\n\n // Remove nodes not in top set\n Object.keys(this.graph).forEach((node) => {\n if (!toKeep.has(node)) {\n delete this.graph[node];\n }\n });\n }\n}\n\nexport function createNavigationGraphLearner(\n config?: Partial<GraphConfig>\n): NavigationGraphLearner {\n return new NavigationGraphLearner(config);\n}\n\nexport function getNavigationGraph(\n storageKey = \"specnav-graph\"\n): NavigationGraph {\n if (typeof localStorage === \"undefined\") return {};\n\n try {\n const stored = localStorage.getItem(storageKey);\n return stored ? JSON.parse(stored) : {};\n } catch {\n return {};\n }\n}\n"]}
|