Bridges third-party status pages into Webex spaces via RSS polling and inbound webhooks (Statuspage / Status.io / Uptime Kuma / generic). Includes an interactive Webex bot (websocket transport) that lets space members register sources with an Adaptive Card instead of hand-editing config/feeds.json: help, add, list, webhook <key>, remove <key>. Ships with an atomic JSON store (per-file mutex, tmp+rename), parallel RSS polling, and unit tests via node:test. All secrets are sourced from environment variables (see .env.example); no credentials in the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
53 lines
1.8 KiB
JavaScript
53 lines
1.8 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
// Per-file promise chain. Any writer for a given path queues behind the previous
|
|
// one so we never have two writers renaming into the same target concurrently.
|
|
const locks = new Map();
|
|
|
|
function withLock(key, fn) {
|
|
const prev = locks.get(key) || Promise.resolve();
|
|
// Ignore the previous task's result/errors — every waiter still gets a chance to run.
|
|
const next = prev.then(fn, fn);
|
|
const tail = next.catch(() => {});
|
|
locks.set(key, tail);
|
|
tail.then(() => {
|
|
if (locks.get(key) === tail) locks.delete(key);
|
|
});
|
|
return next;
|
|
}
|
|
|
|
export function readJSON(filePath) {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
}
|
|
|
|
// Write JSON atomically: serialize -> write to tmp in the same directory ->
|
|
// fsync -> rename over the target. Rename within a single filesystem is atomic
|
|
// on POSIX, so readers either see the old or new file, never a torn write.
|
|
export function writeJSON(filePath, data) {
|
|
return withLock(path.resolve(filePath), async () => {
|
|
const dir = path.dirname(filePath);
|
|
const base = path.basename(filePath);
|
|
const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
const payload = JSON.stringify(data, null, 4);
|
|
|
|
const fh = await fs.promises.open(tmp, 'w');
|
|
try {
|
|
await fh.writeFile(payload);
|
|
await fh.sync();
|
|
} finally {
|
|
await fh.close();
|
|
}
|
|
|
|
try {
|
|
await fs.promises.rename(tmp, filePath);
|
|
} catch (error) {
|
|
// If rename fails, don't leave the tmp file behind.
|
|
fs.promises.unlink(tmp).catch(() => {});
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Exported for tests only.
|
|
export const __test__ = { withLock, locks };
|