@@ -9,13 +9,12 @@ const ASSET_NAMES: Record<Arch, string> = {
99} ;
1010const LATEST_CACHE_TTL = 300 ; // 5 minutes
1111const TAGGED_CACHE_TTL = 86400 ; // 24 hours
12- const STALE_CACHE_TTL = 3600 ; // 1 hour fallback
1312
1413type Arch = "x64" | "arm64" ;
1514
1615interface CachedRelease {
1716 tag : string ;
18- assets : Record < Arch , string > ;
17+ assets : Partial < Record < Arch , string > > ;
1918}
2019
2120interface GitHubRelease {
@@ -38,7 +37,7 @@ export function detectArch(
3837}
3938
4039export function parseRelease ( release : GitHubRelease ) : CachedRelease | null {
41- const assets = { } as Record < Arch , string > ;
40+ const assets : Partial < Record < Arch , string > > = { } ;
4241 for ( const asset of release . assets ) {
4342 if ( asset . name === ASSET_NAMES . x64 ) assets . x64 = asset . browser_download_url ;
4443 if ( asset . name === ASSET_NAMES . arm64 ) assets . arm64 = asset . browser_download_url ;
@@ -47,65 +46,80 @@ export function parseRelease(release: GitHubRelease): CachedRelease | null {
4746 return { tag : release . tag_name , assets } ;
4847}
4948
50- async function fetchRelease (
51- tag : string | undefined ,
52- githubToken : string | undefined ,
53- ) : Promise < GitHubRelease | null > {
49+ async function fetchGitHub ( path : string , githubToken : string | undefined ) : Promise < Response > {
5450 const headers : Record < string , string > = {
5551 Accept : "application/vnd.github.v3+json" ,
5652 "User-Agent" : "vp-setup-exe-downloader" ,
5753 } ;
5854 if ( githubToken ) headers . Authorization = `Bearer ${ githubToken } ` ;
55+ return fetch ( `https://api.github.com${ path } ` , { headers } ) ;
56+ }
5957
58+ async function fetchRelease (
59+ tag : string | undefined ,
60+ githubToken : string | undefined ,
61+ ) : Promise < GitHubRelease | null > {
6062 if ( tag ) {
61- const url = `https://api.github.com/repos/${ GITHUB_OWNER } /${ GITHUB_REPO } /releases/tags/${ tag } ` ;
62- const res = await fetch ( url , { headers } ) ;
63- if ( ! res . ok ) return null ;
63+ const res = await fetchGitHub (
64+ `/repos/${ GITHUB_OWNER } /${ GITHUB_REPO } /releases/tags/${ tag } ` ,
65+ githubToken ,
66+ ) ;
67+ if ( ! res . ok ) {
68+ console . error ( `GitHub API error: ${ res . status } ${ res . statusText } for tag ${ tag } ` ) ;
69+ return null ;
70+ }
6471 return ( await res . json ( ) ) as GitHubRelease ;
6572 }
6673
67- // List releases to find the latest one with exe assets (includes pre-releases)
68- const url = `https://api.github.com/repos/${ GITHUB_OWNER } /${ GITHUB_REPO } /releases?per_page=10` ;
69- const res = await fetch ( url , { headers } ) ;
70- if ( ! res . ok ) return null ;
71- const releases = ( await res . json ( ) ) as GitHubRelease [ ] ;
72- for ( const release of releases ) {
73- if ( release . assets . some ( ( a ) => a . name === ASSET_NAMES . x64 || a . name === ASSET_NAMES . arm64 ) ) {
74- return release ;
75- }
74+ // Includes pre-releases, unlike /releases/latest
75+ const res = await fetchGitHub (
76+ `/repos/${ GITHUB_OWNER } /${ GITHUB_REPO } /releases?per_page=10` ,
77+ githubToken ,
78+ ) ;
79+ if ( ! res . ok ) {
80+ console . error ( `GitHub API error: ${ res . status } ${ res . statusText } for releases list` ) ;
81+ return null ;
7682 }
77- return null ;
83+ const releases = ( await res . json ( ) ) as GitHubRelease [ ] ;
84+ return (
85+ releases . find ( ( r ) =>
86+ r . assets . some ( ( a ) => a . name === ASSET_NAMES . x64 || a . name === ASSET_NAMES . arm64 ) ,
87+ ) ?? null
88+ ) ;
7889}
7990
80- function cacheKey ( tag ? : string ) : string {
91+ function cacheKey ( tag : string | undefined ) : string {
8192 return tag ? `release:tag:${ tag } ` : "release:latest" ;
8293}
8394
95+ function staleCacheKey ( tag : string | undefined ) : string {
96+ return `${ cacheKey ( tag ) } :stale` ;
97+ }
98+
8499async function getRelease (
85100 tag : string | undefined ,
86101 githubToken : string | undefined ,
87102) : Promise < CachedRelease | null > {
88103 const key = cacheKey ( tag ) ;
89-
90- // Try fresh cache
91104 const cached = await kv . get < CachedRelease > ( key ) ;
92105 if ( cached ) return cached ;
93106
94- // Fetch from GitHub
95107 try {
96108 const release = await fetchRelease ( tag , githubToken ) ;
97109 if ( ! release ) return null ;
98110 const parsed = parseRelease ( release ) ;
99111 if ( parsed ) {
100112 const ttl = tag ? TAGGED_CACHE_TTL : LATEST_CACHE_TTL ;
101- await kv . put ( key , parsed , { ttl } ) ;
102- // Store stale fallback with longer TTL
103- await kv . put ( `${ key } :stale` , parsed , { ttl : STALE_CACHE_TTL } ) ;
113+ const staleTtl = ttl + 3600 ;
114+ await Promise . all ( [
115+ kv . put ( key , parsed , { ttl } ) ,
116+ kv . put ( staleCacheKey ( tag ) , parsed , { ttl : staleTtl } ) ,
117+ ] ) ;
104118 }
105119 return parsed ;
106- } catch {
107- // On failure, try stale cache
108- return await kv . get < CachedRelease > ( ` ${ key } :stale` ) ;
120+ } catch ( err ) {
121+ console . error ( "Failed to fetch release from GitHub:" , err ) ;
122+ return await kv . get < CachedRelease > ( staleCacheKey ( tag ) ) ;
109123 }
110124}
111125
@@ -119,7 +133,7 @@ export const GET = defineHandler(async (c) => {
119133 return c . json ( { error : "Invalid architecture. Use 'x64' or 'arm64'" } , 400 ) ;
120134 }
121135
122- const githubToken = ( c . env as Record < string , string > ) . GITHUB_TOKEN ;
136+ const githubToken = c . env . GITHUB_TOKEN as string | undefined ;
123137 const release = await getRelease ( tag || undefined , githubToken ) ;
124138
125139 if ( ! release ) {
0 commit comments