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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @specnav/core
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial public release with trajectory prediction, 3-layer caching, DOM morphing, and all 19 audit bugs fixed.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Trajectory-based prefetch with cursor prediction
|
|
11
|
+
- 3-layer cache (Memory/ServiceWorker/Edge)
|
|
12
|
+
- Surgical DOM morphing with scroll/focus preservation
|
|
13
|
+
- Speculative rendering in detached containers
|
|
14
|
+
- Navigation graph learning for pattern-based prefetch
|
|
15
|
+
- Adaptive mode with battery/network awareness
|
|
16
|
+
- Next.js Link component (drop-in replacement)
|
|
17
|
+
- Edge middleware with rate limiting and CSRF protection
|
|
18
|
+
|
|
19
|
+
Bug fixes:
|
|
20
|
+
- Fixed trajectory prediction actually triggering prefetch
|
|
21
|
+
- Fixed navigation graph self-to-self transitions
|
|
22
|
+
- Fixed cache blocking Next.js **NEXT_DATA** scripts
|
|
23
|
+
- Fixed onNavigateEnd callback implementation
|
|
24
|
+
- Fixed middleware same-origin bypass
|
|
25
|
+
- Fixed null engines on first render
|
|
26
|
+
- Fixed LRU cache duplicate entries
|
|
27
|
+
- Fixed cache exclusion substring matching
|
|
28
|
+
- Fixed async adaptive initialization race
|
|
29
|
+
- Fixed duplicate Link unregistration
|
|
30
|
+
- Fixed morph refocus without containment check
|
|
31
|
+
- Fixed navigation timing polling
|
|
32
|
+
- Fixed cache hit rate tracking
|
|
33
|
+
- And 6 more fixes (see BUG_FIXES.md)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 specnav contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/adaptive.ts
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
batteryThreshold: 0.2,
|
|
6
|
+
connectionTypes: {
|
|
7
|
+
slow: ["2g", "3g", "slow-2g"],
|
|
8
|
+
fast: ["4g"]
|
|
9
|
+
},
|
|
10
|
+
respectSaveData: true,
|
|
11
|
+
respectReducedMotion: true
|
|
12
|
+
};
|
|
13
|
+
var AdaptiveMode = class {
|
|
14
|
+
config;
|
|
15
|
+
battery = null;
|
|
16
|
+
connection = null;
|
|
17
|
+
initPromise;
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
20
|
+
this.initPromise = this.initialize();
|
|
21
|
+
}
|
|
22
|
+
async waitForInit() {
|
|
23
|
+
await this.initPromise;
|
|
24
|
+
}
|
|
25
|
+
async initialize() {
|
|
26
|
+
if (typeof navigator === "undefined") return;
|
|
27
|
+
if ("getBattery" in navigator) {
|
|
28
|
+
try {
|
|
29
|
+
this.battery = await navigator.getBattery();
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if ("connection" in navigator || "mozConnection" in navigator || "webkitConnection" in navigator) {
|
|
34
|
+
this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getStrategy(baseStrategy) {
|
|
38
|
+
if (baseStrategy === "off") return "off";
|
|
39
|
+
if (baseStrategy !== "auto") return baseStrategy;
|
|
40
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
41
|
+
return "off";
|
|
42
|
+
}
|
|
43
|
+
if (this.isLowBattery()) {
|
|
44
|
+
return "conservative";
|
|
45
|
+
}
|
|
46
|
+
if (this.isSlowConnection()) {
|
|
47
|
+
return "conservative";
|
|
48
|
+
}
|
|
49
|
+
return "aggressive";
|
|
50
|
+
}
|
|
51
|
+
shouldPrefetch() {
|
|
52
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (this.isLowBattery()) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
shouldSpeculate() {
|
|
61
|
+
if (!this.shouldPrefetch()) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (this.isSlowConnection()) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
shouldUseTransitions() {
|
|
70
|
+
if (!this.config.respectReducedMotion) return true;
|
|
71
|
+
if (typeof window === "undefined") return true;
|
|
72
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
73
|
+
}
|
|
74
|
+
hasSaveData() {
|
|
75
|
+
if (typeof navigator === "undefined") return false;
|
|
76
|
+
return navigator.connection?.saveData === true || navigator.mozConnection?.saveData === true || navigator.webkitConnection?.saveData === true;
|
|
77
|
+
}
|
|
78
|
+
isLowBattery() {
|
|
79
|
+
if (!this.battery) return false;
|
|
80
|
+
return !this.battery.charging && this.battery.level < this.config.batteryThreshold;
|
|
81
|
+
}
|
|
82
|
+
isSlowConnection() {
|
|
83
|
+
if (!this.connection) return false;
|
|
84
|
+
const effectiveType = this.connection.effectiveType;
|
|
85
|
+
if (!effectiveType) return false;
|
|
86
|
+
return this.config.connectionTypes.slow.includes(effectiveType);
|
|
87
|
+
}
|
|
88
|
+
isFastConnection() {
|
|
89
|
+
if (!this.connection) return true;
|
|
90
|
+
const effectiveType = this.connection.effectiveType;
|
|
91
|
+
if (!effectiveType) return true;
|
|
92
|
+
return this.config.connectionTypes.fast.includes(effectiveType);
|
|
93
|
+
}
|
|
94
|
+
getBatteryLevel() {
|
|
95
|
+
return this.battery?.level ?? null;
|
|
96
|
+
}
|
|
97
|
+
getConnectionType() {
|
|
98
|
+
return this.connection?.effectiveType ?? null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function createAdaptiveMode(config) {
|
|
102
|
+
return new AdaptiveMode(config);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
exports.AdaptiveMode = AdaptiveMode;
|
|
106
|
+
exports.createAdaptiveMode = createAdaptiveMode;
|
|
107
|
+
//# sourceMappingURL=adaptive.cjs.map
|
|
108
|
+
//# sourceMappingURL=adaptive.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adaptive.ts"],"names":[],"mappings":"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","file":"adaptive.cjs","sourcesContent":["import type { AdaptiveConfig, Strategy } from \"./types\";\n\nconst DEFAULT_CONFIG: AdaptiveConfig = {\n batteryThreshold: 0.2,\n connectionTypes: {\n slow: [\"2g\", \"3g\", \"slow-2g\"],\n fast: [\"4g\"],\n },\n respectSaveData: true,\n respectReducedMotion: true,\n};\n\nexport class AdaptiveMode {\n private config: AdaptiveConfig;\n private battery: any = null;\n private connection: any = null;\n private initPromise: Promise<void>;\n\n constructor(config: Partial<AdaptiveConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.initPromise = this.initialize();\n }\n\n async waitForInit(): Promise<void> {\n await this.initPromise;\n }\n\n private async initialize(): Promise<void> {\n if (typeof navigator === \"undefined\") return;\n\n // Battery API\n if (\"getBattery\" in navigator) {\n try {\n this.battery = await (navigator as any).getBattery();\n } catch {\n // Not available\n }\n }\n\n // Network Information API\n if (\"connection\" in navigator || \"mozConnection\" in navigator || \"webkitConnection\" in navigator) {\n this.connection =\n (navigator as any).connection ||\n (navigator as any).mozConnection ||\n (navigator as any).webkitConnection;\n }\n }\n\n getStrategy(baseStrategy: Strategy): Strategy {\n if (baseStrategy === \"off\") return \"off\";\n if (baseStrategy !== \"auto\") return baseStrategy;\n\n // Check Save-Data\n if (this.config.respectSaveData && this.hasSaveData()) {\n return \"off\";\n }\n\n // Check battery\n if (this.isLowBattery()) {\n return \"conservative\";\n }\n\n // Check connection\n if (this.isSlowConnection()) {\n return \"conservative\";\n }\n\n return \"aggressive\";\n }\n\n shouldPrefetch(): boolean {\n if (this.config.respectSaveData && this.hasSaveData()) {\n return false;\n }\n\n if (this.isLowBattery()) {\n return false;\n }\n\n return true;\n }\n\n shouldSpeculate(): boolean {\n if (!this.shouldPrefetch()) {\n return false;\n }\n\n if (this.isSlowConnection()) {\n return false;\n }\n\n return true;\n }\n\n shouldUseTransitions(): boolean {\n if (!this.config.respectReducedMotion) return true;\n\n if (typeof window === \"undefined\") return true;\n\n return !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n }\n\n private hasSaveData(): boolean {\n if (typeof navigator === \"undefined\") return false;\n\n return (\n (navigator as any).connection?.saveData === true ||\n (navigator as any).mozConnection?.saveData === true ||\n (navigator as any).webkitConnection?.saveData === true\n );\n }\n\n private isLowBattery(): boolean {\n if (!this.battery) return false;\n\n return (\n !this.battery.charging &&\n this.battery.level < this.config.batteryThreshold\n );\n }\n\n private isSlowConnection(): boolean {\n if (!this.connection) return false;\n\n const effectiveType = this.connection.effectiveType;\n if (!effectiveType) return false;\n\n return this.config.connectionTypes.slow.includes(effectiveType);\n }\n\n isFastConnection(): boolean {\n if (!this.connection) return true; // Assume fast if unknown\n\n const effectiveType = this.connection.effectiveType;\n if (!effectiveType) return true;\n\n return this.config.connectionTypes.fast.includes(effectiveType);\n }\n\n getBatteryLevel(): number | null {\n return this.battery?.level ?? null;\n }\n\n getConnectionType(): string | null {\n return this.connection?.effectiveType ?? null;\n }\n}\n\nexport function createAdaptiveMode(\n config?: Partial<AdaptiveConfig>\n): AdaptiveMode {\n return new AdaptiveMode(config);\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { A as AdaptiveConfig, S as Strategy } from './types-DnUtmOfQ.cjs';
|
|
2
|
+
|
|
3
|
+
declare class AdaptiveMode {
|
|
4
|
+
private config;
|
|
5
|
+
private battery;
|
|
6
|
+
private connection;
|
|
7
|
+
private initPromise;
|
|
8
|
+
constructor(config?: Partial<AdaptiveConfig>);
|
|
9
|
+
waitForInit(): Promise<void>;
|
|
10
|
+
private initialize;
|
|
11
|
+
getStrategy(baseStrategy: Strategy): Strategy;
|
|
12
|
+
shouldPrefetch(): boolean;
|
|
13
|
+
shouldSpeculate(): boolean;
|
|
14
|
+
shouldUseTransitions(): boolean;
|
|
15
|
+
private hasSaveData;
|
|
16
|
+
private isLowBattery;
|
|
17
|
+
private isSlowConnection;
|
|
18
|
+
isFastConnection(): boolean;
|
|
19
|
+
getBatteryLevel(): number | null;
|
|
20
|
+
getConnectionType(): string | null;
|
|
21
|
+
}
|
|
22
|
+
declare function createAdaptiveMode(config?: Partial<AdaptiveConfig>): AdaptiveMode;
|
|
23
|
+
|
|
24
|
+
export { AdaptiveMode, createAdaptiveMode };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { A as AdaptiveConfig, S as Strategy } from './types-DnUtmOfQ.js';
|
|
2
|
+
|
|
3
|
+
declare class AdaptiveMode {
|
|
4
|
+
private config;
|
|
5
|
+
private battery;
|
|
6
|
+
private connection;
|
|
7
|
+
private initPromise;
|
|
8
|
+
constructor(config?: Partial<AdaptiveConfig>);
|
|
9
|
+
waitForInit(): Promise<void>;
|
|
10
|
+
private initialize;
|
|
11
|
+
getStrategy(baseStrategy: Strategy): Strategy;
|
|
12
|
+
shouldPrefetch(): boolean;
|
|
13
|
+
shouldSpeculate(): boolean;
|
|
14
|
+
shouldUseTransitions(): boolean;
|
|
15
|
+
private hasSaveData;
|
|
16
|
+
private isLowBattery;
|
|
17
|
+
private isSlowConnection;
|
|
18
|
+
isFastConnection(): boolean;
|
|
19
|
+
getBatteryLevel(): number | null;
|
|
20
|
+
getConnectionType(): string | null;
|
|
21
|
+
}
|
|
22
|
+
declare function createAdaptiveMode(config?: Partial<AdaptiveConfig>): AdaptiveMode;
|
|
23
|
+
|
|
24
|
+
export { AdaptiveMode, createAdaptiveMode };
|
package/dist/adaptive.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/adaptive.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
batteryThreshold: 0.2,
|
|
4
|
+
connectionTypes: {
|
|
5
|
+
slow: ["2g", "3g", "slow-2g"],
|
|
6
|
+
fast: ["4g"]
|
|
7
|
+
},
|
|
8
|
+
respectSaveData: true,
|
|
9
|
+
respectReducedMotion: true
|
|
10
|
+
};
|
|
11
|
+
var AdaptiveMode = class {
|
|
12
|
+
config;
|
|
13
|
+
battery = null;
|
|
14
|
+
connection = null;
|
|
15
|
+
initPromise;
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
18
|
+
this.initPromise = this.initialize();
|
|
19
|
+
}
|
|
20
|
+
async waitForInit() {
|
|
21
|
+
await this.initPromise;
|
|
22
|
+
}
|
|
23
|
+
async initialize() {
|
|
24
|
+
if (typeof navigator === "undefined") return;
|
|
25
|
+
if ("getBattery" in navigator) {
|
|
26
|
+
try {
|
|
27
|
+
this.battery = await navigator.getBattery();
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if ("connection" in navigator || "mozConnection" in navigator || "webkitConnection" in navigator) {
|
|
32
|
+
this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
getStrategy(baseStrategy) {
|
|
36
|
+
if (baseStrategy === "off") return "off";
|
|
37
|
+
if (baseStrategy !== "auto") return baseStrategy;
|
|
38
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
39
|
+
return "off";
|
|
40
|
+
}
|
|
41
|
+
if (this.isLowBattery()) {
|
|
42
|
+
return "conservative";
|
|
43
|
+
}
|
|
44
|
+
if (this.isSlowConnection()) {
|
|
45
|
+
return "conservative";
|
|
46
|
+
}
|
|
47
|
+
return "aggressive";
|
|
48
|
+
}
|
|
49
|
+
shouldPrefetch() {
|
|
50
|
+
if (this.config.respectSaveData && this.hasSaveData()) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (this.isLowBattery()) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
shouldSpeculate() {
|
|
59
|
+
if (!this.shouldPrefetch()) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (this.isSlowConnection()) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
shouldUseTransitions() {
|
|
68
|
+
if (!this.config.respectReducedMotion) return true;
|
|
69
|
+
if (typeof window === "undefined") return true;
|
|
70
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
71
|
+
}
|
|
72
|
+
hasSaveData() {
|
|
73
|
+
if (typeof navigator === "undefined") return false;
|
|
74
|
+
return navigator.connection?.saveData === true || navigator.mozConnection?.saveData === true || navigator.webkitConnection?.saveData === true;
|
|
75
|
+
}
|
|
76
|
+
isLowBattery() {
|
|
77
|
+
if (!this.battery) return false;
|
|
78
|
+
return !this.battery.charging && this.battery.level < this.config.batteryThreshold;
|
|
79
|
+
}
|
|
80
|
+
isSlowConnection() {
|
|
81
|
+
if (!this.connection) return false;
|
|
82
|
+
const effectiveType = this.connection.effectiveType;
|
|
83
|
+
if (!effectiveType) return false;
|
|
84
|
+
return this.config.connectionTypes.slow.includes(effectiveType);
|
|
85
|
+
}
|
|
86
|
+
isFastConnection() {
|
|
87
|
+
if (!this.connection) return true;
|
|
88
|
+
const effectiveType = this.connection.effectiveType;
|
|
89
|
+
if (!effectiveType) return true;
|
|
90
|
+
return this.config.connectionTypes.fast.includes(effectiveType);
|
|
91
|
+
}
|
|
92
|
+
getBatteryLevel() {
|
|
93
|
+
return this.battery?.level ?? null;
|
|
94
|
+
}
|
|
95
|
+
getConnectionType() {
|
|
96
|
+
return this.connection?.effectiveType ?? null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function createAdaptiveMode(config) {
|
|
100
|
+
return new AdaptiveMode(config);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { AdaptiveMode, createAdaptiveMode };
|
|
104
|
+
//# sourceMappingURL=adaptive.js.map
|
|
105
|
+
//# sourceMappingURL=adaptive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adaptive.ts"],"names":[],"mappings":"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","file":"adaptive.js","sourcesContent":["import type { AdaptiveConfig, Strategy } from \"./types\";\n\nconst DEFAULT_CONFIG: AdaptiveConfig = {\n batteryThreshold: 0.2,\n connectionTypes: {\n slow: [\"2g\", \"3g\", \"slow-2g\"],\n fast: [\"4g\"],\n },\n respectSaveData: true,\n respectReducedMotion: true,\n};\n\nexport class AdaptiveMode {\n private config: AdaptiveConfig;\n private battery: any = null;\n private connection: any = null;\n private initPromise: Promise<void>;\n\n constructor(config: Partial<AdaptiveConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.initPromise = this.initialize();\n }\n\n async waitForInit(): Promise<void> {\n await this.initPromise;\n }\n\n private async initialize(): Promise<void> {\n if (typeof navigator === \"undefined\") return;\n\n // Battery API\n if (\"getBattery\" in navigator) {\n try {\n this.battery = await (navigator as any).getBattery();\n } catch {\n // Not available\n }\n }\n\n // Network Information API\n if (\"connection\" in navigator || \"mozConnection\" in navigator || \"webkitConnection\" in navigator) {\n this.connection =\n (navigator as any).connection ||\n (navigator as any).mozConnection ||\n (navigator as any).webkitConnection;\n }\n }\n\n getStrategy(baseStrategy: Strategy): Strategy {\n if (baseStrategy === \"off\") return \"off\";\n if (baseStrategy !== \"auto\") return baseStrategy;\n\n // Check Save-Data\n if (this.config.respectSaveData && this.hasSaveData()) {\n return \"off\";\n }\n\n // Check battery\n if (this.isLowBattery()) {\n return \"conservative\";\n }\n\n // Check connection\n if (this.isSlowConnection()) {\n return \"conservative\";\n }\n\n return \"aggressive\";\n }\n\n shouldPrefetch(): boolean {\n if (this.config.respectSaveData && this.hasSaveData()) {\n return false;\n }\n\n if (this.isLowBattery()) {\n return false;\n }\n\n return true;\n }\n\n shouldSpeculate(): boolean {\n if (!this.shouldPrefetch()) {\n return false;\n }\n\n if (this.isSlowConnection()) {\n return false;\n }\n\n return true;\n }\n\n shouldUseTransitions(): boolean {\n if (!this.config.respectReducedMotion) return true;\n\n if (typeof window === \"undefined\") return true;\n\n return !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n }\n\n private hasSaveData(): boolean {\n if (typeof navigator === \"undefined\") return false;\n\n return (\n (navigator as any).connection?.saveData === true ||\n (navigator as any).mozConnection?.saveData === true ||\n (navigator as any).webkitConnection?.saveData === true\n );\n }\n\n private isLowBattery(): boolean {\n if (!this.battery) return false;\n\n return (\n !this.battery.charging &&\n this.battery.level < this.config.batteryThreshold\n );\n }\n\n private isSlowConnection(): boolean {\n if (!this.connection) return false;\n\n const effectiveType = this.connection.effectiveType;\n if (!effectiveType) return false;\n\n return this.config.connectionTypes.slow.includes(effectiveType);\n }\n\n isFastConnection(): boolean {\n if (!this.connection) return true; // Assume fast if unknown\n\n const effectiveType = this.connection.effectiveType;\n if (!effectiveType) return true;\n\n return this.config.connectionTypes.fast.includes(effectiveType);\n }\n\n getBatteryLevel(): number | null {\n return this.battery?.level ?? null;\n }\n\n getConnectionType(): string | null {\n return this.connection?.effectiveType ?? null;\n }\n}\n\nexport function createAdaptiveMode(\n config?: Partial<AdaptiveConfig>\n): AdaptiveMode {\n return new AdaptiveMode(config);\n}\n"]}
|
package/dist/cache.cjs
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/cache.ts
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
memory: {
|
|
6
|
+
enabled: true,
|
|
7
|
+
maxPages: 10
|
|
8
|
+
},
|
|
9
|
+
serviceWorker: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
ttl: 300,
|
|
12
|
+
cacheName: "specnav-v1",
|
|
13
|
+
crossTabSync: true,
|
|
14
|
+
broadcastChannel: "specnav-sync"
|
|
15
|
+
},
|
|
16
|
+
edge: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
maxAge: 60,
|
|
19
|
+
staleWhileRevalidate: 300
|
|
20
|
+
},
|
|
21
|
+
exclude: []
|
|
22
|
+
};
|
|
23
|
+
var CacheManager = class {
|
|
24
|
+
config;
|
|
25
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
26
|
+
accessOrder = [];
|
|
27
|
+
broadcastChannel;
|
|
28
|
+
onCacheHit;
|
|
29
|
+
constructor(config = {}, callbacks) {
|
|
30
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
31
|
+
this.onCacheHit = callbacks?.onCacheHit;
|
|
32
|
+
if (this.config.serviceWorker.enabled && this.config.serviceWorker.crossTabSync && typeof BroadcastChannel !== "undefined") {
|
|
33
|
+
this.broadcastChannel = new BroadcastChannel(
|
|
34
|
+
this.config.serviceWorker.broadcastChannel
|
|
35
|
+
);
|
|
36
|
+
this.broadcastChannel.onmessage = this.handleBroadcast;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async get(href) {
|
|
40
|
+
if (this.isExcluded(href)) return null;
|
|
41
|
+
const memoryHit = this.getFromMemory(href);
|
|
42
|
+
if (memoryHit) {
|
|
43
|
+
this.onCacheHit?.(href, 1);
|
|
44
|
+
return memoryHit;
|
|
45
|
+
}
|
|
46
|
+
if (this.config.serviceWorker.enabled) {
|
|
47
|
+
const swHit = await this.getFromServiceWorker(href);
|
|
48
|
+
if (swHit) {
|
|
49
|
+
this.onCacheHit?.(href, 2);
|
|
50
|
+
this.setInMemory(href, swHit);
|
|
51
|
+
return swHit;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
async set(href, html) {
|
|
57
|
+
if (this.isExcluded(href)) return;
|
|
58
|
+
const sanitizedHref = this.sanitizeHref(href);
|
|
59
|
+
if (!sanitizedHref) return;
|
|
60
|
+
if (this.containsDangerousContent(html)) {
|
|
61
|
+
console.warn(`Refusing to cache potentially dangerous content from ${sanitizedHref}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const page = {
|
|
65
|
+
href: sanitizedHref,
|
|
66
|
+
html,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
ttl: this.config.serviceWorker.ttl
|
|
69
|
+
};
|
|
70
|
+
if (this.config.memory.enabled) {
|
|
71
|
+
this.setInMemory(sanitizedHref, html);
|
|
72
|
+
}
|
|
73
|
+
if (this.config.serviceWorker.enabled) {
|
|
74
|
+
await this.setInServiceWorker(page);
|
|
75
|
+
this.broadcastSet(sanitizedHref);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
containsDangerousContent(html) {
|
|
79
|
+
const dangerousPatterns = [
|
|
80
|
+
/on\w+\s*=\s*["'][^"']*["']/i,
|
|
81
|
+
// Inline event handlers
|
|
82
|
+
/javascript:/i
|
|
83
|
+
// javascript: URLs
|
|
84
|
+
];
|
|
85
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
86
|
+
let match;
|
|
87
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
88
|
+
const attrs = match[1] || "";
|
|
89
|
+
if (/src\s*=/.test(attrs)) continue;
|
|
90
|
+
if (/type\s*=\s*["']?(application\/json|application\/ld\+json|text\/template)["']?/i.test(attrs)) continue;
|
|
91
|
+
if (/id\s*=\s*["']__NEXT_DATA__["']/i.test(attrs)) continue;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return dangerousPatterns.some((pattern) => pattern.test(html));
|
|
95
|
+
}
|
|
96
|
+
sanitizeHref(href) {
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(href, window.location.origin);
|
|
99
|
+
if (url.origin !== window.location.origin) return null;
|
|
100
|
+
return url.pathname + url.search + url.hash;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
clear(href) {
|
|
106
|
+
if (href) {
|
|
107
|
+
this.memoryCache.delete(href);
|
|
108
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
109
|
+
this.clearFromServiceWorker(href);
|
|
110
|
+
} else {
|
|
111
|
+
this.memoryCache.clear();
|
|
112
|
+
this.accessOrder = [];
|
|
113
|
+
this.clearAllServiceWorker();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
getSize() {
|
|
117
|
+
return this.memoryCache.size;
|
|
118
|
+
}
|
|
119
|
+
destroy() {
|
|
120
|
+
this.memoryCache.clear();
|
|
121
|
+
this.accessOrder = [];
|
|
122
|
+
this.broadcastChannel?.close();
|
|
123
|
+
}
|
|
124
|
+
getFromMemory(href) {
|
|
125
|
+
const page = this.memoryCache.get(href);
|
|
126
|
+
if (!page) return null;
|
|
127
|
+
this.accessOrder = this.accessOrder.filter((h) => h !== href);
|
|
128
|
+
this.accessOrder.push(href);
|
|
129
|
+
return page.html;
|
|
130
|
+
}
|
|
131
|
+
setInMemory(href, html) {
|
|
132
|
+
const maxPageSize = 5 * 1024 * 1024;
|
|
133
|
+
if (html.length > maxPageSize) {
|
|
134
|
+
console.warn(`Page ${href} exceeds max size, not caching`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.memoryCache.set(href, {
|
|
138
|
+
href,
|
|
139
|
+
html,
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
ttl: this.config.serviceWorker.ttl
|
|
142
|
+
});
|
|
143
|
+
const existingIndex = this.accessOrder.indexOf(href);
|
|
144
|
+
if (existingIndex !== -1) {
|
|
145
|
+
this.accessOrder.splice(existingIndex, 1);
|
|
146
|
+
}
|
|
147
|
+
this.accessOrder.push(href);
|
|
148
|
+
if (this.memoryCache.size > this.config.memory.maxPages) {
|
|
149
|
+
const oldest = this.accessOrder.shift();
|
|
150
|
+
if (oldest) this.memoryCache.delete(oldest);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async getFromServiceWorker(href) {
|
|
154
|
+
if (typeof caches === "undefined") return null;
|
|
155
|
+
try {
|
|
156
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
157
|
+
const response = await cache.match(href);
|
|
158
|
+
if (!response) return null;
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
const page = data;
|
|
161
|
+
const age = Date.now() - page.timestamp;
|
|
162
|
+
if (age > page.ttl * 1e3) {
|
|
163
|
+
await cache.delete(href);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return page.html;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async setInServiceWorker(page) {
|
|
172
|
+
if (typeof caches === "undefined") return;
|
|
173
|
+
try {
|
|
174
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
175
|
+
const response = new Response(JSON.stringify(page), {
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"Cache-Control": `max-age=${this.config.edge.maxAge}, stale-while-revalidate=${this.config.edge.staleWhileRevalidate}`
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
await cache.put(page.href, response);
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async clearFromServiceWorker(href) {
|
|
186
|
+
if (typeof caches === "undefined") return;
|
|
187
|
+
try {
|
|
188
|
+
const cache = await caches.open(this.config.serviceWorker.cacheName);
|
|
189
|
+
await cache.delete(href);
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async clearAllServiceWorker() {
|
|
194
|
+
if (typeof caches === "undefined") return;
|
|
195
|
+
try {
|
|
196
|
+
await caches.delete(this.config.serviceWorker.cacheName);
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
isExcluded(href) {
|
|
201
|
+
return this.config.exclude.some((pattern) => {
|
|
202
|
+
if (typeof pattern === "string") {
|
|
203
|
+
return pattern.includes("*") ? new RegExp(pattern.replace(/\*/g, ".*")).test(href) : href === pattern;
|
|
204
|
+
}
|
|
205
|
+
return pattern.test(href);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
broadcastSet(href) {
|
|
209
|
+
this.broadcastChannel?.postMessage({ type: "set", href });
|
|
210
|
+
}
|
|
211
|
+
handleBroadcast = (event) => {
|
|
212
|
+
if (event.data.type === "set") {
|
|
213
|
+
this.memoryCache.delete(event.data.href);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
function createCacheManager(config, callbacks) {
|
|
218
|
+
return new CacheManager(config, callbacks);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
exports.CacheManager = CacheManager;
|
|
222
|
+
exports.createCacheManager = createCacheManager;
|
|
223
|
+
//# sourceMappingURL=cache.cjs.map
|
|
224
|
+
//# sourceMappingURL=cache.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts"],"names":[],"mappings":"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","file":"cache.cjs","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/cache.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { C as CacheConfig, a as CacheLayer } from './types-DnUtmOfQ.cjs';
|
|
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 };
|