@onekeyfe/react-native-sni-connect 3.0.85 → 3.0.86

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.
@@ -32,6 +32,7 @@ enum SniConnectValidation {
32
32
  static let maxTotalHeaderBytes = 32 * 1024
33
33
  static let maxActiveRequests = 64
34
34
  static let maxActiveRequestsPerPair = 16
35
+ static let maxPendingRequests = 256
35
36
 
36
37
  /// HTTP methods the module is allowed to issue.
37
38
  private static let allowedMethods: Set<String> = [
@@ -212,6 +213,16 @@ enum SniConnectValidation {
212
213
  throw ValidationError.invalidIP(ip)
213
214
  }
214
215
 
216
+ static func canonicalIPKey(_ ip: String) -> String {
217
+ if let v4 = parseIPv4(ip) {
218
+ return "4:" + v4.map { String($0) }.joined(separator: ".")
219
+ }
220
+ if let v6 = parseIPv6(ip) {
221
+ return "6:" + v6.map { String($0) }.joined(separator: ".")
222
+ }
223
+ return ip
224
+ }
225
+
215
226
  private static func parseIPv4(_ ip: String) -> [UInt8]? {
216
227
  var addr = in_addr()
217
228
  guard ip.withCString({ inet_pton(AF_INET, $0, &addr) }) == 1 else { return nil }
@@ -301,16 +312,27 @@ enum SniConnectValidation {
301
312
  }
302
313
  }
303
314
 
304
- final class SniConnectRequestLimiter {
305
- final class Token {
315
+ final class SniConnectRequestLimiter: @unchecked Sendable {
316
+ static let shared = SniConnectRequestLimiter()
317
+
318
+ struct Snapshot {
319
+ let activeRequests: Int
320
+ let activeRequestsForPair: Int
321
+ let pendingRequests: Int
322
+ let pendingRequestsForPair: Int
323
+ let activeRequestIdsForPair: [String]
324
+ let pendingRequestIdsForPair: [String]
325
+ }
326
+
327
+ final class Token: @unchecked Sendable {
306
328
  private weak var limiter: SniConnectRequestLimiter?
307
- private let key: String
329
+ private let id: UUID
308
330
  private let lock = NSLock()
309
331
  private var released = false
310
332
 
311
- fileprivate init(limiter: SniConnectRequestLimiter, key: String) {
333
+ fileprivate init(limiter: SniConnectRequestLimiter, id: UUID) {
312
334
  self.limiter = limiter
313
- self.key = key
335
+ self.id = id
314
336
  }
315
337
 
316
338
  func release() {
@@ -321,7 +343,7 @@ final class SniConnectRequestLimiter {
321
343
  }
322
344
  released = true
323
345
  lock.unlock()
324
- limiter?.release(key: key)
346
+ limiter?.release(id: id)
325
347
  }
326
348
 
327
349
  deinit {
@@ -329,65 +351,216 @@ final class SniConnectRequestLimiter {
329
351
  }
330
352
  }
331
353
 
354
+ private final class CancellationState: @unchecked Sendable {
355
+ private let lock = NSLock()
356
+ private var cancelled = false
357
+
358
+ var isCancelled: Bool {
359
+ lock.lock()
360
+ defer { lock.unlock() }
361
+ return cancelled
362
+ }
363
+
364
+ func cancel() {
365
+ lock.lock()
366
+ cancelled = true
367
+ lock.unlock()
368
+ }
369
+ }
370
+
371
+ private struct PendingRequest {
372
+ let id: UUID
373
+ let key: String
374
+ let requestId: String?
375
+ let continuation: CheckedContinuation<Token, Error>
376
+ }
377
+
378
+ private struct ActiveRequest {
379
+ let key: String
380
+ let requestId: String?
381
+ }
382
+
332
383
  private let maxActiveRequests: Int
333
384
  private let maxActiveRequestsPerPair: Int
385
+ private let maxPendingRequests: Int
334
386
  private let queue = DispatchQueue(label: "com.onekey.sni.connect.request-limiter")
335
387
  private var activeRequests = 0
336
388
  private var activeRequestsByPair: [String: Int] = [:]
389
+ private var activeRequestsByID: [UUID: ActiveRequest] = [:]
390
+ private var pendingRequests: [PendingRequest] = []
337
391
 
338
392
  init(
339
393
  maxActiveRequests: Int = SniConnectValidation.maxActiveRequests,
340
- maxActiveRequestsPerPair: Int = SniConnectValidation.maxActiveRequestsPerPair
394
+ maxActiveRequestsPerPair: Int = SniConnectValidation.maxActiveRequestsPerPair,
395
+ maxPendingRequests: Int = SniConnectValidation.maxPendingRequests
341
396
  ) {
342
397
  self.maxActiveRequests = maxActiveRequests
343
398
  self.maxActiveRequestsPerPair = maxActiveRequestsPerPair
399
+ self.maxPendingRequests = maxPendingRequests
344
400
  }
345
401
 
346
- func acquire(hostname: String, ip: String) throws -> Token {
402
+ var pendingRequestCount: Int {
403
+ queue.sync {
404
+ pendingRequests.count
405
+ }
406
+ }
407
+
408
+ func snapshot(hostname: String, ip: String) -> Snapshot {
347
409
  let key = pairKey(hostname: hostname, ip: ip)
348
- return try queue.sync {
349
- if activeRequests >= maxActiveRequests {
350
- SniConnectCoreDiagnostics.warn(SniConnectCoreDiagnostics.event("sni_resource_limit", [
351
- ("activeCount", activeRequests),
352
- ("pairCount", activeRequestsByPair[key] ?? 0),
353
- ("limit", maxActiveRequests),
354
- ("reason", "max_active_requests"),
355
- ("hostname", hostname.lowercased()),
356
- ("ipHash", SniConnectCoreDiagnostics.shortHash(ip)),
357
- ]))
358
- throw SniConnectValidation.ValidationError.resourceLimit("Too many active SNI requests")
410
+ return queue.sync {
411
+ let activeForPair = activeRequestsByID.values.filter { $0.key == key }
412
+ let pendingForPair = pendingRequests.filter { $0.key == key }
413
+ return Snapshot(
414
+ activeRequests: activeRequests,
415
+ activeRequestsForPair: activeForPair.count,
416
+ pendingRequests: pendingRequests.count,
417
+ pendingRequestsForPair: pendingForPair.count,
418
+ activeRequestIdsForPair: activeForPair.compactMap { $0.requestId }.sorted(),
419
+ pendingRequestIdsForPair: pendingForPair.compactMap { $0.requestId }.sorted()
420
+ )
421
+ }
422
+ }
423
+
424
+ func acquire(hostname: String, ip: String, requestId: String? = nil) async throws -> Token {
425
+ try Task.checkCancellation()
426
+
427
+ let id = UUID()
428
+ let key = pairKey(hostname: hostname, ip: ip)
429
+ let trackedRequestId = requestId.flatMap { $0.isEmpty ? nil : $0 }
430
+ let cancellationState = CancellationState()
431
+
432
+ let token = try await withTaskCancellationHandler {
433
+ try Task.checkCancellation()
434
+ return try await withCheckedThrowingContinuation { continuation in
435
+ var immediateToken: Token?
436
+ var immediateError: Error?
437
+
438
+ queue.sync {
439
+ if cancellationState.isCancelled {
440
+ immediateError = CancellationError()
441
+ return
442
+ }
443
+
444
+ if hasCapacity(for: key) {
445
+ retainSlot(id: id, key: key, requestId: trackedRequestId)
446
+ immediateToken = Token(limiter: self, id: id)
447
+ return
448
+ }
449
+
450
+ if pendingRequests.count >= maxPendingRequests {
451
+ SniConnectCoreDiagnostics.warn(SniConnectCoreDiagnostics.event("sni_resource_limit", [
452
+ ("activeCount", activeRequests),
453
+ ("pairCount", activeRequestsByPair[key] ?? 0),
454
+ ("pendingCount", pendingRequests.count),
455
+ ("limit", maxPendingRequests),
456
+ ("reason", "max_pending_requests"),
457
+ ("hostname", hostname.lowercased()),
458
+ ("ipHash", SniConnectCoreDiagnostics.shortHash(ip)),
459
+ ]))
460
+ immediateError = SniConnectValidation.ValidationError.resourceLimit(
461
+ "Too many pending SNI requests"
462
+ )
463
+ return
464
+ }
465
+
466
+ pendingRequests.append(PendingRequest(
467
+ id: id,
468
+ key: key,
469
+ requestId: trackedRequestId,
470
+ continuation: continuation
471
+ ))
472
+ }
473
+
474
+ if let immediateToken {
475
+ continuation.resume(returning: immediateToken)
476
+ } else if let immediateError {
477
+ continuation.resume(throwing: immediateError)
478
+ }
479
+ }
480
+ } onCancel: {
481
+ cancellationState.cancel()
482
+ self.cancelPendingRequest(id: id)
483
+ }
484
+
485
+ // Cancellation can race with a pending-to-active handoff. Never return a
486
+ // token to an already-cancelled caller without releasing its retained slot.
487
+ do {
488
+ try Task.checkCancellation()
489
+ return token
490
+ } catch {
491
+ token.release()
492
+ throw error
493
+ }
494
+ }
495
+
496
+ private func release(id: UUID) {
497
+ var nextRequest: PendingRequest?
498
+
499
+ queue.sync {
500
+ guard releaseSlot(id: id) else { return }
501
+
502
+ guard activeRequests < maxActiveRequests,
503
+ let index = pendingRequests.firstIndex(where: { hasCapacity(for: $0.key) }) else {
504
+ return
359
505
  }
360
- let pairCount = activeRequestsByPair[key] ?? 0
361
- if pairCount >= maxActiveRequestsPerPair {
362
- SniConnectCoreDiagnostics.warn(SniConnectCoreDiagnostics.event("sni_resource_limit", [
363
- ("activeCount", activeRequests),
364
- ("pairCount", pairCount),
365
- ("limit", maxActiveRequestsPerPair),
366
- ("reason", "max_active_requests_per_pair"),
367
- ("hostname", hostname.lowercased()),
368
- ("ipHash", SniConnectCoreDiagnostics.shortHash(ip)),
369
- ]))
370
- throw SniConnectValidation.ValidationError.resourceLimit("Too many active SNI requests for destination")
506
+
507
+ nextRequest = pendingRequests.remove(at: index)
508
+ if let nextRequest {
509
+ retainSlot(
510
+ id: nextRequest.id,
511
+ key: nextRequest.key,
512
+ requestId: nextRequest.requestId
513
+ )
371
514
  }
372
- activeRequests += 1
373
- activeRequestsByPair[key] = pairCount + 1
374
- return Token(limiter: self, key: key)
515
+ }
516
+
517
+ if let nextRequest {
518
+ nextRequest.continuation.resume(returning: Token(limiter: self, id: nextRequest.id))
375
519
  }
376
520
  }
377
521
 
378
- private func release(key: String) {
522
+ private func cancelPendingRequest(id: UUID) {
523
+ var cancelledRequest: PendingRequest?
524
+
379
525
  queue.sync {
380
- activeRequests = max(0, activeRequests - 1)
381
- guard let pairCount = activeRequestsByPair[key] else { return }
382
- if pairCount <= 1 {
383
- activeRequestsByPair.removeValue(forKey: key)
384
- } else {
385
- activeRequestsByPair[key] = pairCount - 1
526
+ guard let index = pendingRequests.firstIndex(where: { $0.id == id }) else {
527
+ return
386
528
  }
529
+ cancelledRequest = pendingRequests.remove(at: index)
530
+ }
531
+
532
+ cancelledRequest?.continuation.resume(throwing: CancellationError())
533
+ }
534
+
535
+ private func hasCapacity(for key: String) -> Bool {
536
+ activeRequests < maxActiveRequests &&
537
+ (activeRequestsByPair[key] ?? 0) < maxActiveRequestsPerPair
538
+ }
539
+
540
+ private func retainSlot(id: UUID, key: String, requestId: String?) {
541
+ activeRequestsByID[id] = ActiveRequest(key: key, requestId: requestId)
542
+ activeRequests += 1
543
+ activeRequestsByPair[key, default: 0] += 1
544
+ }
545
+
546
+ private func releaseSlot(id: UUID) -> Bool {
547
+ guard let activeRequest = activeRequestsByID.removeValue(forKey: id) else {
548
+ return false
549
+ }
550
+ let key = activeRequest.key
551
+ activeRequests = max(0, activeRequests - 1)
552
+ guard let pairCount = activeRequestsByPair[key] else {
553
+ return true
554
+ }
555
+ if pairCount <= 1 {
556
+ activeRequestsByPair.removeValue(forKey: key)
557
+ } else {
558
+ activeRequestsByPair[key] = pairCount - 1
387
559
  }
560
+ return true
388
561
  }
389
562
 
390
563
  private func pairKey(hostname: String, ip: String) -> String {
391
- return "\(hostname.lowercased())|\(ip)"
564
+ return "\(hostname.lowercased())|\(SniConnectValidation.canonicalIPKey(ip))"
392
565
  }
393
566
  }
@@ -122,12 +122,18 @@ final class SniConnectValidationTests: XCTestCase {
122
122
  }
123
123
 
124
124
  func testEnforcesRequestIdTimeoutAndBodyLimits() {
125
+ XCTAssertNoThrow(
126
+ try SniConnectValidation.validateRequestId(String(repeating: "界", count: 42))
127
+ )
125
128
  assertValidationFails {
126
129
  try SniConnectValidation.validateRequestId("")
127
130
  }
128
131
  assertValidationFails {
129
132
  try SniConnectValidation.validateRequestId(String(repeating: "x", count: 129))
130
133
  }
134
+ assertValidationFails {
135
+ try SniConnectValidation.validateRequestId(String(repeating: "界", count: 43))
136
+ }
131
137
  assertValidationFails {
132
138
  try SniConnectValidation.validateRequestId("req\n1")
133
139
  }
@@ -204,24 +210,196 @@ final class SniConnectValidationTests: XCTestCase {
204
210
  XCTAssertNoThrow(try SniConnectValidation.validateMethodBody(method: "OPTIONS", body: nil))
205
211
  }
206
212
 
207
- func testRequestLimiterEnforcesGlobalAndPerDestinationLimits() throws {
213
+ func testTwentySamePairRequestsProduceSixteenActiveAndFourPending() async throws {
214
+ let limiter = SniConnectRequestLimiter()
215
+ var tasks: [String: Task<SniConnectRequestLimiter.Token, Error>] = [:]
216
+
217
+ for index in 0..<20 {
218
+ let requestId = String(format: "req-%02d", index)
219
+ tasks[requestId] = Task {
220
+ try await limiter.acquire(
221
+ hostname: "Example.com",
222
+ ip: index.isMultiple(of: 2)
223
+ ? "2001:4860:4860::8888"
224
+ : "2001:4860:4860:0:0:0:0:8888",
225
+ requestId: requestId
226
+ )
227
+ }
228
+ }
229
+
230
+ await waitForPendingRequests(4, in: limiter)
231
+ let saturated = limiter.snapshot(
232
+ hostname: "EXAMPLE.COM",
233
+ ip: "2001:4860:4860::8888"
234
+ )
235
+ XCTAssertEqual(saturated.activeRequests, 16)
236
+ XCTAssertEqual(saturated.activeRequestsForPair, 16)
237
+ XCTAssertEqual(saturated.pendingRequests, 4)
238
+ XCTAssertEqual(saturated.pendingRequestsForPair, 4)
239
+ XCTAssertEqual(saturated.activeRequestIdsForPair.count, 16)
240
+ XCTAssertEqual(saturated.pendingRequestIdsForPair.count, 4)
241
+
242
+ for requestId in saturated.pendingRequestIdsForPair {
243
+ tasks[requestId]?.cancel()
244
+ }
245
+ for requestId in saturated.pendingRequestIdsForPair {
246
+ do {
247
+ _ = try await tasks[requestId]!.value
248
+ XCTFail("Expected pending request \(requestId) to be cancelled")
249
+ } catch is CancellationError {
250
+ // Expected.
251
+ }
252
+ }
253
+
254
+ XCTAssertEqual(limiter.pendingRequestCount, 0)
255
+ for requestId in saturated.activeRequestIdsForPair {
256
+ let token = try await tasks[requestId]!.value
257
+ token.release()
258
+ }
259
+ let drained = limiter.snapshot(
260
+ hostname: "example.com",
261
+ ip: "2001:4860:4860:0:0:0:0:8888"
262
+ )
263
+ XCTAssertEqual(drained.activeRequests, 0)
264
+ XCTAssertEqual(drained.pendingRequests, 0)
265
+
266
+ let recovery = try await limiter.acquire(
267
+ hostname: "example.com",
268
+ ip: "2001:4860:4860::8888",
269
+ requestId: "recovery"
270
+ )
271
+ recovery.release()
272
+ }
273
+
274
+ func testRequestLimiterQueuesGlobalAndPerDestinationLimits() async throws {
208
275
  let limiter = SniConnectRequestLimiter(maxActiveRequests: 2, maxActiveRequestsPerPair: 1)
209
- let firstToken = try limiter.acquire(hostname: "Example.com", ip: "93.184.216.34")
276
+ let firstToken = try await limiter.acquire(hostname: "Example.com", ip: "93.184.216.34")
210
277
 
211
- assertValidationFails {
212
- _ = try limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
278
+ let sameDestinationTask = Task {
279
+ try await limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
213
280
  }
281
+ await waitForPendingRequests(1, in: limiter)
214
282
 
215
- let secondToken = try limiter.acquire(hostname: "example.com", ip: "93.184.216.35")
216
- assertValidationFails {
217
- _ = try limiter.acquire(hostname: "example.net", ip: "93.184.216.36")
283
+ let secondToken = try await limiter.acquire(hostname: "example.com", ip: "93.184.216.35")
284
+ let globallyQueuedTask = Task {
285
+ try await limiter.acquire(hostname: "example.net", ip: "93.184.216.36")
218
286
  }
287
+ await waitForPendingRequests(2, in: limiter)
219
288
 
220
289
  firstToken.release()
221
- let replacementToken = try limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
290
+ let replacementToken = try await sameDestinationTask.value
222
291
  firstToken.release()
223
292
  secondToken.release()
293
+ let globallyQueuedToken = try await globallyQueuedTask.value
224
294
  replacementToken.release()
295
+ globallyQueuedToken.release()
296
+ }
297
+
298
+ func testRequestLimiterSnapshotCanonicalizesTargetAndReturnsRequestIDs() async throws {
299
+ let limiter = SniConnectRequestLimiter(maxActiveRequests: 2, maxActiveRequestsPerPair: 1)
300
+ let activeTarget = try await limiter.acquire(
301
+ hostname: "Example.com",
302
+ ip: "2001:4860:4860::8888",
303
+ requestId: "active-target"
304
+ )
305
+ let activeOther = try await limiter.acquire(
306
+ hostname: "example.com",
307
+ ip: "93.184.216.34",
308
+ requestId: "active-other"
309
+ )
310
+ let pendingTargetTask = Task {
311
+ try await limiter.acquire(
312
+ hostname: "example.com",
313
+ ip: "2001:4860:4860:0:0:0:0:8888",
314
+ requestId: "pending-target"
315
+ )
316
+ }
317
+ let pendingWithoutIDTask = Task {
318
+ try await limiter.acquire(
319
+ hostname: "example.com",
320
+ ip: "2001:4860:4860::8888",
321
+ requestId: ""
322
+ )
323
+ }
324
+ await waitForPendingRequests(2, in: limiter)
325
+
326
+ let snapshot = limiter.snapshot(
327
+ hostname: "EXAMPLE.COM",
328
+ ip: "2001:4860:4860:0:0:0:0:8888"
329
+ )
330
+ XCTAssertEqual(snapshot.activeRequests, 2)
331
+ XCTAssertEqual(snapshot.activeRequestsForPair, 1)
332
+ XCTAssertEqual(snapshot.pendingRequests, 2)
333
+ XCTAssertEqual(snapshot.pendingRequestsForPair, 2)
334
+ XCTAssertEqual(snapshot.activeRequestIdsForPair, ["active-target"])
335
+ XCTAssertEqual(snapshot.pendingRequestIdsForPair, ["pending-target"])
336
+
337
+ pendingTargetTask.cancel()
338
+ pendingWithoutIDTask.cancel()
339
+ _ = try? await pendingTargetTask.value
340
+ _ = try? await pendingWithoutIDTask.value
341
+ activeTarget.release()
342
+ activeOther.release()
343
+ }
344
+
345
+ func testRequestLimiterRejectsTheTwoHundredFiftySeventhPendingRequest() async throws {
346
+ let limiter = SniConnectRequestLimiter(
347
+ maxActiveRequests: 1,
348
+ maxActiveRequestsPerPair: 1,
349
+ maxPendingRequests: 256
350
+ )
351
+ let activeToken = try await limiter.acquire(
352
+ hostname: "example.com",
353
+ ip: "93.184.216.34"
354
+ )
355
+ let queuedTasks = (0..<256).map { index in
356
+ Task {
357
+ try await limiter.acquire(
358
+ hostname: "example.com",
359
+ ip: "93.184.216.34",
360
+ requestId: "pending-\(index)"
361
+ )
362
+ }
363
+ }
364
+ await waitForPendingRequests(256, in: limiter)
365
+
366
+ do {
367
+ _ = try await limiter.acquire(hostname: "example.com", ip: "93.184.216.34")
368
+ XCTFail("Expected the bounded pending queue to reject overflow")
369
+ } catch SniConnectValidation.ValidationError.resourceLimit(_) {
370
+ // Expected.
371
+ }
372
+
373
+ queuedTasks.forEach { $0.cancel() }
374
+ for task in queuedTasks {
375
+ _ = try? await task.value
376
+ }
377
+ XCTAssertEqual(limiter.pendingRequestCount, 0)
378
+ activeToken.release()
379
+ }
380
+
381
+ func testWallClockDeadlineCancelsPendingLimiterWait() async throws {
382
+ let limiter = SniConnectRequestLimiter(maxActiveRequests: 1, maxActiveRequestsPerPair: 1)
383
+ let activeToken = try await limiter.acquire(
384
+ hostname: "example.com",
385
+ ip: "93.184.216.34"
386
+ )
387
+
388
+ do {
389
+ _ = try await SniConnectWallClockDeadline.run(timeoutMilliseconds: 20) {
390
+ try await limiter.acquire(
391
+ hostname: "example.com",
392
+ ip: "93.184.216.34",
393
+ requestId: "deadline-pending"
394
+ )
395
+ }
396
+ XCTFail("Expected queue wait to consume the wall-clock deadline")
397
+ } catch let error as SniConnectTimeout {
398
+ XCTAssertEqual(error, .deadlineExceeded)
399
+ }
400
+
401
+ XCTAssertEqual(limiter.pendingRequestCount, 0)
402
+ activeToken.release()
225
403
  }
226
404
 
227
405
  func testResolverRegistryKeepsResolverClassUntilAllSessionsReleaseIt() throws {
@@ -298,6 +476,20 @@ final class SniConnectValidationTests: XCTestCase {
298
476
  }
299
477
  }
300
478
 
479
+ func testWallClockDeadlineRejectsResultAfterAbsoluteDeadline() async throws {
480
+ let deadline = SniConnectWallClockDeadline.makeDeadline(timeoutMilliseconds: 5)
481
+ try await Task.sleep(nanoseconds: 20_000_000)
482
+
483
+ do {
484
+ _ = try await SniConnectWallClockDeadline.run(until: deadline) {
485
+ return "late"
486
+ }
487
+ XCTFail("Expected an operation result produced after the deadline to be rejected")
488
+ } catch let error as SniConnectTimeout {
489
+ XCTAssertEqual(error, .deadlineExceeded)
490
+ }
491
+ }
492
+
301
493
  func testResponseHeaderMapsPreserveRawRepeatedSetCookieHeaders() throws {
302
494
  let headerMaps = SniConnectResponseHeaders.make(rawHeaderFields: [
303
495
  (name: "Content-Type", value: "application/json"),
@@ -331,4 +523,24 @@ final class SniConnectValidationTests: XCTestCase {
331
523
  private func assertValidationFails(_ block: () throws -> Void, file: StaticString = #filePath, line: UInt = #line) {
332
524
  XCTAssertThrowsError(try block(), file: file, line: line)
333
525
  }
526
+
527
+ private func waitForPendingRequests(
528
+ _ expectedCount: Int,
529
+ in limiter: SniConnectRequestLimiter,
530
+ file: StaticString = #filePath,
531
+ line: UInt = #line
532
+ ) async {
533
+ let deadline = Date().addingTimeInterval(5)
534
+ while Date() < deadline {
535
+ if limiter.pendingRequestCount == expectedCount {
536
+ return
537
+ }
538
+ try? await Task.sleep(nanoseconds: 1_000_000)
539
+ }
540
+ XCTFail(
541
+ "Expected \(expectedCount) pending requests, got \(limiter.pendingRequestCount)",
542
+ file: file,
543
+ line: line
544
+ )
545
+ }
334
546
  }
@@ -13,6 +13,9 @@ export function cancelAllRequests() {
13
13
  export function clearDNSCache() {
14
14
  return NativeSniConnect.clearDNSCache();
15
15
  }
16
+ export function getDebugSnapshot(target) {
17
+ return NativeSniConnect.getDebugSnapshot(target);
18
+ }
16
19
  export function isProxyActiveForUrl(url) {
17
20
  return NativeSniConnect.isProxyActiveForUrl(url);
18
21
  }
@@ -39,6 +39,18 @@ export type SniConnectResponse = {
39
39
  headers: HeaderMap;
40
40
  multiValueHeaders?: MultiValueHeaderMap;
41
41
  };
42
+ export type SniConnectDebugTarget = {
43
+ ip: string;
44
+ hostname: string;
45
+ };
46
+ export type SniConnectDebugSnapshot = {
47
+ activeRequests: Int32;
48
+ activeRequestsForPair: Int32;
49
+ pendingRequests: Int32;
50
+ pendingRequestsForPair: Int32;
51
+ activeRequestIdsForPair: string[];
52
+ pendingRequestIdsForPair: string[];
53
+ };
42
54
  export interface Spec extends TurboModule {
43
55
  request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
44
56
  cancelRequest(requestId: string): Promise<{
@@ -50,6 +62,7 @@ export interface Spec extends TurboModule {
50
62
  clearDNSCache(): Promise<{
51
63
  success: boolean;
52
64
  }>;
65
+ getDebugSnapshot(target: SniConnectDebugTarget): Promise<SniConnectDebugSnapshot>;
53
66
  isProxyActiveForUrl(url: string): Promise<boolean>;
54
67
  }
55
68
  export type SniConnectModule = Spec;
@@ -1,4 +1,4 @@
1
- import { type SniConnectRequest, type SniConnectResponse } from './NativeSniConnect';
1
+ import { type SniConnectDebugSnapshot, type SniConnectDebugTarget, type SniConnectRequest, type SniConnectResponse } from './NativeSniConnect';
2
2
  export declare function request(config: SniConnectRequest): Promise<SniConnectResponse>;
3
3
  export declare function cancelRequest(requestId: string): Promise<{
4
4
  success: boolean;
@@ -9,6 +9,7 @@ export declare function cancelAllRequests(): Promise<{
9
9
  export declare function clearDNSCache(): Promise<{
10
10
  success: boolean;
11
11
  }>;
12
+ export declare function getDebugSnapshot(target: SniConnectDebugTarget): Promise<SniConnectDebugSnapshot>;
12
13
  export declare function isProxyActiveForUrl(url: string): Promise<boolean>;
13
- export type { SniConnectBodylessMethod, SniConnectMethod, SniConnectOptionalBodyMethod, SniConnectRequest, SniConnectRequiredBodyMethod, SniConnectResponse, } from './NativeSniConnect';
14
+ export type { SniConnectBodylessMethod, SniConnectDebugSnapshot, SniConnectDebugTarget, SniConnectMethod, SniConnectOptionalBodyMethod, SniConnectRequest, SniConnectRequiredBodyMethod, SniConnectResponse, } from './NativeSniConnect';
14
15
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-sni-connect",
3
- "version": "3.0.85",
3
+ "version": "3.0.86",
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",
@@ -58,11 +58,28 @@ export type SniConnectResponse = {
58
58
  multiValueHeaders?: MultiValueHeaderMap;
59
59
  };
60
60
 
61
+ export type SniConnectDebugTarget = {
62
+ ip: string;
63
+ hostname: string;
64
+ };
65
+
66
+ export type SniConnectDebugSnapshot = {
67
+ activeRequests: Int32;
68
+ activeRequestsForPair: Int32;
69
+ pendingRequests: Int32;
70
+ pendingRequestsForPair: Int32;
71
+ activeRequestIdsForPair: string[];
72
+ pendingRequestIdsForPair: string[];
73
+ };
74
+
61
75
  export interface Spec extends TurboModule {
62
76
  request(config: NativeSniConnectRequest): Promise<SniConnectResponse>;
63
77
  cancelRequest(requestId: string): Promise<{ success: boolean }>;
64
78
  cancelAllRequests(): Promise<{ success: boolean }>;
65
79
  clearDNSCache(): Promise<{ success: boolean }>;
80
+ getDebugSnapshot(
81
+ target: SniConnectDebugTarget
82
+ ): Promise<SniConnectDebugSnapshot>;
66
83
  isProxyActiveForUrl(url: string): Promise<boolean>;
67
84
  }
68
85