// Re-publishes a shop's configuration at every offer start/end boundary (brief ยง11.5). // Every 30 s it claims shops whose nextSyncAt has passed (atomic update, safe with several app instances) // and publishes them with the shop's offline token. The function's guard covers the seconds in between // for offers that END (see extensions/bundle-discount/src/config.rs), so no discount outlives its offer. import db from "../db.server"; import { unauthenticated } from "../shopify.server"; import { publishShop } from "./publish.server"; const TICK_MS = 30_000; export async function runDueSyncs(now = new Date()) { const due = await db.shop.findMany({ where: { uninstalledAt: null, nextSyncAt: { lte: now } }, select: { id: true, domain: true, nextSyncAt: true }, take: 50, }); for (const shop of due) { // claim: only one instance gets each boundary const claimed = await db.shop.updateMany({ where: { id: shop.id, nextSyncAt: shop.nextSyncAt }, data: { nextSyncAt: new Date(now.getTime() + 5 * 60_000) }, // safety retry if this run dies }); if (!claimed.count) continue; try { const { admin } = await unauthenticated.admin(shop.domain); await publishShop(admin, shop.domain, new Date()); } catch (e) { console.error(`[scheduler] publish failed for ${shop.domain}`, e); } } } declare global { // eslint-disable-next-line no-var var __bundleOffersScheduler: NodeJS.Timeout | undefined; } export function startScheduler() { if (globalThis.__bundleOffersScheduler || process.env.DISABLE_SCHEDULER === "1") return; globalThis.__bundleOffersScheduler = setInterval(() => { runDueSyncs().catch((e) => console.error("[scheduler]", e)); }, TICK_MS); globalThis.__bundleOffersScheduler.unref?.(); }