'use strict'; const fs = require('fs-extra'); const path = require('path'); const debug = require('debug')('agm:fatal'); function normalizeError(errLike) { if (!errLike) { return { code: undefined, message: 'Unknown error', stack: undefined }; } if (errLike instanceof Error) { return { code: errLike.code, message: String(errLike.message || errLike), stack: errLike.stack, }; } if (typeof errLike === 'string') { return { code: undefined, message: errLike, stack: undefined }; } // Promise rejection reasons can be arbitrary try { const message = String(errLike.message || errLike.toString?.() || errLike); return { code: errLike.code, message, stack: errLike.stack }; } catch { return { code: undefined, message: 'Unknown error', stack: undefined }; } } async function readLastReportSafe(filePath) { try { const raw = await fs.readFile(filePath, 'utf8'); if (!raw) return null; return JSON.parse(raw); } catch (err) { // Missing file is fine if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return null; // Corrupt JSON (or other read error): archive it once so we can proceed. try { const dir = path.dirname(filePath); const base = path.basename(filePath); const archived = path.join(dir, `${base}.corrupt.${Date.now()}`); await fs.move(filePath, archived, { overwrite: false }); debug('Archived corrupt fatal report:', archived); } catch (archiveErr) { debug('Failed to archive corrupt fatal report:', archiveErr && (archiveErr.message || archiveErr)); } return null; } } async function atomicWriteJson(filePath, obj) { const dir = path.dirname(filePath); await fs.ensureDir(dir); const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`; const payload = JSON.stringify(obj); await fs.writeFile(tmpPath, payload, 'utf8'); // Atomic on same filesystem await fs.rename(tmpPath, filePath); } function sameError(a, b) { if (!a || !b) return false; if (a.code && b.code && a.code === b.code) return true; return a.message && b.message && a.message === b.message; } function parseWhen(when) { if (!when) return null; try { const d = new Date(when); if (Number.isNaN(d.getTime())) return null; return d; } catch { return null; } } async function reportFatal(opts) { const { filePath, kind, error, message, throttleMs = 2 * 60 * 1000, emailEnabled = false, emailTo, mailer, } = opts || {}; if (!filePath) return; try { const normalized = normalizeError(error); const now = new Date(); const last = await readLastReportSafe(filePath); const lastWhen = parseWhen(last && last.when); const current = { code: normalized.code, message: normalized.message, when: now.toISOString(), kind: kind || 'fatal', }; if (lastWhen && sameError(last, current)) { const delta = now.getTime() - lastWhen.getTime(); if (delta >= 0 && delta < throttleMs) { return; } } await atomicWriteJson(filePath, current); if (emailEnabled && mailer && typeof mailer.sendTextMail === 'function') { const subject = `[Agm-Errors] ${current.kind}`; const body = message || (normalized.stack ? `${normalized.message}\n${normalized.stack}` : normalized.message); await mailer.sendTextMail({ subject, text: body }, emailTo); } } catch (err) { // Never throw from a fatal reporter debug('Fatal reporter failed:', err && (err.message || err)); } } module.exports = { reportFatal, };