@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/src/index.cjs ADDED
@@ -0,0 +1,576 @@
1
+ /**
2
+ * SmartOffline SDK (CommonJS version) - v2.0
3
+ *
4
+ * Complete, reliable offline-first caching SDK for web applications.
5
+ *
6
+ * Usage:
7
+ * ```javascript
8
+ * const { SmartOffline, setupSmartOffline } = require('@soham20/smart-offline-sdk')
9
+ *
10
+ * // Initialize early in your app
11
+ * setupSmartOffline({
12
+ * pages: ['/dashboard/*', '/products/*'],
13
+ * apis: ['/api/v1/*'],
14
+ * debug: true
15
+ * }).then(result => {
16
+ * if (result.success) {
17
+ * console.log('SmartOffline ready!')
18
+ * }
19
+ * })
20
+ * ```
21
+ */
22
+
23
+ // ============================================================================
24
+ // DEFAULT CONFIGURATION
25
+ // ============================================================================
26
+
27
+ const DEFAULT_CONFIG = {
28
+ pages: [],
29
+ apis: [],
30
+ debug: typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production",
31
+ frequencyThreshold: 3,
32
+ recencyThreshold: 24 * 60 * 60 * 1000, // 24 hours
33
+ maxResourceSize: 10 * 1024 * 1024, // 10MB
34
+ networkQuality: "auto",
35
+ significance: {},
36
+ weights: { frequency: 1, recency: 1, size: 1 },
37
+ customPriorityFn: null,
38
+ enableDetailedLogs: false,
39
+ serviceWorkerPath: "/smart-offline-sw.js",
40
+ serviceWorkerScope: "/",
41
+ }
42
+
43
+ // ============================================================================
44
+ // STATE
45
+ // ============================================================================
46
+
47
+ let isInitialized = false
48
+ let serviceWorkerRegistration = null
49
+ let currentConfig = { ...DEFAULT_CONFIG }
50
+
51
+ // Event listener registry
52
+ const eventListeners = {
53
+ cache: [],
54
+ skip: [],
55
+ serve: [],
56
+ clear: [],
57
+ error: [],
58
+ }
59
+
60
+ // ============================================================================
61
+ // MAIN SETUP FUNCTION
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Initialize the SmartOffline SDK with complete setup
66
+ */
67
+ async function setupSmartOffline(config = {}) {
68
+ // Merge with defaults
69
+ currentConfig = { ...DEFAULT_CONFIG, ...config }
70
+
71
+ // Check if already initialized
72
+ if (isInitialized) {
73
+ if (currentConfig.debug) {
74
+ console.warn("[SmartOffline] Already initialized, updating config...")
75
+ }
76
+ await sendConfigToServiceWorker()
77
+ return { success: true, registration: serviceWorkerRegistration }
78
+ }
79
+
80
+ // Check browser support
81
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
82
+ console.error("[SmartOffline] Service Workers not supported in this browser")
83
+ return { success: false, error: "Service Workers not supported" }
84
+ }
85
+
86
+ if (typeof window === "undefined" || !("caches" in window)) {
87
+ console.error("[SmartOffline] Cache API not supported in this browser")
88
+ return { success: false, error: "Cache API not supported" }
89
+ }
90
+
91
+ try {
92
+ // Register service worker
93
+ if (currentConfig.debug) {
94
+ console.log("[SmartOffline] Registering service worker...")
95
+ }
96
+
97
+ serviceWorkerRegistration = await navigator.serviceWorker.register(
98
+ currentConfig.serviceWorkerPath,
99
+ { scope: currentConfig.serviceWorkerScope }
100
+ )
101
+
102
+ if (currentConfig.debug) {
103
+ console.log(
104
+ "[SmartOffline] Service worker registered:",
105
+ serviceWorkerRegistration.scope
106
+ )
107
+ }
108
+
109
+ // Wait for the service worker to be ready
110
+ await navigator.serviceWorker.ready
111
+
112
+ if (currentConfig.debug) {
113
+ console.log("[SmartOffline] Service worker ready")
114
+ }
115
+
116
+ // Send configuration to service worker
117
+ await sendConfigToServiceWorker()
118
+
119
+ // Set up event listeners
120
+ setupEventListeners()
121
+
122
+ isInitialized = true
123
+
124
+ if (currentConfig.debug) {
125
+ console.log("[SmartOffline] Setup complete! Configuration:", {
126
+ pages: currentConfig.pages,
127
+ apis: currentConfig.apis,
128
+ frequencyThreshold: currentConfig.frequencyThreshold,
129
+ recencyThreshold: currentConfig.recencyThreshold / (60 * 60 * 1000) + "h",
130
+ })
131
+ }
132
+
133
+ return { success: true, registration: serviceWorkerRegistration }
134
+ } catch (error) {
135
+ console.error("[SmartOffline] Setup failed:", error)
136
+ return {
137
+ success: false,
138
+ error: error instanceof Error ? error.message : String(error),
139
+ }
140
+ }
141
+ }
142
+
143
+ // ============================================================================
144
+ // HELPER FUNCTIONS
145
+ // ============================================================================
146
+
147
+ async function sendConfigToServiceWorker() {
148
+ const controller = navigator.serviceWorker.controller
149
+
150
+ if (!controller) {
151
+ await new Promise((resolve) => setTimeout(resolve, 100))
152
+ const reg = await navigator.serviceWorker.ready
153
+ const worker = reg.active
154
+
155
+ if (worker) {
156
+ sendConfigMessage(worker)
157
+ }
158
+ } else {
159
+ sendConfigMessage(controller)
160
+ }
161
+ }
162
+
163
+ function sendConfigMessage(worker) {
164
+ const transferableConfig = {
165
+ ...currentConfig,
166
+ customPriorityFn: currentConfig.customPriorityFn
167
+ ? currentConfig.customPriorityFn.toString()
168
+ : null,
169
+ onCacheEvent: undefined,
170
+ }
171
+
172
+ worker.postMessage({
173
+ type: "INIT_CONFIG",
174
+ payload: transferableConfig,
175
+ })
176
+
177
+ if (currentConfig.debug) {
178
+ console.log("[SmartOffline] Config sent to service worker")
179
+ }
180
+ }
181
+
182
+ function setupEventListeners() {
183
+ navigator.serviceWorker.addEventListener("message", (event) => {
184
+ if (event.data && typeof event.data.type === "string") {
185
+ if (event.data.type.startsWith("CACHE_")) {
186
+ const cacheEvent = {
187
+ type: event.data.type,
188
+ url: event.data.url,
189
+ reason: event.data.reason,
190
+ metadata: event.data.metadata || {},
191
+ timestamp: event.data.timestamp || Date.now(),
192
+ }
193
+
194
+ // Call user callback if provided
195
+ if (currentConfig.onCacheEvent) {
196
+ currentConfig.onCacheEvent(cacheEvent)
197
+ }
198
+
199
+ // Call registered event listeners
200
+ const eventType = cacheEvent.type.replace("CACHE_", "").toLowerCase()
201
+ const listeners = eventListeners[eventType] || []
202
+ listeners.forEach((fn) => fn(cacheEvent))
203
+
204
+ // Debug logging
205
+ if (currentConfig.debug) {
206
+ const icon = {
207
+ CACHE_CACHE: "💾",
208
+ CACHE_SKIP: "⏭️",
209
+ CACHE_SERVE: "📤",
210
+ CACHE_ERROR: "❌",
211
+ }
212
+
213
+ console.log(
214
+ "[SmartOffline] " + (icon[cacheEvent.type] || "📝") + " " + cacheEvent.type.replace("CACHE_", "") + ":",
215
+ cacheEvent.url.replace(typeof window !== "undefined" ? window.location.origin : "", ""),
216
+ "(" + cacheEvent.reason + ")"
217
+ )
218
+ }
219
+ }
220
+ }
221
+ })
222
+ }
223
+
224
+ function openDB(name, version) {
225
+ return new Promise((resolve, reject) => {
226
+ const request = indexedDB.open(name, version)
227
+ request.onupgradeneeded = () => {
228
+ const db = request.result
229
+ if (name === "smart-offline-logs-v2" && !db.objectStoreNames.contains("logs")) {
230
+ db.createObjectStore("logs", { autoIncrement: true })
231
+ }
232
+ if (name === "smart-offline-usage-v2" && !db.objectStoreNames.contains("usage")) {
233
+ db.createObjectStore("usage", { keyPath: "url" })
234
+ }
235
+ }
236
+ request.onsuccess = () => resolve(request.result)
237
+ request.onerror = () => reject(request.error)
238
+ })
239
+ }
240
+
241
+ // ============================================================================
242
+ // PUBLIC API
243
+ // ============================================================================
244
+
245
+ async function updateConfig(newConfig) {
246
+ currentConfig = { ...currentConfig, ...newConfig }
247
+ await sendConfigToServiceWorker()
248
+ }
249
+
250
+ function getConfig() {
251
+ return { ...currentConfig }
252
+ }
253
+
254
+ function isSmartOfflineReady() {
255
+ return isInitialized
256
+ }
257
+
258
+ function getServiceWorkerRegistration() {
259
+ return serviceWorkerRegistration
260
+ }
261
+
262
+ function on(eventType, callback) {
263
+ if (eventListeners[eventType]) {
264
+ eventListeners[eventType].push(callback)
265
+ }
266
+ }
267
+
268
+ function off(eventType, callback) {
269
+ if (eventListeners[eventType]) {
270
+ const index = eventListeners[eventType].indexOf(callback)
271
+ if (index > -1) {
272
+ eventListeners[eventType].splice(index, 1)
273
+ }
274
+ }
275
+ }
276
+
277
+ async function getCacheLogs() {
278
+ try {
279
+ const db = await openDB("smart-offline-logs-v2", 1)
280
+ const tx = db.transaction("logs", "readonly")
281
+ const store = tx.objectStore("logs")
282
+ return new Promise((resolve) => {
283
+ const request = store.getAll()
284
+ request.onsuccess = () => resolve(request.result || [])
285
+ request.onerror = () => resolve([])
286
+ })
287
+ } catch {
288
+ return []
289
+ }
290
+ }
291
+
292
+ async function clearCacheLogs() {
293
+ try {
294
+ const db = await openDB("smart-offline-logs-v2", 1)
295
+ const tx = db.transaction("logs", "readwrite")
296
+ const store = tx.objectStore("logs")
297
+ store.clear()
298
+ } catch {
299
+ // Ignore errors
300
+ }
301
+ }
302
+
303
+ async function clearAllCache() {
304
+ const cacheNames = await caches.keys()
305
+ for (const name of cacheNames) {
306
+ if (name.startsWith("smart-offline")) {
307
+ await caches.delete(name)
308
+ }
309
+ }
310
+
311
+ const dbNames = ["smart-offline-usage-v2", "smart-offline-logs-v2"]
312
+ for (const dbName of dbNames) {
313
+ try {
314
+ indexedDB.deleteDatabase(dbName)
315
+ } catch {
316
+ // Ignore errors
317
+ }
318
+ }
319
+
320
+ if (currentConfig.debug) {
321
+ console.log("[SmartOffline] All cache data cleared")
322
+ }
323
+ }
324
+
325
+ async function getCacheStats() {
326
+ let cachedItems = 0
327
+ let cacheSize = 0
328
+ let trackedUrls = 0
329
+
330
+ try {
331
+ const cache = await caches.open("smart-offline-cache-v2")
332
+ const keys = await cache.keys()
333
+ cachedItems = keys.length
334
+
335
+ for (const request of keys) {
336
+ const response = await cache.match(request)
337
+ if (response) {
338
+ const size = parseInt(response.headers.get("content-length") || "0", 10)
339
+ cacheSize += size
340
+ }
341
+ }
342
+ } catch {
343
+ // Ignore errors
344
+ }
345
+
346
+ try {
347
+ const count = await new Promise((resolve) => {
348
+ const request = indexedDB.open("smart-offline-usage-v2", 1)
349
+ request.onsuccess = () => {
350
+ const db = request.result
351
+ try {
352
+ const tx = db.transaction("usage", "readonly")
353
+ const store = tx.objectStore("usage")
354
+ const countReq = store.count()
355
+ countReq.onsuccess = () => resolve(countReq.result)
356
+ countReq.onerror = () => resolve(0)
357
+ } catch {
358
+ resolve(0)
359
+ }
360
+ }
361
+ request.onerror = () => resolve(0)
362
+ })
363
+ trackedUrls = count
364
+ } catch {
365
+ // Ignore errors
366
+ }
367
+
368
+ return { cachedItems, trackedUrls, cacheSize }
369
+ }
370
+
371
+ async function forceUpdate() {
372
+ if (serviceWorkerRegistration) {
373
+ await serviceWorkerRegistration.update()
374
+ if (currentConfig.debug) {
375
+ console.log("[SmartOffline] Service worker update triggered")
376
+ }
377
+ }
378
+ }
379
+
380
+ async function uninstall() {
381
+ if (serviceWorkerRegistration) {
382
+ await serviceWorkerRegistration.unregister()
383
+ }
384
+ await clearAllCache()
385
+ isInitialized = false
386
+ serviceWorkerRegistration = null
387
+
388
+ if (currentConfig.debug) {
389
+ console.log("[SmartOffline] Uninstalled and cleaned up")
390
+ }
391
+ }
392
+
393
+ function init(config = {}) {
394
+ setupSmartOffline(config)
395
+ }
396
+
397
+ // ============================================================================
398
+ // TEST UTILITIES
399
+ // ============================================================================
400
+
401
+ function matchesPattern(url, pattern) {
402
+ if (!pattern.includes("*")) {
403
+ return url.includes(pattern)
404
+ }
405
+ const regexPattern = pattern
406
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
407
+ .replace(/\*/g, ".*")
408
+ return new RegExp(regexPattern).test(url)
409
+ }
410
+
411
+ function isHighPriority(usage, url, config = {}) {
412
+ const finalConfig = { ...DEFAULT_CONFIG, ...config }
413
+
414
+ if (finalConfig.customPriorityFn && typeof finalConfig.customPriorityFn === "function") {
415
+ try {
416
+ return finalConfig.customPriorityFn(usage, url, finalConfig) > 50
417
+ } catch (e) {
418
+ console.error("Custom priority function error:", e)
419
+ }
420
+ }
421
+
422
+ for (const pattern in finalConfig.significance) {
423
+ if (url.includes(pattern)) {
424
+ if (finalConfig.significance[pattern] === "high") return true
425
+ if (finalConfig.significance[pattern] === "low") return false
426
+ }
427
+ }
428
+
429
+ if (!usage) return false
430
+
431
+ const weights = finalConfig.weights || { frequency: 1, recency: 1, size: 1 }
432
+ const frequencyScore = Math.min(100, (usage.count / finalConfig.frequencyThreshold) * 100)
433
+ const timeSinceAccess = Date.now() - usage.lastAccessed
434
+ const recencyScore = Math.max(0, 100 - (timeSinceAccess / finalConfig.recencyThreshold) * 100)
435
+ const totalWeight = weights.frequency + weights.recency
436
+ const weightedScore = (frequencyScore * weights.frequency + recencyScore * weights.recency) / totalWeight
437
+
438
+ return weightedScore > 50
439
+ }
440
+
441
+ function shouldCacheUrl(url, config = {}) {
442
+ const pages = config.pages || []
443
+ const apis = config.apis || []
444
+ const isPage = pages.some((p) => matchesPattern(url, p))
445
+ const isAPI = apis.some((a) => matchesPattern(url, a))
446
+ return { isPage, isAPI }
447
+ }
448
+
449
+ // SmartOfflineTestSuite class
450
+ function SmartOfflineTestSuite(config = {}) {
451
+ this.config = { ...DEFAULT_CONFIG, ...config }
452
+ this.results = []
453
+ }
454
+
455
+ SmartOfflineTestSuite.prototype.runAll = async function() {
456
+ this.results = []
457
+ // Add basic algorithm tests
458
+ this.testPatternMatching()
459
+ this.testFrequencyPriority()
460
+ return this.results
461
+ }
462
+
463
+ SmartOfflineTestSuite.prototype.testPatternMatching = function() {
464
+ const tests = [
465
+ { url: "/admin/charts", pattern: "/admin/charts", expected: true },
466
+ { url: "/admin/charts/123", pattern: "/admin/charts/*", expected: true },
467
+ ]
468
+ let passed = true
469
+ for (const test of tests) {
470
+ if (matchesPattern(test.url, test.pattern) !== test.expected) {
471
+ passed = false
472
+ break
473
+ }
474
+ }
475
+ this.results.push({ name: "Pattern Matching", passed, message: passed ? "All patterns matched" : "Pattern matching failed" })
476
+ }
477
+
478
+ SmartOfflineTestSuite.prototype.testFrequencyPriority = function() {
479
+ const highUsage = { url: "/test", count: 5, lastAccessed: Date.now() }
480
+ const result = isHighPriority(highUsage, "/test", this.config)
481
+ this.results.push({ name: "Frequency Priority", passed: result, message: result ? "High frequency works" : "Frequency check failed" })
482
+ }
483
+
484
+ SmartOfflineTestSuite.prototype.printResults = function() {
485
+ console.info("\n========================================")
486
+ console.info(" SmartOffline SDK Test Results")
487
+ console.info("========================================\n")
488
+ for (const result of this.results) {
489
+ console.info((result.passed ? "✅" : "❌") + " " + result.name + ": " + result.message)
490
+ }
491
+ }
492
+
493
+ // CacheInspector class
494
+ function CacheInspector() {}
495
+
496
+ CacheInspector.prototype.getCachedItems = async function() {
497
+ try {
498
+ const cache = await caches.open("smart-offline-cache-v2")
499
+ const keys = await cache.keys()
500
+ const items = []
501
+ for (const request of keys) {
502
+ const response = await cache.match(request)
503
+ if (response) {
504
+ items.push({
505
+ url: request.url,
506
+ size: parseInt(response.headers.get("content-length") || "0", 10),
507
+ contentType: response.headers.get("content-type") || "unknown",
508
+ })
509
+ }
510
+ }
511
+ return items
512
+ } catch {
513
+ return []
514
+ }
515
+ }
516
+
517
+ CacheInspector.prototype.showAll = async function() {
518
+ const items = await this.getCachedItems()
519
+ console.info("\n🔍 SmartOffline Cache Inspector")
520
+ console.info("📦 Cached Items (" + items.length + "):")
521
+ console.table(items)
522
+ }
523
+
524
+ async function runSmartOfflineTests(config) {
525
+ const suite = new SmartOfflineTestSuite(config)
526
+ const results = await suite.runAll()
527
+ suite.printResults()
528
+ return results
529
+ }
530
+
531
+ // ============================================================================
532
+ // EXPORTS
533
+ // ============================================================================
534
+
535
+ const SmartOffline = {
536
+ setup: setupSmartOffline,
537
+ init,
538
+ updateConfig,
539
+ getConfig,
540
+ isReady: isSmartOfflineReady,
541
+ getRegistration: getServiceWorkerRegistration,
542
+ on,
543
+ off,
544
+ clearCache: clearAllCache,
545
+ getStats: getCacheStats,
546
+ getCacheLogs,
547
+ clearCacheLogs,
548
+ forceUpdate,
549
+ uninstall,
550
+ }
551
+
552
+ module.exports = {
553
+ SmartOffline,
554
+ setupSmartOffline,
555
+ updateConfig,
556
+ getConfig,
557
+ isSmartOfflineReady,
558
+ getServiceWorkerRegistration,
559
+ on,
560
+ off,
561
+ clearAllCache,
562
+ getCacheStats,
563
+ getCacheLogs,
564
+ clearCacheLogs,
565
+ forceUpdate,
566
+ uninstall,
567
+ init,
568
+ matchesPattern,
569
+ isHighPriority,
570
+ shouldCacheUrl,
571
+ SmartOfflineTestSuite,
572
+ CacheInspector,
573
+ runSmartOfflineTests,
574
+ }
575
+
576
+ module.exports.default = SmartOffline