/** * Inventory sync from the Prime Intellect availability API. * * Runs on an interval, upserting listings on their natural key so repeated * syncs converge rather than accumulating near-duplicates. * * Two decisions worth explaining. * * **Stale listings are marked, not deleted.** A listing that disappears * upstream has usually sold out rather than ceased to exist, and deleting it * would destroy the price history that makes the inventory useful for * negotiation. Anything not seen in this pass is marked `Unavailable`, keeping * the record and its history while removing it from search. * * **A failed sync is not fatal.** The upstream publishes no rate limits, so * some failures are expected. The job logs and waits for the next tick rather * than crashing the server that people are using. */ import { and, eq, inArray, lt, sql } from 'drizzle-orm'; import type { Database } from '@pig/db'; import { inventoryListings } from '@pig/db'; import { PrimeClient, mapListing } from '@pig/prime'; import type { Config } from '../lib/config'; export function startPrimeSync(config: Config, db: Database): () => void { if (!config.PRIME_SYNC_ENABLED || !config.PRIME_API_KEY) { if (config.PRIME_SYNC_ENABLED) { console.warn('[pig] Prime sync enabled but no API key — not starting.'); } return () => {}; } const client = new PrimeClient({ apiKey: config.PRIME_API_KEY, baseUrl: config.PRIME_API_BASE, onRetry: ({ attempt, delayMs, reason }) => console.warn(`[pig] prime retry ${attempt} in ${delayMs}ms (${reason})`), }); let running = false; async function runOnce() { // Overlapping runs would fight over the same rows for no benefit. if (running) return; running = true; const startedAt = new Date(); let upserted = 0; try { for await (const page of client.iterateGpuAvailability()) { const rows = page .map(mapListing) .filter((row): row is NonNullable => row !== null); if (rows.length === 0) continue; for (const row of rows) { await db .insert(inventoryListings) .values({ ...row, socket: row.socket as 'PCIe' | null, source: 'prime_api', observedAt: startedAt, }) .onConflictDoUpdate({ target: [ inventoryListings.externalCloudId, inventoryListings.gpuType, inventoryListings.socket, inventoryListings.gpuCount, inventoryListings.securityTier, ], set: { stockStatus: row.stockStatus, onDemandPriceCents: row.onDemandPriceCents, communityPriceCents: row.communityPriceCents, priceIsVariable: row.priceIsVariable, interconnectGbps: row.interconnectGbps, interconnectType: row.interconnectType, provisioningMinutes: row.provisioningMinutes, region: row.region, country: row.country, images: row.images, raw: row.raw, observedAt: startedAt, updatedAt: new Date(), }, }); upserted += 1; } } // Anything the provider stopped listing is out of stock, not gone. const stale = await db .update(inventoryListings) .set({ stockStatus: 'Unavailable', updatedAt: new Date() }) .where( and( eq(inventoryListings.source, 'prime_api'), lt(inventoryListings.observedAt, startedAt), sql`${inventoryListings.stockStatus} <> 'Unavailable'`, ), ) .returning({ id: inventoryListings.id }); console.log( `[pig] prime sync: ${upserted} listing(s) upserted, ${stale.length} marked unavailable`, ); } catch (error) { // Logged, not thrown. A provider outage must not take down the CRM. console.error('[pig] prime sync failed:', error instanceof Error ? error.message : error); } finally { running = false; } } // Delay the first run so startup is not competing with an outbound sync. const initial = setTimeout(() => void runOnce(), 10_000); const interval = setInterval( () => void runOnce(), config.PRIME_SYNC_INTERVAL_MINUTES * 60_000, ); console.log(`[pig] prime sync every ${config.PRIME_SYNC_INTERVAL_MINUTES}m`); return () => { clearTimeout(initial); clearInterval(interval); }; }