1- import type { CredentialsParams } from "../types/public" ;
1+ import { HUB_URL } from "../consts" ;
2+ import type { CredentialsParams , RepoDesignation , RepoId } from "../types/public" ;
23import { checkCredentials } from "./checkCredentials" ;
4+ import { toRepoId } from "./toRepoId" ;
5+
6+ const JWT_SAFETY_PERIOD = 60_000 ;
7+ const JWT_CACHE_SIZE = 1_000 ;
38
49type XetBlobCreateOptions = {
510 /**
611 * Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
712 */
813 fetch ?: typeof fetch ;
14+ repo : RepoDesignation ;
15+ hash : string ;
16+ hubUrl ?: string ;
917} & Partial < CredentialsParams > ;
1018
1119/**
@@ -14,11 +22,103 @@ type XetBlobCreateOptions = {
1422export class XetBlob extends Blob {
1523 fetch : typeof fetch ;
1624 accessToken ?: string ;
25+ repoId : RepoId ;
26+ hubUrl : string ;
1727
1828 constructor ( params : XetBlobCreateOptions ) {
1929 super ( [ ] ) ;
2030
2131 this . fetch = params . fetch ?? fetch ;
2232 this . accessToken = checkCredentials ( params ) ;
33+ this . repoId = toRepoId ( params . repo ) ;
34+ this . hubUrl = params . hubUrl ?? HUB_URL ;
35+ }
36+ }
37+
38+ const jwtPromises : Map < string , Promise < { accessToken : string ; casUrl : string } > > = new Map ( ) ;
39+ /**
40+ * Cache to store JWTs, to avoid making many auth requests when downloading multiple files from the same repo
41+ */
42+ const jwts : Map <
43+ string ,
44+ {
45+ accessToken : string ;
46+ expiresAt : Date ;
47+ casUrl : string ;
48+ }
49+ > = new Map ( ) ;
50+
51+ function cacheKey ( params : { repoId : RepoId ; initialAccessToken : string | undefined } ) : string {
52+ return `${ params . repoId . type } :${ params . repoId . name } :${ params . initialAccessToken } ` ;
53+ }
54+
55+ async function getAccessToken (
56+ repoId : RepoId ,
57+ initialAccessToken : string | undefined ,
58+ customFetch : typeof fetch ,
59+ hubUrl : string
60+ ) : Promise < { accessToken : string ; casUrl : string } > {
61+ const key = cacheKey ( { repoId, initialAccessToken } ) ;
62+
63+ const jwt = jwts . get ( key ) ;
64+
65+ if ( jwt && jwt . expiresAt > new Date ( Date . now ( ) + JWT_SAFETY_PERIOD ) ) {
66+ return { accessToken : jwt . accessToken , casUrl : jwt . casUrl } ;
67+ }
68+
69+ // If we already have a promise for this repo, return it
70+ const existingPromise = jwtPromises . get ( key ) ;
71+ if ( existingPromise ) {
72+ return existingPromise ;
2373 }
74+
75+ const promise = ( async ( ) => {
76+ const url = `${ hubUrl } /api/${ repoId . type } s/${ repoId . name } /xet-read-token/main` ;
77+ const resp = await customFetch ( url , {
78+ headers : {
79+ ...( initialAccessToken
80+ ? {
81+ Authorization : `Bearer ${ initialAccessToken } ` ,
82+ }
83+ : { } ) ,
84+ } ,
85+ } ) ;
86+
87+ if ( ! resp . ok ) {
88+ throw new Error ( `Failed to get JWT token: ${ resp . status } ${ await resp . text ( ) } ` ) ;
89+ }
90+
91+ const json = await resp . json ( ) ;
92+ const jwt = {
93+ repoId,
94+ accessToken : json . token ,
95+ expiresAt : new Date ( json . exp * 1000 ) ,
96+ initialAccessToken,
97+ hubUrl,
98+ casUrl : json . casUrl ,
99+ } ;
100+
101+ jwtPromises . delete ( key ) ;
102+
103+ for ( const [ key , value ] of jwts . entries ( ) ) {
104+ if ( value . expiresAt < new Date ( Date . now ( ) + JWT_SAFETY_PERIOD ) ) {
105+ jwts . delete ( key ) ;
106+ } else {
107+ break ;
108+ }
109+ }
110+ if ( jwts . size >= JWT_CACHE_SIZE ) {
111+ const keyToDelete = jwts . keys ( ) . next ( ) . value ;
112+ if ( keyToDelete ) {
113+ jwts . delete ( keyToDelete ) ;
114+ }
115+ }
116+ jwts . set ( key , jwt ) ;
117+
118+ return jwt . accessToken ;
119+ } ) ( ) ;
120+
121+ jwtPromises . set ( repoId . name , promise ) ;
122+
123+ return promise ;
24124}
0 commit comments