Fix Steam sync logic: extract retry helper, add 429/rate-limit handling, game type validation, set syncStatus on errors
- Extract recordSyncFailure() helper to deduplicate 3 identical retry/backoff blocks - Set syncStatus: 'error' on all failure paths (was missing before) - Handle HTTP 429 (rate limiting) with Retry-After header support - Reject non-game Steam app types (DLC, soundtrack, demo) during sync - Add type field to SteamAppDetails interface
This commit is contained in:
+39
-52
@@ -63,6 +63,7 @@ interface SteamAppDetails {
|
|||||||
recommendations?: { total: number }
|
recommendations?: { total: number }
|
||||||
price_overview?: { currency: string; initial: number; final: number }
|
price_overview?: { currency: string; initial: number; final: number }
|
||||||
is_free?: boolean
|
is_free?: boolean
|
||||||
|
type?: string
|
||||||
release_date?: { coming_soon: boolean; date: string }
|
release_date?: { coming_soon: boolean; date: string }
|
||||||
categories?: { id: string; description: string }[]
|
categories?: { id: string; description: string }[]
|
||||||
platforms?: { windows: boolean; mac: boolean; linux: boolean }
|
platforms?: { windows: boolean; mac: boolean; linux: boolean }
|
||||||
@@ -72,6 +73,26 @@ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
|
|||||||
const ONE_HOUR_MS = 60 * 60 * 1000
|
const ONE_HOUR_MS = 60 * 60 * 1000
|
||||||
const MAX_RETRY_DELAY_MS = SEVEN_DAYS_MS
|
const MAX_RETRY_DELAY_MS = SEVEN_DAYS_MS
|
||||||
|
|
||||||
|
async function recordSyncFailure(steamAppId: number, errorMsg: string): Promise<void> {
|
||||||
|
const [currentGame] = await db
|
||||||
|
.select({ retryCount: games.syncRetryCount })
|
||||||
|
.from(games)
|
||||||
|
.where(eq(games.steamAppId, steamAppId))
|
||||||
|
.limit(1)
|
||||||
|
const retryCount = (currentGame?.retryCount ?? 0) + 1
|
||||||
|
const backoffMs = Math.min(ONE_HOUR_MS * Math.pow(2, retryCount - 1), MAX_RETRY_DELAY_MS)
|
||||||
|
await db
|
||||||
|
.update(games)
|
||||||
|
.set({
|
||||||
|
syncStatus: "error",
|
||||||
|
syncError: errorMsg,
|
||||||
|
syncRetryCount: retryCount,
|
||||||
|
syncNextRetry: new Date(Date.now() + backoffMs),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(games.steamAppId, steamAppId))
|
||||||
|
}
|
||||||
|
|
||||||
export function isSyncStale(lastSync: Date | null): boolean {
|
export function isSyncStale(lastSync: Date | null): boolean {
|
||||||
if (!lastSync) return true
|
if (!lastSync) return true
|
||||||
return Date.now() - new Date(lastSync).getTime() > SEVEN_DAYS_MS
|
return Date.now() - new Date(lastSync).getTime() > SEVEN_DAYS_MS
|
||||||
@@ -154,25 +175,17 @@ export async function syncSteamGame(
|
|||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (res.status === 429) {
|
||||||
|
const retryAfterHeader = res.headers.get("Retry-After");
|
||||||
|
const retryAfterSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 60;
|
||||||
|
const errorMsg = `Rate limited by Steam (429). Retry after ${retryAfterSec}s`;
|
||||||
|
await recordSyncFailure(steamAppId, errorMsg);
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorMsg = `Steam API returned ${res.status}`
|
const errorMsg = `Steam API returned ${res.status}`
|
||||||
// Get current retry count for exponential backoff
|
await recordSyncFailure(steamAppId, errorMsg)
|
||||||
const [currentGame] = await db
|
|
||||||
.select({ retryCount: games.syncRetryCount })
|
|
||||||
.from(games)
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
.limit(1)
|
|
||||||
const retryCount = (currentGame?.retryCount ?? 0) + 1
|
|
||||||
const backoffMs = Math.min(ONE_HOUR_MS * Math.pow(2, retryCount - 1), MAX_RETRY_DELAY_MS)
|
|
||||||
await db
|
|
||||||
.update(games)
|
|
||||||
.set({
|
|
||||||
syncError: errorMsg,
|
|
||||||
syncRetryCount: retryCount,
|
|
||||||
syncNextRetry: new Date(Date.now() + backoffMs),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,28 +197,19 @@ export async function syncSteamGame(
|
|||||||
|
|
||||||
if (!entry?.success || !entry.data) {
|
if (!entry?.success || !entry.data) {
|
||||||
const errorMsg = `No data returned from Steam for app ${steamAppId}`
|
const errorMsg = `No data returned from Steam for app ${steamAppId}`
|
||||||
// Get current retry count for exponential backoff
|
await recordSyncFailure(steamAppId, errorMsg)
|
||||||
const [currentGame] = await db
|
|
||||||
.select({ retryCount: games.syncRetryCount })
|
|
||||||
.from(games)
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
.limit(1)
|
|
||||||
const retryCount = (currentGame?.retryCount ?? 0) + 1
|
|
||||||
const backoffMs = Math.min(ONE_HOUR_MS * Math.pow(2, retryCount - 1), MAX_RETRY_DELAY_MS)
|
|
||||||
await db
|
|
||||||
.update(games)
|
|
||||||
.set({
|
|
||||||
syncError: errorMsg,
|
|
||||||
syncRetryCount: retryCount,
|
|
||||||
syncNextRetry: new Date(Date.now() + backoffMs),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = entry.data
|
const d = entry.data
|
||||||
|
|
||||||
|
// Reject non-game types (DLC, soundtrack, demo, etc.)
|
||||||
|
if (d.type && d.type !== "game") {
|
||||||
|
const errorMsg = `Steam app ${steamAppId} is not a game (type: ${d.type})`;
|
||||||
|
await recordSyncFailure(steamAppId, errorMsg);
|
||||||
|
return { success: false, error: errorMsg };
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch review data
|
// Fetch review data
|
||||||
const reviewData = await fetchSteamReviews(steamAppId);
|
const reviewData = await fetchSteamReviews(steamAppId);
|
||||||
|
|
||||||
@@ -273,25 +277,8 @@ export async function syncSteamGame(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : String(err)
|
const errorMsg = err instanceof Error ? err.message : String(err)
|
||||||
try {
|
try {
|
||||||
// Get current retry count for exponential backoff
|
await recordSyncFailure(steamAppId, errorMsg)
|
||||||
const [currentGame] = await db
|
|
||||||
.select({ retryCount: games.syncRetryCount })
|
|
||||||
.from(games)
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
.limit(1)
|
|
||||||
const retryCount = (currentGame?.retryCount ?? 0) + 1
|
|
||||||
const backoffMs = Math.min(ONE_HOUR_MS * Math.pow(2, retryCount - 1), MAX_RETRY_DELAY_MS)
|
|
||||||
await db
|
|
||||||
.update(games)
|
|
||||||
.set({
|
|
||||||
syncError: errorMsg,
|
|
||||||
syncRetryCount: retryCount,
|
|
||||||
syncNextRetry: new Date(Date.now() + backoffMs),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(games.steamAppId, steamAppId))
|
|
||||||
} catch {
|
} catch {
|
||||||
// If DB update fails too, just log it
|
|
||||||
console.error(`Failed to update error tracking for ${steamAppId}`)
|
console.error(`Failed to update error tracking for ${steamAppId}`)
|
||||||
}
|
}
|
||||||
return { success: false, error: errorMsg }
|
return { success: false, error: errorMsg }
|
||||||
|
|||||||
Reference in New Issue
Block a user