1
- import type { CredentialsParams } from "../types/public" ;
1
+ import { HUB_URL } from "../consts" ;
2
+ import type { CredentialsParams , RepoDesignation , RepoId } from "../types/public" ;
2
3
import { checkCredentials } from "./checkCredentials" ;
4
+ import { toRepoId } from "./toRepoId" ;
5
+
6
+ const JWT_SAFETY_PERIOD = 60_000 ;
7
+ const JWT_CACHE_SIZE = 1_000 ;
3
8
4
9
type XetBlobCreateOptions = {
5
10
/**
6
11
* Custom fetch function to use instead of the default one, for example to use a proxy or edit headers.
7
12
*/
8
13
fetch ?: typeof fetch ;
14
+ repo : RepoDesignation ;
15
+ hash : string ;
16
+ hubUrl ?: string ;
9
17
} & Partial < CredentialsParams > ;
10
18
11
19
/**
@@ -14,11 +22,103 @@ type XetBlobCreateOptions = {
14
22
export class XetBlob extends Blob {
15
23
fetch : typeof fetch ;
16
24
accessToken ?: string ;
25
+ repoId : RepoId ;
26
+ hubUrl : string ;
17
27
18
28
constructor ( params : XetBlobCreateOptions ) {
19
29
super ( [ ] ) ;
20
30
21
31
this . fetch = params . fetch ?? fetch ;
22
32
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 ;
23
73
}
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 ;
24
124
}
0 commit comments