@onekeyfe/react-native-sni-connect 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,10 +38,16 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
38
38
  private const val EVENT_NAME = "SniConnectLog"
39
39
  }
40
40
 
41
+ /**
42
+ * Cache key for OkHttpClient instances
43
+ * Uses hostname:IP combination to ensure:
44
+ * - Different IPs for the same hostname are isolated (accurate speed testing)
45
+ * - Same hostname+IP combination can reuse connections (performance optimization)
46
+ * Note: timeout is NOT part of the key to allow connection reuse across different timeout values
47
+ */
41
48
  private data class ClientKey(
42
- val ip: String,
43
49
  val hostname: String,
44
- val timeoutMillis: Long,
50
+ val ip: String,
45
51
  )
46
52
 
47
53
  private val clientCache = ConcurrentHashMap<ClientKey, OkHttpClient>()
@@ -136,6 +142,9 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
136
142
  val request = buildRequest(config)
137
143
  val call = client.newCall(request)
138
144
 
145
+ // Apply per-request timeout
146
+ call.timeout().timeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
147
+
139
148
  // Register the call if requestId is provided
140
149
  config.requestId?.let { requestId ->
141
150
  activeCalls[requestId] = call
@@ -191,14 +200,19 @@ class SniConnectModule(reactContext: ReactApplicationContext) :
191
200
 
192
201
  private fun getOrCreateClient(config: RequestConfig): OkHttpClient {
193
202
  val normalizedHost = config.hostname.lowercase(Locale.US)
194
- val key = ClientKey(config.ip, normalizedHost, config.timeoutMillis)
203
+ val key = ClientKey(normalizedHost, config.ip)
195
204
 
196
205
  return clientCache.getOrPut(key) {
206
+ // Note: timeout is not part of the cache key to allow connection reuse
207
+ // Use reasonable default timeouts for the client
208
+ // Per-request timeout is applied via call.timeout() in performRequest
209
+ val defaultTimeout = 60_000L // 60 seconds
210
+
197
211
  OkHttpClient.Builder()
198
- .connectTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
199
- .readTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
200
- .writeTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
201
- .callTimeout(config.timeoutMillis, TimeUnit.MILLISECONDS)
212
+ .connectTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
213
+ .readTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
214
+ .writeTimeout(defaultTimeout, TimeUnit.MILLISECONDS)
215
+ .callTimeout(0, TimeUnit.MILLISECONDS) // Disable client-level call timeout
202
216
  .hostnameVerifier { _, session ->
203
217
  HttpsURLConnection.getDefaultHostnameVerifier().verify(config.hostname, session)
204
218
  }
@@ -179,8 +179,10 @@ final class SniConnectClient {
179
179
  private static let cache = DNSCache()
180
180
 
181
181
  /// LRU cache for DNS mappings with size limit and TTL support
182
+ /// Uses hostname:IP as cache key to ensure different IPs for the same hostname are isolated
182
183
  private class DNSCache {
183
184
  private struct CacheEntry {
185
+ let hostname: String
184
186
  let ip: String
185
187
  let timestamp: TimeInterval
186
188
  }
@@ -188,33 +190,34 @@ final class SniConnectClient {
188
190
  private var storage: [String: CacheEntry] = [:]
189
191
  private var accessOrder: [String] = []
190
192
 
193
+ // Mapping from hostname to IP for DNS resolution
194
+ // This stores the most recently set IP for each hostname
195
+ private var hostnameToIP: [String: String] = [:]
196
+
191
197
  // Maximum cache entries (100 as specified in requirements)
192
198
  private let maxSize = 100
193
199
  // TTL in seconds (5 minutes default)
194
200
  private let ttl: TimeInterval = 300
195
201
 
196
- func get(_ domain: String) -> String? {
197
- let key = domain.lowercased()
198
- guard let entry = storage[key] else { return nil }
199
-
200
- // Check if entry has expired
201
- let now = Date().timeIntervalSince1970
202
- if now - entry.timestamp > ttl {
203
- remove(key)
204
- return nil
205
- }
202
+ /// Generate cache key using hostname:IP format
203
+ /// This ensures different IPs for the same hostname are isolated (accurate speed testing)
204
+ private func makeCacheKey(hostname: String, ip: String) -> String {
205
+ return "\(hostname.lowercased()):\(ip)"
206
+ }
206
207
 
207
- // Update access order (LRU)
208
- if let index = accessOrder.firstIndex(of: key) {
209
- accessOrder.remove(at: index)
210
- }
211
- accessOrder.append(key)
208
+ func get(_ domain: String) -> String? {
209
+ let normalizedDomain = domain.lowercased()
212
210
 
213
- return entry.ip
211
+ // Return the most recently set IP for this hostname
212
+ return hostnameToIP[normalizedDomain]
214
213
  }
215
214
 
216
215
  func set(_ ip: String, for domain: String) {
217
- let key = domain.lowercased()
216
+ let normalizedDomain = domain.lowercased()
217
+ let key = makeCacheKey(hostname: normalizedDomain, ip: ip)
218
+
219
+ // Update hostname to IP mapping
220
+ hostnameToIP[normalizedDomain] = ip
218
221
 
219
222
  // Evict oldest entry if cache is full
220
223
  if storage.count >= maxSize && storage[key] == nil {
@@ -225,7 +228,7 @@ final class SniConnectClient {
225
228
  }
226
229
 
227
230
  // Add or update entry
228
- let entry = CacheEntry(ip: ip, timestamp: Date().timeIntervalSince1970)
231
+ let entry = CacheEntry(hostname: normalizedDomain, ip: ip, timestamp: Date().timeIntervalSince1970)
229
232
  storage[key] = entry
230
233
 
231
234
  // Update access order
@@ -235,8 +238,7 @@ final class SniConnectClient {
235
238
  accessOrder.append(key)
236
239
  }
237
240
 
238
- func remove(_ domain: String) {
239
- let key = domain.lowercased()
241
+ func remove(_ key: String) {
240
242
  storage.removeValue(forKey: key)
241
243
  if let index = accessOrder.firstIndex(of: key) {
242
244
  accessOrder.remove(at: index)
@@ -246,6 +248,7 @@ final class SniConnectClient {
246
248
  func clear() {
247
249
  storage.removeAll()
248
250
  accessOrder.removeAll()
251
+ hostnameToIP.removeAll()
249
252
  }
250
253
 
251
254
  func cleanExpired() {
@@ -253,6 +256,10 @@ final class SniConnectClient {
253
256
  let expiredKeys = storage.filter { now - $0.value.timestamp > ttl }.map { $0.key }
254
257
  for key in expiredKeys {
255
258
  remove(key)
259
+ // Also remove from hostnameToIP if this was the active mapping
260
+ if let entry = storage[key], hostnameToIP[entry.hostname] == entry.ip {
261
+ hostnameToIP.removeValue(forKey: entry.hostname)
262
+ }
256
263
  }
257
264
  }
258
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-sni-connect",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A React Native library for SNI-based HTTP requests with DNS caching and request management",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",