Ctrl K
ComunidadGuías…

dead-boards.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Persistent board-existence memory for reverse ATS sweeps (#2840).
// Only a real HTTP 404 may advance a miss counter. Throttles, transport
// failures and DNS errors mean "unknown", never "dead".

import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';

export const DEAD_BOARD_HEADER = 'ats\tboard\tmisses\tlast_checked';
export const DEAD_BOARD_MISSES = 3;
export const DEAD_BOARD_RECHECK_DAYS = 30;

function key(ats, board) { return `${ats}\t${board}`; }

export function loadDeadBoards(file, now = Date.now()) {
  const rows = new Map();
  if (!existsSync(file)) return rows;
  for (const line of readFileSync(file, 'utf8').split(/\r?\n/).slice(1)) {
    const [ats, board, missesRaw, checked] = line.split('\t');
    const misses = Number(missesRaw);
    const lastChecked = Date.parse(checked || '');
    if (!ats || !board || !Number.isInteger(misses) || misses < 1 || !Number.isFinite(lastChecked)) continue;
    rows.set(key(ats, board), { ats, board, misses, lastChecked, now });
  }
  return rows;
}

export function boardKey(entry) {
  return String(entry?.careers_url || entry?.name || '').trim();
}

export function shouldSkipDeadBoard(rows, ats, board, now = Date.now()) {
  const row = rows.get(key(ats, board));
  return Boolean(row && row.misses >= DEAD_BOARD_MISSES && now - row.lastChecked < DEAD_BOARD_RECHECK_DAYS * 86_400_000);
}

export function recordBoardResult(rows, ats, board, status, now = Date.now()) {
  const rowKey = key(ats, board);
  if (status === 200) {
    rows.delete(rowKey);
    return;
  }
  const previous = rows.get(rowKey);
  if (status !== 404) {
    if (!previous || previous.misses < DEAD_BOARD_MISSES) {
      rows.delete(rowKey);
    } else {
      rows.set(rowKey, { ...previous, lastChecked: now });
    }
    return;
  }
  const misses = Math.min(DEAD_BOARD_MISSES, (previous?.misses || 0) + 1);
  rows.set(rowKey, { ats, board, misses, lastChecked: now });
}

/**
 * Record a failed fetch, given the error instead of a status picked off it.
 *
 * A provider that walks several pages marks its errors with `firstPage`
 * (providers/icims.mjs). `firstPage: false` means the board had already
 * answered its first page with postings when a later request failed, so the
 * board is there whatever that later status was: it is recorded the way a
 * success is. Without this a 404 on page 3 of a live board counted as a miss,
 * and three sweeps of it retired the board for the whole re-probe window.
 *
 * An error without the mark is read as before, by its status.
 *
 * @param {Map<string, object>} rows - Dead-board rows from loadDeadBoards().
 * @param {string} ats - Source id.
 * @param {string} board - Board key from boardKey().
 * @param {unknown} err - What the provider threw.
 * @param {number} [now] - Clock, for tests.
 * @returns {void}
 */
export function recordBoardFailure(rows, ats, board, err, now = Date.now()) {
  recordBoardResult(rows, ats, board, err?.firstPage === false ? 200 : err?.status, now);
}

export function saveDeadBoards(file, rows) {
  mkdirSync(dirname(file), { recursive: true });
  const body = [...rows.values()]
    .sort((a, b) => a.ats.localeCompare(b.ats) || a.board.localeCompare(b.board))
    .map((row) => `${row.ats}\t${row.board}\t${row.misses}\t${new Date(row.lastChecked).toISOString()}`);
  const tmp = `${file}.tmp`;
  writeFileSync(tmp, `${DEAD_BOARD_HEADER}\n${body.length ? `${body.join('\n')}\n` : ''}`, 'utf8');
  renameSync(tmp, file);
}