Players get a fast TTL-backed homepage (local profile / Cloudflare D1) with dense UI polish; login unlocks /home without blocking on every OpenDota refresh. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.2 KiB
JavaScript
152 lines
4.2 KiB
JavaScript
/** 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;
|
|
}
|