BillTracker/services/bankSyncWorker.js

129 lines
3.0 KiB
JavaScript
Raw Normal View History

'use strict';
const { getDb } = require('../db/database');
const { getBankSyncConfig } = require('./bankSyncConfigService');
const { syncDataSource } = require('./bankSyncService');
const DEFAULT_INTERVAL_HOURS = 4;
// Skip a source if it was synced less than this long ago (catches recent manual syncs)
const MIN_SYNC_AGE_MS = 60 * 60 * 1000; // 1 hour
// Pause between each source to avoid hammering SimpleFIN
const STAGGER_DELAY_MS = 3000;
let timer = null;
let running = false;
let lastRunAt = null;
let nextRunAt = null;
function intervalMs() {
const hours = parseFloat(process.env.SIMPLEFIN_SYNC_INTERVAL_HOURS);
return Number.isFinite(hours) && hours >= 0.5
? Math.round(hours * 3600000)
: DEFAULT_INTERVAL_HOURS * 3600000;
}
function needsSync(source) {
if (!source.last_sync_at) return true;
const age = Date.now() - new Date(source.last_sync_at).getTime();
return age >= MIN_SYNC_AGE_MS;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function runCycle() {
if (running) return;
const { enabled } = getBankSyncConfig();
if (!enabled) return;
let db, sources;
try {
db = getDb();
sources = db.prepare(`
SELECT * FROM data_sources
WHERE type = 'provider_sync' AND provider = 'simplefin'
ORDER BY last_sync_at ASC
`).all();
} catch (err) {
console.error('[bankSync] Worker failed to load sources:', err.message);
return;
}
if (sources.length === 0) return;
running = true;
lastRunAt = new Date().toISOString();
let synced = 0;
let skipped = 0;
let failed = 0;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
if (!needsSync(source)) {
skipped++;
continue;
}
try {
await syncDataSource(db, source.user_id, source.id);
synced++;
} catch {
// syncDataSource already writes last_error to the data_sources row
failed++;
}
// Stagger requests — don't fire them all simultaneously
if (i < sources.length - 1) await sleep(STAGGER_DELAY_MS);
}
if (synced > 0 || failed > 0) {
console.log(`[bankSync] Auto-sync complete: ${synced} synced, ${failed} failed, ${skipped} skipped`);
}
running = false;
}
function scheduleNext() {
const ms = intervalMs();
nextRunAt = new Date(Date.now() + ms).toISOString();
timer = setTimeout(() => {
runCycle()
.catch(err => {
console.error('[bankSync] Worker cycle error:', err.message);
running = false;
})
.finally(scheduleNext);
}, ms);
// Don't hold the event loop open if the server is shutting down
if (timer.unref) timer.unref();
}
function start() {
if (timer) return;
scheduleNext();
const hours = intervalMs() / 3600000;
console.log(`[bankSync] Auto-sync worker started (interval: ${hours}h)`);
}
function stop() {
clearTimeout(timer);
timer = null;
}
function getStatus() {
return {
running,
interval_hours: intervalMs() / 3600000,
last_run_at: lastRunAt,
next_run_at: nextRunAt,
};
}
module.exports = { start, stop, getStatus };