@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.
@@ -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;