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