@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,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartOffline SDK - Complete Setup & Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file provides a complete, reliable implementation of the SmartOffline SDK
|
|
5
|
+
* for offline-first caching. Import and call `setupSmartOffline()` early in your
|
|
6
|
+
* application entry point.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { setupSmartOffline } from '@soham20/smart-offline-sdk'
|
|
11
|
+
*
|
|
12
|
+
* // Call this early in your app initialization
|
|
13
|
+
* await setupSmartOffline()
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CONFIGURATION
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export interface SmartOfflineConfig {
|
|
22
|
+
/**
|
|
23
|
+
* URL patterns for pages to cache (supports * wildcard)
|
|
24
|
+
* @example ['/admin/*', '/dashboard', '/products/*']
|
|
25
|
+
*/
|
|
26
|
+
pages: string[];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* URL patterns for API endpoints to cache
|
|
30
|
+
* @example ['/api/v1/*', '/graphql']
|
|
31
|
+
*/
|
|
32
|
+
apis: string[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Enable debug logging in console
|
|
36
|
+
* @default false in production, true in development
|
|
37
|
+
*/
|
|
38
|
+
debug: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Number of accesses before URL is considered high priority
|
|
42
|
+
* @default 3
|
|
43
|
+
*/
|
|
44
|
+
frequencyThreshold: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Time in ms within which access is considered "recent"
|
|
48
|
+
* @default 86400000 (24 hours)
|
|
49
|
+
*/
|
|
50
|
+
recencyThreshold: number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum resource size in bytes to cache (skip larger files)
|
|
54
|
+
* @default Infinity
|
|
55
|
+
*/
|
|
56
|
+
maxResourceSize: number;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Network quality detection mode
|
|
60
|
+
* - 'auto': Detect from navigator.connection
|
|
61
|
+
* - 'slow': Treat as slow network (only cache high priority)
|
|
62
|
+
* - 'fast': Treat as fast network (cache everything)
|
|
63
|
+
* @default 'auto'
|
|
64
|
+
*/
|
|
65
|
+
networkQuality: "auto" | "slow" | "fast";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Override priority for specific URL patterns
|
|
69
|
+
* @example { '/critical/*': 'high', '/logs/*': 'low' }
|
|
70
|
+
*/
|
|
71
|
+
significance: Record<string, "high" | "low">;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Weights for priority calculation
|
|
75
|
+
* Higher weight = more influence on final score
|
|
76
|
+
*/
|
|
77
|
+
weights: {
|
|
78
|
+
frequency: number;
|
|
79
|
+
recency: number;
|
|
80
|
+
size: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Custom function to calculate priority (0-100)
|
|
85
|
+
* If provided, overrides the default algorithm
|
|
86
|
+
*/
|
|
87
|
+
customPriorityFn:
|
|
88
|
+
| ((
|
|
89
|
+
usage: { url: string; count: number; lastAccessed: number } | null,
|
|
90
|
+
url: string,
|
|
91
|
+
config: SmartOfflineConfig,
|
|
92
|
+
) => number)
|
|
93
|
+
| null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Enable detailed event logging (for debugging/visualization)
|
|
97
|
+
* @default false
|
|
98
|
+
*/
|
|
99
|
+
enableDetailedLogs: boolean;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Callback when cache events occur
|
|
103
|
+
*/
|
|
104
|
+
onCacheEvent?: (event: CacheEvent) => void;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Path to the service worker file
|
|
108
|
+
* @default '/smart-offline-sw.js'
|
|
109
|
+
*/
|
|
110
|
+
serviceWorkerPath: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Service worker scope
|
|
114
|
+
* @default '/'
|
|
115
|
+
*/
|
|
116
|
+
serviceWorkerScope: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface CacheEvent {
|
|
120
|
+
type: "CACHE_CACHE" | "CACHE_SKIP" | "CACHE_SERVE" | "CACHE_ERROR";
|
|
121
|
+
url: string;
|
|
122
|
+
reason: string;
|
|
123
|
+
metadata: Record<string, unknown>;
|
|
124
|
+
timestamp: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface UsageData {
|
|
128
|
+
url: string;
|
|
129
|
+
count: number;
|
|
130
|
+
lastAccessed: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CacheLog {
|
|
134
|
+
type: string;
|
|
135
|
+
url: string;
|
|
136
|
+
reason: string;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
timestamp: number;
|
|
139
|
+
date: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SetupResult {
|
|
143
|
+
success: boolean;
|
|
144
|
+
registration?: ServiceWorkerRegistration;
|
|
145
|
+
error?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface CacheStats {
|
|
149
|
+
cachedItems: number;
|
|
150
|
+
trackedUrls: number;
|
|
151
|
+
cacheSize: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// DEFAULT CONFIGURATION
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
const DEFAULT_CONFIG: SmartOfflineConfig = {
|
|
159
|
+
pages: [],
|
|
160
|
+
apis: [],
|
|
161
|
+
debug:
|
|
162
|
+
typeof process !== "undefined" && process.env?.NODE_ENV !== "production",
|
|
163
|
+
frequencyThreshold: 3,
|
|
164
|
+
recencyThreshold: 24 * 60 * 60 * 1000, // 24 hours
|
|
165
|
+
maxResourceSize: 10 * 1024 * 1024, // 10MB
|
|
166
|
+
networkQuality: "auto",
|
|
167
|
+
significance: {},
|
|
168
|
+
weights: { frequency: 1, recency: 1, size: 1 },
|
|
169
|
+
customPriorityFn: null,
|
|
170
|
+
enableDetailedLogs: false,
|
|
171
|
+
serviceWorkerPath: "/smart-offline-sw.js",
|
|
172
|
+
serviceWorkerScope: "/",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// STATE
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
let isInitialized = false;
|
|
180
|
+
let serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
|
|
181
|
+
let currentConfig: SmartOfflineConfig = { ...DEFAULT_CONFIG };
|
|
182
|
+
|
|
183
|
+
// Event listener registry
|
|
184
|
+
const eventListeners: Record<string, Array<(event: CacheEvent) => void>> = {
|
|
185
|
+
cache: [],
|
|
186
|
+
skip: [],
|
|
187
|
+
serve: [],
|
|
188
|
+
clear: [],
|
|
189
|
+
error: [],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// MAIN SETUP FUNCTION
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Initialize the SmartOffline SDK with complete setup
|
|
198
|
+
*
|
|
199
|
+
* This function:
|
|
200
|
+
* 1. Checks browser support
|
|
201
|
+
* 2. Registers the service worker
|
|
202
|
+
* 3. Sends configuration to the service worker
|
|
203
|
+
* 4. Sets up event listeners
|
|
204
|
+
*
|
|
205
|
+
* @param config - Partial configuration (merged with defaults)
|
|
206
|
+
* @returns Promise that resolves when setup is complete
|
|
207
|
+
*/
|
|
208
|
+
export async function setupSmartOffline(
|
|
209
|
+
config: Partial<SmartOfflineConfig> = {},
|
|
210
|
+
): Promise<SetupResult> {
|
|
211
|
+
// Merge with defaults
|
|
212
|
+
currentConfig = { ...DEFAULT_CONFIG, ...config };
|
|
213
|
+
|
|
214
|
+
// Check if already initialized
|
|
215
|
+
if (isInitialized) {
|
|
216
|
+
if (currentConfig.debug) {
|
|
217
|
+
console.warn("[SmartOffline] Already initialized, updating config...");
|
|
218
|
+
}
|
|
219
|
+
await sendConfigToServiceWorker();
|
|
220
|
+
return { success: true, registration: serviceWorkerRegistration! };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check browser support
|
|
224
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
|
|
225
|
+
console.error(
|
|
226
|
+
"[SmartOffline] Service Workers not supported in this browser",
|
|
227
|
+
);
|
|
228
|
+
return { success: false, error: "Service Workers not supported" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (typeof window === "undefined" || !("caches" in window)) {
|
|
232
|
+
console.error("[SmartOffline] Cache API not supported in this browser");
|
|
233
|
+
return { success: false, error: "Cache API not supported" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Register service worker
|
|
238
|
+
if (currentConfig.debug) {
|
|
239
|
+
console.log("[SmartOffline] Registering service worker...");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
serviceWorkerRegistration = await navigator.serviceWorker.register(
|
|
243
|
+
currentConfig.serviceWorkerPath,
|
|
244
|
+
{ scope: currentConfig.serviceWorkerScope },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (currentConfig.debug) {
|
|
248
|
+
console.log(
|
|
249
|
+
"[SmartOffline] Service worker registered:",
|
|
250
|
+
serviceWorkerRegistration.scope,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wait for the service worker to be ready
|
|
255
|
+
await navigator.serviceWorker.ready;
|
|
256
|
+
|
|
257
|
+
if (currentConfig.debug) {
|
|
258
|
+
console.log("[SmartOffline] Service worker ready");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Send configuration to service worker
|
|
262
|
+
await sendConfigToServiceWorker();
|
|
263
|
+
|
|
264
|
+
// Set up event listeners
|
|
265
|
+
setupEventListeners();
|
|
266
|
+
|
|
267
|
+
isInitialized = true;
|
|
268
|
+
|
|
269
|
+
if (currentConfig.debug) {
|
|
270
|
+
console.log("[SmartOffline] Setup complete! Configuration:", {
|
|
271
|
+
pages: currentConfig.pages,
|
|
272
|
+
apis: currentConfig.apis,
|
|
273
|
+
frequencyThreshold: currentConfig.frequencyThreshold,
|
|
274
|
+
recencyThreshold: `${currentConfig.recencyThreshold / (60 * 60 * 1000)}h`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { success: true, registration: serviceWorkerRegistration };
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error("[SmartOffline] Setup failed:", error);
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
error: error instanceof Error ? error.message : String(error),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// HELPER FUNCTIONS
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Send configuration to the active service worker
|
|
294
|
+
*/
|
|
295
|
+
async function sendConfigToServiceWorker(): Promise<void> {
|
|
296
|
+
const controller = navigator.serviceWorker.controller;
|
|
297
|
+
|
|
298
|
+
if (!controller) {
|
|
299
|
+
// No controller yet, wait a bit and retry
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
301
|
+
const reg = await navigator.serviceWorker.ready;
|
|
302
|
+
const worker = reg.active;
|
|
303
|
+
|
|
304
|
+
if (worker) {
|
|
305
|
+
sendConfigMessage(worker);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
sendConfigMessage(controller);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Send the config message to a service worker
|
|
314
|
+
*/
|
|
315
|
+
function sendConfigMessage(worker: ServiceWorker): void {
|
|
316
|
+
// Prepare config for transfer (can't send functions directly)
|
|
317
|
+
const transferableConfig = {
|
|
318
|
+
...currentConfig,
|
|
319
|
+
customPriorityFn: currentConfig.customPriorityFn
|
|
320
|
+
? currentConfig.customPriorityFn.toString()
|
|
321
|
+
: null,
|
|
322
|
+
// Don't send non-serializable properties
|
|
323
|
+
onCacheEvent: undefined,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
worker.postMessage({
|
|
327
|
+
type: "INIT_CONFIG",
|
|
328
|
+
payload: transferableConfig,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (currentConfig.debug) {
|
|
332
|
+
console.log("[SmartOffline] Config sent to service worker");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Set up event listeners for cache events
|
|
338
|
+
*/
|
|
339
|
+
function setupEventListeners(): void {
|
|
340
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
341
|
+
if (event.data && typeof event.data.type === "string") {
|
|
342
|
+
if (event.data.type.startsWith("CACHE_")) {
|
|
343
|
+
const cacheEvent: CacheEvent = {
|
|
344
|
+
type: event.data.type,
|
|
345
|
+
url: event.data.url,
|
|
346
|
+
reason: event.data.reason,
|
|
347
|
+
metadata: event.data.metadata || {},
|
|
348
|
+
timestamp: event.data.timestamp || Date.now(),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Call user callback if provided
|
|
352
|
+
if (currentConfig.onCacheEvent) {
|
|
353
|
+
currentConfig.onCacheEvent(cacheEvent);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Call registered event listeners
|
|
357
|
+
const eventType = cacheEvent.type.replace("CACHE_", "").toLowerCase();
|
|
358
|
+
const listeners = eventListeners[eventType] || [];
|
|
359
|
+
listeners.forEach((fn) => fn(cacheEvent));
|
|
360
|
+
|
|
361
|
+
// Debug logging
|
|
362
|
+
if (currentConfig.debug) {
|
|
363
|
+
const icon: Record<string, string> = {
|
|
364
|
+
CACHE_CACHE: "💾",
|
|
365
|
+
CACHE_SKIP: "⏭️",
|
|
366
|
+
CACHE_SERVE: "📤",
|
|
367
|
+
CACHE_ERROR: "❌",
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
console.log(
|
|
371
|
+
`[SmartOffline] ${icon[cacheEvent.type] || "📝"} ${cacheEvent.type.replace("CACHE_", "")}:`,
|
|
372
|
+
cacheEvent.url.replace(
|
|
373
|
+
typeof window !== "undefined" ? window.location.origin : "",
|
|
374
|
+
"",
|
|
375
|
+
),
|
|
376
|
+
`(${cacheEvent.reason})`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Open IndexedDB database
|
|
386
|
+
*/
|
|
387
|
+
function openDB(name: string, version: number): Promise<IDBDatabase> {
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
const request = indexedDB.open(name, version);
|
|
390
|
+
request.onupgradeneeded = () => {
|
|
391
|
+
const db = request.result;
|
|
392
|
+
if (
|
|
393
|
+
name === "smart-offline-logs-v2" &&
|
|
394
|
+
!db.objectStoreNames.contains("logs")
|
|
395
|
+
) {
|
|
396
|
+
db.createObjectStore("logs", { autoIncrement: true });
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
name === "smart-offline-usage-v2" &&
|
|
400
|
+
!db.objectStoreNames.contains("usage")
|
|
401
|
+
) {
|
|
402
|
+
db.createObjectStore("usage", { keyPath: "url" });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
request.onsuccess = () => resolve(request.result);
|
|
406
|
+
request.onerror = () => reject(request.error);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// PUBLIC API
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Update configuration at runtime
|
|
416
|
+
*/
|
|
417
|
+
export async function updateConfig(
|
|
418
|
+
newConfig: Partial<SmartOfflineConfig>,
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
currentConfig = { ...currentConfig, ...newConfig };
|
|
421
|
+
await sendConfigToServiceWorker();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Get current configuration
|
|
426
|
+
*/
|
|
427
|
+
export function getConfig(): SmartOfflineConfig {
|
|
428
|
+
return { ...currentConfig };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if SDK is initialized
|
|
433
|
+
*/
|
|
434
|
+
export function isSmartOfflineReady(): boolean {
|
|
435
|
+
return isInitialized;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get service worker registration
|
|
440
|
+
*/
|
|
441
|
+
export function getServiceWorkerRegistration(): ServiceWorkerRegistration | null {
|
|
442
|
+
return serviceWorkerRegistration;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Add event listener for cache events
|
|
447
|
+
*/
|
|
448
|
+
export function on(
|
|
449
|
+
eventType: "cache" | "skip" | "serve" | "clear" | "error",
|
|
450
|
+
callback: (event: CacheEvent) => void,
|
|
451
|
+
): void {
|
|
452
|
+
if (eventListeners[eventType]) {
|
|
453
|
+
eventListeners[eventType].push(callback);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Remove event listener for cache events
|
|
459
|
+
*/
|
|
460
|
+
export function off(
|
|
461
|
+
eventType: "cache" | "skip" | "serve" | "clear" | "error",
|
|
462
|
+
callback: (event: CacheEvent) => void,
|
|
463
|
+
): void {
|
|
464
|
+
if (eventListeners[eventType]) {
|
|
465
|
+
const index = eventListeners[eventType].indexOf(callback);
|
|
466
|
+
if (index > -1) {
|
|
467
|
+
eventListeners[eventType].splice(index, 1);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get cache logs from IndexedDB
|
|
474
|
+
*/
|
|
475
|
+
export async function getCacheLogs(): Promise<CacheLog[]> {
|
|
476
|
+
try {
|
|
477
|
+
const db = await openDB("smart-offline-logs-v2", 1);
|
|
478
|
+
const tx = db.transaction("logs", "readonly");
|
|
479
|
+
const store = tx.objectStore("logs");
|
|
480
|
+
return new Promise((resolve) => {
|
|
481
|
+
const request = store.getAll();
|
|
482
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
483
|
+
request.onerror = () => resolve([]);
|
|
484
|
+
});
|
|
485
|
+
} catch {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Clear cache logs from IndexedDB
|
|
492
|
+
*/
|
|
493
|
+
export async function clearCacheLogs(): Promise<void> {
|
|
494
|
+
try {
|
|
495
|
+
const db = await openDB("smart-offline-logs-v2", 1);
|
|
496
|
+
const tx = db.transaction("logs", "readwrite");
|
|
497
|
+
const store = tx.objectStore("logs");
|
|
498
|
+
store.clear();
|
|
499
|
+
} catch {
|
|
500
|
+
// Ignore errors
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Clear all cached data
|
|
506
|
+
*/
|
|
507
|
+
export async function clearAllCache(): Promise<void> {
|
|
508
|
+
// Clear Cache Storage
|
|
509
|
+
const cacheNames = await caches.keys();
|
|
510
|
+
for (const name of cacheNames) {
|
|
511
|
+
if (name.startsWith("smart-offline")) {
|
|
512
|
+
await caches.delete(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Clear IndexedDB
|
|
517
|
+
const dbNames = ["smart-offline-usage-v2", "smart-offline-logs-v2"];
|
|
518
|
+
for (const dbName of dbNames) {
|
|
519
|
+
try {
|
|
520
|
+
indexedDB.deleteDatabase(dbName);
|
|
521
|
+
} catch {
|
|
522
|
+
// Ignore errors
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (currentConfig.debug) {
|
|
527
|
+
console.log("[SmartOffline] All cache data cleared");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get cache statistics
|
|
533
|
+
*/
|
|
534
|
+
export async function getCacheStats(): Promise<CacheStats> {
|
|
535
|
+
let cachedItems = 0;
|
|
536
|
+
let cacheSize = 0;
|
|
537
|
+
let trackedUrls = 0;
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
// Count cached items
|
|
541
|
+
const cache = await caches.open("smart-offline-cache-v2");
|
|
542
|
+
const keys = await cache.keys();
|
|
543
|
+
cachedItems = keys.length;
|
|
544
|
+
|
|
545
|
+
// Estimate cache size
|
|
546
|
+
for (const request of keys) {
|
|
547
|
+
const response = await cache.match(request);
|
|
548
|
+
if (response) {
|
|
549
|
+
const size = parseInt(
|
|
550
|
+
response.headers.get("content-length") || "0",
|
|
551
|
+
10,
|
|
552
|
+
);
|
|
553
|
+
cacheSize += size;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
// Ignore errors
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
// Count tracked URLs from IndexedDB
|
|
562
|
+
const count = await new Promise<number>((resolve) => {
|
|
563
|
+
const request = indexedDB.open("smart-offline-usage-v2", 1);
|
|
564
|
+
request.onsuccess = () => {
|
|
565
|
+
const db = request.result;
|
|
566
|
+
try {
|
|
567
|
+
const tx = db.transaction("usage", "readonly");
|
|
568
|
+
const store = tx.objectStore("usage");
|
|
569
|
+
const countReq = store.count();
|
|
570
|
+
countReq.onsuccess = () => resolve(countReq.result);
|
|
571
|
+
countReq.onerror = () => resolve(0);
|
|
572
|
+
} catch {
|
|
573
|
+
resolve(0);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
request.onerror = () => resolve(0);
|
|
577
|
+
});
|
|
578
|
+
trackedUrls = count;
|
|
579
|
+
} catch {
|
|
580
|
+
// Ignore errors
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return { cachedItems, trackedUrls, cacheSize };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Force update the service worker
|
|
588
|
+
*/
|
|
589
|
+
export async function forceUpdate(): Promise<void> {
|
|
590
|
+
if (serviceWorkerRegistration) {
|
|
591
|
+
await serviceWorkerRegistration.update();
|
|
592
|
+
if (currentConfig.debug) {
|
|
593
|
+
console.log("[SmartOffline] Service worker update triggered");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Unregister the service worker and clean up
|
|
600
|
+
*/
|
|
601
|
+
export async function uninstall(): Promise<void> {
|
|
602
|
+
if (serviceWorkerRegistration) {
|
|
603
|
+
await serviceWorkerRegistration.unregister();
|
|
604
|
+
}
|
|
605
|
+
await clearAllCache();
|
|
606
|
+
isInitialized = false;
|
|
607
|
+
serviceWorkerRegistration = null;
|
|
608
|
+
|
|
609
|
+
if (currentConfig.debug) {
|
|
610
|
+
console.log("[SmartOffline] Uninstalled and cleaned up");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ============================================================================
|
|
615
|
+
// LEGACY API (for backwards compatibility)
|
|
616
|
+
// ============================================================================
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Initialize the SDK (legacy API, use setupSmartOffline instead)
|
|
620
|
+
* @deprecated Use setupSmartOffline() instead
|
|
621
|
+
*/
|
|
622
|
+
export function init(config: Partial<SmartOfflineConfig> = {}): void {
|
|
623
|
+
void setupSmartOffline(config);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ============================================================================
|
|
627
|
+
// EXPORT EVERYTHING
|
|
628
|
+
// ============================================================================
|
|
629
|
+
|
|
630
|
+
export const SmartOffline = {
|
|
631
|
+
// Main setup
|
|
632
|
+
setup: setupSmartOffline,
|
|
633
|
+
init, // Legacy API
|
|
634
|
+
|
|
635
|
+
// Configuration
|
|
636
|
+
updateConfig,
|
|
637
|
+
getConfig,
|
|
638
|
+
|
|
639
|
+
// Status
|
|
640
|
+
isReady: isSmartOfflineReady,
|
|
641
|
+
getRegistration: getServiceWorkerRegistration,
|
|
642
|
+
|
|
643
|
+
// Event handling
|
|
644
|
+
on,
|
|
645
|
+
off,
|
|
646
|
+
|
|
647
|
+
// Cache management
|
|
648
|
+
clearCache: clearAllCache,
|
|
649
|
+
getStats: getCacheStats,
|
|
650
|
+
getCacheLogs,
|
|
651
|
+
clearCacheLogs,
|
|
652
|
+
|
|
653
|
+
// Lifecycle
|
|
654
|
+
forceUpdate,
|
|
655
|
+
uninstall,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
export default SmartOffline;
|