129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import { watch, readFileSync } from 'fs';
|
|
import { readdir, readFile, stat } from 'fs/promises';
|
|
import { resolve, basename, extname, join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
function loadEnv(): void {
|
|
const envPath = resolve(__dirname, '.env');
|
|
try {
|
|
const content = readFileSync(envPath, 'utf-8');
|
|
for (const line of content.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const [key, value] = trimmed.split('=', 2);
|
|
if (key && value !== undefined) {
|
|
process.env[key.trim()] = value.trim().replace(/^"|"$/g, '');
|
|
}
|
|
}
|
|
console.log('Loaded configuration from .env');
|
|
} catch {
|
|
console.error('FATAL: Could not read .env file.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
loadEnv();
|
|
|
|
const { INGEST_BASE_URL, API_TOKEN } = process.env;
|
|
|
|
if (!INGEST_BASE_URL || !API_TOKEN) {
|
|
console.error('FATAL: INGEST_BASE_URL and API_TOKEN must be defined in .env');
|
|
process.exit(1);
|
|
}
|
|
|
|
const WATCH_PATH = resolve(__dirname, '..', '..', '..', 'script-output', 'signal_export');
|
|
const DEBOUNCE_DELAY_MS = 250;
|
|
const debounceTimers = new Map<string, NodeJS.Timeout>();
|
|
|
|
async function sendToWebhook(filePath: string, mtimeMs: number): Promise<void> {
|
|
const combinatorName = basename(filePath, extname(filePath));
|
|
const url = `${INGEST_BASE_URL}/api/ingest/${encodeURIComponent(combinatorName)}?token=${API_TOKEN}`;
|
|
|
|
try {
|
|
const content = await readFile(filePath, 'utf-8');
|
|
if (!content.trim()) return;
|
|
JSON.parse(content); // validate before sending
|
|
|
|
console.log(`Sending '${combinatorName}' (mtime: ${new Date(mtimeMs).toISOString()})`);
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-File-Mtime': String(mtimeMs),
|
|
},
|
|
body: content,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
console.log(`OK '${combinatorName}' → ${response.status}`);
|
|
} catch (err: any) {
|
|
if (err instanceof SyntaxError) {
|
|
console.error(`Skip '${combinatorName}': invalid JSON`);
|
|
} else {
|
|
console.error(`Error '${combinatorName}':`, err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleFileChange(filename: string | null): void {
|
|
if (!filename?.endsWith('.json')) return;
|
|
|
|
const existing = debounceTimers.get(filename);
|
|
if (existing) clearTimeout(existing);
|
|
|
|
const timer = setTimeout(async () => {
|
|
const fullPath = join(WATCH_PATH, filename);
|
|
try {
|
|
const stats = await stat(fullPath);
|
|
if (stats.isFile()) {
|
|
await sendToWebhook(fullPath, stats.mtimeMs);
|
|
}
|
|
} catch {
|
|
// file removed before processing — ignore
|
|
} finally {
|
|
debounceTimers.delete(filename);
|
|
}
|
|
}, DEBOUNCE_DELAY_MS);
|
|
|
|
debounceTimers.set(filename, timer);
|
|
}
|
|
|
|
async function initialScan(): Promise<void> {
|
|
console.log(`Initial scan: ${WATCH_PATH}`);
|
|
try {
|
|
const files = (await readdir(WATCH_PATH)).filter(f => f.endsWith('.json'));
|
|
console.log(`Found ${files.length} file(s)`);
|
|
for (const file of files) {
|
|
const fullPath = join(WATCH_PATH, file);
|
|
const stats = await stat(fullPath);
|
|
await sendToWebhook(fullPath, stats.mtimeMs);
|
|
}
|
|
} catch (err: any) {
|
|
console.error(`Initial scan failed: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
await initialScan();
|
|
|
|
console.log(`Watching: ${WATCH_PATH}`);
|
|
try {
|
|
watch(WATCH_PATH, (eventType, filename) => {
|
|
if (filename) {
|
|
console.log(`[${new Date().toLocaleTimeString()}] ${eventType}: ${filename}`);
|
|
handleFileChange(filename);
|
|
}
|
|
});
|
|
console.log('Watcher running. Ctrl+C to stop.');
|
|
} catch (err: any) {
|
|
console.error(`FATAL: Watcher failed: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
})();
|