// captura.mjs — la lectura de red que NO se puede hacer mal por descuido. // // POR QUE EXISTE: el 2026-09-20 cometi TRES VECES EN UN DIA el mismo fallo, en tres // herramientas distintas, todas escritas despues de haber anotado la regla en el protocolo: // // 1. el barrido leia el 13% de lo disponible porque mandaba `search` al pool entero // dentro de un catch vacio, y los reles sin NIP-50 rechazan la consulta ENTERA; // 2. una medicion dijo NO_EVALUABLE con 1.741 eventos; troceada daba 5.539; // 3. un buscador de recompensas devolvio 0 porque pedia 180 dias con limit 500, y el // rele devuelve los 500 MAS RECIENTES — lo de hace tres meses no salia nunca. // // Las tres son la misma historia: **un tope de rele no se nota**. No hay error, no hay // aviso; solo una muestra mas pequena de lo que crees, y un cero que parece un hallazgo. // // Escribir la regla en un documento no la instala. Esto la instala: si lees a traves de // aqui, el troceado, la deduplicacion y la declaracion de cobertura no son opcionales. // // USO: // import { leer } from './lib/captura.mjs' // const r = await leer(pool, RELAYS, { kinds: [1] }, { dias: 7 }) // r.eventos // Map id -> evento, unicos // r.cobertura // { rele: n | 'ERROR' } // r.topes // cuantos tramos llegaron al limite -> las cifras son SUELOS // r.esSuelo // boolean: si true, NO se publica un numero sin decir que es un suelo // r.informe() // texto listo para pegar en una publicacion const LIMITE = 500 export async function leer (pool, relays, filtro, opciones = {}) { const { dias = 1, tramoSegundos = 3600, // 1 hora por defecto: lo bastante fino para no topar limite = LIMITE, maxWait = 10000, hasta = Math.floor(Date.now() / 1000), } = opciones const desde = hasta - Math.round(dias * 86400) const eventos = new Map() const cobertura = {} const errores = {} let topes = 0 const tramosTopados = [] for (const url of relays) { let n = 0 let fallo = null for (let t = desde; t < hasta; t += tramoSegundos) { const fin = Math.min(t + tramoSegundos, hasta) let ev = null try { ev = await pool.querySync([url], { ...filtro, since: t, until: fin, limit: limite }, { maxWait }) } catch (e) { // NUNCA un catch vacio: el motivo se guarda y se declara. fallo = String(e?.message || e).slice(0, 80) ev = null } if (ev === null) continue if (ev.length >= limite) { topes++; tramosTopados.push({ url, desde: t }) } n += ev.length for (const e of ev) eventos.set(e.id, e) } cobertura[url] = fallo && n === 0 ? 'ERROR' : n if (fallo) errores[url] = fallo } const aportan = Object.values(cobertura).filter((v) => typeof v === 'number' && v > 0).length const esSuelo = topes > 0 || aportan < relays.length return { eventos, cobertura, errores, topes, tramosTopados, aportan, esSuelo, informe () { const L = [] L.push(`eventos unicos: ${eventos.size} | reles que aportan: ${aportan} de ${relays.length}`) for (const [u, v] of Object.entries(cobertura)) { L.push(` ${u.replace('wss://', '').padEnd(28)} ${v === 0 ? 'contesta, NO TIENE' : v}${errores[u] ? ' (' + errores[u] + ')' : ''}`) } if (topes) L.push(`⚠️ ${topes} tramo(s) alcanzaron el limite de ${limite}: TODA cifra derivada es un SUELO.`) else L.push(`ningun tramo alcanzo el limite: las cifras no estan recortadas por el tope del rele.`) if (aportan < relays.length) L.push(`⚠️ ${relays.length - aportan} rele(s) no aportaron nada: cualquier cero es parcialmente mio.`) return L.join('\n') }, } } // Atajo para cuando el filtro lleva un campo OPCIONAL (como `search`) que algunos reles // rechazan por completo: se intenta con el campo y, si un rele no da nada, se reintenta // SIN el. Un rele que rechaza la consulta entera no puede quedarse fuera en silencio. export async function leerConOpcional (pool, relays, filtroBase, campoOpcional, opciones = {}) { const conOpc = await leer(pool, relays, { ...filtroBase, ...campoOpcional }, opciones) const mudos = relays.filter((u) => !conOpc.cobertura[u]) if (!mudos.length) return { ...conOpc, rescatados: [] } const rescate = await leer(pool, mudos, filtroBase, opciones) for (const [id, e] of rescate.eventos) conOpc.eventos.set(id, e) for (const [u, v] of Object.entries(rescate.cobertura)) conOpc.cobertura[u] = v conOpc.topes += rescate.topes conOpc.aportan = Object.values(conOpc.cobertura).filter((v) => typeof v === 'number' && v > 0).length conOpc.esSuelo = conOpc.topes > 0 || conOpc.aportan < relays.length return { ...conOpc, rescatados: mudos.filter((u) => conOpc.cobertura[u] > 0) } }