/** D1 helpers for Pages Functions (subset of cloudflare/player-sync/src/db.js). */ export function utcNow() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); } export function jsonResponse(body, status = 200, extra = {}) { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", ...extra, }, }); } export async function loadPlayerBundle(db, accountId) { if (!db) return null; const user = await db .prepare(`SELECT * FROM users WHERE account_id = ?`) .bind(accountId) .first(); if (!user) return null; const profile = await db .prepare(`SELECT * FROM player_profiles WHERE account_id = ?`) .bind(accountId) .first(); const statsRows = await db .prepare(`SELECT * FROM player_stats WHERE account_id = ?`) .bind(accountId) .all(); const heroes = await db .prepare( `SELECT * FROM player_heroes WHERE account_id = ? ORDER BY games DESC LIMIT 8` ) .bind(accountId) .all(); const peers = await db .prepare( `SELECT * FROM player_peers WHERE account_id = ? ORDER BY games DESC LIMIT 8` ) .bind(accountId) .all(); const recent = await db .prepare( `SELECT * FROM player_matches WHERE account_id = ? ORDER BY start_time DESC LIMIT 20` ) .bind(accountId) .all(); const statsByScope = {}; for (const row of (statsRows && statsRows.results) || []) { try { statsByScope[row.scope] = row.payload_json ? JSON.parse(row.payload_json) : row; } catch { statsByScope[row.scope] = row; } } return { account_id: accountId, personaname: user.personaname, avatar: user.avatar, public_share: !!user.public_share, rank_tier: profile && profile.rank_tier, leaderboard_rank: profile && profile.leaderboard_rank, availability: profile ? { status: profile.availability_status, note: profile.availability_note, complete: !!profile.availability_complete, source: profile.source, fetched_at: profile.fetched_at, stale: false, } : null, career: statsByScope.career || null, recent_20: statsByScope.recent20 || null, activity_180: statsByScope.recent180 || null, top_heroes: ((heroes && heroes.results) || []).map((h) => ({ hero_id: h.hero_id, hero_key: h.hero_key, hero_name_loc: h.hero_name_loc, games: h.games, wins: h.wins, winrate: h.winrate, last_played: h.last_played, })), peers: ((peers && peers.results) || []).map((p) => ({ account_id: p.peer_account_id, personaname: p.personaname, avatar: p.avatar, games: p.games, wins: p.wins, winrate: p.winrate, })), recent: ((recent && recent.results) || []).map((r) => ({ match_id: r.match_id, start_time: r.start_time, duration: r.duration, won: !!r.won, hero_id: r.hero_id, hero_key: r.hero_key, hero_name_loc: r.hero_name_loc, kills: r.kills, deaths: r.deaths, assists: r.assists, kda: r.kda, gpm: r.gpm, xpm: r.xpm, hero_damage: r.hero_damage, game_mode: r.game_mode, lobby_type: r.lobby_type, })), updated_at: (profile && profile.updated_at) || user.last_login_at, enriched_at: profile && profile.enriched_at, }; } export function isStale(fetchedAt, maxAgeMs = 10 * 60 * 1000) { if (!fetchedAt) return true; const t = Date.parse(fetchedAt); if (!Number.isFinite(t)) return true; return Date.now() - t > maxAgeMs; } export async function enqueueRefresh(env, payload) { if (!env.SYNC_QUEUE) return false; const jobId = crypto.randomUUID(); const now = utcNow(); if (env.DB) { await env.DB.prepare( `INSERT INTO sync_jobs (id, account_id, kind, match_id, status, attempts, created_at, updated_at) VALUES (?, ?, ?, ?, 'queued', 0, ?, ?)` ) .bind( jobId, payload.account_id, payload.kind || "login_refresh", payload.match_id || null, now, now ) .run(); } await env.SYNC_QUEUE.send({ ...payload, job_id: jobId }); return true; }