@@ -4,6 +4,10 @@ import Foundation
44 import FoundationNetworking
55#endif
66
7+ #if canImport(Xet)
8+ import Xet
9+ #endif
10+
711/// A Hugging Face Hub API client.
812///
913/// This client provides methods to interact with the Hugging Face Hub API,
@@ -32,8 +36,19 @@ public final class HubClient: Sendable {
3236 /// environment variable (defaults to https://huggingface.co).
3337 public static let `default` = HubClient ( )
3438
39+ /// Indicates whether Xet acceleration is enabled for this client.
40+ public let isXetEnabled : Bool
41+
3542 /// The underlying HTTP client.
3643 internal let httpClient : HTTPClient
44+
45+ #if canImport(Xet)
46+ /// Xet client instance for connection reuse (created once during initialization)
47+ private let xetClient : XetClient ?
48+
49+ /// Thread-safe JWT cache for CAS access tokens
50+ private let jwtCache : JwtCache
51+ #endif
3752
3853 /// The host URL for requests made by the client.
3954 public var host : URL {
@@ -67,13 +82,15 @@ public final class HubClient: Sendable {
6782 /// - userAgent: The value for the `User-Agent` header sent in requests, if any. Defaults to `nil`.
6883 public convenience init (
6984 session: URLSession = URLSession ( configuration: . default) ,
70- userAgent: String ? = nil
85+ userAgent: String ? = nil ,
86+ enableXet: Bool = HubClient . isXetSupported
7187 ) {
7288 self . init (
7389 session: session,
7490 host: Self . detectHost ( ) ,
7591 userAgent: userAgent,
76- tokenProvider: . environment
92+ tokenProvider: . environment,
93+ enableXet: enableXet
7794 )
7895 }
7996
@@ -88,13 +105,15 @@ public final class HubClient: Sendable {
88105 session: URLSession = URLSession ( configuration: . default) ,
89106 host: URL ,
90107 userAgent: String ? = nil ,
91- bearerToken: String ? = nil
108+ bearerToken: String ? = nil ,
109+ enableXet: Bool = HubClient . isXetSupported
92110 ) {
93111 self . init (
94112 session: session,
95113 host: host,
96114 userAgent: userAgent,
97- tokenProvider: bearerToken. map { . fixed( token: $0) } ?? . none
115+ tokenProvider: bearerToken. map { . fixed( token: $0) } ?? . none,
116+ enableXet: enableXet
98117 )
99118 }
100119
@@ -109,14 +128,28 @@ public final class HubClient: Sendable {
109128 session: URLSession = URLSession ( configuration: . default) ,
110129 host: URL ,
111130 userAgent: String ? = nil ,
112- tokenProvider: TokenProvider
131+ tokenProvider: TokenProvider ,
132+ enableXet: Bool = HubClient . isXetSupported
113133 ) {
134+ self . isXetEnabled = enableXet && HubClient . isXetSupported
114135 self . httpClient = HTTPClient (
115136 host: host,
116137 userAgent: userAgent,
117138 tokenProvider: tokenProvider,
118139 session: session
119140 )
141+
142+ #if canImport(Xet)
143+ self . jwtCache = JwtCache ( )
144+
145+ if self . isXetEnabled {
146+ // Create XetClient once during initialization
147+ let token = try ? tokenProvider. getToken ( )
148+ self . xetClient = try ? ( token. map { try XetClient . withToken ( token: $0) } ?? XetClient ( ) )
149+ } else {
150+ self . xetClient = nil
151+ }
152+ #endif
120153 }
121154
122155 // MARK: - Auto-detection
@@ -134,4 +167,104 @@ public final class HubClient: Sendable {
134167 }
135168 return defaultHost
136169 }
137- }
170+
171+ public static var isXetSupported : Bool {
172+ #if canImport(Xet)
173+ return true
174+ #else
175+ return false
176+ #endif
177+ }
178+
179+ // MARK: - Xet Client
180+
181+ #if canImport(Xet)
182+ /// Thread-safe cache for CAS JWT tokens
183+ private final class JwtCache : @unchecked Sendable {
184+ private struct CacheKey : Hashable {
185+ let repo : String
186+ let revision : String
187+ }
188+
189+ private struct CachedJwt {
190+ let jwt : CasJwtInfo
191+ let expiresAt : Date
192+
193+ var isExpired : Bool {
194+ Date ( ) >= expiresAt
195+ }
196+ }
197+
198+ private var cache : [ CacheKey : CachedJwt ] = [ : ]
199+ private let lock = NSLock ( )
200+
201+ func get( repo: String , revision: String ) -> CasJwtInfo ? {
202+ lock. lock ( )
203+ defer { lock. unlock ( ) }
204+
205+ let key = CacheKey ( repo: repo, revision: revision)
206+ if let cached = cache [ key] , !cached. isExpired {
207+ return cached. jwt
208+ }
209+ return nil
210+ }
211+
212+ func set( jwt: CasJwtInfo , repo: String , revision: String ) {
213+ lock. lock ( )
214+ defer { lock. unlock ( ) }
215+
216+ let key = CacheKey ( repo: repo, revision: revision)
217+ // Cache with expiration (5 minutes before actual expiry for safety)
218+ let expiresAt = Date ( timeIntervalSince1970: TimeInterval ( jwt. exp ( ) ) ) - 300
219+ cache [ key] = CachedJwt ( jwt: jwt, expiresAt: expiresAt)
220+ }
221+ }
222+
223+ /// Returns the Xet client for faster downloads.
224+ ///
225+ /// The client is created once during initialization and reused across downloads
226+ /// to enable connection pooling and avoid reinitialization overhead.
227+ ///
228+ /// - Returns: A Xet client instance.
229+ internal func getXetClient( ) throws -> XetClient {
230+ guard isXetEnabled, let client = xetClient else {
231+ throw HTTPClientError . requestError ( " Xet support is disabled for this client. " )
232+ }
233+ return client
234+ }
235+
236+ /// Gets or fetches a CAS JWT for the given repository and revision.
237+ ///
238+ /// JWTs are cached to avoid redundant API calls.
239+ ///
240+ /// - Parameters:
241+ /// - xetClient: The Xet client to use for fetching the JWT
242+ /// - repo: Repository identifier
243+ /// - revision: Git revision
244+ /// - isUpload: Whether this JWT is for upload (true) or download (false)
245+ /// - Returns: A CAS JWT info object
246+ internal func getCachedJwt(
247+ xetClient: XetClient ,
248+ repo: String ,
249+ revision: String ,
250+ isUpload: Bool
251+ ) throws -> CasJwtInfo {
252+ // Check cache first
253+ if let cached = jwtCache. get ( repo: repo, revision: revision) {
254+ return cached
255+ }
256+
257+ // Fetch a new JWT
258+ let jwt = try xetClient. getCasJwt (
259+ repo: repo,
260+ revision: revision,
261+ isUpload: isUpload
262+ )
263+
264+ // Cache it
265+ jwtCache. set ( jwt: jwt, repo: repo, revision: revision)
266+
267+ return jwt
268+ }
269+ #endif
270+ }
0 commit comments