@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
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartOffline SDK - Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive testing utilities for the SmartOffline SDK.
|
|
5
|
+
* These utilities can be used both in browser console and in test frameworks.
|
|
6
|
+
*
|
|
7
|
+
* Usage in browser console:
|
|
8
|
+
* ```javascript
|
|
9
|
+
* // Run all tests
|
|
10
|
+
* await runSmartOfflineTests()
|
|
11
|
+
*
|
|
12
|
+
* // Use cache inspector
|
|
13
|
+
* const inspector = new CacheInspector()
|
|
14
|
+
* await inspector.showAll()
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
SmartOfflineConfig,
|
|
20
|
+
CacheEvent,
|
|
21
|
+
UsageData,
|
|
22
|
+
} from "./SmartOfflineSetup";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// TYPES
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface TestResult {
|
|
29
|
+
name: string;
|
|
30
|
+
passed: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
duration?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CacheInspectorData {
|
|
36
|
+
cachedItems: Array<{ url: string; size: number; contentType: string }>;
|
|
37
|
+
usageData: UsageData[];
|
|
38
|
+
logs: Array<{ type: string; url: string; timestamp: number }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// ALGORITHM FUNCTIONS (for testing)
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Matches URL against wildcard pattern
|
|
47
|
+
* This is the same algorithm used in smart-offline-sw.js
|
|
48
|
+
*/
|
|
49
|
+
export function matchesPattern(url: string, pattern: string): boolean {
|
|
50
|
+
if (!pattern.includes("*")) {
|
|
51
|
+
return url.includes(pattern);
|
|
52
|
+
}
|
|
53
|
+
const regexPattern = pattern
|
|
54
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
55
|
+
.replace(/\*/g, ".*");
|
|
56
|
+
return new RegExp(regexPattern).test(url);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculates if a URL should be treated as HIGH priority
|
|
61
|
+
* This is the core caching algorithm
|
|
62
|
+
*/
|
|
63
|
+
export function isHighPriority(
|
|
64
|
+
usage: UsageData | null,
|
|
65
|
+
url: string,
|
|
66
|
+
config: Partial<SmartOfflineConfig> = {},
|
|
67
|
+
): boolean {
|
|
68
|
+
const defaultConfig: SmartOfflineConfig = {
|
|
69
|
+
pages: [],
|
|
70
|
+
apis: [],
|
|
71
|
+
debug: false,
|
|
72
|
+
frequencyThreshold: 3,
|
|
73
|
+
recencyThreshold: 24 * 60 * 60 * 1000,
|
|
74
|
+
maxResourceSize: Infinity,
|
|
75
|
+
networkQuality: "auto",
|
|
76
|
+
significance: {},
|
|
77
|
+
weights: { frequency: 1, recency: 1, size: 1 },
|
|
78
|
+
customPriorityFn: null,
|
|
79
|
+
enableDetailedLogs: false,
|
|
80
|
+
serviceWorkerPath: "/smart-offline-sw.js",
|
|
81
|
+
serviceWorkerScope: "/",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const finalConfig = { ...defaultConfig, ...config };
|
|
85
|
+
|
|
86
|
+
// Custom priority function takes precedence
|
|
87
|
+
if (
|
|
88
|
+
finalConfig.customPriorityFn &&
|
|
89
|
+
typeof finalConfig.customPriorityFn === "function"
|
|
90
|
+
) {
|
|
91
|
+
try {
|
|
92
|
+
return finalConfig.customPriorityFn(usage, url, finalConfig) > 50;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error("Custom priority function error:", e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check significance overrides
|
|
99
|
+
for (const pattern in finalConfig.significance) {
|
|
100
|
+
if (url.includes(pattern)) {
|
|
101
|
+
if (finalConfig.significance[pattern] === "high") return true;
|
|
102
|
+
if (finalConfig.significance[pattern] === "low") return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// No usage data = low priority
|
|
107
|
+
if (!usage) return false;
|
|
108
|
+
|
|
109
|
+
// Calculate weighted priority score
|
|
110
|
+
const weights = finalConfig.weights || { frequency: 1, recency: 1, size: 1 };
|
|
111
|
+
|
|
112
|
+
// Frequency score: 0-100 based on access count vs threshold
|
|
113
|
+
const frequencyScore = Math.min(
|
|
114
|
+
100,
|
|
115
|
+
(usage.count / finalConfig.frequencyThreshold) * 100,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Recency score: 100 for just accessed, 0 for older than threshold
|
|
119
|
+
const timeSinceAccess = Date.now() - usage.lastAccessed;
|
|
120
|
+
const recencyScore = Math.max(
|
|
121
|
+
0,
|
|
122
|
+
100 - (timeSinceAccess / finalConfig.recencyThreshold) * 100,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Weighted average
|
|
126
|
+
const totalWeight = weights.frequency + weights.recency;
|
|
127
|
+
const weightedScore =
|
|
128
|
+
(frequencyScore * weights.frequency + recencyScore * weights.recency) /
|
|
129
|
+
totalWeight;
|
|
130
|
+
|
|
131
|
+
return weightedScore > 50;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Determines if a URL should be cached based on pattern matching
|
|
136
|
+
*/
|
|
137
|
+
export function shouldCacheUrl(
|
|
138
|
+
url: string,
|
|
139
|
+
config: Partial<SmartOfflineConfig> = {},
|
|
140
|
+
): { isPage: boolean; isAPI: boolean } {
|
|
141
|
+
const pages = config.pages || [];
|
|
142
|
+
const apis = config.apis || [];
|
|
143
|
+
const isPage = pages.some((p) => matchesPattern(url, p));
|
|
144
|
+
const isAPI = apis.some((a) => matchesPattern(url, a));
|
|
145
|
+
return { isPage, isAPI };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// TEST SUITE CLASS
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
export class SmartOfflineTestSuite {
|
|
153
|
+
private results: TestResult[] = [];
|
|
154
|
+
private config: Partial<SmartOfflineConfig>;
|
|
155
|
+
|
|
156
|
+
constructor(config: Partial<SmartOfflineConfig> = {}) {
|
|
157
|
+
this.config = {
|
|
158
|
+
pages: ["/admin/*", "/admin/charts", "/admin/charts/*", "/grapher/*"],
|
|
159
|
+
apis: ["/admin/api/*"],
|
|
160
|
+
frequencyThreshold: 3,
|
|
161
|
+
recencyThreshold: 24 * 60 * 60 * 1000,
|
|
162
|
+
...config,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run all tests
|
|
168
|
+
*/
|
|
169
|
+
async runAll(): Promise<TestResult[]> {
|
|
170
|
+
this.results = [];
|
|
171
|
+
|
|
172
|
+
// Algorithm tests
|
|
173
|
+
this.testPatternMatching();
|
|
174
|
+
this.testFrequencyPriority();
|
|
175
|
+
this.testRecencyPriority();
|
|
176
|
+
this.testWeightedScoring();
|
|
177
|
+
this.testSignificanceOverrides();
|
|
178
|
+
|
|
179
|
+
// Browser environment tests (if available)
|
|
180
|
+
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
|
|
181
|
+
await this.testServiceWorkerActive();
|
|
182
|
+
await this.testCacheAPIAvailable();
|
|
183
|
+
await this.testIndexedDBAvailable();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this.results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Test pattern matching
|
|
191
|
+
*/
|
|
192
|
+
private testPatternMatching(): void {
|
|
193
|
+
const tests = [
|
|
194
|
+
{ url: "/admin/charts", pattern: "/admin/charts", expected: true },
|
|
195
|
+
{ url: "/admin/charts/123", pattern: "/admin/charts/*", expected: true },
|
|
196
|
+
{ url: "/grapher/gdp", pattern: "/grapher/*", expected: true },
|
|
197
|
+
{ url: "/random", pattern: "/admin/*", expected: false },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
let passed = true;
|
|
201
|
+
let message = "All patterns matched correctly";
|
|
202
|
+
|
|
203
|
+
for (const test of tests) {
|
|
204
|
+
const result = matchesPattern(test.url, test.pattern);
|
|
205
|
+
if (result !== test.expected) {
|
|
206
|
+
passed = false;
|
|
207
|
+
message = `Pattern ${test.pattern} failed for ${test.url}`;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.results.push({
|
|
213
|
+
name: "Pattern Matching",
|
|
214
|
+
passed,
|
|
215
|
+
message,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Test frequency-based priority
|
|
221
|
+
*/
|
|
222
|
+
private testFrequencyPriority(): void {
|
|
223
|
+
const highUsage: UsageData = {
|
|
224
|
+
url: "/test",
|
|
225
|
+
count: 5,
|
|
226
|
+
lastAccessed: Date.now(),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const lowUsage: UsageData = {
|
|
230
|
+
url: "/test",
|
|
231
|
+
count: 1,
|
|
232
|
+
lastAccessed: Date.now() - 25 * 60 * 60 * 1000,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const highResult = isHighPriority(highUsage, "/test", this.config);
|
|
236
|
+
const lowResult = isHighPriority(lowUsage, "/test", this.config);
|
|
237
|
+
|
|
238
|
+
this.results.push({
|
|
239
|
+
name: "Frequency Priority",
|
|
240
|
+
passed: highResult === true && lowResult === false,
|
|
241
|
+
message:
|
|
242
|
+
highResult === true && lowResult === false
|
|
243
|
+
? "High frequency URLs correctly prioritized"
|
|
244
|
+
: `Expected high:true, low:false but got high:${highResult}, low:${lowResult}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Test recency-based priority
|
|
250
|
+
*/
|
|
251
|
+
private testRecencyPriority(): void {
|
|
252
|
+
const recentUsage: UsageData = {
|
|
253
|
+
url: "/test",
|
|
254
|
+
count: 1,
|
|
255
|
+
lastAccessed: Date.now() - 1000,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const oldUsage: UsageData = {
|
|
259
|
+
url: "/test",
|
|
260
|
+
count: 1,
|
|
261
|
+
lastAccessed: Date.now() - 48 * 60 * 60 * 1000,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const recentResult = isHighPriority(recentUsage, "/test", this.config);
|
|
265
|
+
const oldResult = isHighPriority(oldUsage, "/test", this.config);
|
|
266
|
+
|
|
267
|
+
this.results.push({
|
|
268
|
+
name: "Recency Priority",
|
|
269
|
+
passed: recentResult === true && oldResult === false,
|
|
270
|
+
message:
|
|
271
|
+
recentResult === true && oldResult === false
|
|
272
|
+
? "Recent URLs correctly prioritized"
|
|
273
|
+
: `Expected recent:true, old:false but got recent:${recentResult}, old:${oldResult}`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Test weighted scoring
|
|
279
|
+
*/
|
|
280
|
+
private testWeightedScoring(): void {
|
|
281
|
+
const configWithWeights: Partial<SmartOfflineConfig> = {
|
|
282
|
+
...this.config,
|
|
283
|
+
frequencyThreshold: 10,
|
|
284
|
+
weights: { frequency: 10, recency: 1, size: 1 },
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// High frequency should win with high frequency weight
|
|
288
|
+
const highFreq: UsageData = {
|
|
289
|
+
url: "/test",
|
|
290
|
+
count: 5,
|
|
291
|
+
lastAccessed: Date.now() - 20 * 60 * 60 * 1000,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const result = isHighPriority(highFreq, "/test", configWithWeights);
|
|
295
|
+
|
|
296
|
+
this.results.push({
|
|
297
|
+
name: "Weighted Scoring",
|
|
298
|
+
passed: result === true,
|
|
299
|
+
message:
|
|
300
|
+
result === true
|
|
301
|
+
? "Weighted scoring works correctly"
|
|
302
|
+
: "Weighted scoring calculation may have issues",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Test significance overrides
|
|
308
|
+
*/
|
|
309
|
+
private testSignificanceOverrides(): void {
|
|
310
|
+
const configWithSig: Partial<SmartOfflineConfig> = {
|
|
311
|
+
...this.config,
|
|
312
|
+
significance: { "/important/": "high", "/logs/": "low" },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const importantResult = isHighPriority(
|
|
316
|
+
null,
|
|
317
|
+
"/important/page",
|
|
318
|
+
configWithSig,
|
|
319
|
+
);
|
|
320
|
+
const logsResult = isHighPriority(
|
|
321
|
+
{ url: "/logs/access", count: 100, lastAccessed: Date.now() },
|
|
322
|
+
"/logs/access",
|
|
323
|
+
configWithSig,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
this.results.push({
|
|
327
|
+
name: "Significance Overrides",
|
|
328
|
+
passed: importantResult === true && logsResult === false,
|
|
329
|
+
message:
|
|
330
|
+
importantResult === true && logsResult === false
|
|
331
|
+
? "Significance overrides work correctly"
|
|
332
|
+
: `Expected important:true, logs:false but got important:${importantResult}, logs:${logsResult}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Test service worker is active
|
|
338
|
+
*/
|
|
339
|
+
private async testServiceWorkerActive(): Promise<void> {
|
|
340
|
+
try {
|
|
341
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
342
|
+
const passed = !!registration?.active;
|
|
343
|
+
|
|
344
|
+
this.results.push({
|
|
345
|
+
name: "Service Worker Active",
|
|
346
|
+
passed,
|
|
347
|
+
message: passed
|
|
348
|
+
? `Service worker active at scope: ${registration?.scope}`
|
|
349
|
+
: "No active service worker found",
|
|
350
|
+
});
|
|
351
|
+
} catch (e) {
|
|
352
|
+
this.results.push({
|
|
353
|
+
name: "Service Worker Active",
|
|
354
|
+
passed: false,
|
|
355
|
+
message: `Error: ${e}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Test Cache API is available
|
|
362
|
+
*/
|
|
363
|
+
private async testCacheAPIAvailable(): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
const cache = await caches.open("smart-offline-test");
|
|
366
|
+
const keys = await cache.keys();
|
|
367
|
+
await caches.delete("smart-offline-test");
|
|
368
|
+
|
|
369
|
+
this.results.push({
|
|
370
|
+
name: "Cache API Available",
|
|
371
|
+
passed: true,
|
|
372
|
+
message: "Cache API is accessible",
|
|
373
|
+
});
|
|
374
|
+
} catch (e) {
|
|
375
|
+
this.results.push({
|
|
376
|
+
name: "Cache API Available",
|
|
377
|
+
passed: false,
|
|
378
|
+
message: `Error: ${e}`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Test IndexedDB is available
|
|
385
|
+
*/
|
|
386
|
+
private async testIndexedDBAvailable(): Promise<void> {
|
|
387
|
+
try {
|
|
388
|
+
const passed = await new Promise<boolean>((resolve) => {
|
|
389
|
+
const request = indexedDB.open("smart-offline-test-db", 1);
|
|
390
|
+
request.onsuccess = () => {
|
|
391
|
+
request.result.close();
|
|
392
|
+
indexedDB.deleteDatabase("smart-offline-test-db");
|
|
393
|
+
resolve(true);
|
|
394
|
+
};
|
|
395
|
+
request.onerror = () => resolve(false);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
this.results.push({
|
|
399
|
+
name: "IndexedDB Available",
|
|
400
|
+
passed,
|
|
401
|
+
message: passed ? "IndexedDB is accessible" : "IndexedDB not available",
|
|
402
|
+
});
|
|
403
|
+
} catch (e) {
|
|
404
|
+
this.results.push({
|
|
405
|
+
name: "IndexedDB Available",
|
|
406
|
+
passed: false,
|
|
407
|
+
message: `Error: ${e}`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Print results to console
|
|
414
|
+
*/
|
|
415
|
+
printResults(): void {
|
|
416
|
+
console.info("\n========================================");
|
|
417
|
+
console.info(" SmartOffline SDK Test Results");
|
|
418
|
+
console.info("========================================\n");
|
|
419
|
+
|
|
420
|
+
for (const result of this.results) {
|
|
421
|
+
console.info(`${result.passed ? "ā
" : "ā"} ${result.name}`);
|
|
422
|
+
console.info(` ${result.message}\n`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const passed = this.results.filter((r) => r.passed).length;
|
|
426
|
+
console.info("----------------------------------------");
|
|
427
|
+
console.info(`Total: ${passed}/${this.results.length} tests passed`);
|
|
428
|
+
console.info("========================================\n");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ============================================================================
|
|
433
|
+
// CACHE INSPECTOR
|
|
434
|
+
// ============================================================================
|
|
435
|
+
|
|
436
|
+
export class CacheInspector {
|
|
437
|
+
/**
|
|
438
|
+
* Get all cached items
|
|
439
|
+
*/
|
|
440
|
+
async getCachedItems(): Promise<
|
|
441
|
+
Array<{ url: string; size: number; contentType: string }>
|
|
442
|
+
> {
|
|
443
|
+
try {
|
|
444
|
+
const cache = await caches.open("smart-offline-cache-v2");
|
|
445
|
+
const keys = await cache.keys();
|
|
446
|
+
|
|
447
|
+
const items = [];
|
|
448
|
+
for (const request of keys) {
|
|
449
|
+
const response = await cache.match(request);
|
|
450
|
+
if (response) {
|
|
451
|
+
items.push({
|
|
452
|
+
url: request.url,
|
|
453
|
+
size: parseInt(response.headers.get("content-length") || "0", 10),
|
|
454
|
+
contentType: response.headers.get("content-type") || "unknown",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return items;
|
|
459
|
+
} catch {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get usage tracking data
|
|
466
|
+
*/
|
|
467
|
+
async getUsageData(): Promise<UsageData[]> {
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
try {
|
|
470
|
+
const request = indexedDB.open("smart-offline-usage-v2", 1);
|
|
471
|
+
request.onsuccess = () => {
|
|
472
|
+
const db = request.result;
|
|
473
|
+
try {
|
|
474
|
+
const tx = db.transaction("usage", "readonly");
|
|
475
|
+
const store = tx.objectStore("usage");
|
|
476
|
+
const getAllReq = store.getAll();
|
|
477
|
+
getAllReq.onsuccess = () => resolve(getAllReq.result || []);
|
|
478
|
+
getAllReq.onerror = () => resolve([]);
|
|
479
|
+
} catch {
|
|
480
|
+
resolve([]);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
request.onerror = () => resolve([]);
|
|
484
|
+
} catch {
|
|
485
|
+
resolve([]);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get cache logs
|
|
492
|
+
*/
|
|
493
|
+
async getLogs(): Promise<
|
|
494
|
+
Array<{ type: string; url: string; timestamp: number }>
|
|
495
|
+
> {
|
|
496
|
+
return new Promise((resolve) => {
|
|
497
|
+
try {
|
|
498
|
+
const request = indexedDB.open("smart-offline-logs-v2", 1);
|
|
499
|
+
request.onsuccess = () => {
|
|
500
|
+
const db = request.result;
|
|
501
|
+
try {
|
|
502
|
+
const tx = db.transaction("logs", "readonly");
|
|
503
|
+
const store = tx.objectStore("logs");
|
|
504
|
+
const getAllReq = store.getAll();
|
|
505
|
+
getAllReq.onsuccess = () => resolve(getAllReq.result || []);
|
|
506
|
+
getAllReq.onerror = () => resolve([]);
|
|
507
|
+
} catch {
|
|
508
|
+
resolve([]);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
request.onerror = () => resolve([]);
|
|
512
|
+
} catch {
|
|
513
|
+
resolve([]);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Show all cache data in console
|
|
520
|
+
*/
|
|
521
|
+
async showAll(): Promise<void> {
|
|
522
|
+
console.info("\nš SmartOffline Cache Inspector\n");
|
|
523
|
+
|
|
524
|
+
const cachedItems = await this.getCachedItems();
|
|
525
|
+
console.info(`š¦ Cached Items (${cachedItems.length}):`);
|
|
526
|
+
console.table(cachedItems);
|
|
527
|
+
|
|
528
|
+
const usageData = await this.getUsageData();
|
|
529
|
+
console.info(`\nš Usage Tracking (${usageData.length} URLs):`);
|
|
530
|
+
console.table(
|
|
531
|
+
usageData.map((u) => ({
|
|
532
|
+
url: u.url.replace(window.location.origin, ""),
|
|
533
|
+
count: u.count,
|
|
534
|
+
lastAccessed: new Date(u.lastAccessed).toLocaleString(),
|
|
535
|
+
})),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const logs = await this.getLogs();
|
|
539
|
+
console.info(`\nš Recent Logs (${logs.length}):`);
|
|
540
|
+
console.table(logs.slice(-20));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Get all data as object
|
|
545
|
+
*/
|
|
546
|
+
async getAllData(): Promise<CacheInspectorData> {
|
|
547
|
+
return {
|
|
548
|
+
cachedItems: await this.getCachedItems(),
|
|
549
|
+
usageData: await this.getUsageData(),
|
|
550
|
+
logs: await this.getLogs(),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// CONVENIENCE FUNCTION
|
|
557
|
+
// ============================================================================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Run all SmartOffline tests and print results
|
|
561
|
+
*/
|
|
562
|
+
export async function runSmartOfflineTests(
|
|
563
|
+
config?: Partial<SmartOfflineConfig>,
|
|
564
|
+
): Promise<TestResult[]> {
|
|
565
|
+
const suite = new SmartOfflineTestSuite(config);
|
|
566
|
+
const results = await suite.runAll();
|
|
567
|
+
suite.printResults();
|
|
568
|
+
return results;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Export for browser usage
|
|
572
|
+
if (typeof window !== "undefined") {
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
574
|
+
const win = window as any;
|
|
575
|
+
win.SmartOfflineTestSuite = SmartOfflineTestSuite;
|
|
576
|
+
win.runSmartOfflineTests = runSmartOfflineTests;
|
|
577
|
+
win.CacheInspector = CacheInspector;
|
|
578
|
+
}
|