-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsw.js
More file actions
161 lines (149 loc) · 5.38 KB
/
Copy pathsw.js
File metadata and controls
161 lines (149 loc) · 5.38 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
// Service Worker: cache static assets and API bundle with stale-while-revalidate
// Cache version is derived from the SW URL query (?v=BUILD_ID) for easy deploy bumps
const __sw_url = (() => { try { return new URL(self.location.href); } catch(e) { return null; } })();
const __build_from_url = (__sw_url && __sw_url.searchParams.get('v')) || '';
const CACHE_VERSION = (__build_from_url ? ('v' + __build_from_url) : 'v-dev');
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DATA_CACHE = `data-${CACHE_VERSION}`;
const API_CACHE = `api-${CACHE_VERSION}`;
// Minimal pre-cache of critical assets; others are cached on demand
const PRECACHE_URLS = [
'./',
'./index.html',
'./stemcijfers.html',
'./anpupdates.html',
'./nosupdates.html',
'./anpstemmen.html',
'./styles.css',
'./favicon.ico',
// JS
'./js/config.js',
'./js/year-nav.js',
'./js/data.js',
'./js/ui.js',
'./js/auto_refresh.js',
'./js/app.js',
'./js/stemcijfers.js',
'./js/metrics.js',
'./js/anpupdates.js',
'./js/nosupdates.js',
'./js/anpstemmen.js',
'./js/sound.js',
// Static datasets
'./data/partylabels.json',
'./data/votes_kiesraad.json',
'./data/TK2021/anp_votes.json',
'./data/TK2021/anp_last_update.json',
'./data/TK2021/nos_index.json',
'./data/TK2023/anp_votes.json',
'./data/TK2023/anp_last_update.json',
'./data/TK2023/nos_index.json',
'./data/TK2025/anp_votes.json',
'./data/TK2025/anp_last_update.json',
'./data/TK2025/nos_index.json'
];
self.addEventListener('install', event => {
// Do not skipWaiting immediately — activation will be triggered
// by the page after a short, user-friendly delay.
event.waitUntil((async () => {
try {
const cache = await caches.open(STATIC_CACHE);
// Bypass the HTTP cache when precaching to ensure fresh assets on version bump
const requests = PRECACHE_URLS.map((url) => new Request(url, { cache: 'reload' }));
const responses = await Promise.all(requests.map((req) => fetch(req)));
await Promise.all(responses.map((res, i) => cache.put(requests[i], res.clone())));
} catch(e) {
// best effort
}
})());
});
self.addEventListener('activate', event => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys.filter(k => ![STATIC_CACHE, DATA_CACHE, API_CACHE].includes(k))
.map(k => caches.delete(k))
);
await self.clients.claim();
})());
});
// Allow the page to request activation when convenient (deferred takeover)
self.addEventListener('message', (event) => {
const msg = event && event.data;
if (msg && (msg === 'SKIP_WAITING' || (msg.type && msg.type === 'SKIP_WAITING'))) {
try { self.skipWaiting(); } catch(e) {}
}
});
function isSameOrigin(url) {
try { const u = new URL(url); return u.origin === self.location.origin; } catch(e){ return false; }
}
function isStaticAsset(url){
return isSameOrigin(url) && (
url.endsWith('.css') || url.endsWith('.js') || url.endsWith('.ico') ||
url.endsWith('.png') || url.endsWith('.jpg') || url.endsWith('.svg') ||
url.endsWith('index.html') || url.endsWith('.html') ||
url.endsWith('data/partylabels.json') || url.endsWith('data/votes_kiesraad.json')
);
}
function isFinalizedData(url){
// Support new TKYYYY folders (preferred) and legacy YYYY folders
return isSameOrigin(url) && /\/data\/(TK\d{4}|\d{4})\/[^?]+\.json$/.test(url);
}
function isFunctionAPI(url){
// Match our serverless getdata endpoint (domain-agnostic). Adjust if your path changes.
return /\/default\/getdata(\?|$)/.test(url);
}
self.addEventListener('fetch', event => {
const req = event.request;
if (req.method !== 'GET') return;
const url = req.url;
// Static assets: Cache First
if (isStaticAsset(url)) {
event.respondWith((async () => {
const cached = await caches.match(req);
if (cached) return cached;
try {
const res = await fetch(new Request(req, { cache: 'reload' }));
const cache = await caches.open(STATIC_CACHE);
cache.put(req, res.clone());
return res;
} catch(e) {
return cached || Response.error();
}
})());
return;
}
// Finalized data: Cache First (effectively immutable)
if (isFinalizedData(url)) {
event.respondWith((async () => {
const cached = await caches.match(req);
if (cached) return cached;
try {
const res = await fetch(new Request(req, { cache: 'reload' }));
const cache = await caches.open(DATA_CACHE);
cache.put(req, res.clone());
return res;
} catch(e) {
return cached || Response.error();
}
})());
return;
}
// Function API (live data): Stale-While-Revalidate with graceful network failure handling
if (isFunctionAPI(url)) {
event.respondWith((async () => {
const cache = await caches.open(API_CACHE);
const cached = await cache.match(req);
try {
const res = await fetch(req);
try { cache.put(req, res.clone()); } catch(e) {}
return cached || res; // serve cached immediately if present
} catch (e) {
// Network/CORS failed — serve cached if available, otherwise synthesize a JSON error to avoid uncaught rejections
if (cached) return cached;
return new Response(JSON.stringify({ error: 'network', message: 'Failed to fetch live data' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
}
})());
return;
}
});