/* AO3 Mirror Service Worker v5 — Client-side cache + offline fallback + navigation intercept
*
* Deployed at /sw-YYYYMMDD.js (versioned), /sw.js redirects to latest.
* Server injects into HTML pages.
*/
'use strict';
const MIRROR_DOMAINS = ['agento3.miscs.dev'];
const PRIMARY_DOMAIN = 'agento3.miscs.dev';
// Static asset types to cache aggressively
const STATIC_EXTENSIONS = [
'.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.ico',
'.woff', '.woff2', '.ttf', '.svg', '.webp', '.json',
];
// Cache names
const STATIC_CACHE = 'ao3-static-v1';
const HTML_CACHE = 'ao3-html-v1';
// Navigation timeout (25s, like go3)
const NAV_FETCH_TIMEOUT_MS = 25000;
// ─── Helpers ──────────────────────────────────────────────────────────
function primaryMirrorHost() {
try {
var scopeHost = new URL(self.registration.scope).hostname;
if (scopeHost && scopeHost.indexOf('.') !== -1) return scopeHost;
} catch (e) {}
for (var i = 0; i < MIRROR_DOMAINS.length; i++) {
var h = MIRROR_DOMAINS[i];
if (h && h.indexOf('.') !== -1) return h;
}
return PRIMARY_DOMAIN;
}
function isStaticAsset(url) {
var path = url.pathname.toLowerCase();
for (var i = 0; i < STATIC_EXTENSIONS.length; i++) {
if (path.endsWith(STATIC_EXTENSIONS[i])) return true;
}
return false;
}
function hostFromRequest(request) {
try {
return new URL(request.url).hostname || '';
} catch (e) {
return '';
}
}
function repairNavigationURL(url) {
var host = url.hostname;
if (!host || host.indexOf('.') !== -1 || host === 'localhost') return url;
var mirror = primaryMirrorHost();
if (!mirror) return url;
var fixed = new URL(url.toString());
fixed.hostname = mirror;
fixed.pathname = '/' + host + (fixed.pathname || '/');
return fixed;
}
// ─── Offline / mirror picker page ─────────────────────────────────────
function buildMirrorPickerPage(currentHost, mirrors) {
var mirrorItems = mirrors
.filter(function (h) { return h; })
.map(function (h) {
return '
' + h + '';
})
.join('');
return (
'无法访问镜像站点' +
'' +
'
无法连接到镜像站点
' +
'
浏览器未能与 ' + currentHost + ' 建立网络连接(例如断网、DNS 失败或被防火墙拦截)。请按下面顺序逐步排查。
' +
'
' +
'1先检查网络连接
' +
'确认设备已联网:可尝试打开其他网站或 App。若使用 Wi‑Fi,请检查路由器是否正常。
' +
'2尝试切换到移动流量
' +
'部分宽带或校园网可能对镜像域名有限制。请关闭 Wi‑Fi,使用手机 4G / 5G 流量 重新访问。
' +
'3尝试备用域名
' +
'点击下方备用镜像站点:
' +
' ' +
'
' +
'
页面由 AO3 Mirror Service Worker 提供。若以上步骤后仍无法打开,请稍后再试。
' +
'
'
);
}
function offlineGuideResponse(currentHost) {
return new Response(buildMirrorPickerPage(currentHost, MIRROR_DOMAINS), {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
},
});
}
// ─── Network strategies ───────────────────────────────────────────────
function handleNavigation(request) {
var currentHost = hostFromRequest(request);
var url = new URL(request.url);
// Repair malformed navigation URLs
var navUrl = repairNavigationURL(url);
var fetchInit = {};
if (navUrl.href !== request.url) {
fetchInit = {
method: request.method,
headers: request.headers,
credentials: request.credentials,
redirect: 'follow',
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
};
}
var ctrl = new AbortController();
var timer = setTimeout(function () { ctrl.abort(); }, NAV_FETCH_TIMEOUT_MS);
var fetchTarget = navUrl.href !== request.url ? new Request(navUrl.href, fetchInit) : request;
return fetch(fetchTarget, { signal: ctrl.signal })
.finally(function () { clearTimeout(timer); })
.then(function (response) {
if (response.redirected && response.url) {
return Response.redirect(response.url, 302);
}
return response;
})
.catch(function () {
return offlineGuideResponse(currentHost);
});
}
function handleStatic(request) {
// Cache-first for static assets
return caches.open(STATIC_CACHE).then(function (cache) {
return cache.match(request).then(function (cached) {
if (cached) {
// Background revalidation
fetch(request).then(function (response) {
if (response && response.ok) {
cache.put(request, response);
}
}).catch(function () {});
return cached;
}
// Network with cache fallback
return fetch(request).then(function (response) {
if (response && response.ok) {
var cloned = response.clone();
cache.put(request, cloned);
}
return response;
}).catch(function () {
// Offline — return whatever we have
return cache.match(request);
});
});
});
}
// ─── Install / Activate ───────────────────────────────────────────────
self.addEventListener('install', function (event) {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
// Clean old caches
event.waitUntil(
caches.keys().then(function (keys) {
return Promise.all(
keys.map(function (key) {
if (key !== STATIC_CACHE && key !== HTML_CACHE) {
return caches.delete(key);
}
})
);
})
);
});
// ─── Fetch handler ────────────────────────────────────────────────────
self.addEventListener('fetch', function (event) {
if (event.request.method !== 'GET') return;
var url = new URL(event.request.url);
// Don't intercept SW or monitor paths
if (url.pathname.startsWith('/sw') || url.pathname === '/mirror-domains.json') return;
if (url.pathname === '/_monitor' || url.pathname.startsWith('/_monitor/')) return;
if (url.pathname === '/stats' || url.pathname === '/metrics' || url.pathname === '/health') return;
// Static assets: cache-first
if (isStaticAsset(url)) {
event.respondWith(handleStatic(event.request));
return;
}
// Navigation (HTML pages): network-first with offline fallback
if (event.request.mode === 'navigate' || event.request.destination === 'document') {
event.respondWith(
handleNavigation(event.request).catch(function () {
return offlineGuideResponse(hostFromRequest(event.request));
})
);
}
// Other requests pass through to server normally
});