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:
@@ -36,7 +36,8 @@ const syndicate = require('./syndicate');
|
||||
const releases = require('./releases');
|
||||
const ledger = require('./ledger');
|
||||
const videosweep = require('./videosweep');
|
||||
const sbcheck = require('./sbcheck'); // Google Safe Browsing screen on advertiser destinations
|
||||
const sbcheck = require('./sbcheck');
|
||||
const marks = require('./marks'); // one-time claims shared across running instances (deploy overlap) // Google Safe Browsing screen on advertiser destinations
|
||||
const updates = require('./updates');
|
||||
const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14)
|
||||
const leaderboard = require('./leaderboard');
|
||||
@@ -434,7 +435,23 @@ async function frameCheck(url) {
|
||||
}
|
||||
async function boot() {
|
||||
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
|
||||
chain.init({ onEvent: ev => { attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev)); emailOnEvent(ev).catch(() => {}); telegramOnEvent(ev).catch(() => {}); friday.onEvent(ev).catch(e => console.error('friday bonus', e.message)); sponsorSyncOnEvent(ev).catch(e => console.error('sponsor sync', e.message)); } });
|
||||
marks.init({ db });
|
||||
marks.ensureTable().catch(e => console.error('marks table', e.message));
|
||||
setInterval(() => marks.prune().catch(() => {}), 6 * 3600 * 1000).unref();
|
||||
chain.init({ onEvent: ev => {
|
||||
// the in-memory feed is per instance and always runs
|
||||
attachNames([ev]).then(a => pushFeed(a[0])).catch(() => pushFeed(ev));
|
||||
// everything outward (email, Telegram, the Friday bonus, sponsor sync) happens ONCE across
|
||||
// every running copy of the app: during a deploy two copies watch the chain for ~30 s,
|
||||
// and on 2026-09-25 both emailed Marty and both granted his Friday bonus
|
||||
marks.claim('ev:' + ev.tx + ':' + ev.li).then(mine => {
|
||||
if (!mine) return;
|
||||
emailOnEvent(ev).catch(() => {});
|
||||
telegramOnEvent(ev).catch(() => {});
|
||||
friday.onEvent(ev).catch(e => console.error('friday bonus', e.message));
|
||||
sponsorSyncOnEvent(ev).catch(e => console.error('sponsor sync', e.message));
|
||||
}).catch(e => console.error('marks claim', e.message));
|
||||
} });
|
||||
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
|
||||
accounts.init({ dataDir: DATA_DIR });
|
||||
ads.init({ dataDir: DATA_DIR, chain, tiers: () => geo.tierLists(siteConfig()) });
|
||||
@@ -453,7 +470,7 @@ async function boot() {
|
||||
snapshot.init({ dataDir: DATA_DIR, db, chain, siteConfig, publicDir: PUBLIC_DIR,
|
||||
send: async (c, t, th) => { await telegramSend(c, t, th); return true; },
|
||||
sendPhoto: (c, jpeg, cap, th) => telegramSendPhoto(c, jpeg, cap, th) });
|
||||
friday.init({ dataDir: DATA_DIR, siteConfig, chain, ads, accounts, mailer, drip,
|
||||
friday.init({ dataDir: DATA_DIR, siteConfig, chain, ads, accounts, mailer, drip, marks,
|
||||
telegram: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return false; return telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + text, sc.telegramEchoTopicId); },
|
||||
notify: async (memberId, subject, text, kind) => { const a = await accounts.byMemberId(memberId); if (!a || !a.email) return; const html = '<p>' + String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>').split('\n\n').join('</p><p>').replace(/\n/g, '<br>') + '</p>'; await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [a.email], subject, html, kind || 'notice'); } });
|
||||
// Campaign refill notices: an advertiser is told when an ad is nearly out and when it has stopped,
|
||||
|
||||
Reference in New Issue
Block a user