This commit is contained in:
Clove 2026-06-19 01:05:10 +01:00
parent cb9ebad0fa
commit 2479564b8b
3 changed files with 40 additions and 10 deletions

View File

@ -224,7 +224,13 @@ export class GatewayManager implements DurableObject {
token: this.env.DISCORD_BOT_TOKEN, token: this.env.DISCORD_BOT_TOKEN,
intents: INTENTS, intents: INTENTS,
properties: { os: "linux", browser: "dough-restful", device: "dough-restful" }, properties: { os: "linux", browser: "dough-restful", device: "dough-restful" },
presence: { status: "invisible", activities: [], since: 0, afk: false }, presence: {
status: "idle",
afk: false,
since: 0,
// Custom status (type 4): the text shown is the `state` field.
activities: [{ name: "Custom Status", type: 4, state: "meow meow mrrp meow" }],
},
}, },
}); });
} }

View File

@ -91,7 +91,7 @@ export default {
} }
if (sub === "profile") { if (sub === "profile") {
const profile = await getProfile(env, id); const profile = await getProfile(env, id, ctx);
if (!profile) { if (!profile) {
return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404); return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404);
} }
@ -99,7 +99,7 @@ export default {
} }
// Unified record: profile (REST) + presence (gateway), in parallel. // Unified record: profile (REST) + presence (gateway), in parallel.
const [profile, presence] = await Promise.all([getProfile(env, id), fetchPresence(env, id)]); const [profile, presence] = await Promise.all([getProfile(env, id, ctx), fetchPresence(env, id)]);
if (!profile) { if (!profile) {
return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404); return json({ success: false, error: { code: "not_found", message: "User not found." } }, 404);
} }

View File

@ -84,18 +84,43 @@ function cacheKey(id: string): string {
return `profile:${id}`; return `profile:${id}`;
} }
/** Build profile from Discord, with a KV read-through cache. */ /**
export async function getProfile(env: Env, id: string): Promise<ProfileResult | null> { * Build profile from Discord LIVE-FIRST.
*
* Always fetches fresh from Discord so profile/badges/connections are current,
* and only falls back to the KV copy if the REST call fails (e.g. Discord is
* rate-limiting us). The KV copy is refreshed in the background, throttled to
* at most one write per TTL window so we don't blow KV write limits.
*/
export async function getProfile(
env: Env,
id: string,
ctx?: ExecutionContext
): Promise<ProfileResult | null> {
const fresh = await buildFreshProfile(env, id);
if (fresh) {
const refresh = maybeRefreshCache(env, id, fresh);
if (ctx) ctx.waitUntil(refresh);
else await refresh;
return fresh;
}
// Discord REST failed — serve last-known-good from KV if we have it.
const cached = await env.PROFILE_CACHE.get(cacheKey(id), "json"); const cached = await env.PROFILE_CACHE.get(cacheKey(id), "json");
if (cached) { if (cached) {
const c = cached as Omit<ProfileResult, "source">; const c = cached as Omit<ProfileResult, "source">;
return { ...c, source: "cache" }; return { ...c, source: "cache" };
} }
return null;
}
const result = await buildFreshProfile(env, id); /** Write the fallback copy to KV, but at most once per TTL window. */
if (!result) return null; async function maybeRefreshCache(env: Env, id: string, result: ProfileResult): Promise<void> {
const ttl = Math.max(60, Number(env.PROFILE_CACHE_TTL_SECONDS || "300")); const ttl = Math.max(60, Number(env.PROFILE_CACHE_TTL_SECONDS || "300"));
const { metadata } = await env.PROFILE_CACHE.getWithMetadata(cacheKey(id));
const lastWrite = (metadata as { t?: number } | null)?.t ?? 0;
if (Date.now() - lastWrite < ttl * 1000) return; // throttled
await env.PROFILE_CACHE.put( await env.PROFILE_CACHE.put(
cacheKey(id), cacheKey(id),
JSON.stringify({ JSON.stringify({
@ -103,9 +128,8 @@ export async function getProfile(env: Env, id: string): Promise<ProfileResult |
badges: result.badges, badges: result.badges,
connected_accounts: result.connected_accounts, connected_accounts: result.connected_accounts,
}), }),
{ expirationTtl: ttl } { expirationTtl: ttl * 2, metadata: { t: Date.now() } }
); );
return result;
} }
async function buildFreshProfile(env: Env, id: string): Promise<ProfileResult | null> { async function buildFreshProfile(env: Env, id: string): Promise<ProfileResult | null> {