a tool for shared writing and social publishing
0

Configure Feed

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

share oauth client per function

Jared Pereira (May 28, 2026, 2:51 PM EDT) 7460c5c2 94a3a441

+59 -15
+5 -12
app/api/rpc/[command]/get_profile_data.ts
··· 5 5 import { supabaseServerClient } from "supabase/serverClient"; 6 6 import { Agent } from "@atproto/api"; 7 7 import { getIdentityData } from "actions/getIdentityData"; 8 - import { createOauthClient } from "src/atproto-oauth"; 8 + import { restoreOAuthSession } from "src/atproto-oauth"; 9 9 import { 10 10 normalizePublicationRow, 11 11 hasValidPublication, ··· 35 35 let agent; 36 36 let authed_identity = await getIdentityData(); 37 37 if (authed_identity?.atp_did) { 38 - try { 39 - const oauthClient = await createOauthClient(); 40 - let credentialSession = await oauthClient.restore( 41 - authed_identity.atp_did, 42 - ); 43 - agent = new Agent(credentialSession); 44 - } catch (e) { 45 - agent = new Agent({ 46 - service: "https://public.api.bsky.app", 47 - }); 48 - } 38 + const restored = await restoreOAuthSession(authed_identity.atp_did); 39 + agent = restored.ok 40 + ? new Agent(restored.value) 41 + : new Agent({ service: "https://public.api.bsky.app" }); 49 42 } else { 50 43 agent = new Agent({ 51 44 service: "https://public.api.bsky.app",
+54 -3
src/atproto-oauth.ts
··· 12 12 import Client from "ioredis"; 13 13 import Redlock from "redlock"; 14 14 import { Result, Ok, Err } from "./result"; 15 - export async function createOauthClient() { 15 + 16 + // Module-scoped singleton: NodeOAuthClient, ioredis connection, and Redlock 17 + // have no per-request state — keys/stores live above the user — so building 18 + // them once per Node instance avoids reconnect + keyset re-import on every call. 19 + // Stashed on globalThis so Next.js dev hot-reload doesn't leak Redis sockets. 20 + const globalForOauth = globalThis as unknown as { 21 + __oauthClient?: Promise<NodeOAuthClient>; 22 + }; 23 + 24 + export function createOauthClient(): Promise<NodeOAuthClient> { 25 + if (!globalForOauth.__oauthClient) { 26 + globalForOauth.__oauthClient = buildOauthClient(); 27 + } 28 + return globalForOauth.__oauthClient; 29 + } 30 + 31 + async function buildOauthClient(): Promise<NodeOAuthClient> { 16 32 let keyset = 17 33 process.env.NODE_ENV === "production" 18 34 ? await Promise.all([ ··· 99 115 did: string; 100 116 }; 101 117 118 + // In-process dedupe: collapse concurrent restore() calls for the same DID into 119 + // one underlying restore + Redlock acquisition. Successful entries linger 120 + // briefly so a burst of requests (e.g. hover-fired ProfilePopovers) share one 121 + // result; rejected promises evict immediately so a transient failure doesn't 122 + // stick around poisoning subsequent calls. 123 + const RESTORE_DEDUPE_TTL_MS = 5_000; 124 + const inFlightRestores = new Map<string, Promise<OAuthSession>>(); 125 + 126 + function dedupedRestore(did: string): Promise<OAuthSession> { 127 + let existing = inFlightRestores.get(did); 128 + if (existing) return existing; 129 + 130 + const promise = (async () => { 131 + const oauthClient = await createOauthClient(); 132 + return oauthClient.restore(did); 133 + })(); 134 + inFlightRestores.set(did, promise); 135 + 136 + promise.then( 137 + () => { 138 + setTimeout(() => { 139 + if (inFlightRestores.get(did) === promise) { 140 + inFlightRestores.delete(did); 141 + } 142 + }, RESTORE_DEDUPE_TTL_MS); 143 + }, 144 + () => { 145 + if (inFlightRestores.get(did) === promise) { 146 + inFlightRestores.delete(did); 147 + } 148 + }, 149 + ); 150 + 151 + return promise; 152 + } 153 + 102 154 export async function restoreOAuthSession( 103 155 did: string 104 156 ): Promise<Result<OAuthSession, OAuthSessionError>> { 105 157 try { 106 - const oauthClient = await createOauthClient(); 107 - const session = await oauthClient.restore(did); 158 + const session = await dedupedRestore(did); 108 159 return Ok(session); 109 160 } catch (error) { 110 161 return Err({