Repository navigation
Expand file tree
/
Copy pathsw.js
More file actions
293 lines (264 loc) · 9.26 KB
/
Copy pathsw.js
File metadata and controls
293 lines (264 loc) · 9.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// StudyFlow Service Worker v2.5
// Enhanced offline mode with IndexedDB sync queue
const CACHE_NAME = 'studyflow-v2.5-production';
const RUNTIME_CACHE = 'studyflow-runtime-v2.5';
const CDN_CACHE = 'studyflow-cdn-v2.5';
const IDB_NAME = 'studyflow-offline-db';
const IDB_VERSION = 1;
// Static assets to cache immediately
const urlsToCache = [
'./',
'./index.html',
'./Assets/style.css',
'./Assets/script.js',
'./Assets/critical.css',
'./Assets/notes.js',
'./Assets/export.js',
'./manifest.json',
'./Assets/brain-duotone.png',
'./Assets/chart.umd.min.js'
];
// CDN resources to cache (with longer TTL)
const cdnUrls = [
'https://cdn.tailwindcss.com',
'https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.15/vue.global.prod.min.js',
'https://cdn.jsdelivr.net/npm/@phosphor-icons/web',
'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js'
];
// IndexedDB helpers
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(IDB_NAME, IDB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object stores
if (!db.objectStoreNames.contains('notes')) {
db.createObjectStore('notes', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('offlineQueue')) {
const queueStore = db.createObjectStore('offlineQueue', { keyPath: 'id', autoIncrement: true });
queueStore.createIndex('timestamp', 'timestamp', { unique: false });
}
if (!db.objectStoreNames.contains('sessions')) {
db.createObjectStore('sessions', { keyPath: 'id', autoIncrement: true });
}
if (!db.objectStoreNames.contains('tasks')) {
db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true });
}
console.log('✅ IndexedDB stores created');
};
});
}
// Helper to promisify IDB Request
function idbRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Add to offline queue
async function addToOfflineQueue(request) {
try {
const db = await openDB();
const tx = db.transaction('offlineQueue', 'readwrite');
const store = tx.objectStore('offlineQueue');
const queueItem = {
url: request.url,
method: request.method,
headers: Array.from(request.headers.entries()),
body: await request.clone().text(),
timestamp: Date.now()
};
await idbRequest(store.add(queueItem));
console.log('📥 Added to offline queue:', request.url);
} catch (error) {
console.error('Failed to add to queue:', error);
}
}
// Process offline queue when online
async function processOfflineQueue() {
try {
const db = await openDB();
const tx = db.transaction('offlineQueue', 'readwrite');
const store = tx.objectStore('offlineQueue');
const items = await idbRequest(store.getAll());
if (!items || !items.length) return;
for (const item of items) {
try {
const response = await fetch(item.url, {
method: item.method,
headers: new Headers(item.headers),
body: item.body || undefined
});
if (response.ok) {
// Re-open transaction for delete (transactions auto-close)
const delTx = db.transaction('offlineQueue', 'readwrite');
await idbRequest(delTx.objectStore('offlineQueue').delete(item.id));
console.log('✅ Synced offline request:', item.url);
}
} catch (error) {
console.warn('Failed to sync:', item.url, error);
}
}
} catch (error) {
console.error('Queue processing error:', error);
}
}
// Install Service Worker
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('✅ Service Worker: Cache opened');
return cache.addAll(urlsToCache);
})
.catch((error) => {
console.error('❌ Service Worker: Cache install failed', error);
})
);
self.skipWaiting();
});
// Fetch Event - Optimized Caching Strategy
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip unsupported schemes (e.g., chrome-extension://)
if (!url.protocol.startsWith('http')) return;
// Skip non-GET requests
if (request.method !== 'GET') return;
// API calls - Network First with offline fallback
if (url.pathname.includes('/api') || url.hostname.includes('render.com')) {
event.respondWith(
fetch(request)
.then((response) => {
// Only cache successful responses
if (response && response.status === 200 && response.type === 'basic') {
const responseClone = response.clone();
caches.open(RUNTIME_CACHE).then((cache) => {
cache.put(request, responseClone).catch(err => console.log('Cache put error:', err));
});
}
return response;
})
.catch(async (error) => {
// For POST/PUT/DELETE, add to offline queue
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) {
await addToOfflineQueue(request);
return new Response(JSON.stringify({
offline: true,
message: 'Request queued for sync when online'
}), {
status: 202,
headers: { 'Content-Type': 'application/json' }
});
}
// For GET, try cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
return new Response(JSON.stringify({
error: 'Offline - No cached version available'
}), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
})
);
return;
}
// CDN Resources - Cache First with 7-day expiry
if (url.hostname.includes('cdn.') || url.hostname.includes('cdnjs.') || url.hostname.includes('unpkg.')) {
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
// Check if cache is older than 7 days
const cacheDate = new Date(cachedResponse.headers.get('date'));
const now = new Date();
const daysDiff = (now - cacheDate) / (1000 * 60 * 60 * 24);
if (daysDiff < 7) {
return cachedResponse;
}
}
return fetch(request).then((response) => {
// Cache successful responses (including opaque responses from CDN)
if (response && (response.status === 200 || response.type === 'opaque')) {
const responseClone = response.clone();
caches.open(CDN_CACHE).then((cache) => {
cache.put(request, responseClone).catch(err => {
// Silently fail for opaque responses
if (response.type !== 'opaque') {
console.log('CDN cache error:', err);
}
});
});
}
return response;
}).catch(() => cachedResponse || fetch(request));
})
);
return;
}
// Local Assets - Stale-While-Revalidate
event.respondWith(
caches.match(request).then((cachedResponse) => {
const fetchPromise = fetch(request).then((response) => {
if (response && response.status === 200 && response.type === 'basic') {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone).catch(err => console.log('Asset cache error:', err));
});
}
return response;
});
return cachedResponse || fetchPromise;
}).catch(() => {
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('./index.html');
}
return new Response('Offline - No cached version available', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({ 'Content-Type': 'text/plain' })
});
})
);
});
// Activate & Remove Old Caches
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME, RUNTIME_CACHE, CDN_CACHE];
event.waitUntil(
Promise.all([
// Clean old caches
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
console.log('🗑️ Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// Process offline queue
processOfflineQueue()
])
);
self.clients.claim();
console.log('✅ Service Worker v2.5 activated, offline queue processed');
});
// Listen for online event to sync
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SYNC_OFFLINE_QUEUE') {
processOfflineQueue();
}
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-offline-data') {
event.waitUntil(processOfflineQueue());
}
});