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/index.cjs
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
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
|
+
// src/cache.ts
|
|
144
|
+
var DEFAULT_CONFIG2 = {
|
|
145
|
+
memory: {
|
|
146
|
+
enabled: true,
|
|
147
|
+
maxPages: 10
|
|
148
|
+
},
|
|
149
|
+
serviceWorker: {
|
|
150
|
+
enabled: true,
|
|
151
|
+
ttl: 300,
|
|
152
|
+
cacheName: "specnav-v1",
|
|
153
|
+
crossTabSync: true,
|
|
154
|
+
broadcastChannel: "specnav-sync"
|
|
155
|
+
},
|
|
156
|
+
edge: {
|
|
157
|
+
enabled: true,
|
|
158
|
+
maxAge: 60,
|
|
159
|
+
staleWhileRevalidate: 300
|
|
160
|
+
},
|
|
161
|
+
exclude: []
|
|
162
|
+
};
|
|
163
|
+
var CacheManager = class {
|
|
164
|
+
config;
|
|
165
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
166
|
+
accessOrder = [];
|
|
167
|
+
broadcastChannel;
|
|
168
|
+
onCacheHit;
|
|
169
|
+
constructor(config = {}, callbacks) {
|
|
170
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
171
|
+
this.onCacheHit = callbacks?.onCacheHit;
|
|
172
|
+
if (this.config.serviceWorker.enabled && this.config.serviceWorker.crossTabSync && typeof BroadcastChannel !== "undefined") {
|
|
173
|
+
this.broadcastChannel = new BroadcastChannel(
|
|
174
|
+
this.config.serviceWorker.broadcastChannel
|
|
175
|
+
);
|
|
176
|
+
this.broadcastChannel.onmessage = this.handleBroadcast;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async get(href) {
|
|
180
|
+
if (this.isExcluded(href)) return null;
|
|
181
|
+
const memoryHit = this.getFromMemory(href);
|
|
182
|
+
if (memoryHit) {
|
|
183
|
+
this.onCacheHit?.(href, 1);
|
|
184
|
+
return memoryHit;
|
|
185
|
+
}
|
|
186
|
+
if (this.config.serviceWorker.enabled) {
|
|
187
|
+
const swHit = await this.getFromServiceWorker(href);
|
|
188
|
+
if (swHit) {
|
|
189
|
+
this.onCacheHit?.(href, 2);
|
|
190
|
+
this.setInMemory(href, swHit);
|
|
191
|
+
return swHit;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
async set(href, html) {
|
|
197
|
+
if (this.isExcluded(href)) return;
|
|
198
|
+
const sanitizedHref = this.sanitizeHref(href);
|
|
199
|
+
if (!sanitizedHref) return;
|
|
200
|
+
if (this.containsDangerousContent(html)) {
|
|
201
|
+
console.warn(`Refusing to cache potentially dangerous content from ${sanitizedHref}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const page = {
|
|
205
|
+
href: sanitizedHref,
|
|
206
|
+
html,
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
ttl: this.config.serviceWorker.ttl
|
|
209
|
+
};
|
|
210
|
+
if (this.config.memory.enabled) {
|
|
211
|
+
this.setInMemory(sanitizedHref, html);
|
|
212
|
+
}
|
|
213
|
+
if (this.config.serviceWorker.enabled) {
|
|
214
|
+
await this.setInServiceWorker(page);
|
|
215
|
+
this.broadcastSet(sanitizedHref);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
containsDangerousContent(html) {
|
|
219
|
+
const dangerousPatterns = [
|
|
220
|
+
/on\w+\s*=\s*["'][^"']*["']/i,
|
|
221
|
+
// Inline event handlers
|
|
222
|
+
/javascript:/i
|
|
223
|
+
// javascript: URLs
|
|
224
|
+
];
|
|
225
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
226
|
+
let match;
|
|
227
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
228
|
+
const attrs = match[1] || "";
|
|
229
|
+
if (/src\s*=/.test(attrs)) continue;
|
|
230
|
+
if (/type\s*=\s*["']?(application\/json|application\/ld\+json|text\/template)["']?/i.test(attrs)) continue;
|
|
231
|
+
if (/id\s*=\s*["']__NEXT_DATA__["']/i.test(attrs)) continue;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
return dangerousPatterns.some((pattern) => pattern.test(html));
|
|
235
|
+
}
|
|
236
|
+
sanitizeHref(href) {
|
|
237
|
+
try {
|
|
238
|
+
const url = new URL(href, window.location.origin);
|
|
239
|
+
if (url.origin !== window.location.origin) return null;
|
|
240
|
+
return url.pathname + url.search + url.hash;
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
clear(href) {
|
|
246
|
+
if (href) {
|
|
247
|
+
this.memoryCache.delete(href);
|
|
248
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
249
|
+
this.clearFromServiceWorker(href);
|
|
250
|
+
} else {
|
|
251
|
+
this.memoryCache.clear();
|
|
252
|
+
this.accessOrder = [];
|
|
253
|
+
this.clearAllServiceWorker();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
getSize() {
|
|
257
|
+
return this.memoryCache.size;
|
|
258
|
+
}
|
|
259
|
+
destroy() {
|
|
260
|
+
this.memoryCache.clear();
|
|
261
|
+
this.accessOrder = [];
|
|
262
|
+
this.broadcastChannel?.close();
|
|
263
|
+
}
|
|
264
|
+
getFromMemory(href) {
|
|
265
|
+
const page = this.memoryCache.get(href);
|
|
266
|
+
if (!page) return null;
|
|
267
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
268
|
+
this.accessOrder.push(href);
|
|
269
|
+
return page.html;
|
|
270
|
+
}
|
|
271
|
+
setInMemory(href, html) {
|
|
272
|
+
const maxPageSize = 5 * 1024 * 1024;
|
|
273
|
+
if (html.length > maxPageSize) {
|
|
274
|
+
console.warn(`Page ${href} exceeds max size, not caching`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.memoryCache.set(href, {
|
|
278
|
+
href,
|
|
279
|
+
html,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
ttl: this.config.serviceWorker.ttl
|
|
282
|
+
});
|
|
283
|
+
const existingIndex = this.accessOrder.indexOf(href);
|
|
284
|
+
if (existingIndex !== -1) {
|
|
285
|
+
this.accessOrder.splice(existingIndex, 1);
|
|
286
|
+
}
|
|
287
|
+
this.accessOrder.push(href);
|
|
288
|
+
if (this.memoryCache.size > this.config.memory.maxPages) {
|
|
289
|
+
const oldest = this.accessOrder.shift();
|
|
290
|
+
if (oldest) this.memoryCache.delete(oldest);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async getFromServiceWorker(href) {
|
|
294
|
+
if (typeof caches === "undefined") return null;
|
|
295
|
+
try {
|
|
296
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
297
|
+
const response = await cache.match(href);
|
|
298
|
+
if (!response) return null;
|
|
299
|
+
const data = await response.json();
|
|
300
|
+
const page = data;
|
|
301
|
+
const age = Date.now() - page.timestamp;
|
|
302
|
+
if (age > page.ttl * 1e3) {
|
|
303
|
+
await cache.delete(href);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return page.html;
|
|
307
|
+
} catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async setInServiceWorker(page) {
|
|
312
|
+
if (typeof caches === "undefined") return;
|
|
313
|
+
try {
|
|
314
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
315
|
+
const response = new Response(JSON.stringify(page), {
|
|
316
|
+
headers: {
|
|
317
|
+
"Content-Type": "application/json",
|
|
318
|
+
"Cache-Control": `max-age=${this.config.edge.maxAge}, stale-while-revalidate=${this.config.edge.staleWhileRevalidate}`
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
await cache.put(page.href, response);
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async clearFromServiceWorker(href) {
|
|
326
|
+
if (typeof caches === "undefined") return;
|
|
327
|
+
try {
|
|
328
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
329
|
+
await cache.delete(href);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async clearAllServiceWorker() {
|
|
334
|
+
if (typeof caches === "undefined") return;
|
|
335
|
+
try {
|
|
336
|
+
await caches.delete(this.config.serviceWorker.cacheName);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
isExcluded(href) {
|
|
341
|
+
return this.config.exclude.some((pattern) => {
|
|
342
|
+
if (typeof pattern === "string") {
|
|
343
|
+
return pattern.includes("*") ? new RegExp(pattern.replace(/\*/g, ".*")).test(href) : href === pattern;
|
|
344
|
+
}
|
|
345
|
+
return pattern.test(href);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
broadcastSet(href) {
|
|
349
|
+
this.broadcastChannel?.postMessage({ type: "set", href });
|
|
350
|
+
}
|
|
351
|
+
handleBroadcast = (event) => {
|
|
352
|
+
if (event.data.type === "set") {
|
|
353
|
+
this.memoryCache.delete(event.data.href);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
function createCacheManager(config, callbacks) {
|
|
358
|
+
return new CacheManager(config, callbacks);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/morpher.ts
|
|
362
|
+
var DEFAULT_CONFIG3 = {
|
|
363
|
+
preserveScroll: true,
|
|
364
|
+
preserveFocus: true
|
|
365
|
+
};
|
|
366
|
+
var DOMmorpher = class {
|
|
367
|
+
config;
|
|
368
|
+
constructor(config = {}) {
|
|
369
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
370
|
+
}
|
|
371
|
+
morph(from, to) {
|
|
372
|
+
const ctx = this.createContext();
|
|
373
|
+
requestAnimationFrame(() => {
|
|
374
|
+
this.morphElement(from, to, ctx);
|
|
375
|
+
this.restoreContext(ctx);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
createContext() {
|
|
379
|
+
const ctx = {
|
|
380
|
+
config: this.config,
|
|
381
|
+
focusedElement: null,
|
|
382
|
+
scrollPositions: /* @__PURE__ */ new Map()
|
|
383
|
+
};
|
|
384
|
+
if (this.config.preserveFocus) {
|
|
385
|
+
ctx.focusedElement = document.activeElement;
|
|
386
|
+
}
|
|
387
|
+
if (this.config.preserveScroll) {
|
|
388
|
+
this.captureScrollPositions(document.body, ctx.scrollPositions);
|
|
389
|
+
}
|
|
390
|
+
return ctx;
|
|
391
|
+
}
|
|
392
|
+
restoreContext(ctx) {
|
|
393
|
+
if (ctx.config.preserveFocus && ctx.focusedElement) {
|
|
394
|
+
const element = ctx.focusedElement;
|
|
395
|
+
if (element.focus && document.body.contains(element)) {
|
|
396
|
+
element.focus();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (ctx.config.preserveScroll) {
|
|
400
|
+
ctx.scrollPositions.forEach((pos, element) => {
|
|
401
|
+
element.scrollTop = pos.top;
|
|
402
|
+
element.scrollLeft = pos.left;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
captureScrollPositions(element, positions) {
|
|
407
|
+
if (element.scrollTop > 0 || element.scrollLeft > 0) {
|
|
408
|
+
positions.set(element, {
|
|
409
|
+
top: element.scrollTop,
|
|
410
|
+
left: element.scrollLeft
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
Array.from(element.children).forEach((child) => {
|
|
414
|
+
this.captureScrollPositions(child, positions);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
morphElement(from, to, ctx) {
|
|
418
|
+
this.syncAttributes(from, to);
|
|
419
|
+
this.morphChildren(from, to, ctx);
|
|
420
|
+
}
|
|
421
|
+
syncAttributes(from, to) {
|
|
422
|
+
const fromAttrs = from.attributes;
|
|
423
|
+
const toAttrs = to.attributes;
|
|
424
|
+
for (let i = fromAttrs.length - 1; i >= 0; i--) {
|
|
425
|
+
const attr = fromAttrs[i];
|
|
426
|
+
if (!to.hasAttribute(attr.name)) {
|
|
427
|
+
from.removeAttribute(attr.name);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
for (let i = 0; i < toAttrs.length; i++) {
|
|
431
|
+
const attr = toAttrs[i];
|
|
432
|
+
if (from.getAttribute(attr.name) !== attr.value) {
|
|
433
|
+
from.setAttribute(attr.name, attr.value);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
morphChildren(from, to, ctx) {
|
|
438
|
+
const fromChildren = Array.from(from.childNodes);
|
|
439
|
+
const toChildren = Array.from(to.childNodes);
|
|
440
|
+
let fromIndex = 0;
|
|
441
|
+
let toIndex = 0;
|
|
442
|
+
while (toIndex < toChildren.length) {
|
|
443
|
+
const toNode = toChildren[toIndex];
|
|
444
|
+
const fromNode = fromChildren[fromIndex];
|
|
445
|
+
if (!fromNode) {
|
|
446
|
+
from.appendChild(toNode.cloneNode(true));
|
|
447
|
+
toIndex++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (this.nodesMatch(fromNode, toNode)) {
|
|
451
|
+
if (fromNode.nodeType === Node.TEXT_NODE) {
|
|
452
|
+
if (fromNode.nodeValue !== toNode.nodeValue) {
|
|
453
|
+
fromNode.nodeValue = toNode.nodeValue;
|
|
454
|
+
}
|
|
455
|
+
} else if (fromNode.nodeType === Node.ELEMENT_NODE) {
|
|
456
|
+
this.morphElement(fromNode, toNode, ctx);
|
|
457
|
+
}
|
|
458
|
+
fromIndex++;
|
|
459
|
+
toIndex++;
|
|
460
|
+
} else {
|
|
461
|
+
const matchIndex = this.findMatchingNode(
|
|
462
|
+
fromChildren,
|
|
463
|
+
fromIndex + 1,
|
|
464
|
+
toNode
|
|
465
|
+
);
|
|
466
|
+
if (matchIndex !== -1) {
|
|
467
|
+
for (let i = fromIndex; i < matchIndex; i++) {
|
|
468
|
+
from.removeChild(fromChildren[i]);
|
|
469
|
+
}
|
|
470
|
+
fromIndex = matchIndex;
|
|
471
|
+
} else {
|
|
472
|
+
from.insertBefore(toNode.cloneNode(true), fromNode);
|
|
473
|
+
toIndex++;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
while (fromIndex < fromChildren.length) {
|
|
478
|
+
from.removeChild(fromChildren[fromIndex]);
|
|
479
|
+
fromIndex++;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
nodesMatch(a, b) {
|
|
483
|
+
if (a.nodeType !== b.nodeType) return false;
|
|
484
|
+
if (a.nodeType === Node.ELEMENT_NODE) {
|
|
485
|
+
const aEl = a;
|
|
486
|
+
const bEl = b;
|
|
487
|
+
if (aEl.tagName !== bEl.tagName) return false;
|
|
488
|
+
const aId = aEl.getAttribute("id");
|
|
489
|
+
const bId = bEl.getAttribute("id");
|
|
490
|
+
if (aId && bId) return aId === bId;
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
findMatchingNode(nodes, startIndex, target) {
|
|
496
|
+
for (let i = startIndex; i < nodes.length; i++) {
|
|
497
|
+
if (this.nodesMatch(nodes[i], target)) {
|
|
498
|
+
return i;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return -1;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
function createMorpher(config) {
|
|
505
|
+
return new DOMmorpher(config);
|
|
506
|
+
}
|
|
507
|
+
function morph(from, to, config) {
|
|
508
|
+
const morpher = createMorpher(config);
|
|
509
|
+
morpher.morph(from, to);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/speculator.ts
|
|
513
|
+
var SpeculativeRenderer = class {
|
|
514
|
+
speculations = /* @__PURE__ */ new Map();
|
|
515
|
+
maxSpeculations;
|
|
516
|
+
constructor(maxSpeculations = 3) {
|
|
517
|
+
this.maxSpeculations = maxSpeculations;
|
|
518
|
+
}
|
|
519
|
+
async speculate(href, html) {
|
|
520
|
+
if (this.speculations.has(href)) return;
|
|
521
|
+
if (this.speculations.size >= this.maxSpeculations) {
|
|
522
|
+
const oldest = Array.from(this.speculations.values()).sort(
|
|
523
|
+
(a, b) => a.timestamp - b.timestamp
|
|
524
|
+
)[0];
|
|
525
|
+
if (oldest) this.cancel(oldest.href);
|
|
526
|
+
}
|
|
527
|
+
const container = this.createDetachedContainer();
|
|
528
|
+
container.innerHTML = html;
|
|
529
|
+
this.speculations.set(href, {
|
|
530
|
+
href,
|
|
531
|
+
html,
|
|
532
|
+
container,
|
|
533
|
+
timestamp: Date.now()
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
get(href) {
|
|
537
|
+
const speculation = this.speculations.get(href);
|
|
538
|
+
return speculation?.container ?? null;
|
|
539
|
+
}
|
|
540
|
+
cancel(href) {
|
|
541
|
+
const speculation = this.speculations.get(href);
|
|
542
|
+
if (speculation) {
|
|
543
|
+
speculation.container.remove();
|
|
544
|
+
this.speculations.delete(href);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
cancelAll() {
|
|
548
|
+
this.speculations.forEach((spec) => spec.container.remove());
|
|
549
|
+
this.speculations.clear();
|
|
550
|
+
}
|
|
551
|
+
has(href) {
|
|
552
|
+
return this.speculations.has(href);
|
|
553
|
+
}
|
|
554
|
+
createDetachedContainer() {
|
|
555
|
+
const container = document.createElement("div");
|
|
556
|
+
container.style.display = "none";
|
|
557
|
+
container.setAttribute("data-specnav-speculation", "true");
|
|
558
|
+
return container;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
function createSpeculativeRenderer(maxSpeculations) {
|
|
562
|
+
return new SpeculativeRenderer(maxSpeculations);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/graph.ts
|
|
566
|
+
var DEFAULT_CONFIG4 = {
|
|
567
|
+
enabled: true,
|
|
568
|
+
minConfidence: 0.6,
|
|
569
|
+
sessionWeight: 0.7,
|
|
570
|
+
storageKey: "specnav-graph",
|
|
571
|
+
maxNodes: 50
|
|
572
|
+
};
|
|
573
|
+
var NavigationGraphLearner = class {
|
|
574
|
+
config;
|
|
575
|
+
graph = {};
|
|
576
|
+
sessionGraph = {};
|
|
577
|
+
constructor(config = {}) {
|
|
578
|
+
this.config = { ...DEFAULT_CONFIG4, ...config };
|
|
579
|
+
if (this.config.enabled) {
|
|
580
|
+
this.loadGraph();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
recordNavigation(fromHref, toHref) {
|
|
584
|
+
if (!this.config.enabled) return;
|
|
585
|
+
if (!this.sessionGraph[fromHref]) {
|
|
586
|
+
this.sessionGraph[fromHref] = {};
|
|
587
|
+
}
|
|
588
|
+
const current = this.sessionGraph[fromHref][toHref] ?? 0;
|
|
589
|
+
this.sessionGraph[fromHref][toHref] = current + 1;
|
|
590
|
+
if (!this.graph[fromHref]) {
|
|
591
|
+
this.graph[fromHref] = {};
|
|
592
|
+
}
|
|
593
|
+
const persistentCurrent = this.graph[fromHref][toHref] ?? 0;
|
|
594
|
+
this.graph[fromHref][toHref] = persistentCurrent + 1;
|
|
595
|
+
this.saveGraph();
|
|
596
|
+
}
|
|
597
|
+
getPredictions(fromHref) {
|
|
598
|
+
if (!this.config.enabled) return [];
|
|
599
|
+
const sessionEdges = this.sessionGraph[fromHref] ?? {};
|
|
600
|
+
const persistentEdges = this.graph[fromHref] ?? {};
|
|
601
|
+
const merged = {};
|
|
602
|
+
Object.entries(persistentEdges).forEach(([href, count]) => {
|
|
603
|
+
merged[href] = count * (1 - this.config.sessionWeight);
|
|
604
|
+
});
|
|
605
|
+
Object.entries(sessionEdges).forEach(([href, count]) => {
|
|
606
|
+
merged[href] = (merged[href] ?? 0) + count * this.config.sessionWeight;
|
|
607
|
+
});
|
|
608
|
+
const total = Object.values(merged).reduce((sum, val) => sum + val, 0);
|
|
609
|
+
if (total === 0) return [];
|
|
610
|
+
const predictions = Object.entries(merged).map(([href, count]) => ({
|
|
611
|
+
href,
|
|
612
|
+
confidence: count / total
|
|
613
|
+
})).filter((p) => p.confidence >= this.config.minConfidence).sort((a, b) => b.confidence - a.confidence).map((p) => p.href);
|
|
614
|
+
return predictions;
|
|
615
|
+
}
|
|
616
|
+
getGraph() {
|
|
617
|
+
return { ...this.graph };
|
|
618
|
+
}
|
|
619
|
+
clearGraph() {
|
|
620
|
+
this.graph = {};
|
|
621
|
+
this.sessionGraph = {};
|
|
622
|
+
this.saveGraph();
|
|
623
|
+
}
|
|
624
|
+
loadGraph() {
|
|
625
|
+
if (typeof localStorage === "undefined") return;
|
|
626
|
+
try {
|
|
627
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
628
|
+
if (stored) {
|
|
629
|
+
this.graph = JSON.parse(stored);
|
|
630
|
+
this.pruneGraph();
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
saveGraph() {
|
|
636
|
+
if (typeof localStorage === "undefined") return;
|
|
637
|
+
try {
|
|
638
|
+
this.pruneGraph();
|
|
639
|
+
const serialized = JSON.stringify(this.graph);
|
|
640
|
+
if (serialized.length > 4 * 1024 * 1024) {
|
|
641
|
+
console.warn("Navigation graph too large, pruning more aggressively");
|
|
642
|
+
const reducedMaxNodes = Math.floor(this.config.maxNodes / 2);
|
|
643
|
+
const originalMaxNodes = this.config.maxNodes;
|
|
644
|
+
this.config.maxNodes = reducedMaxNodes;
|
|
645
|
+
this.pruneGraph();
|
|
646
|
+
this.config.maxNodes = originalMaxNodes;
|
|
647
|
+
}
|
|
648
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(this.graph));
|
|
649
|
+
} catch (error) {
|
|
650
|
+
if (error instanceof Error && error.name === "QuotaExceededError") {
|
|
651
|
+
console.warn("localStorage quota exceeded, clearing navigation graph");
|
|
652
|
+
this.clearGraph();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
pruneGraph() {
|
|
657
|
+
const nodes = Object.keys(this.graph);
|
|
658
|
+
if (nodes.length <= this.config.maxNodes) return;
|
|
659
|
+
const nodeCounts = nodes.map((node) => ({
|
|
660
|
+
node,
|
|
661
|
+
count: Object.values(this.graph[node]).reduce(
|
|
662
|
+
(sum, val) => sum + val,
|
|
663
|
+
0
|
|
664
|
+
)
|
|
665
|
+
}));
|
|
666
|
+
nodeCounts.sort((a, b) => b.count - a.count);
|
|
667
|
+
const toKeep = new Set(
|
|
668
|
+
nodeCounts.slice(0, this.config.maxNodes).map((n) => n.node)
|
|
669
|
+
);
|
|
670
|
+
Object.keys(this.graph).forEach((node) => {
|
|
671
|
+
if (!toKeep.has(node)) {
|
|
672
|
+
delete this.graph[node];
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
function createNavigationGraphLearner(config) {
|
|
678
|
+
return new NavigationGraphLearner(config);
|
|
679
|
+
}
|
|
680
|
+
function getNavigationGraph(storageKey = "specnav-graph") {
|
|
681
|
+
if (typeof localStorage === "undefined") return {};
|
|
682
|
+
try {
|
|
683
|
+
const stored = localStorage.getItem(storageKey);
|
|
684
|
+
return stored ? JSON.parse(stored) : {};
|
|
685
|
+
} catch {
|
|
686
|
+
return {};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/adaptive.ts
|
|
691
|
+
var DEFAULT_CONFIG5 = {
|
|
692
|
+
batteryThreshold: 0.2,
|
|
693
|
+
connectionTypes: {
|
|
694
|
+
slow: ["2g", "3g", "slow-2g"],
|
|
695
|
+
fast: ["4g"]
|
|
696
|
+
},
|
|
697
|
+
respectSaveData: true,
|
|
698
|
+
respectReducedMotion: true
|
|
699
|
+
};
|
|
700
|
+
var AdaptiveMode = class {
|
|
701
|
+
config;
|
|
702
|
+
battery = null;
|
|
703
|
+
connection = null;
|
|
704
|
+
initPromise;
|
|
705
|
+
constructor(config = {}) {
|
|
706
|
+
this.config = { ...DEFAULT_CONFIG5, ...config };
|
|
707
|
+
this.initPromise = this.initialize();
|
|
708
|
+
}
|
|
709
|
+
async waitForInit() {
|
|
710
|
+
await this.initPromise;
|
|
711
|
+
}
|
|
712
|
+
async initialize() {
|
|
713
|
+
if (typeof navigator === "undefined") return;
|
|
714
|
+
if ("getBattery" in navigator) {
|
|
715
|
+
try {
|
|
716
|
+
this.battery = await navigator.getBattery();
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if ("connection" in navigator || "mozConnection" in navigator || "webkitConnection" in navigator) {
|
|
721
|
+
this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
getStrategy(baseStrategy) {
|
|
725
|
+
if (baseStrategy === "off") return "off";
|
|
726
|
+
if (baseStrategy !== "auto") return baseStrategy;
|
|
727
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
728
|
+
return "off";
|
|
729
|
+
}
|
|
730
|
+
if (this.isLowBattery()) {
|
|
731
|
+
return "conservative";
|
|
732
|
+
}
|
|
733
|
+
if (this.isSlowConnection()) {
|
|
734
|
+
return "conservative";
|
|
735
|
+
}
|
|
736
|
+
return "aggressive";
|
|
737
|
+
}
|
|
738
|
+
shouldPrefetch() {
|
|
739
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
if (this.isLowBattery()) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
shouldSpeculate() {
|
|
748
|
+
if (!this.shouldPrefetch()) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (this.isSlowConnection()) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
shouldUseTransitions() {
|
|
757
|
+
if (!this.config.respectReducedMotion) return true;
|
|
758
|
+
if (typeof window === "undefined") return true;
|
|
759
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
760
|
+
}
|
|
761
|
+
hasSaveData() {
|
|
762
|
+
if (typeof navigator === "undefined") return false;
|
|
763
|
+
return navigator.connection?.saveData === true || navigator.mozConnection?.saveData === true || navigator.webkitConnection?.saveData === true;
|
|
764
|
+
}
|
|
765
|
+
isLowBattery() {
|
|
766
|
+
if (!this.battery) return false;
|
|
767
|
+
return !this.battery.charging && this.battery.level < this.config.batteryThreshold;
|
|
768
|
+
}
|
|
769
|
+
isSlowConnection() {
|
|
770
|
+
if (!this.connection) return false;
|
|
771
|
+
const effectiveType = this.connection.effectiveType;
|
|
772
|
+
if (!effectiveType) return false;
|
|
773
|
+
return this.config.connectionTypes.slow.includes(effectiveType);
|
|
774
|
+
}
|
|
775
|
+
isFastConnection() {
|
|
776
|
+
if (!this.connection) return true;
|
|
777
|
+
const effectiveType = this.connection.effectiveType;
|
|
778
|
+
if (!effectiveType) return true;
|
|
779
|
+
return this.config.connectionTypes.fast.includes(effectiveType);
|
|
780
|
+
}
|
|
781
|
+
getBatteryLevel() {
|
|
782
|
+
return this.battery?.level ?? null;
|
|
783
|
+
}
|
|
784
|
+
getConnectionType() {
|
|
785
|
+
return this.connection?.effectiveType ?? null;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
function createAdaptiveMode(config) {
|
|
789
|
+
return new AdaptiveMode(config);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/performance.ts
|
|
793
|
+
var PerformanceMonitor = class {
|
|
794
|
+
metrics = [];
|
|
795
|
+
maxMetrics = 100;
|
|
796
|
+
recordNavigation(metrics) {
|
|
797
|
+
this.metrics.push(metrics);
|
|
798
|
+
if (this.metrics.length > this.maxMetrics) {
|
|
799
|
+
this.metrics.shift();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
getAverageNavigationTime() {
|
|
803
|
+
if (this.metrics.length === 0) return 0;
|
|
804
|
+
const sum = this.metrics.reduce((acc, m) => acc + m.navigationTime, 0);
|
|
805
|
+
return sum / this.metrics.length;
|
|
806
|
+
}
|
|
807
|
+
getCacheHitRate() {
|
|
808
|
+
if (this.metrics.length === 0) return 0;
|
|
809
|
+
const hits = this.metrics.filter((m) => m.cacheHit).length;
|
|
810
|
+
return hits / this.metrics.length;
|
|
811
|
+
}
|
|
812
|
+
getSpeculativeHitRate() {
|
|
813
|
+
if (this.metrics.length === 0) return 0;
|
|
814
|
+
const hits = this.metrics.filter((m) => m.speculativeHit).length;
|
|
815
|
+
return hits / this.metrics.length;
|
|
816
|
+
}
|
|
817
|
+
getMetrics() {
|
|
818
|
+
return [...this.metrics];
|
|
819
|
+
}
|
|
820
|
+
clear() {
|
|
821
|
+
this.metrics = [];
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
var performanceMonitor = new PerformanceMonitor();
|
|
825
|
+
|
|
826
|
+
exports.AdaptiveMode = AdaptiveMode;
|
|
827
|
+
exports.CacheManager = CacheManager;
|
|
828
|
+
exports.DOMmorpher = DOMmorpher;
|
|
829
|
+
exports.NavigationGraphLearner = NavigationGraphLearner;
|
|
830
|
+
exports.PerformanceMonitor = PerformanceMonitor;
|
|
831
|
+
exports.SpeculativeRenderer = SpeculativeRenderer;
|
|
832
|
+
exports.TrajectoryEngine = TrajectoryEngine;
|
|
833
|
+
exports.createAdaptiveMode = createAdaptiveMode;
|
|
834
|
+
exports.createCacheManager = createCacheManager;
|
|
835
|
+
exports.createMorpher = createMorpher;
|
|
836
|
+
exports.createNavigationGraphLearner = createNavigationGraphLearner;
|
|
837
|
+
exports.createSpeculativeRenderer = createSpeculativeRenderer;
|
|
838
|
+
exports.createTrajectoryEngine = createTrajectoryEngine;
|
|
839
|
+
exports.getNavigationGraph = getNavigationGraph;
|
|
840
|
+
exports.morph = morph;
|
|
841
|
+
exports.performanceMonitor = performanceMonitor;
|
|
842
|
+
//# sourceMappingURL=index.cjs.map
|
|
843
|
+
//# sourceMappingURL=index.cjs.map
|