grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: non-blocking ensureRepo and direct PDS profile fetch on login

ensureRepo was blocking the OAuth callback for up to 25+ seconds on
large repos (e.g. 44MB), causing ASWebAuthenticationSession timeouts
and "Invalid code" errors on retry. Now ensureRepo runs in the
background and the bsky profile is fetched directly from the user's
PDS via getRecord (public, instant). Also fixes empty grain profiles
not being backfilled from bsky when the record already existed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+30 -14
+30 -14
server/on-login.ts
··· 1 - import { defineHook, type GrainActorProfile, type BskyActorProfile } from "$hatk"; 1 + import { defineHook, type GrainActorProfile } from "$hatk"; 2 2 3 3 export default defineHook("on-login", async (ctx) => { 4 - const { did, ensureRepo, lookup } = ctx; 4 + const { did, ensureRepo, lookup, db } = ctx; 5 5 6 - // Backfill the user's repo and wait for completion 7 - await ensureRepo(did); 6 + // Backfill repo in the background — large repos can block the login redirect 7 + ensureRepo(did).catch((err) => 8 + console.error(`[on-login] ensureRepo failed for ${did}:`, err) 9 + ); 8 10 9 - // Check if user already has a grain profile 11 + // Check if user already has a populated grain profile 10 12 const grainProfiles = await lookup<GrainActorProfile>("social.grain.actor.profile", "did", [did]); 11 - if (grainProfiles.has(did)) return; 13 + const grainProfile = grainProfiles.get(did); 14 + if (grainProfile?.value.displayName) return; 12 15 13 - // No grain profile — copy from bsky profile if available 14 - const bskyProfiles = await lookup<BskyActorProfile>("app.bsky.actor.profile", "did", [did]); 15 - const bsky = bskyProfiles.get(did); 16 + // Fetch bsky profile directly from the user's PDS (fast, no backfill needed) 17 + const rows = await db.query( 18 + "SELECT pds_endpoint FROM _oauth_sessions WHERE did = $1", 19 + [did] 20 + ) as { pds_endpoint: string }[]; 21 + const pdsEndpoint = rows[0]?.pds_endpoint; 22 + if (!pdsEndpoint) return; 23 + 24 + const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self`; 25 + const res = await fetch(url); 26 + if (!res.ok) return; 27 + const { value: bsky } = (await res.json()) as { value: Record<string, unknown> }; 16 28 if (!bsky) return; 17 29 18 30 const record: Record<string, unknown> = { 19 - createdAt: new Date().toISOString(), 31 + createdAt: grainProfile?.value.createdAt ?? new Date().toISOString(), 20 32 }; 21 - if (bsky.value.displayName) record.displayName = bsky.value.displayName; 22 - if (bsky.value.description) record.description = bsky.value.description; 23 - if (bsky.value.avatar) record.avatar = bsky.value.avatar; 33 + if (bsky.displayName) record.displayName = bsky.displayName; 34 + if (bsky.description) record.description = bsky.description; 35 + if (bsky.avatar) record.avatar = bsky.avatar; 24 36 25 - await ctx.createRecord("social.grain.actor.profile", record, { rkey: "self" }); 37 + if (grainProfile) { 38 + await ctx.putRecord("social.grain.actor.profile", "self", record); 39 + } else { 40 + await ctx.createRecord("social.grain.actor.profile", record, { rkey: "self" }); 41 + } 26 42 });