Ctrl K
ComunidadGuías…

src/schedule.js

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

Archivo original ↗Preguntar a KiBot
const FIELDS = [['minute', 0, 59], ['hour', 0, 23], ['day', 1, 31], ['month', 1, 12], ['weekday', 0, 7]];
const MINUTE = 60_000;
export const CATCH_UP_MS = 7 * 24 * 60 * MINUTE;

function parseField(name, src, lo, hi) {
  if (src === '*') return null;
  const out = new Set();
  for (const item of src.split(',')) {
    const m = /^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/.exec(item);
    if (!m) throw new Error(`The ${name} field '${item}' is not a cron term: use *, a number, a-b, or a step like */5`);
    const step = m[3] ? Number(m[3]) : 1;
    const from = m[1] === '*' ? lo : Number(m[1]);
    const to = m[2] ? Number(m[2]) : m[1] === '*' ? hi : from;
    if (step < 1 || from < lo || to > hi || from > to) throw new Error(`The ${name} field '${item}' is out of range ${lo}-${hi}`);
    for (let v = from; v <= to; v += step) out.add(name === 'weekday' && v === 7 ? 0 : v);
  }
  return out;
}

export function parseCron(expr) {
  const parts = String(expr ?? '').trim().split(/\s+/).filter(Boolean);
  if (parts.length !== 5) throw new Error(`A cron schedule has five fields (minute hour day month weekday), not ${parts.length}`);
  const cron = { text: parts.join(' ') };
  FIELDS.forEach(([name, lo, hi], i) => { cron[name] = parseField(name, parts[i], lo, hi); });
  return cron;
}

export function cronMatches(cron, at) {
  const fits = (name) => cron[name] === null || cron[name].has(at[name]);
  if (!fits('minute') || !fits('hour') || !fits('month')) return false;
  if (cron.day !== null && cron.weekday !== null) return fits('day') || fits('weekday');
  return fits('day') && fits('weekday');
}

const formatters = new Map();
const WEEKDAYS = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };

export function zonedParts(date, zone) {
  let fmt = formatters.get(zone);
  if (!fmt) {
    fmt = new Intl.DateTimeFormat('en-US', { timeZone: zone, hourCycle: 'h23', minute: 'numeric', hour: 'numeric', day: 'numeric', month: 'numeric', weekday: 'short' });
    formatters.set(zone, fmt);
  }
  const at = {};
  for (const { type, value } of fmt.formatToParts(date)) {
    if (type === 'weekday') at.weekday = WEEKDAYS[value];
    else if (type === 'minute' || type === 'hour' || type === 'day' || type === 'month') at[type] = Number(value);
  }
  return at;
}

export function dueBetween(cron, zone, since, until) {
  const from = Math.max(since.getTime(), until.getTime() - CATCH_UP_MS);
  const last = Math.floor(until.getTime() / MINUTE) * MINUTE;
  for (let t = last; t > from; t -= MINUTE) {
    const d = new Date(t);
    if (cronMatches(cron, zonedParts(d, zone))) return d;
  }
  return null;
}

export const POLL_WINDOW_MS = 60 * MINUTE;
export const POLL_SLOWEST_DEFAULT = 60;
export const POLL_SLOWEST_MAX = 24 * 60;

export function nextEvery(every, changed, slowest) {
  return changed ? 1 : Math.min(Math.max(1, every) * 2, slowest);
}

const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);

export function diffItems(snapshot, items, keyOf) {
  const changes = [];
  const seen = {};
  for (const item of items) {
    const key = keyOf(item);
    if (key == null || key === '' || typeof key === 'object') continue;
    const k = String(key);
    seen[k] = item;
    if (!snapshot) continue;
    const prev = Object.hasOwn(snapshot, k) ? snapshot[k] : undefined;
    if (prev !== undefined && same(prev, item)) continue;
    const fields = item && typeof item === 'object' ? Object.keys(item) : [];
    const changed = prev && typeof prev === 'object'
      ? [...new Set([...fields, ...Object.keys(prev)])].filter((f) => !same(prev[f], item?.[f]))
      : fields;
    changes.push({ key: k, item, values: Object.fromEntries(changed.filter((f) => item && f in item).map((f) => [f, item[f]])) });
  }
  return { changes, seen };
}

export function retryAfterMs(header, now) {
  if (header == null || header === '') return null;
  const text = String(header).trim();
  if (/^\d+$/.test(text)) return Number(text) * 1000;
  const at = Date.parse(text);
  return Number.isNaN(at) ? null : Math.max(0, at - now);
}

export function pollPrune(log, now) {
  return (log ?? []).filter((at) => now - at < POLL_WINDOW_MS);
}

export function armSchedules(tick, { every = MINUTE, setTimer = setInterval, clearTimer = clearInterval, log = console.error } = {}) {
  const timer = setTimer(() => {
    try { tick(); } catch (err) { log(`weave: schedule tick failed: ${err?.message ?? err}`); }
  }, every);
  timer?.unref?.();
  return { stop() { clearTimer(timer); } };
}