Fuente M440, modo sin conexión (servidor sin internet y app sin servidor), cola del dispositivo en Colas y arreglo del reescalado de tiras altas

- M440 (m440.in, español): búsqueda, populares, actualizados paginados, ficha, capítulos, descargas y seguimiento. La lista de capítulos viene cifrada: la clave se saca de un script ofuscado del sitio ejecutándolo en un proceso aislado con permisos mínimos (m440-key.mjs) y se renueva sola.
- Servidor sin internet (connectivity.js): se detecta en segundos; las fuentes fallan al instante, la portada muestra lo descargado en el servidor con buscador, portadas locales de respaldo, descargas y comprobaciones en pausa y reanudación automática al volver la conexión.
- App sin conexión con el servidor: el service worker usa la copia guardada si el servidor no contesta en 3 s, las peticiones comprueban si el servidor sigue vivo en vez de quedarse colgadas, aviso arriba e inicio con lo guardado en el dispositivo; al volver, se reconecta sola y envía el progreso pendiente.
- Colas: sección «En este dispositivo» con el guardado en curso en directo y cancelación (sin dejar capítulos a medias); también visible sin servidor.
- Imágenes: el proxy y el guardado en el dispositivo reintentan con tiempo límite cuando el CDN de la fuente falla.
- Reescalado HD: las páginas que superan 16383 px al reescalar (tiras de webtoon) se reducen al límite de WebP en vez de hacer fallar el capítulo.
- Entidades HTML con nombre en las sinopsis; pestañas de fuentes cortas en móvil; portadas rotas con icono de respaldo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 09:52:19 -06:00
co-authored by Claude Opus 5.5
parent d3e196a2f3
commit 86999b5725
11 changed files with 712 additions and 78 deletions
+27 -9
View File
@@ -13,20 +13,38 @@ self.addEventListener('activate', e => {
.then(() => self.clients.claim()))
})
// Red primero (así siempre se ve la última versión) y la caché si no hay conexión.
// Red primero (así siempre se ve la última versión) y la caché si no hay conexión. Si hay copia guardada
// y el servidor no contesta en NET_WAIT ms (red que no llega, servidor caído o colgado), se usa la copia
// sin esperar a que el navegador dé la conexión por perdida; la petición sigue y actualiza la caché.
const NET_WAIT = 3000
const DOWN_FOR = 20e3
let downUntil = 0
// La app distingue así una copia guardada de una respuesta real del servidor.
async function fromCache(res) {
const headers = new Headers(res.headers)
headers.set('x-from-cache', '1')
return new Response(await res.blob(), { status: res.status, statusText: res.statusText, headers })
}
async function networkFirst(req, cacheName, { store = true, ignoreSearch = false, fallback } = {}) {
try {
const res = await fetch(req)
const cached = () => caches.match(req, { ignoreSearch, cacheName }).then(hit => hit || (fallback && caches.match(fallback)))
const net = fetch(req).then(async res => {
if (store && res.ok) {
const c = await caches.open(cacheName)
if (store === true || (await c.match(req, { ignoreSearch }))) c.put(req, res.clone())
if (store === true || (await c.match(req, { ignoreSearch }))) await c.put(req, res.clone())
}
return res
} catch (err) {
const hit = await caches.match(req, { ignoreSearch, cacheName }) || (fallback && await caches.match(fallback))
if (hit) return hit
throw err
}
})
net.then(() => { downUntil = 0 }, () => {})
const hit = await cached()
if (!hit) return net // sin copia: solo queda esperar a la red
// Si hace poco que el servidor no contestaba, la copia directamente (la red sigue intentándolo).
if (Date.now() < downUntil) return fromCache(hit)
let timer
const timeout = new Promise(r => { timer = setTimeout(() => r(null), NET_WAIT) })
const res = await Promise.race([net.catch(() => null), timeout])
clearTimeout(timer)
if (!res) downUntil = Date.now() + DOWN_FOR
return res || fromCache(hit)
}
self.addEventListener('fetch', e => {