Chain-event side effects happen once across running instances
A Coolify deploy starts the new container and stops the old one only once the new one is healthy, so for about half a minute two copies of this app watch the chain. On 25 September, 06:26 CT, both saw Marty's $5 purchase: two confirmation emails, and the Five Dollar Friday bonus granted twice (+100 and +100, nineteen seconds apart). Every dedupe until now lived in process memory or a JSON file on the volume; a second process shares neither in time. marks.js is the shared version: a row in MySQL that only one INSERT IGNORE can win. The chain-event fan-out claims 'ev:<tx>:<logIndex>' before the email, Telegram post, Friday bonus and sponsor sync; the in-memory feed stays per instance. The Friday scheduler claims its Thursday email, kickoff and wrap-up the same way, with the file marker kept as the second line of defence and the thing an operator can edit by hand. Without a database it degrades to a per-process Set, which is exactly the old single-instance behaviour. Proven on the production MySQL before deploy: first insert 1 row, second 0. Marty's duplicate 100 credits reversed with a labelled adjust row; no other member was double-granted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ const isFriday = ts => weekdayOf(ts) === 'Fri';
|
||||
function init(opts) { X = opts; FILE = path.join(opts.dataDir, 'friday.json'); S = load(); }
|
||||
function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { granted: {}, posts: {}, badges: {}, sent: {} }; } }
|
||||
function save() { try { fs.writeFileSync(FILE, JSON.stringify(S)); } catch (e) {} }
|
||||
// shared across instances when the app is wired with marks (server.js); true when nobody else has it
|
||||
async function claimOnce(key) { try { return X.marks ? await X.marks.claim(key) : true; } catch (e) { return true; } }
|
||||
|
||||
function cfg() {
|
||||
const c = (X.siteConfig && X.siteConfig()) || {};
|
||||
@@ -106,19 +108,19 @@ async function tick() {
|
||||
const c = cfg(); if (!c.on) return;
|
||||
const now = Date.now(); const day = dayOf(now); const wd = weekdayOf(now); const h = hourOf(now);
|
||||
const next = nextFriday(now);
|
||||
if (wd === 'Thu' && h >= 18 && next && !S.sent['thu:' + next]) {
|
||||
if (wd === 'Thu' && h >= 18 && next && !S.sent['thu:' + next] && await claimOnce('fri:thu:' + next)) {
|
||||
S.sent['thu:' + next] = now; save();
|
||||
const subject = 'Tomorrow is Five Dollar Friday';
|
||||
const text = 'Tomorrow is Five Dollar Friday on InstantAdPay.\n\nAny ad package of $5 or more bought on Friday (Central time) earns +' + c.pct + '% bonus credits, added the moment the purchase settles. Your $5 pays your sponsor line the second it clears, and your line’s $5s pay you. Friday is when we all push at once.\n\nLine up your pack: https://instantadpay.com/my#buy\nWatch the wave on the live ledger: https://instantadpay.com/ledger';
|
||||
await broadcast(subject, text);
|
||||
}
|
||||
if (wd === 'Fri' && h >= 9 && day >= c.start && !S.sent['kick:' + day]) {
|
||||
if (wd === 'Fri' && h >= 9 && day >= c.start && !S.sent['kick:' + day] && await claimOnce('fri:kick:' + day)) {
|
||||
S.sent['kick:' + day] = now; save();
|
||||
X.telegram('\u{1F4B5} <b>It’s Five Dollar Friday.</b> Any ad package from $5 up earns <b>+' + c.pct + '% credits</b> today, and every pack pays its sponsor line in the same transaction. Buy yours, then watch the ledger fill up: https://instantadpay.com/my#buy').catch(() => {});
|
||||
}
|
||||
if (wd === 'Sat' && h < 3) {
|
||||
const fri = dayOf(now - 86400000);
|
||||
if (isFriday(now - 86400000) && fri >= c.start && !S.sent['wrap:' + fri]) {
|
||||
if (isFriday(now - 86400000) && fri >= c.start && !S.sent['wrap:' + fri] && await claimOnce('fri:wrap:' + fri)) {
|
||||
S.sent['wrap:' + fri] = now; save();
|
||||
const st = stats(fri); const top = await topLines(fri);
|
||||
X.telegram('\u{1F3C1} <b>Five Dollar Friday wrap-up</b> (' + labelOf(fri) + ')\n<b>' + st.packs + ' packs</b> from ' + st.buyers + ' buyers · $' + st.usd.toFixed(0) + ' in ad packages · <b>' + st.polPaid.toFixed(2) + ' POL</b> paid to members · ' + st.bonusCredits.toLocaleString() + ' bonus credits handed out'
|
||||
|
||||
Reference in New Issue
Block a user