Ship Steam login, D1 player sync, and cached「我」dashboard.
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>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/** 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;
|
||||
}
|
||||
Reference in New Issue
Block a user