#!/usr/bin/env node /** * Sweep worker. Runs runSweep on a tick — trash hard-delete, asset * orphans, audit log rotation, marketing preview queue drain. * * Usage: * node scripts/sweep-worker.mjs # one-shot * node scripts/sweep-worker.mjs --loop # run on interval * * Production: run as a separate process (PM2, systemd, etc.) so the * main API server doesn't lose responsiveness when sweep does heavy * work (especially preview rendering, which can take seconds per * template at scale). * * For v1 the sweep interval is 60 minutes when run with --loop. * Adjust via the --interval= flag. */ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { runSweep, marketingPreviewRenderer, createTemplateIndex, } from 'template-manager/server'; import { PRODUCT_TYPES } from '../src/constants/products.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const STORAGE_PATH = join(__dirname, '..', 'templates'); const args = process.argv.slice(2); const LOOP = args.includes('--loop'); const intervalArg = args.find((a) => a.startsWith('--interval=')); const INTERVAL_MIN = intervalArg ? parseInt(intervalArg.split('=')[1], 10) : 60; async function buildRenderer() { try { const { renderDesignComposite } = await import('goods-editor/server'); const canvasMod = await import('canvas'); const sharpMod = await import('sharp').catch(() => null); const factory = marketingPreviewRenderer({ editorRenderer: async (args) => renderDesignComposite({ ...args, canvas: canvasMod.default || canvasMod, sharp: sharpMod ? (sharpMod.default || sharpMod) : null, mockupsDir: join(__dirname, '..', 'mockups'), uploadsDir: join(__dirname, '..', 'uploads'), stickersDir: join(__dirname, '..', 'public', 'stickers'), storageDir: STORAGE_PATH, }), }); // The factory returns a renderer expecting context. We don't have // the in-memory marketingPreviewQueue here — that lives in the // running API server's process. The worker reconciles via the // metadata.autoStaleSince field: any template with that field // set is a regen candidate. return async (templateId) => { // Skip if no audit log handle in this script — that's mounted // server-side. The renderer logs failures via the audit log // when one is provided; without one, errors silently retry on // the next sweep. const { createAuditLog } = await import('template-manager/server/lib/auditLog.js').catch(() => ({})); const auditLog = createAuditLog ? createAuditLog(STORAGE_PATH) : null; return factory({ templateId, storagePath: STORAGE_PATH, variantCatalog: PRODUCT_TYPES, auditLog, }); }; } catch (err) { console.warn('⚠ Renderer not available; sweep will skip preview regen:', err.message); return null; } } async function runOnce(renderPreview) { const templateIndex = createTemplateIndex(STORAGE_PATH); const summary = await runSweep({ storagePath: STORAGE_PATH, templateIndex, renderPreview, // marketingPreviewQueue is intentionally absent — the in-process // queue lives in the API server. Worker process reconciles via // metadata.autoStaleSince instead. marketingPreviewQueue: null, }); console.log('Sweep:', JSON.stringify(summary, null, 2)); } async function main() { const renderPreview = await buildRenderer(); if (!LOOP) { await runOnce(renderPreview); process.exit(0); } console.log(`▶ Sweep worker loop (interval: ${INTERVAL_MIN} min)`); const tick = async () => { try { await runOnce(renderPreview); } catch (err) { console.error('Sweep tick failed:', err); } }; await tick(); setInterval(tick, INTERVAL_MIN * 60 * 1000); } main().catch((err) => { console.error('Sweep worker failed:', err); process.exit(1); });