@@ -76,6 +76,48 @@ type OgData = {
7676 image ?: string
7777}
7878
79+ const ogCache = new Map < string , OgData > ( )
80+ const ogInflight = new Map < string , Promise < OgData | null > > ( )
81+ const ogFailures = new Map < string , number > ( )
82+ const OG_FAILURE_TTL = 30_000
83+
84+ function fetchOgData ( url : string ) : Promise < OgData | null > {
85+ const cached = ogCache . get ( url )
86+ if ( cached ) return Promise . resolve ( cached )
87+
88+ const failedAt = ogFailures . get ( url )
89+ if ( failedAt && Date . now ( ) - failedAt < OG_FAILURE_TTL ) {
90+ return Promise . resolve ( null )
91+ }
92+
93+ const inflight = ogInflight . get ( url )
94+ if ( inflight ) return inflight
95+
96+ const promise = fetch ( `/api/og?url=${ encodeURIComponent ( url ) } ` )
97+ . then ( ( res ) => {
98+ if ( ! res . ok ) throw new Error ( "Failed" )
99+ return res . json ( )
100+ } )
101+ . then ( ( data ) => {
102+ const result : OgData = { title : data ?. title , image : data ?. image }
103+ if ( ! result . title && ! result . image ) {
104+ throw new Error ( "Empty metadata" )
105+ }
106+ ogCache . set ( url , result )
107+ ogInflight . delete ( url )
108+ ogFailures . delete ( url )
109+ return result
110+ } )
111+ . catch ( ( ) => {
112+ ogInflight . delete ( url )
113+ ogFailures . set ( url , Date . now ( ) )
114+ return null
115+ } )
116+
117+ ogInflight . set ( url , promise )
118+ return promise
119+ }
120+
79121const PAGE_SIZE = 100
80122const MAX_TOTAL = 1000
81123
@@ -682,7 +724,6 @@ const DocumentCard = memo(
682724 const [ rotation , setRotation ] = useState ( { rotateX : 0 , rotateY : 0 } )
683725 const cardRef = useRef < HTMLButtonElement > ( null )
684726 const [ ogData , setOgData ] = useState < OgData | null > ( null )
685- const [ isLoadingOg , setIsLoadingOg ] = useState ( false )
686727
687728 const ogImage = ( document as DocumentWithMemories & { ogImage ?: string } )
688729 . ogImage
@@ -699,27 +740,31 @@ const DocumentCard = memo(
699740 const hideURL = document . url ?. includes ( "docs.googleapis.com" )
700741
701742 useEffect ( ( ) => {
702- if ( needsOgData && ! ogData && ! isLoadingOg && document . url ) {
703- setIsLoadingOg ( true )
704- fetch ( `/api/og?url=${ encodeURIComponent ( document . url ) } ` )
705- . then ( ( res ) => {
706- if ( ! res . ok ) throw new Error ( "Failed" )
707- return res . json ( )
708- } )
709- . then ( ( data ) => {
710- setOgData ( {
711- title : data ?. title ,
712- image : data ?. image ,
713- } )
714- } )
715- . catch ( ( ) => {
716- setOgData ( { } )
717- } )
718- . finally ( ( ) => {
719- setIsLoadingOg ( false )
720- } )
743+ if ( ! needsOgData || ogData || ! document . url ) return
744+
745+ let timeoutId : ReturnType < typeof setTimeout >
746+ let mounted = true
747+
748+ const attemptFetch = ( ) => {
749+ if ( ! mounted || ! document . url ) return
750+ fetchOgData ( document . url ) . then ( ( data ) => {
751+ if ( ! mounted ) return
752+ if ( data ) {
753+ setOgData ( data )
754+ } else {
755+ // Retry when the global TTL expires
756+ timeoutId = setTimeout ( attemptFetch , 30_000 )
757+ }
758+ } )
759+ }
760+
761+ attemptFetch ( )
762+
763+ return ( ) => {
764+ mounted = false
765+ clearTimeout ( timeoutId )
721766 }
722- } , [ needsOgData , ogData , isLoadingOg , document . url ] )
767+ } , [ needsOgData , ogData , document . url ] )
723768
724769 useEffect ( ( ) => {
725770 if ( isSelectionMode ) setRotation ( { rotateX : 0 , rotateY : 0 } )
0 commit comments