Actualizar index.js
This commit is contained in:
parent
ed229f2e0d
commit
fbabfe0c9e
356
index.js
356
index.js
|
|
@ -1,10 +1,14 @@
|
||||||
// estados-homeserve/index.js
|
// estados-homeserve/index.js
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
const { chromium } = require('playwright');
|
const { chromium } = require('playwright');
|
||||||
const admin = require('firebase-admin');
|
const admin = require('firebase-admin');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Firebase Admin Init
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
function mustEnv(name) {
|
function mustEnv(name) {
|
||||||
const v = process.env[name];
|
const v = process.env[name];
|
||||||
if (!v) throw new Error(`Missing env: ${name}`);
|
if (!v) throw new Error(`Missing env: ${name}`);
|
||||||
|
|
@ -13,6 +17,7 @@ function mustEnv(name) {
|
||||||
|
|
||||||
function initFirebase() {
|
function initFirebase() {
|
||||||
if (!process.env.FIREBASE_PRIVATE_KEY) throw new Error('Missing env: FIREBASE_PRIVATE_KEY');
|
if (!process.env.FIREBASE_PRIVATE_KEY) throw new Error('Missing env: FIREBASE_PRIVATE_KEY');
|
||||||
|
|
||||||
if (!admin.apps.length) {
|
if (!admin.apps.length) {
|
||||||
admin.initializeApp({
|
admin.initializeApp({
|
||||||
credential: admin.credential.cert({
|
credential: admin.credential.cert({
|
||||||
|
|
@ -25,20 +30,32 @@ function initFirebase() {
|
||||||
return admin.firestore();
|
return admin.firestore();
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG = {
|
const db = initFirebase();
|
||||||
HOMESERVE_BASE_URL: process.env.HOMESERVE_BASE_URL || 'https://gestor.homeserve.es/',
|
|
||||||
HOMESERVE_USER: mustEnv('HOMESERVE_USER'),
|
|
||||||
HOMESERVE_PASS: mustEnv('HOMESERVE_PASS'),
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Config
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
const CONFIG = {
|
||||||
|
// HomeServe base URL (fallback si no hay en Firestore)
|
||||||
|
HOMESERVE_BASE_URL: process.env.HOMESERVE_BASE_URL || 'https://gestor.homeserve.es/',
|
||||||
|
|
||||||
|
// Colecciones
|
||||||
QUEUE_COLLECTION: process.env.QUEUE_COLLECTION || 'homeserve_cambios_estado',
|
QUEUE_COLLECTION: process.env.QUEUE_COLLECTION || 'homeserve_cambios_estado',
|
||||||
RESULT_COLLECTION: process.env.RESULT_COLLECTION || 'homeserve_cambios_estado_log',
|
RESULT_COLLECTION: process.env.RESULT_COLLECTION || 'homeserve_cambios_estado_log',
|
||||||
|
|
||||||
// TTL de claim (si un worker muere, otro puede reintentar)
|
// Credenciales en Firestore
|
||||||
|
PROVIDER_CREDENTIALS_COLLECTION: process.env.PROVIDER_CREDENTIALS_COLLECTION || 'providerCredentials',
|
||||||
|
PROVIDER_DOC_ID: process.env.PROVIDER_DOC_ID || 'homeserve',
|
||||||
|
|
||||||
|
// Control de “claim”
|
||||||
CLAIM_TTL_MINUTES: parseInt(process.env.CLAIM_TTL_MINUTES || '10', 10),
|
CLAIM_TTL_MINUTES: parseInt(process.env.CLAIM_TTL_MINUTES || '10', 10),
|
||||||
|
|
||||||
// rescaneo por si un listener se pierde un evento (seguridad)
|
// Concurrencia (para que no te arranque 20 Chromiums)
|
||||||
RESCAN_SECONDS: parseInt(process.env.RESCAN_SECONDS || '60', 10),
|
MAX_CONCURRENCY: parseInt(process.env.MAX_CONCURRENCY || '1', 10),
|
||||||
|
|
||||||
|
// Selectores HomeServe (como ya tenías)
|
||||||
SEL: {
|
SEL: {
|
||||||
user: process.env.SEL_USER || 'input[type="text"]',
|
user: process.env.SEL_USER || 'input[type="text"]',
|
||||||
pass: process.env.SEL_PASS || 'input[type="password"]',
|
pass: process.env.SEL_PASS || 'input[type="password"]',
|
||||||
|
|
@ -65,20 +82,52 @@ const STATE_MAP = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
function nowMs() { return Date.now(); }
|
const nowMs = () => Date.now();
|
||||||
function toServerTimestamp() { return admin.firestore.FieldValue.serverTimestamp(); }
|
const toServerTimestamp = () => admin.firestore.FieldValue.serverTimestamp();
|
||||||
|
|
||||||
function isPendingStatus(st) {
|
/**
|
||||||
return st === undefined || st === null || st === 'PENDING';
|
* =========================
|
||||||
|
* Credenciales desde Firestore (con cache)
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
let credsCache = null;
|
||||||
|
let credsCacheAt = 0;
|
||||||
|
const CREDS_TTL_MS = 60_000; // 1 min
|
||||||
|
|
||||||
|
async function getHomeServeCreds() {
|
||||||
|
const now = nowMs();
|
||||||
|
if (credsCache && (now - credsCacheAt) < CREDS_TTL_MS) return credsCache;
|
||||||
|
|
||||||
|
const ref = db.collection(CONFIG.PROVIDER_CREDENTIALS_COLLECTION).doc(CONFIG.PROVIDER_DOC_ID);
|
||||||
|
const snap = await ref.get();
|
||||||
|
if (!snap.exists) throw new Error(`Missing provider credentials doc: ${CONFIG.PROVIDER_CREDENTIALS_COLLECTION}/${CONFIG.PROVIDER_DOC_ID}`);
|
||||||
|
|
||||||
|
const d = snap.data() || {};
|
||||||
|
const user = d.user || d.username || d.email;
|
||||||
|
const pass = d.pass || d.password;
|
||||||
|
const baseUrl = d.baseUrl || d.baseURL || d.homeserveBaseUrl || CONFIG.HOMESERVE_BASE_URL;
|
||||||
|
|
||||||
|
if (!user || !pass) throw new Error('HomeServe credentials missing in Firestore doc (need fields user & pass)');
|
||||||
|
|
||||||
|
credsCache = { user: String(user), pass: String(pass), baseUrl: String(baseUrl) };
|
||||||
|
credsCacheAt = now;
|
||||||
|
return credsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =========================
|
||||||
|
* Browser helpers
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
async function withBrowser(fn) {
|
async function withBrowser(fn) {
|
||||||
const browser = await chromium.launch({
|
const browser = await chromium.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fn(page);
|
return await fn(page);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -86,13 +135,14 @@ async function withBrowser(fn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(page) {
|
async function login(page, creds) {
|
||||||
await page.goto(CONFIG.HOMESERVE_BASE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
await page.goto(creds.baseUrl, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
||||||
|
|
||||||
await page.waitForSelector(CONFIG.SEL.user, { timeout: 60000 });
|
await page.waitForSelector(CONFIG.SEL.user, { timeout: 60000 });
|
||||||
await page.fill(CONFIG.SEL.user, CONFIG.HOMESERVE_USER);
|
await page.fill(CONFIG.SEL.user, creds.user);
|
||||||
|
|
||||||
await page.waitForSelector(CONFIG.SEL.pass, { timeout: 60000 });
|
await page.waitForSelector(CONFIG.SEL.pass, { timeout: 60000 });
|
||||||
await page.fill(CONFIG.SEL.pass, CONFIG.HOMESERVE_PASS);
|
await page.fill(CONFIG.SEL.pass, creds.pass);
|
||||||
|
|
||||||
const btn = await page.$(CONFIG.SEL.submit);
|
const btn = await page.$(CONFIG.SEL.submit);
|
||||||
if (btn) await btn.click();
|
if (btn) await btn.click();
|
||||||
|
|
@ -108,14 +158,16 @@ async function openParte(page, parteId) {
|
||||||
const btn = await page.$(CONFIG.SEL.searchBtn);
|
const btn = await page.$(CONFIG.SEL.searchBtn);
|
||||||
if (btn) await btn.click();
|
if (btn) await btn.click();
|
||||||
else await page.keyboard.press('Enter');
|
else await page.keyboard.press('Enter');
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||||
await sleep(1500);
|
await sleep(1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForSelector(CONFIG.SEL.openRow, { timeout: 60000 });
|
await page.waitForSelector(CONFIG.SEL.openRow, { timeout: 60000 });
|
||||||
await page.click(CONFIG.SEL.openRow);
|
await page.click(CONFIG.SEL.openRow);
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||||
await sleep(1200);
|
await sleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setEstado(page, nuevoEstado, nota) {
|
async function setEstado(page, nuevoEstado, nota) {
|
||||||
|
|
@ -123,6 +175,7 @@ async function setEstado(page, nuevoEstado, nota) {
|
||||||
|
|
||||||
await page.waitForSelector(CONFIG.SEL.statusDropdown, { timeout: 60000 });
|
await page.waitForSelector(CONFIG.SEL.statusDropdown, { timeout: 60000 });
|
||||||
|
|
||||||
|
// 1) selectOption por label
|
||||||
let selected = false;
|
let selected = false;
|
||||||
for (const label of candidates) {
|
for (const label of candidates) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -132,19 +185,21 @@ async function setEstado(page, nuevoEstado, nota) {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) fallback DOM match (case-insensitive)
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
const ok = await page.evaluate(({ sel, candidates }) => {
|
const ok = await page.evaluate(({ sel, candidates }) => {
|
||||||
const s = document.querySelector(sel);
|
const s = document.querySelector(sel);
|
||||||
if (!s) return false;
|
if (!s) return false;
|
||||||
const opts = Array.from(s.querySelectorAll('option'));
|
const opts = Array.from(s.querySelectorAll('option'));
|
||||||
const hit = opts.find(o =>
|
const hit = opts.find(o =>
|
||||||
candidates.some(c => (o.textContent || '').trim().toLowerCase() === c.trim().toLowerCase())
|
candidates.some(c => (o.textContent || '').trim().toLowerCase() === String(c).trim().toLowerCase())
|
||||||
);
|
);
|
||||||
if (!hit) return false;
|
if (!hit) return false;
|
||||||
s.value = hit.value;
|
s.value = hit.value;
|
||||||
s.dispatchEvent(new Event('change', { bubbles: true }));
|
s.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
return true;
|
return true;
|
||||||
}, { sel: CONFIG.SEL.statusDropdown, candidates });
|
}, { sel: CONFIG.SEL.statusDropdown, candidates });
|
||||||
|
|
||||||
if (!ok) throw new Error(`No matching status option for "${nuevoEstado}"`);
|
if (!ok) throw new Error(`No matching status option for "${nuevoEstado}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,49 +210,51 @@ async function setEstado(page, nuevoEstado, nota) {
|
||||||
|
|
||||||
const save = await page.$(CONFIG.SEL.saveBtn);
|
const save = await page.$(CONFIG.SEL.saveBtn);
|
||||||
if (!save) throw new Error('Save button not found');
|
if (!save) throw new Error('Save button not found');
|
||||||
|
|
||||||
await save.click();
|
await save.click();
|
||||||
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
await page.waitForLoadState('networkidle', { timeout: 120000 });
|
||||||
await sleep(1500);
|
await sleep(1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claim por ID (mantiene la “seguridad” si hay 2 robots)
|
/**
|
||||||
async function claimJobById(db, jobId) {
|
* =========================
|
||||||
|
* Queue job claim (transaction)
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
async function claimJob(jobRef) {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
const ttlMs = CONFIG.CLAIM_TTL_MINUTES * 60 * 1000;
|
const ttlMs = CONFIG.CLAIM_TTL_MINUTES * 60 * 1000;
|
||||||
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
|
|
||||||
|
|
||||||
const res = await db.runTransaction(async (tx) => {
|
return await db.runTransaction(async (tx) => {
|
||||||
const snap = await tx.get(ref);
|
const fresh = await tx.get(jobRef);
|
||||||
if (!snap.exists) return null;
|
if (!fresh.exists) return null;
|
||||||
|
|
||||||
const d = snap.data() || {};
|
const d = fresh.data() || {};
|
||||||
const st = d.status ?? 'PENDING';
|
const st = d.status ?? 'PENDING';
|
||||||
|
|
||||||
const claimedAt = d.claimedAt?.toMillis ? d.claimedAt.toMillis() : null;
|
// si no está pendiente, fuera
|
||||||
const isStale = claimedAt && (now - claimedAt > ttlMs);
|
if (st !== 'PENDING' && st !== null) {
|
||||||
|
// OJO: si está RUNNING y “caducado”, lo re-claim
|
||||||
if (st === 'DONE') return null;
|
if (st !== 'RUNNING') return null;
|
||||||
if (st === 'RUNNING' && !isStale) return null;
|
|
||||||
|
|
||||||
if (!isPendingStatus(st) && st !== 'FAILED' && !(st === 'RUNNING' && isStale)) {
|
|
||||||
// Si alguien mete estados raros, lo ignoramos
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.set(ref, {
|
const claimedAtMs = d.claimedAt?.toMillis ? d.claimedAt.toMillis() : null;
|
||||||
|
const stale = claimedAtMs && (now - claimedAtMs > ttlMs);
|
||||||
|
|
||||||
|
if (st === 'RUNNING' && !stale) return null;
|
||||||
|
|
||||||
|
tx.set(jobRef, {
|
||||||
status: 'RUNNING',
|
status: 'RUNNING',
|
||||||
claimedAt: toServerTimestamp(),
|
claimedAt: toServerTimestamp(),
|
||||||
claimedBy: process.env.HOSTNAME || 'estados-homeserve',
|
claimedBy: process.env.HOSTNAME || 'estados-homeserve',
|
||||||
lastSeenAt: toServerTimestamp(),
|
lastSeenAt: toServerTimestamp(),
|
||||||
}, { merge: true });
|
}, { merge: true });
|
||||||
|
|
||||||
return { id: jobId, ...d };
|
return { id: fresh.id, ...d };
|
||||||
});
|
});
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markDone(db, jobId, result) {
|
async function markDone(jobId, result) {
|
||||||
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
|
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
|
||||||
await ref.set({
|
await ref.set({
|
||||||
status: 'DONE',
|
status: 'DONE',
|
||||||
|
|
@ -213,7 +270,7 @@ async function markDone(db, jobId, result) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markFailed(db, jobId, err) {
|
async function markFailed(jobId, err) {
|
||||||
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
|
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
|
||||||
await ref.set({
|
await ref.set({
|
||||||
status: 'FAILED',
|
status: 'FAILED',
|
||||||
|
|
@ -236,133 +293,126 @@ async function markFailed(db, jobId, err) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processJob(db, job) {
|
/**
|
||||||
const jobId = job.id;
|
* =========================
|
||||||
const parteId = job.parteId || job.parte || job.codigo || job.serviceId;
|
* Concurrency (semaforo simple)
|
||||||
const nuevoEstado = job.nuevoEstado || job.estado || job.statusTo;
|
* =========================
|
||||||
const nota = job.nota || job.note || '';
|
*/
|
||||||
|
let running = 0;
|
||||||
|
const waiters = [];
|
||||||
|
|
||||||
if (!parteId || !nuevoEstado) {
|
function acquire() {
|
||||||
await markFailed(db, jobId, new Error('Job missing parteId or nuevoEstado'));
|
return new Promise((resolve) => {
|
||||||
return;
|
if (running < CONFIG.MAX_CONCURRENCY) {
|
||||||
}
|
running++;
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
waiters.push(resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function release() {
|
||||||
const started = new Date().toISOString();
|
running = Math.max(0, running - 1);
|
||||||
|
const next = waiters.shift();
|
||||||
await withBrowser(async (page) => {
|
if (next) {
|
||||||
await login(page);
|
running++;
|
||||||
await openParte(page, parteId);
|
next();
|
||||||
await setEstado(page, nuevoEstado, nota);
|
|
||||||
});
|
|
||||||
|
|
||||||
await markDone(db, jobId, {
|
|
||||||
ok: true,
|
|
||||||
startedAtISO: started,
|
|
||||||
parteId: String(parteId),
|
|
||||||
nuevoEstado: String(nuevoEstado),
|
|
||||||
nota: String(nota || ''),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
await markFailed(db, jobId, err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modo reactivo:
|
* =========================
|
||||||
* - Listener Firestore detecta jobs PENDING y los mete en cola interna
|
* Process a job NOW (sin esperar)
|
||||||
* - Procesamos de uno en uno (Playwright mejor así)
|
* =========================
|
||||||
*/
|
*/
|
||||||
function createWorker(db) {
|
async function processJob(jobId, jobData) {
|
||||||
const queue = [];
|
const parteId = jobData.parteId || jobData.parte || jobData.codigo || jobData.serviceId;
|
||||||
const queued = new Set();
|
const nuevoEstado = jobData.nuevoEstado || jobData.estado || jobData.statusTo;
|
||||||
let running = false;
|
const nota = jobData.nota || jobData.note || '';
|
||||||
|
|
||||||
async function drain() {
|
if (!parteId || !nuevoEstado) {
|
||||||
if (running) return;
|
await markFailed(jobId, new Error('Job missing parteId or nuevoEstado'));
|
||||||
running = true;
|
return;
|
||||||
try {
|
|
||||||
while (queue.length) {
|
|
||||||
const id = queue.shift();
|
|
||||||
queued.delete(id);
|
|
||||||
|
|
||||||
const claimed = await claimJobById(db, id);
|
|
||||||
if (!claimed) continue; // otro worker lo pilló o ya no aplica
|
|
||||||
|
|
||||||
await processJob(db, claimed);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function enqueue(id) {
|
const started = new Date().toISOString();
|
||||||
if (queued.has(id)) return;
|
const creds = await getHomeServeCreds();
|
||||||
queued.add(id);
|
|
||||||
queue.push(id);
|
|
||||||
// Arranca al momento, sin esperar
|
|
||||||
drain().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rescanPending() {
|
await withBrowser(async (page) => {
|
||||||
try {
|
await login(page, creds);
|
||||||
const snap = await db.collection(CONFIG.QUEUE_COLLECTION)
|
await openParte(page, parteId);
|
||||||
.orderBy('createdAt', 'asc')
|
await setEstado(page, nuevoEstado, nota);
|
||||||
.limit(50)
|
});
|
||||||
.get();
|
|
||||||
|
|
||||||
snap.forEach((doc) => {
|
await markDone(jobId, {
|
||||||
const d = doc.data() || {};
|
ok: true,
|
||||||
if (isPendingStatus(d.status)) enqueue(doc.id);
|
startedAtISO: started,
|
||||||
});
|
parteId: String(parteId),
|
||||||
} catch (e) {
|
nuevoEstado: String(nuevoEstado),
|
||||||
console.error('Rescan error:', e);
|
nota: String(nota || ''),
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListener() {
|
/**
|
||||||
// Listener general (evita problemas de query con null en "in")
|
* =========================
|
||||||
return db.collection(CONFIG.QUEUE_COLLECTION)
|
* Firestore listener (event-driven)
|
||||||
.orderBy('createdAt', 'asc')
|
* =========================
|
||||||
.limit(50)
|
*/
|
||||||
.onSnapshot((snap) => {
|
function startQueueListener() {
|
||||||
for (const ch of snap.docChanges()) {
|
console.log(`[HS] Listening queue: ${CONFIG.QUEUE_COLLECTION} ...`);
|
||||||
if (ch.type !== 'added' && ch.type !== 'modified') continue;
|
|
||||||
const d = ch.doc.data() || {};
|
const q = db.collection(CONFIG.QUEUE_COLLECTION)
|
||||||
if (isPendingStatus(d.status)) enqueue(ch.doc.id);
|
.where('status', 'in', ['PENDING', null]);
|
||||||
|
|
||||||
|
q.onSnapshot(async (snap) => {
|
||||||
|
// Procesa solo cambios relevantes
|
||||||
|
const changes = snap.docChanges()
|
||||||
|
.filter(ch => ch.type === 'added' || ch.type === 'modified')
|
||||||
|
.map(ch => ch.doc);
|
||||||
|
|
||||||
|
for (const doc of changes) {
|
||||||
|
const ref = doc.ref;
|
||||||
|
const data = doc.data() || {};
|
||||||
|
|
||||||
|
// Seguridad: si ya no está pending, ignora
|
||||||
|
const st = data.status ?? 'PENDING';
|
||||||
|
if (st !== 'PENDING' && st !== null) continue;
|
||||||
|
|
||||||
|
// Concurrency guard
|
||||||
|
await acquire();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const claimed = await claimJob(ref);
|
||||||
|
if (!claimed) return;
|
||||||
|
|
||||||
|
await processJob(doc.id, claimed);
|
||||||
|
} catch (err) {
|
||||||
|
await markFailed(doc.id, err);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
}
|
}
|
||||||
}, (err) => {
|
})();
|
||||||
console.error('onSnapshot error:', err);
|
}
|
||||||
});
|
}, (err) => {
|
||||||
}
|
console.error('[HS] Listener error:', err);
|
||||||
|
// Si el listener cae, reinicia el proceso (CapRover lo levantará)
|
||||||
return { startListener, rescanPending };
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
/**
|
||||||
const db = initFirebase();
|
* =========================
|
||||||
|
* Main
|
||||||
|
* =========================
|
||||||
|
*/
|
||||||
|
startQueueListener();
|
||||||
|
|
||||||
const worker = createWorker(db);
|
process.on('unhandledRejection', (e) => {
|
||||||
const unsubscribe = worker.startListener();
|
console.error('[HS] unhandledRejection', e);
|
||||||
|
});
|
||||||
// rescaneo “por si acaso”
|
process.on('uncaughtException', (e) => {
|
||||||
await worker.rescanPending();
|
console.error('[HS] uncaughtException', e);
|
||||||
setInterval(() => worker.rescanPending(), CONFIG.RESCAN_SECONDS * 1000);
|
|
||||||
|
|
||||||
// no salimos nunca (servicio)
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
try { unsubscribe && unsubscribe(); } catch (_) {}
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
try { unsubscribe && unsubscribe(); } catch (_) {}
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ estados-homeserve listo (modo reactivo, sin polling).');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue