@soham20/smart-offline-sdk 0.1.4 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soham20/smart-offline-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "Smart offline-first JavaScript SDK with intelligent caching for web applications",
5
5
  "main": "./src/index.cjs.js",
6
6
  "module": "./src/index.js",
@@ -15,6 +15,9 @@ let SDK_CONFIG = {
15
15
  maxResourceSize: Infinity,
16
16
  networkQuality: "auto", // 'auto' | 'fast' | 'slow'
17
17
  significance: {}, // { urlPattern: 'high' | 'normal' | 'low' }
18
+ weights: { frequency: 1, recency: 1, size: 1 },
19
+ customPriorityFn: null,
20
+ enableDetailedLogs: false
18
21
  };
19
22
 
20
23
  /**
@@ -24,12 +27,66 @@ self.addEventListener("message", (event) => {
24
27
  if (event.data && event.data.type === "INIT_CONFIG") {
25
28
  SDK_CONFIG = event.data.payload;
26
29
 
30
+ // Deserialize custom priority function if provided
31
+ if (SDK_CONFIG.customPriorityFn && typeof SDK_CONFIG.customPriorityFn === 'string') {
32
+ try {
33
+ SDK_CONFIG.customPriorityFn = eval(`(${SDK_CONFIG.customPriorityFn})`);
34
+ } catch (e) {
35
+ console.error('[SmartOffline] Failed to parse customPriorityFn:', e);
36
+ SDK_CONFIG.customPriorityFn = null;
37
+ }
38
+ }
39
+
27
40
  if (SDK_CONFIG.debug) {
28
41
  console.log("[SmartOffline] Config received:", SDK_CONFIG);
29
42
  }
30
43
  }
31
44
  });
32
45
 
46
+ /**
47
+ * Log cache events to IndexedDB
48
+ */
49
+ function logEvent(type, url, reason, metadata = {}) {
50
+ if (!SDK_CONFIG.enableDetailedLogs) return;
51
+
52
+ const request = indexedDB.open("smart-offline-logs", 1);
53
+
54
+ request.onupgradeneeded = () => {
55
+ const db = request.result;
56
+ if (!db.objectStoreNames.contains("logs")) {
57
+ db.createObjectStore("logs", { autoIncrement: true });
58
+ }
59
+ };
60
+
61
+ request.onsuccess = () => {
62
+ const db = request.result;
63
+ const tx = db.transaction("logs", "readwrite");
64
+ const store = tx.objectStore("logs");
65
+
66
+ store.add({
67
+ type,
68
+ url,
69
+ reason,
70
+ metadata,
71
+ timestamp: Date.now(),
72
+ date: new Date().toISOString()
73
+ });
74
+ };
75
+
76
+ // Also send to main thread
77
+ self.clients.matchAll().then(clients => {
78
+ clients.forEach(client => {
79
+ client.postMessage({
80
+ type: `CACHE_${type.toUpperCase()}`,
81
+ url,
82
+ reason,
83
+ metadata,
84
+ timestamp: Date.now()
85
+ });
86
+ });
87
+ });
88
+ }
89
+
33
90
  /**
34
91
  * -------- Usage Tracking (IndexedDB) --------
35
92
  * Tracks frequency + recency per URL
@@ -86,9 +143,19 @@ function getUsage(url) {
86
143
  }
87
144
 
88
145
  /**
89
- * Decide priority based on real usage and developer-tuned config
146
+ * Decide priority based on real usage and developer-tuned config with weights
90
147
  */
