brackt/server/snapshots.ts
Chris Parsons a054247a55
Fix standings snapshot upsert: atomic writes, correct date, and event-driven updates (#177)
- Add unique index on (team_id, season_id, snapshot_date) in team_standings_snapshots
  so concurrent writes can't produce duplicate rows (migration 0051)
- Rewrite createDailySnapshot to use INSERT ... ON CONFLICT DO UPDATE inside a
  transaction, replacing the SELECT-then-insert/skip pattern (N+1 queries, race-prone)
- Remove early-exit guard in server/snapshots.ts background job so it always
  upserts rather than skipping seasons that already have a snapshot for today
- Call createDailySnapshot in recalculateAffectedLeagues after recalculateStandings,
  so every bracket score update refreshes today's snapshot; wrapped in try/catch so
  a snapshot failure can't block Discord notifications
- Also call createDailySnapshot from the admin rescore and finalize-bracket actions
- Fix UTC date bug: replace toISOString().split('T')[0] with local date parts in
  both standings.ts and server/snapshots.ts (and the 7-day lookback query)
- Convert all dynamic await import() calls in bracket.server.ts to static imports

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:15:28 -07:00

116 lines
3.6 KiB
TypeScript

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "~/database/schema";
import { eq, inArray, or } from "drizzle-orm";
import { createDailySnapshot } from "~/models/standings";
// Create a dedicated database connection for the snapshot system
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL is required for snapshot system");
}
const client = postgres(connectionString);
const db = drizzle(client, { schema });
let snapshotInterval: NodeJS.Timeout | null = null;
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // Check once per day (in milliseconds)
/**
* Start the daily snapshot system
* Runs once per day to create snapshots for all active seasons
*/
export function startSnapshotSystem(): void {
if (snapshotInterval) {
console.log("[Snapshots] Snapshot system already running");
return;
}
// Run immediately on startup
void createDailySnapshots();
// Then run once per day
snapshotInterval = setInterval(async () => {
try {
await createDailySnapshots();
} catch (error) {
console.error("[Snapshots] Error creating daily snapshots:", error);
}
}, CHECK_INTERVAL);
console.log("[Snapshots] Daily snapshot system started (runs once per day)");
}
/**
* Stop the snapshot system
*/
export function stopSnapshotSystem(): void {
if (snapshotInterval) {
clearInterval(snapshotInterval);
snapshotInterval = null;
console.log("[Snapshots] Snapshot system stopped");
}
}
/**
* Create daily snapshots for all active seasons
* Only creates snapshots if they don't already exist for today
*/
async function createDailySnapshots(): Promise<void> {
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
console.log(`[Snapshots] Checking for snapshots to create (${today})`);
// Get all seasons that are active or in draft (we want to track standings for these)
const activeSeasons = await db.query.seasons.findMany({
where: or(
eq(schema.seasons.status, "active"),
eq(schema.seasons.status, "draft")
),
});
if (activeSeasons.length === 0) {
console.log("[Snapshots] No active seasons found");
return;
}
console.log(`[Snapshots] Found ${activeSeasons.length} active season(s)`);
for (const season of activeSeasons) {
try {
await createDailySnapshot(season.id, db);
console.log(`[Snapshots] ✅ Upserted snapshot for season ${season.id}`);
} catch (error) {
console.error(`[Snapshots] Error creating snapshot for season ${season.id}:`, error);
}
}
console.log("[Snapshots] Daily snapshot check complete");
}
/**
* Manually trigger snapshot creation for specific seasons
* Useful for admin tools or manual triggers
*/
export async function createSnapshotsForSeasons(seasonIds: string[]): Promise<void> {
console.log(`[Snapshots] Manual trigger for ${seasonIds.length} season(s)`);
for (const seasonId of seasonIds) {
try {
await createDailySnapshot(seasonId, db);
console.log(`[Snapshots] ✅ Created snapshot for season ${seasonId}`);
} catch (error) {
console.error(`[Snapshots] Error creating snapshot for season ${seasonId}:`, error);
throw error;
}
}
}
/**
* Manually trigger snapshot creation for all active seasons
* Useful for testing or manual refreshes
*/
export async function createSnapshotsForAllSeasons(): Promise<void> {
console.log("[Snapshots] Manual trigger for all active seasons");
await createDailySnapshots();
}