@soham20/smart-offline-sdk 0.2.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -35
- package/package.json +4 -3
- package/smart-offline-sw.js +181 -41
- package/src/SmartOfflineSetup.ts +658 -0
- package/src/SmartOfflineTestUtils.ts +578 -0
- package/src/index.cjs +576 -0
- package/src/index.cjs.js +563 -97
- package/src/index.d.ts +320 -52
- package/src/index.js +985 -125
package/src/index.js
CHANGED
|
@@ -1,112 +1,510 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SmartOffline SDK
|
|
2
|
+
* SmartOffline SDK - v1.0.0
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* Complete, reliable offline-first caching SDK for web applications.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```javascript
|
|
8
|
+
* import { setupSmartOffline, SmartOffline } from '@soham20/smart-offline-sdk'
|
|
9
|
+
*
|
|
10
|
+
* // Initialize early in your app
|
|
11
|
+
* await setupSmartOffline({
|
|
12
|
+
* pages: ['/dashboard/*', '/products/*'],
|
|
13
|
+
* apis: ['/api/v1/*'],
|
|
14
|
+
* debug: true
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// DEFAULT CONFIGURATION
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG = {
|
|
24
|
+
pages: [],
|
|
25
|
+
apis: [],
|
|
26
|
+
debug:
|
|
27
|
+
typeof process !== "undefined" && process.env?.NODE_ENV !== "production",
|
|
28
|
+
frequencyThreshold: 3,
|
|
29
|
+
recencyThreshold: 24 * 60 * 60 * 1000, // 24 hours
|
|
30
|
+
maxResourceSize: 10 * 1024 * 1024, // 10MB
|
|
31
|
+
networkQuality: "auto",
|
|
32
|
+
significance: {},
|
|
33
|
+
weights: { frequency: 1, recency: 1, size: 1 },
|
|
34
|
+
customPriorityFn: null,
|
|
35
|
+
enableDetailedLogs: false,
|
|
36
|
+
serviceWorkerPath: "/smart-offline-sw.js",
|
|
37
|
+
serviceWorkerScope: "/",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// STATE
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
let isInitialized = false;
|
|
45
|
+
let serviceWorkerRegistration = null;
|
|
46
|
+
let currentConfig = { ...DEFAULT_CONFIG };
|
|
47
|
+
|
|
15
48
|
// Event listener registry
|
|
16
49
|
const eventListeners = {
|
|
17
50
|
cache: [],
|
|
18
51
|
skip: [],
|
|
19
52
|
serve: [],
|
|
20
53
|
clear: [],
|
|
21
|
-
error: []
|
|
54
|
+
error: [],
|
|
22
55
|
};
|
|
23
56
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const sdkConfig = {
|
|
31
|
-
pages: config.pages || [],
|
|
32
|
-
apis: config.apis || [],
|
|
33
|
-
debug: config.debug || false,
|
|
34
|
-
frequencyThreshold: config.frequencyThreshold ?? 3,
|
|
35
|
-
recencyThreshold: config.recencyThreshold ?? 24 * 60 * 60 * 1000,
|
|
36
|
-
maxResourceSize: config.maxResourceSize ?? Infinity,
|
|
37
|
-
networkQuality: config.networkQuality ?? "auto",
|
|
38
|
-
significance: config.significance ?? {},
|
|
39
|
-
|
|
40
|
-
// New features
|
|
41
|
-
weights: {
|
|
42
|
-
frequency: config.weights?.frequency ?? 1,
|
|
43
|
-
recency: config.weights?.recency ?? 1,
|
|
44
|
-
size: config.weights?.size ?? 1
|
|
45
|
-
},
|
|
46
|
-
customPriorityFn: config.customPriorityFn ? config.customPriorityFn.toString() : null,
|
|
47
|
-
enableDetailedLogs: config.enableDetailedLogs ?? false
|
|
48
|
-
};
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// MAIN SETUP FUNCTION
|
|
59
|
+
// ============================================================================
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the SmartOffline SDK with complete setup
|
|
63
|
+
* @param {Partial<SmartOfflineConfig>} config - Configuration options
|
|
64
|
+
* @returns {Promise<{success: boolean, registration?: ServiceWorkerRegistration, error?: string}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function setupSmartOffline(config = {}) {
|
|
67
|
+
// Merge with defaults
|
|
68
|
+
currentConfig = { ...DEFAULT_CONFIG, ...config };
|
|
69
|
+
|
|
70
|
+
// Check if already initialized
|
|
71
|
+
if (isInitialized) {
|
|
72
|
+
if (currentConfig.debug) {
|
|
73
|
+
console.warn("[SmartOffline] Already initialized, updating config...");
|
|
74
|
+
}
|
|
75
|
+
await sendConfigToServiceWorker();
|
|
76
|
+
return { success: true, registration: serviceWorkerRegistration };
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
79
|
+
// Check browser support
|
|
80
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
|
|
81
|
+
console.error(
|
|
82
|
+
"[SmartOffline] Service Workers not supported in this browser",
|
|
83
|
+
);
|
|
84
|
+
return { success: false, error: "Service Workers not supported" };
|
|
85
|
+
}
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
if (typeof window === "undefined" || !("caches" in window)) {
|
|
88
|
+
console.error("[SmartOffline] Cache API not supported in this browser");
|
|
89
|
+
return { success: false, error: "Cache API not supported" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Register service worker
|
|
94
|
+
if (currentConfig.debug) {
|
|
95
|
+
printStartupBanner();
|
|
96
|
+
console.log("%c🔧 Registering service worker...", "color: #94a3b8;");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
serviceWorkerRegistration = await navigator.serviceWorker.register(
|
|
100
|
+
currentConfig.serviceWorkerPath,
|
|
101
|
+
{ scope: currentConfig.serviceWorkerScope },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (currentConfig.debug) {
|
|
105
|
+
console.log(
|
|
106
|
+
"%c✓ Service worker registered", "color: #22c55e;",
|
|
107
|
+
`(scope: ${serviceWorkerRegistration.scope})`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wait for the service worker to be ready
|
|
112
|
+
await navigator.serviceWorker.ready;
|
|
113
|
+
|
|
114
|
+
if (currentConfig.debug) {
|
|
115
|
+
console.log("%c✓ Service worker ready", "color: #22c55e;");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Send configuration to service worker
|
|
119
|
+
await sendConfigToServiceWorker();
|
|
120
|
+
|
|
121
|
+
// Set up event listeners
|
|
122
|
+
setupEventListenersInternal();
|
|
123
|
+
|
|
124
|
+
isInitialized = true;
|
|
125
|
+
|
|
126
|
+
if (currentConfig.debug) {
|
|
127
|
+
printConfigSummary();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { success: true, registration: serviceWorkerRegistration };
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("[SmartOffline] Setup failed:", error);
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// HELPER FUNCTIONS
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Print startup banner
|
|
146
|
+
*/
|
|
147
|
+
function printStartupBanner() {
|
|
148
|
+
console.log(
|
|
149
|
+
`%c
|
|
150
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
151
|
+
║ ║
|
|
152
|
+
║ 🚀 SmartOffline SDK v1.0.0 ║
|
|
153
|
+
║ Intelligent offline-first caching ║
|
|
154
|
+
║ ║
|
|
155
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
156
|
+
`,
|
|
157
|
+
"color: #3b82f6; font-weight: bold;"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Print configuration summary
|
|
163
|
+
*/
|
|
164
|
+
function printConfigSummary() {
|
|
165
|
+
console.log(
|
|
166
|
+
`%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
167
|
+
"color: #475569;"
|
|
168
|
+
);
|
|
169
|
+
console.log(
|
|
170
|
+
`%c📋 Configuration Summary`,
|
|
171
|
+
"color: #22c55e; font-weight: bold; font-size: 14px;"
|
|
172
|
+
);
|
|
173
|
+
console.log(
|
|
174
|
+
`%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
175
|
+
"color: #475569;"
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Pages
|
|
179
|
+
console.log("%c📄 Pages to cache:", "color: #60a5fa; font-weight: bold;");
|
|
180
|
+
if (currentConfig.pages.length > 0) {
|
|
181
|
+
currentConfig.pages.forEach((p) =>
|
|
182
|
+
console.log(` %c• ${p}`, "color: #94a3b8;")
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(" %c(none)", "color: #64748b; font-style: italic;");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// APIs
|
|
189
|
+
console.log("%c🔌 APIs to cache:", "color: #60a5fa; font-weight: bold;");
|
|
190
|
+
if (currentConfig.apis.length > 0) {
|
|
191
|
+
currentConfig.apis.forEach((a) =>
|
|
192
|
+
console.log(` %c• ${a}`, "color: #94a3b8;")
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(" %c(none)", "color: #64748b; font-style: italic;");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Priority settings
|
|
199
|
+
console.log("%c⚡ Priority settings:", "color: #60a5fa; font-weight: bold;");
|
|
200
|
+
console.log(
|
|
201
|
+
` %cFrequency threshold: ${currentConfig.frequencyThreshold} accesses`,
|
|
202
|
+
"color: #94a3b8;"
|
|
203
|
+
);
|
|
204
|
+
console.log(
|
|
205
|
+
` %cRecency threshold: ${currentConfig.recencyThreshold / (60 * 60 * 1000)}h`,
|
|
206
|
+
"color: #94a3b8;"
|
|
207
|
+
);
|
|
208
|
+
console.log(
|
|
209
|
+
` %cMax resource size: ${formatBytes(currentConfig.maxResourceSize)}`,
|
|
210
|
+
"color: #94a3b8;"
|
|
211
|
+
);
|
|
212
|
+
console.log(
|
|
213
|
+
` %cNetwork quality: ${currentConfig.networkQuality}`,
|
|
214
|
+
"color: #94a3b8;"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Significance overrides
|
|
218
|
+
const sigKeys = Object.keys(currentConfig.significance || {});
|
|
219
|
+
if (sigKeys.length > 0) {
|
|
220
|
+
console.log(
|
|
221
|
+
"%c🎯 Significance overrides:",
|
|
222
|
+
"color: #60a5fa; font-weight: bold;"
|
|
223
|
+
);
|
|
224
|
+
sigKeys.forEach((key) => {
|
|
225
|
+
const val = currentConfig.significance[key];
|
|
226
|
+
const icon = val === "high" ? "🔴" : val === "low" ? "🔵" : "⚪";
|
|
227
|
+
console.log(` %c${icon} ${key} → ${val}`, "color: #94a3b8;");
|
|
69
228
|
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(
|
|
232
|
+
`%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
233
|
+
"color: #475569;"
|
|
234
|
+
);
|
|
235
|
+
console.log(
|
|
236
|
+
"%c✅ SmartOffline is now active! Cache events will appear below.",
|
|
237
|
+
"color: #22c55e; font-weight: bold;"
|
|
238
|
+
);
|
|
239
|
+
console.log(
|
|
240
|
+
`%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
241
|
+
"color: #475569;"
|
|
242
|
+
);
|
|
243
|
+
console.log(""); // Empty line for spacing
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function sendConfigToServiceWorker() {
|
|
247
|
+
const controller = navigator.serviceWorker.controller;
|
|
248
|
+
|
|
249
|
+
if (!controller) {
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
251
|
+
const reg = await navigator.serviceWorker.ready;
|
|
252
|
+
const worker = reg.active;
|
|
253
|
+
|
|
254
|
+
if (worker) {
|
|
255
|
+
sendConfigMessage(worker);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
sendConfigMessage(controller);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sendConfigMessage(worker) {
|
|
263
|
+
const transferableConfig = {
|
|
264
|
+
...currentConfig,
|
|
265
|
+
customPriorityFn: currentConfig.customPriorityFn
|
|
266
|
+
? currentConfig.customPriorityFn.toString()
|
|
267
|
+
: null,
|
|
268
|
+
onCacheEvent: undefined,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
worker.postMessage({
|
|
272
|
+
type: "INIT_CONFIG",
|
|
273
|
+
payload: transferableConfig,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (currentConfig.debug) {
|
|
277
|
+
console.log("[SmartOffline] Config sent to service worker");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
70
280
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
281
|
+
function setupEventListenersInternal() {
|
|
282
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
283
|
+
if (event.data && typeof event.data.type === "string") {
|
|
284
|
+
if (event.data.type.startsWith("CACHE_")) {
|
|
285
|
+
const cacheEvent = {
|
|
286
|
+
type: event.data.type,
|
|
287
|
+
url: event.data.url,
|
|
288
|
+
reason: event.data.reason,
|
|
289
|
+
metadata: event.data.metadata || {},
|
|
290
|
+
timestamp: event.data.timestamp || Date.now(),
|
|
291
|
+
};
|
|
77
292
|
|
|
78
|
-
if
|
|
79
|
-
|
|
293
|
+
// Call user callback if provided
|
|
294
|
+
if (currentConfig.onCacheEvent) {
|
|
295
|
+
currentConfig.onCacheEvent(cacheEvent);
|
|
80
296
|
}
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
297
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(
|
|
93
|
-
"[SmartOffline] Config sent after controllerchange:",
|
|
94
|
-
sdkConfig,
|
|
95
|
-
);
|
|
298
|
+
// Call registered event listeners
|
|
299
|
+
const eventType = cacheEvent.type.replace("CACHE_", "").toLowerCase();
|
|
300
|
+
const listeners = eventListeners[eventType] || [];
|
|
301
|
+
listeners.forEach((fn) => fn(cacheEvent));
|
|
302
|
+
|
|
303
|
+
// Rich debug logging with colors
|
|
304
|
+
if (currentConfig.debug) {
|
|
305
|
+
logCacheEventToConsole(cacheEvent);
|
|
96
306
|
}
|
|
97
307
|
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Rich console logging for cache events with colors and formatting
|
|
314
|
+
*/
|
|
315
|
+
function logCacheEventToConsole(cacheEvent) {
|
|
316
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
317
|
+
const shortUrl = cacheEvent.url.replace(origin, "") || cacheEvent.url;
|
|
318
|
+
const time = new Date(cacheEvent.timestamp).toLocaleTimeString();
|
|
319
|
+
|
|
320
|
+
// Style configurations for each event type
|
|
321
|
+
const styles = {
|
|
322
|
+
CACHE_INTERCEPT: {
|
|
323
|
+
icon: "🔍",
|
|
324
|
+
label: "INTERCEPT",
|
|
325
|
+
bgColor: "#6366f1",
|
|
326
|
+
textColor: "#fff",
|
|
327
|
+
},
|
|
328
|
+
CACHE_CACHE: {
|
|
329
|
+
icon: "💾",
|
|
330
|
+
label: "CACHED",
|
|
331
|
+
bgColor: "#22c55e",
|
|
332
|
+
textColor: "#fff",
|
|
333
|
+
},
|
|
334
|
+
CACHE_SKIP: {
|
|
335
|
+
icon: "⏭️",
|
|
336
|
+
label: "SKIPPED",
|
|
337
|
+
bgColor: "#f59e0b",
|
|
338
|
+
textColor: "#000",
|
|
339
|
+
},
|
|
340
|
+
CACHE_SERVE: {
|
|
341
|
+
icon: "📤",
|
|
342
|
+
label: "SERVED",
|
|
343
|
+
bgColor: "#3b82f6",
|
|
344
|
+
textColor: "#fff",
|
|
345
|
+
},
|
|
346
|
+
CACHE_ERROR: {
|
|
347
|
+
icon: "❌",
|
|
348
|
+
label: "MISS",
|
|
349
|
+
bgColor: "#ef4444",
|
|
350
|
+
textColor: "#fff",
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const style = styles[cacheEvent.type] || {
|
|
355
|
+
icon: "📝",
|
|
356
|
+
label: "EVENT",
|
|
357
|
+
bgColor: "#64748b",
|
|
358
|
+
textColor: "#fff",
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Build metadata string
|
|
362
|
+
let metaInfo = "";
|
|
363
|
+
if (cacheEvent.metadata) {
|
|
364
|
+
const meta = cacheEvent.metadata;
|
|
365
|
+
const parts = [];
|
|
366
|
+
if (meta.type) parts.push(`type: ${meta.type}`);
|
|
367
|
+
if (meta.priority) parts.push(`priority: ${meta.priority}`);
|
|
368
|
+
if (meta.size) parts.push(`size: ${formatBytes(meta.size)}`);
|
|
369
|
+
if (meta.networkQuality) parts.push(`network: ${meta.networkQuality}`);
|
|
370
|
+
if (meta.isPage !== undefined) parts.push(meta.isPage ? "PAGE" : "API");
|
|
371
|
+
if (parts.length > 0) metaInfo = ` [${parts.join(", ")}]`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Reason badge
|
|
375
|
+
const reasonText = cacheEvent.reason ? ` → ${formatReason(cacheEvent.reason)}` : "";
|
|
376
|
+
|
|
377
|
+
// Log with styling
|
|
378
|
+
console.log(
|
|
379
|
+
`%c ${style.icon} SmartOffline %c ${style.label} %c ${time} %c ${shortUrl}${reasonText}${metaInfo}`,
|
|
380
|
+
`background: #1e293b; color: #fff; padding: 2px 6px; border-radius: 3px 0 0 3px; font-weight: bold;`,
|
|
381
|
+
`background: ${style.bgColor}; color: ${style.textColor}; padding: 2px 8px; font-weight: bold;`,
|
|
382
|
+
`background: #334155; color: #94a3b8; padding: 2px 6px;`,
|
|
383
|
+
`background: transparent; color: #e2e8f0; padding: 2px 6px;`
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// For cached items, show additional details in a group
|
|
387
|
+
if (cacheEvent.type === "CACHE_CACHE" && cacheEvent.metadata) {
|
|
388
|
+
console.groupCollapsed(` ↳ Details for ${shortUrl}`);
|
|
389
|
+
console.table({
|
|
390
|
+
URL: shortUrl,
|
|
391
|
+
Type: cacheEvent.metadata.type || "unknown",
|
|
392
|
+
Priority: cacheEvent.metadata.priority || "normal",
|
|
393
|
+
Size: cacheEvent.metadata.size ? formatBytes(cacheEvent.metadata.size) : "unknown",
|
|
394
|
+
Timestamp: new Date(cacheEvent.timestamp).toISOString(),
|
|
395
|
+
});
|
|
396
|
+
console.groupEnd();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// For serve events, show usage data if available
|
|
400
|
+
if (cacheEvent.type === "CACHE_SERVE" && cacheEvent.metadata?.usage) {
|
|
401
|
+
console.groupCollapsed(` ↳ Usage data for ${shortUrl}`);
|
|
402
|
+
console.table({
|
|
403
|
+
"Access Count": cacheEvent.metadata.usage.count,
|
|
404
|
+
"Last Accessed": new Date(cacheEvent.metadata.usage.lastAccessed).toISOString(),
|
|
405
|
+
Priority: cacheEvent.metadata.priority || "normal",
|
|
98
406
|
});
|
|
407
|
+
console.groupEnd();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Format bytes to human readable string
|
|
413
|
+
*/
|
|
414
|
+
function formatBytes(bytes) {
|
|
415
|
+
if (bytes === 0) return "0 B";
|
|
416
|
+
const k = 1024;
|
|
417
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
418
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
419
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Format reason string to be more readable
|
|
424
|
+
*/
|
|
425
|
+
function formatReason(reason) {
|
|
426
|
+
return reason
|
|
427
|
+
.replace(/_/g, " ")
|
|
428
|
+
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function openDB(name, version) {
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const request = indexedDB.open(name, version);
|
|
434
|
+
request.onupgradeneeded = () => {
|
|
435
|
+
const db = request.result;
|
|
436
|
+
if (
|
|
437
|
+
name === "smart-offline-logs-v2" &&
|
|
438
|
+
!db.objectStoreNames.contains("logs")
|
|
439
|
+
) {
|
|
440
|
+
db.createObjectStore("logs", { autoIncrement: true });
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
name === "smart-offline-usage-v2" &&
|
|
444
|
+
!db.objectStoreNames.contains("usage")
|
|
445
|
+
) {
|
|
446
|
+
db.createObjectStore("usage", { keyPath: "url" });
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
request.onsuccess = () => resolve(request.result);
|
|
450
|
+
request.onerror = () => reject(request.error);
|
|
99
451
|
});
|
|
100
452
|
}
|
|
101
453
|
|
|
102
|
-
//
|
|
103
|
-
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// PUBLIC API
|
|
456
|
+
// ============================================================================
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Update configuration at runtime
|
|
460
|
+
* @param {Partial<SmartOfflineConfig>} newConfig
|
|
461
|
+
*/
|
|
462
|
+
export async function updateConfig(newConfig) {
|
|
463
|
+
currentConfig = { ...currentConfig, ...newConfig };
|
|
464
|
+
await sendConfigToServiceWorker();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get current configuration
|
|
469
|
+
* @returns {SmartOfflineConfig}
|
|
470
|
+
*/
|
|
471
|
+
export function getConfig() {
|
|
472
|
+
return { ...currentConfig };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Check if SDK is initialized
|
|
477
|
+
* @returns {boolean}
|
|
478
|
+
*/
|
|
479
|
+
export function isSmartOfflineReady() {
|
|
480
|
+
return isInitialized;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get service worker registration
|
|
485
|
+
* @returns {ServiceWorkerRegistration | null}
|
|
486
|
+
*/
|
|
487
|
+
export function getServiceWorkerRegistration() {
|
|
488
|
+
return serviceWorkerRegistration;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Add event listener for cache events
|
|
493
|
+
* @param {'cache' | 'skip' | 'serve' | 'clear' | 'error'} eventType
|
|
494
|
+
* @param {(event: CacheEvent) => void} callback
|
|
495
|
+
*/
|
|
496
|
+
export function on(eventType, callback) {
|
|
104
497
|
if (eventListeners[eventType]) {
|
|
105
498
|
eventListeners[eventType].push(callback);
|
|
106
499
|
}
|
|
107
500
|
}
|
|
108
501
|
|
|
109
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Remove event listener for cache events
|
|
504
|
+
* @param {'cache' | 'skip' | 'serve' | 'clear' | 'error'} eventType
|
|
505
|
+
* @param {(event: CacheEvent) => void} callback
|
|
506
|
+
*/
|
|
507
|
+
export function off(eventType, callback) {
|
|
110
508
|
if (eventListeners[eventType]) {
|
|
111
509
|
const index = eventListeners[eventType].indexOf(callback);
|
|
112
510
|
if (index > -1) {
|
|
@@ -115,66 +513,528 @@ function off(eventType, callback) {
|
|
|
115
513
|
}
|
|
116
514
|
}
|
|
117
515
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Get cache logs from IndexedDB
|
|
518
|
+
* @returns {Promise<CacheLog[]>}
|
|
519
|
+
*/
|
|
520
|
+
export async function getCacheLogs() {
|
|
521
|
+
try {
|
|
522
|
+
const db = await openDB("smart-offline-logs-v2", 1);
|
|
523
|
+
const tx = db.transaction("logs", "readonly");
|
|
524
|
+
const store = tx.objectStore("logs");
|
|
525
|
+
return new Promise((resolve) => {
|
|
526
|
+
const request = store.getAll();
|
|
527
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
528
|
+
request.onerror = () => resolve([]);
|
|
529
|
+
});
|
|
530
|
+
} catch {
|
|
531
|
+
return [];
|
|
532
|
+
}
|
|
128
533
|
}
|
|
129
534
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
535
|
+
/**
|
|
536
|
+
* Clear cache logs from IndexedDB
|
|
537
|
+
*/
|
|
538
|
+
export async function clearCacheLogs() {
|
|
539
|
+
try {
|
|
540
|
+
const db = await openDB("smart-offline-logs-v2", 1);
|
|
541
|
+
const tx = db.transaction("logs", "readwrite");
|
|
542
|
+
const store = tx.objectStore("logs");
|
|
543
|
+
store.clear();
|
|
544
|
+
} catch {
|
|
545
|
+
// Ignore errors
|
|
546
|
+
}
|
|
136
547
|
}
|
|
137
548
|
|
|
138
|
-
|
|
139
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Clear all cached data
|
|
551
|
+
*/
|
|
552
|
+
export async function clearAllCache() {
|
|
140
553
|
const cacheNames = await caches.keys();
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
554
|
+
for (const name of cacheNames) {
|
|
555
|
+
if (name.startsWith("smart-offline")) {
|
|
556
|
+
await caches.delete(name);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const dbNames = ["smart-offline-usage-v2", "smart-offline-logs-v2"];
|
|
561
|
+
for (const dbName of dbNames) {
|
|
562
|
+
try {
|
|
563
|
+
indexedDB.deleteDatabase(dbName);
|
|
564
|
+
} catch {
|
|
565
|
+
// Ignore errors
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (currentConfig.debug) {
|
|
570
|
+
console.log("[SmartOffline] All cache data cleared");
|
|
571
|
+
}
|
|
151
572
|
}
|
|
152
573
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Get cache statistics
|
|
576
|
+
* @returns {Promise<{cachedItems: number, trackedUrls: number, cacheSize: number}>}
|
|
577
|
+
*/
|
|
578
|
+
export async function getCacheStats() {
|
|
579
|
+
let cachedItems = 0;
|
|
580
|
+
let cacheSize = 0;
|
|
581
|
+
let trackedUrls = 0;
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
const cache = await caches.open("smart-offline-cache-v2");
|
|
585
|
+
const keys = await cache.keys();
|
|
586
|
+
cachedItems = keys.length;
|
|
587
|
+
|
|
588
|
+
for (const request of keys) {
|
|
589
|
+
const response = await cache.match(request);
|
|
590
|
+
if (response) {
|
|
591
|
+
const size = parseInt(
|
|
592
|
+
response.headers.get("content-length") || "0",
|
|
593
|
+
10,
|
|
594
|
+
);
|
|
595
|
+
cacheSize += size;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
// Ignore errors
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const count = await new Promise((resolve) => {
|
|
604
|
+
const request = indexedDB.open("smart-offline-usage-v2", 1);
|
|
605
|
+
request.onsuccess = () => {
|
|
606
|
+
const db = request.result;
|
|
607
|
+
try {
|
|
608
|
+
const tx = db.transaction("usage", "readonly");
|
|
609
|
+
const store = tx.objectStore("usage");
|
|
610
|
+
const countReq = store.count();
|
|
611
|
+
countReq.onsuccess = () => resolve(countReq.result);
|
|
612
|
+
countReq.onerror = () => resolve(0);
|
|
613
|
+
} catch {
|
|
614
|
+
resolve(0);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
request.onerror = () => resolve(0);
|
|
618
|
+
});
|
|
619
|
+
trackedUrls = count;
|
|
620
|
+
} catch {
|
|
621
|
+
// Ignore errors
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return { cachedItems, trackedUrls, cacheSize };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Force update the service worker
|
|
629
|
+
*/
|
|
630
|
+
export async function forceUpdate() {
|
|
631
|
+
if (serviceWorkerRegistration) {
|
|
632
|
+
await serviceWorkerRegistration.update();
|
|
633
|
+
if (currentConfig.debug) {
|
|
634
|
+
console.log("[SmartOffline] Service worker update triggered");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Unregister the service worker and clean up
|
|
641
|
+
*/
|
|
642
|
+
export async function uninstall() {
|
|
643
|
+
if (serviceWorkerRegistration) {
|
|
644
|
+
await serviceWorkerRegistration.unregister();
|
|
645
|
+
}
|
|
646
|
+
await clearAllCache();
|
|
647
|
+
isInitialized = false;
|
|
648
|
+
serviceWorkerRegistration = null;
|
|
649
|
+
|
|
650
|
+
if (currentConfig.debug) {
|
|
651
|
+
console.log("[SmartOffline] Uninstalled and cleaned up");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Initialize the SDK (legacy API)
|
|
657
|
+
* @deprecated Use setupSmartOffline() instead
|
|
658
|
+
* @param {Partial<SmartOfflineConfig>} config
|
|
659
|
+
*/
|
|
660
|
+
export function init(config = {}) {
|
|
661
|
+
setupSmartOffline(config);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ============================================================================
|
|
665
|
+
// TEST UTILITIES
|
|
666
|
+
// ============================================================================
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Matches URL against wildcard pattern
|
|
670
|
+
* @param {string} url
|
|
671
|
+
* @param {string} pattern
|
|
672
|
+
* @returns {boolean}
|
|
673
|
+
*/
|
|
674
|
+
export function matchesPattern(url, pattern) {
|
|
675
|
+
if (!pattern.includes("*")) {
|
|
676
|
+
return url.includes(pattern);
|
|
677
|
+
}
|
|
678
|
+
const regexPattern = pattern
|
|
679
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
680
|
+
.replace(/\*/g, ".*");
|
|
681
|
+
return new RegExp(regexPattern).test(url);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Calculates if a URL should be treated as HIGH priority
|
|
686
|
+
* @param {{ url: string; count: number; lastAccessed: number } | null} usage
|
|
687
|
+
* @param {string} url
|
|
688
|
+
* @param {Partial<SmartOfflineConfig>} config
|
|
689
|
+
* @returns {boolean}
|
|
690
|
+
*/
|
|
691
|
+
export function isHighPriority(usage, url, config = {}) {
|
|
692
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
693
|
+
|
|
694
|
+
if (
|
|
695
|
+
finalConfig.customPriorityFn &&
|
|
696
|
+
typeof finalConfig.customPriorityFn === "function"
|
|
697
|
+
) {
|
|
698
|
+
try {
|
|
699
|
+
return finalConfig.customPriorityFn(usage, url, finalConfig) > 50;
|
|
700
|
+
} catch (e) {
|
|
701
|
+
console.error("Custom priority function error:", e);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
for (const pattern in finalConfig.significance) {
|
|
706
|
+
if (url.includes(pattern)) {
|
|
707
|
+
if (finalConfig.significance[pattern] === "high") return true;
|
|
708
|
+
if (finalConfig.significance[pattern] === "low") return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (!usage) return false;
|
|
713
|
+
|
|
714
|
+
const weights = finalConfig.weights || { frequency: 1, recency: 1, size: 1 };
|
|
715
|
+
const frequencyScore = Math.min(
|
|
716
|
+
100,
|
|
717
|
+
(usage.count / finalConfig.frequencyThreshold) * 100,
|
|
718
|
+
);
|
|
719
|
+
const timeSinceAccess = Date.now() - usage.lastAccessed;
|
|
720
|
+
const recencyScore = Math.max(
|
|
721
|
+
0,
|
|
722
|
+
100 - (timeSinceAccess / finalConfig.recencyThreshold) * 100,
|
|
723
|
+
);
|
|
724
|
+
const totalWeight = weights.frequency + weights.recency;
|
|
725
|
+
const weightedScore =
|
|
726
|
+
(frequencyScore * weights.frequency + recencyScore * weights.recency) /
|
|
727
|
+
totalWeight;
|
|
728
|
+
|
|
729
|
+
return weightedScore > 50;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Determines if a URL should be cached based on pattern matching
|
|
734
|
+
* @param {string} url
|
|
735
|
+
* @param {Partial<SmartOfflineConfig>} config
|
|
736
|
+
* @returns {{ isPage: boolean; isAPI: boolean }}
|
|
737
|
+
*/
|
|
738
|
+
export function shouldCacheUrl(url, config = {}) {
|
|
739
|
+
const pages = config.pages || [];
|
|
740
|
+
const apis = config.apis || [];
|
|
741
|
+
const isPage = pages.some((p) => matchesPattern(url, p));
|
|
742
|
+
const isAPI = apis.some((a) => matchesPattern(url, a));
|
|
743
|
+
return { isPage, isAPI };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Test suite class for SmartOffline SDK
|
|
748
|
+
*/
|
|
749
|
+
export class SmartOfflineTestSuite {
|
|
750
|
+
constructor(config = {}) {
|
|
751
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
752
|
+
this.results = [];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async runAll() {
|
|
756
|
+
this.results = [];
|
|
757
|
+
this.testPatternMatching();
|
|
758
|
+
this.testFrequencyPriority();
|
|
759
|
+
this.testRecencyPriority();
|
|
760
|
+
|
|
761
|
+
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
|
|
762
|
+
await this.testServiceWorkerActive();
|
|
763
|
+
await this.testCacheAPIAvailable();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return this.results;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
testPatternMatching() {
|
|
770
|
+
const tests = [
|
|
771
|
+
{ url: "/admin/charts", pattern: "/admin/charts", expected: true },
|
|
772
|
+
{ url: "/admin/charts/123", pattern: "/admin/charts/*", expected: true },
|
|
773
|
+
{ url: "/grapher/gdp", pattern: "/grapher/*", expected: true },
|
|
774
|
+
{ url: "/random", pattern: "/admin/*", expected: false },
|
|
775
|
+
];
|
|
776
|
+
|
|
777
|
+
let passed = true;
|
|
778
|
+
let message = "All patterns matched correctly";
|
|
779
|
+
|
|
780
|
+
for (const test of tests) {
|
|
781
|
+
const result = matchesPattern(test.url, test.pattern);
|
|
782
|
+
if (result !== test.expected) {
|
|
783
|
+
passed = false;
|
|
784
|
+
message = `Pattern ${test.pattern} failed for ${test.url}`;
|
|
785
|
+
break;
|
|
160
786
|
}
|
|
161
|
-
|
|
162
|
-
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
this.results.push({ name: "Pattern Matching", passed, message });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
testFrequencyPriority() {
|
|
793
|
+
const highUsage = { url: "/test", count: 5, lastAccessed: Date.now() };
|
|
794
|
+
const lowUsage = {
|
|
795
|
+
url: "/test",
|
|
796
|
+
count: 1,
|
|
797
|
+
lastAccessed: Date.now() - 25 * 60 * 60 * 1000,
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
const highResult = isHighPriority(highUsage, "/test", this.config);
|
|
801
|
+
const lowResult = isHighPriority(lowUsage, "/test", this.config);
|
|
802
|
+
|
|
803
|
+
this.results.push({
|
|
804
|
+
name: "Frequency Priority",
|
|
805
|
+
passed: highResult === true && lowResult === false,
|
|
806
|
+
message:
|
|
807
|
+
highResult === true && lowResult === false
|
|
808
|
+
? "High frequency URLs correctly prioritized"
|
|
809
|
+
: `Expected high:true, low:false but got high:${highResult}, low:${lowResult}`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
testRecencyPriority() {
|
|
814
|
+
const recentUsage = {
|
|
815
|
+
url: "/test",
|
|
816
|
+
count: 1,
|
|
817
|
+
lastAccessed: Date.now() - 1000,
|
|
818
|
+
};
|
|
819
|
+
const oldUsage = {
|
|
820
|
+
url: "/test",
|
|
821
|
+
count: 1,
|
|
822
|
+
lastAccessed: Date.now() - 48 * 60 * 60 * 1000,
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const recentResult = isHighPriority(recentUsage, "/test", this.config);
|
|
826
|
+
const oldResult = isHighPriority(oldUsage, "/test", this.config);
|
|
827
|
+
|
|
828
|
+
this.results.push({
|
|
829
|
+
name: "Recency Priority",
|
|
830
|
+
passed: recentResult === true && oldResult === false,
|
|
831
|
+
message:
|
|
832
|
+
recentResult === true && oldResult === false
|
|
833
|
+
? "Recent URLs correctly prioritized"
|
|
834
|
+
: `Expected recent:true, old:false but got recent:${recentResult}, old:${oldResult}`,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async testServiceWorkerActive() {
|
|
839
|
+
try {
|
|
840
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
841
|
+
const passed = !!registration?.active;
|
|
842
|
+
|
|
843
|
+
this.results.push({
|
|
844
|
+
name: "Service Worker Active",
|
|
845
|
+
passed,
|
|
846
|
+
message: passed
|
|
847
|
+
? `Service worker active at scope: ${registration?.scope}`
|
|
848
|
+
: "No active service worker found",
|
|
849
|
+
});
|
|
850
|
+
} catch (e) {
|
|
851
|
+
this.results.push({
|
|
852
|
+
name: "Service Worker Active",
|
|
853
|
+
passed: false,
|
|
854
|
+
message: `Error: ${e}`,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async testCacheAPIAvailable() {
|
|
860
|
+
try {
|
|
861
|
+
const cache = await caches.open("smart-offline-test");
|
|
862
|
+
await caches.delete("smart-offline-test");
|
|
863
|
+
this.results.push({
|
|
864
|
+
name: "Cache API Available",
|
|
865
|
+
passed: true,
|
|
866
|
+
message: "Cache API is accessible",
|
|
867
|
+
});
|
|
868
|
+
} catch (e) {
|
|
869
|
+
this.results.push({
|
|
870
|
+
name: "Cache API Available",
|
|
871
|
+
passed: false,
|
|
872
|
+
message: `Error: ${e}`,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
printResults() {
|
|
878
|
+
console.info("\n========================================");
|
|
879
|
+
console.info(" SmartOffline SDK Test Results");
|
|
880
|
+
console.info("========================================\n");
|
|
881
|
+
|
|
882
|
+
for (const result of this.results) {
|
|
883
|
+
console.info(`${result.passed ? "✅" : "❌"} ${result.name}`);
|
|
884
|
+
console.info(` ${result.message}\n`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const passed = this.results.filter((r) => r.passed).length;
|
|
888
|
+
console.info("----------------------------------------");
|
|
889
|
+
console.info(`Total: ${passed}/${this.results.length} tests passed`);
|
|
890
|
+
console.info("========================================\n");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Cache inspector for debugging
|
|
896
|
+
*/
|
|
897
|
+
export class CacheInspector {
|
|
898
|
+
async getCachedItems() {
|
|
899
|
+
try {
|
|
900
|
+
const cache = await caches.open("smart-offline-cache-v2");
|
|
901
|
+
const keys = await cache.keys();
|
|
902
|
+
const items = [];
|
|
903
|
+
for (const request of keys) {
|
|
904
|
+
const response = await cache.match(request);
|
|
905
|
+
if (response) {
|
|
906
|
+
items.push({
|
|
907
|
+
url: request.url,
|
|
908
|
+
size: parseInt(response.headers.get("content-length") || "0", 10),
|
|
909
|
+
contentType: response.headers.get("content-type") || "unknown",
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return items;
|
|
914
|
+
} catch {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async getUsageData() {
|
|
920
|
+
return new Promise((resolve) => {
|
|
921
|
+
try {
|
|
922
|
+
const request = indexedDB.open("smart-offline-usage-v2", 1);
|
|
923
|
+
request.onsuccess = () => {
|
|
924
|
+
const db = request.result;
|
|
925
|
+
try {
|
|
926
|
+
const tx = db.transaction("usage", "readonly");
|
|
927
|
+
const store = tx.objectStore("usage");
|
|
928
|
+
const getAllReq = store.getAll();
|
|
929
|
+
getAllReq.onsuccess = () => resolve(getAllReq.result || []);
|
|
930
|
+
getAllReq.onerror = () => resolve([]);
|
|
931
|
+
} catch {
|
|
932
|
+
resolve([]);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
request.onerror = () => resolve([]);
|
|
936
|
+
} catch {
|
|
937
|
+
resolve([]);
|
|
163
938
|
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async getLogs() {
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
try {
|
|
945
|
+
const request = indexedDB.open("smart-offline-logs-v2", 1);
|
|
946
|
+
request.onsuccess = () => {
|
|
947
|
+
const db = request.result;
|
|
948
|
+
try {
|
|
949
|
+
const tx = db.transaction("logs", "readonly");
|
|
950
|
+
const store = tx.objectStore("logs");
|
|
951
|
+
const getAllReq = store.getAll();
|
|
952
|
+
getAllReq.onsuccess = () => resolve(getAllReq.result || []);
|
|
953
|
+
getAllReq.onerror = () => resolve([]);
|
|
954
|
+
} catch {
|
|
955
|
+
resolve([]);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
request.onerror = () => resolve([]);
|
|
959
|
+
} catch {
|
|
960
|
+
resolve([]);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async showAll() {
|
|
966
|
+
console.info("\n🔍 SmartOffline Cache Inspector\n");
|
|
967
|
+
|
|
968
|
+
const cachedItems = await this.getCachedItems();
|
|
969
|
+
console.info(`📦 Cached Items (${cachedItems.length}):`);
|
|
970
|
+
console.table(cachedItems);
|
|
971
|
+
|
|
972
|
+
const usageData = await this.getUsageData();
|
|
973
|
+
console.info(`\n📊 Usage Tracking (${usageData.length} URLs):`);
|
|
974
|
+
console.table(
|
|
975
|
+
usageData.map((u) => ({
|
|
976
|
+
url: u.url.replace(window.location.origin, ""),
|
|
977
|
+
count: u.count,
|
|
978
|
+
lastAccessed: new Date(u.lastAccessed).toLocaleString(),
|
|
979
|
+
})),
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
const logs = await this.getLogs();
|
|
983
|
+
console.info(`\n📝 Recent Logs (${logs.length}):`);
|
|
984
|
+
console.table(logs.slice(-20));
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
async getAllData() {
|
|
988
|
+
return {
|
|
989
|
+
cachedItems: await this.getCachedItems(),
|
|
990
|
+
usageData: await this.getUsageData(),
|
|
991
|
+
logs: await this.getLogs(),
|
|
164
992
|
};
|
|
165
|
-
|
|
166
|
-
request.onerror = () => reject(request.error);
|
|
167
|
-
});
|
|
993
|
+
}
|
|
168
994
|
}
|
|
169
995
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
996
|
+
/**
|
|
997
|
+
* Run all SmartOffline tests and print results
|
|
998
|
+
* @param {Partial<SmartOfflineConfig>} config
|
|
999
|
+
* @returns {Promise<TestResult[]>}
|
|
1000
|
+
*/
|
|
1001
|
+
export async function runSmartOfflineTests(config) {
|
|
1002
|
+
const suite = new SmartOfflineTestSuite(config);
|
|
1003
|
+
const results = await suite.runAll();
|
|
1004
|
+
suite.printResults();
|
|
1005
|
+
return results;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ============================================================================
|
|
1009
|
+
// MAIN EXPORT OBJECT
|
|
1010
|
+
// ============================================================================
|
|
1011
|
+
|
|
1012
|
+
export const SmartOffline = {
|
|
1013
|
+
// Main setup
|
|
1014
|
+
setup: setupSmartOffline,
|
|
1015
|
+
init, // Legacy API
|
|
1016
|
+
|
|
1017
|
+
// Configuration
|
|
1018
|
+
updateConfig,
|
|
1019
|
+
getConfig,
|
|
1020
|
+
|
|
1021
|
+
// Status
|
|
1022
|
+
isReady: isSmartOfflineReady,
|
|
1023
|
+
getRegistration: getServiceWorkerRegistration,
|
|
1024
|
+
|
|
1025
|
+
// Event handling
|
|
1026
|
+
on,
|
|
1027
|
+
off,
|
|
1028
|
+
|
|
1029
|
+
// Cache management
|
|
1030
|
+
clearCache: clearAllCache,
|
|
1031
|
+
getStats: getCacheStats,
|
|
1032
|
+
getCacheLogs,
|
|
175
1033
|
clearCacheLogs,
|
|
176
|
-
|
|
1034
|
+
|
|
1035
|
+
// Lifecycle
|
|
1036
|
+
forceUpdate,
|
|
1037
|
+
uninstall,
|
|
177
1038
|
};
|
|
178
1039
|
|
|
179
|
-
export
|
|
180
|
-
export default SmartOffline; // ✅ default export
|
|
1040
|
+
export default SmartOffline;
|