91
148
  function isHighPriority(usage, url) {
149
+ // Custom priority function override
150
+ if (SDK_CONFIG.customPriorityFn && typeof SDK_CONFIG.customPriorityFn === 'function') {
151
+ try {
152
+ const score = SDK_CONFIG.customPriorityFn(usage, url, SDK_CONFIG);
153
+ return score > 50; // Scores above 50 are high priority
154
+ } catch (e) {
155
+ console.error('[SmartOffline] Custom priority function error:', e);
156
+ }
157
+ }
158
+
92
159
  // Manual significance override
93
160
  for (const pattern in SDK_CONFIG.significance) {
94
161
  if (url.includes(pattern)) {
@@ -101,10 +168,25 @@ function isHighPriority(usage, url) {
101
168
 
102
169
  if (!usage) return false;
103
170
 
104
- const frequent = usage.count >= SDK_CONFIG.frequencyThreshold;
105
- const recent = Date.now() - usage.lastAccessed <= SDK_CONFIG.recencyThreshold;
106
-
107
- return frequent || recent;
171
+ // Weighted priority calculation
172
+ const weights = SDK_CONFIG.weights || { frequency: 1, recency: 1, size: 1 };
173
+
174
+ // Frequency score (0-100)
175
+ const frequencyScore = Math.min(100, (usage.count / SDK_CONFIG.frequencyThreshold) * 100);
176
+
177
+ // Recency score (0-100)
178
+ const timeSinceAccess = Date.now() - usage.lastAccessed;
179
+ const recencyScore = Math.max(0, 100 - (timeSinceAccess / SDK_CONFIG.recencyThreshold) * 100);
180
+
181
+ // Weighted total
182
+ const totalWeight = weights.frequency + weights.recency;
183
+ const weightedScore = (
184
+ (frequencyScore * weights.frequency) +
185
+ (recencyScore * weights.recency)
186
+ ) / totalWeight;
187
+
188
+ // High priority if weighted score > 50
189
+ return weightedScore > 50;
108
190
  }
109
191
 
110
192
  /**
@@ -182,6 +264,11 @@ self.addEventListener("fetch", (event) => {
182
264
  const contentLength = response.headers.get("content-length");
183
265
  const size = contentLength ? parseInt(contentLength, 10) : 0;
184
266
  if (size > SDK_CONFIG.maxResourceSize) {
267
+ logEvent('skip', request.url, 'size_limit_exceeded', {
268
+ size,
269
+ limit: SDK_CONFIG.maxResourceSize
270
+ });
271
+
185
272
  if (SDK_CONFIG.debug) {
186
273
  console.log(
187
274
  `[SmartOffline] Skipped caching (size ${size} > ${SDK_CONFIG.maxResourceSize}):`,
@@ -193,8 +280,15 @@ self.addEventListener("fetch", (event) => {
193
280
 
194
281
  // Network quality aware caching
195
282
  const netQuality = getEffectiveNetworkQuality();
196
- if (netQuality === "slow" && !isHighPriority(null, request.url)) {
197
- // On slow network, skip caching low priority resources proactively
283
+ const usage = await getUsage(request.url);
284
+ const highPriority = isHighPriority(usage, request.url);
285
+
286
+ if (netQuality === "slow" && !highPriority) {
287
+ logEvent('skip', request.url, 'slow_network_low_priority', {
288
+ networkQuality: netQuality,
289
+ priority: 'low'
290
+ });
291
+
198
292
  if (SDK_CONFIG.debug) {
199
293
  console.log(
200
294
  `[SmartOffline] Skipped caching (slow network, not high priority):`,
@@ -207,6 +301,11 @@ self.addEventListener("fetch", (event) => {
207
301
  const clone = response.clone();
208
302
  caches.open(CACHE_NAME).then((cache) => {
209
303
  cache.put(request.url, clone);
304
+ logEvent('cache', request.url, 'network_fetch_success', {
305
+ type: isAPI ? 'API' : 'PAGE',
306
+ size,
307
+ priority: highPriority ? 'high' : 'normal'
308
+ });
210
309
  });
211
310
 
212
311
  if (SDK_CONFIG.debug) {
@@ -218,25 +317,33 @@ self.addEventListener("fetch", (event) => {
218
317
 
219
318
  return response;
220
319
  })
221
- .catch(() => {
320
+ .catch(async () => {
222
321
  // Offline / network failure
223
322
  trackUsage(request.url);
224
323
 
225
- return getUsage(request.url).then((usage) => {
226
- const highPriority = isHighPriority(usage, request.url);
324
+ const usage = await getUsage(request.url);
325
+ const highPriority = isHighPriority(usage, request.url);
326
+ const cached = await caches.match(request.url);
327
+
328
+ if (cached) {
329
+ logEvent('serve', request.url, 'offline_cache_hit', {
330
+ priority: highPriority ? 'high' : 'normal',
331
+ usage: usage ? { count: usage.count, lastAccessed: usage.lastAccessed } : null
332
+ });
227
333
 
228
334
  if (SDK_CONFIG.debug) {
229
335
  console.log(
230
- `[SmartOffline] ${
231
- highPriority ? "HIGH" : "NORMAL"
232
- } priority:`,
336
+ `[SmartOffline] Served from cache (${highPriority ? "HIGH" : "NORMAL"} priority):`,
233
337
  request.url
234
338
  );
235
339
  }
340
+ } else {
341
+ logEvent('error', request.url, 'cache_miss_offline', {
342
+ priority: highPriority ? 'high' : 'normal'
343
+ });
344
+ }
236
345
 
237
- // v1 behavior: both return cache, but priority is decided & logged
238
- return caches.match(request.url);
239
- });
346
+ return cached;
240
347
  })
241
348
  );
242
349
  });
package/src/index.cjs.js CHANGED
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * SmartOffline SDK (CommonJS version)
3
3
  */
4
+ const eventListeners = {
5
+ cache: [],
6
+ skip: [],
7
+ serve: [],
8
+ clear: [],
9
+ error: []
10
+ };
11
+
4
12
  function init(config = {}) {
5
13
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
6
14
  console.warn("Service Workers not supported");
@@ -16,11 +24,34 @@ function init(config = {}) {
16
24
  maxResourceSize: config.maxResourceSize ?? Infinity,
17
25
  networkQuality: config.networkQuality ?? "auto",
18
26
  significance: config.significance ?? {},
27
+ weights: {
28
+ frequency: config.weights?.frequency ?? 1,
29
+ recency: config.weights?.recency ?? 1,
30
+ size: config.weights?.size ?? 1
31
+ },
32
+ customPriorityFn: config.customPriorityFn ? config.customPriorityFn.toString() : null,
33
+ enableDetailedLogs: config.enableDetailedLogs ?? false
19
34
  };
20
35
 
36
+ if (config.onCacheEvent) {
37
+ eventListeners.cache.push(config.onCacheEvent);
38
+ eventListeners.skip.push(config.onCacheEvent);
39
+ eventListeners.serve.push(config.onCacheEvent);
40
+ eventListeners.clear.push(config.onCacheEvent);
41
+ eventListeners.error.push(config.onCacheEvent);
42
+ }
43
+
21
44
  navigator.serviceWorker.register("/smart-offline-sw.js").then(() => {
22
45
  console.log("Smart Offline Service Worker registered");
23
46
 
47
+ navigator.serviceWorker.addEventListener('message', (event) => {
48
+ if (event.data && event.data.type) {
49
+ const eventType = event.data.type.replace('CACHE_', '').toLowerCase();
50
+ const listeners = eventListeners[eventType] || [];
51
+ listeners.forEach(fn => fn(event.data));
52
+ }
53
+ });
54
+
24
55
  navigator.serviceWorker.ready.then(() => {
25
56
  if (navigator.serviceWorker.controller) {
26
57
  navigator.serviceWorker.controller.postMessage({
@@ -52,7 +83,71 @@ function init(config = {}) {
52
83
  });
53
84
  }
54
85
 
55
- const SmartOffline = { init };
86
+ function on(eventType, callback) {
87
+ if (eventListeners[eventType]) {
88
+ eventListeners[eventType].push(callback);
89
+ }
90
+ }
91
+
92
+ function off(eventType, callback) {
93
+ if (eventListeners[eventType]) {
94
+ const index = eventListeners[eventType].indexOf(callback);
95
+ if (index > -1) {
96
+ eventListeners[eventType].splice(index, 1);
97
+ }
98
+ }
99
+ }
100
+
101
+ async function getCacheLogs() {
102
+ const db = await openDB('smart-offline-logs', 1);
103
+ const tx = db.transaction('logs', 'readonly');
104
+ const store = tx.objectStore('logs');
105
+ return new Promise((resolve) => {
106
+ const request = store.getAll();
107
+ request.onsuccess = () => resolve(request.result || []);
108
+ request.onerror = () => resolve([]);
109
+ });
110
+ }
111
+
112
+ async function clearCacheLogs() {
113
+ const db = await openDB('smart-offline-logs', 1);
114
+ const tx = db.transaction('logs', 'readwrite');
115
+ const store = tx.objectStore('logs');
116
+ store.clear();
117
+ }
118
+
119
+ async function clearCache() {
120
+ const cacheNames = await caches.keys();
121
+ const smartOfflineCaches = cacheNames.filter(name => name.includes('smart-offline'));
122
+ await Promise.all(smartOfflineCaches.map(name => caches.delete(name)));
123
+
124
+ // Also clear usage tracking database
125
+ const db = await openDB('smart-offline', 1);
126
+ const tx = db.transaction('usage', 'readwrite');
127
+ const store = tx.objectStore('usage');
128
+ store.clear();
129
+
130
+ console.log('🗑️ All SmartOffline caches and usage data cleared');
131
+ }
132
+
133
+ function openDB(name, version) {
134
+ return new Promise((resolve, reject) => {
135
+ const request = indexedDB.open(name, version);
136
+ request.onupgradeneeded = () => {
137
+ const db = request.result;
138
+ if (name === 'smart-offline-logs' && !db.objectStoreNames.contains('logs')) {
139
+ db.createObjectStore('logs', { autoIncrement: true });
140
+ }
141
+ if (name === 'smart-offline' && !db.objectStoreNames.contains('usage')) {
142
+ db.createObjectStore('usage', { keyPath: 'url' });
143
+ }
144
+ };
145
+ request.onsuccess = () => resolve(request.result);
146
+ request.onerror = () => reject(request.error);
147
+ });
148
+ }
149
+
150
+ const SmartOffline = { init, on, off, getCacheLogs, clearCacheLogs, clearCache };
56
151
 
57
152
  module.exports = { SmartOffline };
58
153
  module.exports.default = SmartOffline;
package/src/index.d.ts CHANGED
@@ -1,3 +1,32 @@
1
+ export interface UsageData {
2
+ url: string;
3
+ count: number;
4
+ lastAccessed: number;
5
+ }
6
+
7
+ export interface CacheWeights {
8
+ /** Weight for frequency score (default: 1) */
9
+ frequency?: number;
10
+ /** Weight for recency score (default: 1) */
11
+ recency?: number;
12
+ /** Weight for size consideration (default: 1) */
13
+ size?: number;
14
+ }
15
+
16
+ export interface CacheEvent {
17
+ type: 'CACHE_CACHE' | 'CACHE_SKIP' | 'CACHE_SERVE' | 'CACHE_CLEAR' | 'CACHE_ERROR';
18
+ url: string;
19
+ reason: string;
20
+ metadata?: any;
21
+ timestamp: number;
22
+ }
23
+
24
+ export type CustomPriorityFunction = (
25
+ usage: UsageData | null,
26
+ url: string,
27
+ config: SmartOfflineConfig
28
+ ) => number; // Return 0-100, >50 is high priority
29
+
1
30
  export interface SmartOfflineConfig {
2
31
  /** Array of page URL patterns to cache */
3
32
  pages?: string[];
@@ -12,14 +41,37 @@ export interface SmartOfflineConfig {
12
41
  /** Max bytes to cache per resource; larger resources skipped (default: Infinity) */
13
42
  maxResourceSize?: number;
14
43
  /** Network quality setting: 'auto' | 'fast' | 'slow' (default: 'auto') */
15
- networkQuality?: 'auto' | 'fast' | 'slow';
44
+ networkQuality?: "auto" | "fast" | "slow";
16
45
  /** Manual priority overrides by URL pattern */
17
- significance?: Record<string, 'high' | 'normal' | 'low'>;
46
+ significance?: Record<string, "high" | "normal" | "low">;
47
+ /** Weights for priority calculation (default: all 1) */
48
+ weights?: CacheWeights;
49
+ /** Custom priority function for complete control */
50
+ customPriorityFn?: CustomPriorityFunction;
51
+ /** Enable detailed event logging to IndexedDB */
52
+ enableDetailedLogs?: boolean;
53
+ /** Callback for all cache events */
54
+ onCacheEvent?: (event: CacheEvent) => void;
55
+ }
56
+
57
+ export interface CacheLog {
58
+ type: string;
59
+ url: string;
60
+ reason: string;
61
+ metadata?: any;
62
+ timestamp: number;
63
+ date: string;
18
64
  }
19
65
 
20
66
  export interface SmartOfflineSDK {
21
67
  init(config?: SmartOfflineConfig): void;
68
+ on(eventType: 'cache' | 'skip' | 'serve' | 'clear' | 'error', callback: (event: CacheEvent) => void): void;
69
+ off(eventType: 'cache' | 'skip' | 'serve' | 'clear' | 'error', callback: (event: CacheEvent) => void): void;
70
+ getCacheLogs(): Promise<CacheLog[]>;
71
+ clearCacheLogs(): Promise<void>;
72
+ clearCache(): Promise<void>;
22
73
  }
23
74
 
24
75
  export declare const SmartOffline: SmartOfflineSDK;
25
76
  export default SmartOffline;
77
+
package/src/index.js CHANGED
@@ -7,7 +7,20 @@
7
7
  * - maxResourceSize: max bytes to cache per resource; larger resources skipped (default Infinity)
8
8
  * - networkQuality: 'auto' | 'fast' | 'slow' — affects caching aggressiveness (default 'auto')
9
9
  * - significance: { [urlPattern]: 'high' | 'normal' | 'low' } — manual priority overrides
10
+ * - weights: { frequency: number, recency: number, size: number } — priority weights (default all 1)
11
+ * - customPriorityFn: (usage, url, config) => number — custom priority function (0-100)
12
+ * - onCacheEvent: (event) => void — callback for cache events (cache, skip, serve, clear)
10
13
  */
14
+
15
+ // Event listener registry
16
+ const eventListeners = {
17
+ cache: [],
18
+ skip: [],
19
+ serve: [],
20
+ clear: [],
21
+ error: []
22
+ };
23
+
11
24
  function init(config = {}) {
12
25
  if (!("serviceWorker" in navigator)) {
13
26
  console.warn("Service Workers not supported");
@@ -23,11 +36,38 @@ function init(config = {}) {
23
36
  maxResourceSize: config.maxResourceSize ?? Infinity,
24
37
  networkQuality: config.networkQuality ?? "auto",
25
38
  significance: config.significance ?? {},
39
+
40
+ // New features
41
+ weights: {
42
+ frequency: config.weights?.frequency ?? 1,
43
+ recency: config.weights?.recency ?? 1,
44
+ size: config.weights?.size ?? 1
45
+ },
46
+ customPriorityFn: config.customPriorityFn ? config.customPriorityFn.toString() : null,
47
+ enableDetailedLogs: config.enableDetailedLogs ?? false
26
48
  };
27
49
 
28
- navigator.serviceWorker.register("/smart-offline-sw.js").then(() => {
50
+ // Register event listener if provided
51
+ if (config.onCacheEvent) {
52
+ eventListeners.cache.push(config.onCacheEvent);
53
+ eventListeners.skip.push(config.onCacheEvent);
54
+ eventListeners.serve.push(config.onCacheEvent);
55
+ eventListeners.clear.push(config.onCacheEvent);
56
+ eventListeners.error.push(config.onCacheEvent);
57
+ }
58
+
59
+ navigator.serviceWorker.register("/smart-offline-sw.js").then((registration) => {
29
60
  console.log("Smart Offline Service Worker registered");
30
61
 
62
+ // Listen for messages from SW
63
+ navigator.serviceWorker.addEventListener('message', (event) => {
64
+ if (event.data && event.data.type) {
65
+ const eventType = event.data.type.replace('CACHE_', '').toLowerCase();
66
+ const listeners = eventListeners[eventType] || [];
67
+ listeners.forEach(fn => fn(event.data));
68
+ }
69
+ });
70
+
31
71
  navigator.serviceWorker.ready.then(() => {
32
72
  if (navigator.serviceWorker.controller) {
33
73
  navigator.serviceWorker.controller.postMessage({
@@ -59,7 +99,82 @@ function init(config = {}) {
59
99
  });
60
100
  }
61
101
 
62
- const SmartOffline = { init };
102
+ // API for managing event listeners
103
+ function on(eventType, callback) {
104
+ if (eventListeners[eventType]) {
105
+ eventListeners[eventType].push(callback);
106
+ }
107
+ }
108
+
109
+ function off(eventType, callback) {
110
+ if (eventListeners[eventType]) {
111
+ const index = eventListeners[eventType].indexOf(callback);
112
+ if (index > -1) {
113
+ eventListeners[eventType].splice(index, 1);
114
+ }
115
+ }
116
+ }
117
+
118
+ // API to get cache logs
119
+ async function getCacheLogs() {
120
+ const db = await openDB('smart-offline-logs', 1);
121
+ const tx = db.transaction('logs', 'readonly');
122
+ const store = tx.objectStore('logs');
123
+ return new Promise((resolve) => {
124
+ const request = store.getAll();
125
+ request.onsuccess = () => resolve(request.result || []);
126
+ request.onerror = () => resolve([]);
127
+ });
128
+ }
129
+
130
+ // API to clear cache logs
131
+ async function clearCacheLogs() {
132
+ const db = await openDB('smart-offline-logs', 1);
133
+ const tx = db.transaction('logs', 'readwrite');
134
+ const store = tx.objectStore('logs');
135
+ store.clear();
136
+ }
137
+
138
+ // API to clear all caches
139
+ async function clearCache() {
140
+ const cacheNames = await caches.keys();
141
+ const smartOfflineCaches = cacheNames.filter(name => name.includes('smart-offline'));
142
+ await Promise.all(smartOfflineCaches.map(name => caches.delete(name)));
143
+
144
+ // Also clear usage tracking database
145
+ const db = await openDB('smart-offline', 1);
146
+ const tx = db.transaction('usage', 'readwrite');
147
+ const store = tx.objectStore('usage');
148
+ store.clear();
149
+
150
+ console.log('🗑️ All SmartOffline caches and usage data cleared');
151
+ }
152
+
153
+ function openDB(name, version) {
154
+ return new Promise((resolve, reject) => {
155
+ const request = indexedDB.open(name, version);
156
+ request.onupgradeneeded = () => {
157
+ const db = request.result;
158
+ if (name === 'smart-offline-logs' && !db.objectStoreNames.contains('logs')) {
159
+ db.createObjectStore('logs', { autoIncrement: true });
160
+ }
161
+ if (name === 'smart-offline' && !db.objectStoreNames.contains('usage')) {
162
+ db.createObjectStore('usage', { keyPath: 'url' });
163
+ }
164
+ };
165
+ request.onsuccess = () => resolve(request.result);
166
+ request.onerror = () => reject(request.error);
167
+ });
168
+ }
169
+
170
+ const SmartOffline = {
171
+ init,
172
+ on,
173
+ off,
174
+ getCacheLogs,
175
+ clearCacheLogs,
176
+ clearCache
177
+ };
63
178
 
64
179
  export { SmartOffline }; // ✅ named export
65
180
  export default SmartOffline; // ✅ default export