@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.
- package/README.md +7 -0
- package/android/src/main/java/com/sniconnect/SniConnectModule.kt +204 -60
- package/android/src/main/java/com/sniconnect/SniConnectRequestAdmission.kt +276 -0
- package/android/src/main/java/com/sniconnect/SniConnectValidation.kt +12 -78
- package/android/src/test/java/com/sniconnect/SniConnectRequestAdmissionTest.kt +309 -0
- package/android/src/test/java/com/sniconnect/SniConnectValidationTest.kt +23 -26
- package/ios/SniConnect.mm +27 -2
- package/ios/SniConnect.swift +34 -0
- package/ios/SniConnectClient.swift +102 -33
- package/ios/SniConnectCore.swift +49 -3
- package/ios/SniConnectValidation.swift +214 -41
- package/ios/Tests/SniConnectValidationTests/SniConnectValidationTests.swift +220 -8
- package/lib/module/index.js +3 -0
- package/lib/typescript/src/NativeSniConnect.d.ts +13 -0
- package/lib/typescript/src/index.d.ts +3 -2
- package/package.json +1 -1
- package/src/NativeSniConnect.ts +17 -0
- package/src/index.tsx +10 -0
|
@@ -28,6 +28,14 @@ class SniConnectValidationTest {
|
|
|
28
28
|
assertEquals("/v1?q=1", SniConnectValidation.normalizePath("v1?q=1"))
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
@Test
|
|
32
|
+
fun canonicalizesEquivalentPublicIpLiterals() {
|
|
33
|
+
assertEquals(
|
|
34
|
+
SniConnectValidation.canonicalizePublicIp("2001:4860:4860::8888"),
|
|
35
|
+
SniConnectValidation.canonicalizePublicIp("2001:4860:4860:0:0:0:0:8888"),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
@Test
|
|
32
40
|
fun rejectsIpLiteralHostnames() {
|
|
33
41
|
assertValidationFails { SniConnectValidation.validateHostname("93.184.216.34") }
|
|
@@ -57,6 +65,7 @@ class SniConnectValidationTest {
|
|
|
57
65
|
"example.com",
|
|
58
66
|
"93.184.216.34:443",
|
|
59
67
|
" 93.184.216.34",
|
|
68
|
+
"093.184.216.034",
|
|
60
69
|
"10.0.0.1",
|
|
61
70
|
"127.0.0.1",
|
|
62
71
|
"100.64.0.1",
|
|
@@ -117,8 +126,10 @@ class SniConnectValidationTest {
|
|
|
117
126
|
|
|
118
127
|
@Test
|
|
119
128
|
fun enforcesRequestIdTimeoutAndBodyLimits() {
|
|
129
|
+
SniConnectValidation.validateRequestId("界".repeat(42))
|
|
120
130
|
assertValidationFails { SniConnectValidation.validateRequestId("") }
|
|
121
131
|
assertValidationFails { SniConnectValidation.validateRequestId("x".repeat(129)) }
|
|
132
|
+
assertValidationFails { SniConnectValidation.validateRequestId("界".repeat(43)) }
|
|
122
133
|
assertValidationFails { SniConnectValidation.validateRequestId("req\n1") }
|
|
123
134
|
assertValidationFails { SniConnectValidation.validateTimeout(0) }
|
|
124
135
|
assertValidationFails { SniConnectValidation.validateTimeout(120_001) }
|
|
@@ -184,32 +195,6 @@ class SniConnectValidationTest {
|
|
|
184
195
|
SniConnectValidation.validateMethodBody("OPTIONS", null)
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
@Test
|
|
188
|
-
fun requestLimiterEnforcesGlobalAndPerDestinationLimits() {
|
|
189
|
-
val limiter = SniConnectRequestLimiter(
|
|
190
|
-
maxActiveRequests = 2,
|
|
191
|
-
maxActiveRequestsPerPair = 1,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
val firstToken = limiter.acquire("Example.com", "93.184.216.34")
|
|
195
|
-
assertValidationFails {
|
|
196
|
-
limiter.acquire("example.com", "93.184.216.34")
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
val secondToken = limiter.acquire("example.com", "93.184.216.35")
|
|
200
|
-
assertValidationFails {
|
|
201
|
-
limiter.acquire("example.net", "93.184.216.36")
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
firstToken.release()
|
|
205
|
-
val replacementToken = limiter.acquire("example.com", "93.184.216.34")
|
|
206
|
-
firstToken.release()
|
|
207
|
-
secondToken.release()
|
|
208
|
-
replacementToken.release()
|
|
209
|
-
|
|
210
|
-
assertTrue(true)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
198
|
@Test
|
|
214
199
|
fun classifiesSecurityFailuresAsFailClosedErrorCodes() {
|
|
215
200
|
assertEquals(
|
|
@@ -244,6 +229,18 @@ class SniConnectValidationTest {
|
|
|
244
229
|
"SNI_REQUEST_FAILED",
|
|
245
230
|
classifySniFailureCode(IOException("connection reset")),
|
|
246
231
|
)
|
|
232
|
+
assertEquals(
|
|
233
|
+
"SNI_CANCELLED",
|
|
234
|
+
classifySniResponseFailureCode(IOException("cancelled"), true),
|
|
235
|
+
)
|
|
236
|
+
assertEquals(
|
|
237
|
+
"SNI_REQUEST_TIMEOUT",
|
|
238
|
+
classifySniResponseFailureCode(SocketTimeoutException("timeout"), false),
|
|
239
|
+
)
|
|
240
|
+
assertEquals(
|
|
241
|
+
"SNI_RESPONSE_FAILED",
|
|
242
|
+
classifySniResponseFailureCode(IOException("bad body"), false),
|
|
243
|
+
)
|
|
247
244
|
}
|
|
248
245
|
|
|
249
246
|
private fun assertValidationFails(block: () -> Unit) {
|
package/ios/SniConnect.mm
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTInvalidating.h>
|
|
2
3
|
#import <React/RCTUtils.h>
|
|
3
4
|
|
|
4
5
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -8,6 +9,7 @@
|
|
|
8
9
|
// Forward declaration of the Swift implementation
|
|
9
10
|
@interface SniConnectImpl : NSObject
|
|
10
11
|
- (instancetype)init;
|
|
12
|
+
- (void)invalidate;
|
|
11
13
|
- (void)request:(NSDictionary *)config
|
|
12
14
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
13
15
|
reject:(RCTPromiseRejectBlock)reject;
|
|
@@ -18,6 +20,9 @@
|
|
|
18
20
|
reject:(RCTPromiseRejectBlock)reject;
|
|
19
21
|
- (void)clearDNSCache:(RCTPromiseResolveBlock)resolve
|
|
20
22
|
reject:(RCTPromiseRejectBlock)reject;
|
|
23
|
+
- (void)getDebugSnapshot:(NSDictionary *)target
|
|
24
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
25
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
21
26
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
22
27
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
23
28
|
reject:(RCTPromiseRejectBlock)reject;
|
|
@@ -25,9 +30,9 @@
|
|
|
25
30
|
|
|
26
31
|
@interface SniConnect : NSObject
|
|
27
32
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
28
|
-
<NativeSniConnectSpec>
|
|
33
|
+
<NativeSniConnectSpec, RCTInvalidating>
|
|
29
34
|
#else
|
|
30
|
-
<RCTBridgeModule>
|
|
35
|
+
<RCTBridgeModule, RCTInvalidating>
|
|
31
36
|
#endif
|
|
32
37
|
@end
|
|
33
38
|
|
|
@@ -48,6 +53,10 @@ RCT_EXPORT_MODULE(SniConnect)
|
|
|
48
53
|
return self;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
- (void)invalidate {
|
|
57
|
+
[_implementation invalidate];
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
52
61
|
// TurboModule interface implementation
|
|
53
62
|
- (void)request:(JS::NativeSniConnect::NativeSniConnectRequest &)config
|
|
@@ -89,6 +98,16 @@ RCT_EXPORT_MODULE(SniConnect)
|
|
|
89
98
|
[_implementation clearDNSCache:resolve reject:reject];
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
- (void)getDebugSnapshot:(JS::NativeSniConnect::SniConnectDebugTarget &)target
|
|
102
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
103
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
104
|
+
NSDictionary *targetDict = @{
|
|
105
|
+
@"ip": target.ip() ?: @"",
|
|
106
|
+
@"hostname": target.hostname() ?: @"",
|
|
107
|
+
};
|
|
108
|
+
[_implementation getDebugSnapshot:targetDict resolve:resolve reject:reject];
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
- (void)isProxyActiveForUrl:(NSString *)url
|
|
93
112
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
94
113
|
reject:(RCTPromiseRejectBlock)reject {
|
|
@@ -124,6 +143,12 @@ RCT_EXPORT_METHOD(clearDNSCache:(RCTPromiseResolveBlock)resolver
|
|
|
124
143
|
[_implementation clearDNSCache:resolver reject:rejecter];
|
|
125
144
|
}
|
|
126
145
|
|
|
146
|
+
RCT_EXPORT_METHOD(getDebugSnapshot:(NSDictionary *)target
|
|
147
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
148
|
+
rejecter:(RCTPromiseRejectBlock)rejecter) {
|
|
149
|
+
[_implementation getDebugSnapshot:target resolve:resolver reject:rejecter];
|
|
150
|
+
}
|
|
151
|
+
|
|
127
152
|
RCT_EXPORT_METHOD(isProxyActiveForUrl:(NSString *)url
|
|
128
153
|
resolver:(RCTPromiseResolveBlock)resolver
|
|
129
154
|
rejecter:(RCTPromiseRejectBlock)rejecter) {
|
package/ios/SniConnect.swift
CHANGED
|
@@ -16,6 +16,11 @@ final class SniConnectImpl: NSObject {
|
|
|
16
16
|
super.init()
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
@objc
|
|
20
|
+
public func invalidate() {
|
|
21
|
+
client.invalidate()
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
@objc
|
|
20
25
|
public func request(
|
|
21
26
|
_ config: NSDictionary,
|
|
@@ -133,6 +138,35 @@ final class SniConnectImpl: NSObject {
|
|
|
133
138
|
resolve(["success": true])
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
@objc
|
|
142
|
+
public func getDebugSnapshot(
|
|
143
|
+
_ target: NSDictionary,
|
|
144
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
145
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
146
|
+
) {
|
|
147
|
+
do {
|
|
148
|
+
guard let ip = target["ip"] as? String, !ip.isEmpty else {
|
|
149
|
+
throw SniConnectError.invalidConfig("Missing ip")
|
|
150
|
+
}
|
|
151
|
+
guard let hostname = target["hostname"] as? String, !hostname.isEmpty else {
|
|
152
|
+
throw SniConnectError.invalidConfig("Missing hostname")
|
|
153
|
+
}
|
|
154
|
+
try SniConnectValidation.validatePublicIP(ip)
|
|
155
|
+
try SniConnectValidation.validateHostname(hostname)
|
|
156
|
+
let snapshot = client.debugSnapshot(hostname: hostname, ip: ip)
|
|
157
|
+
resolve([
|
|
158
|
+
"activeRequests": snapshot.activeRequests,
|
|
159
|
+
"activeRequestsForPair": snapshot.activeRequestsForPair,
|
|
160
|
+
"pendingRequests": snapshot.pendingRequests,
|
|
161
|
+
"pendingRequestsForPair": snapshot.pendingRequestsForPair,
|
|
162
|
+
"activeRequestIdsForPair": snapshot.activeRequestIdsForPair,
|
|
163
|
+
"pendingRequestIdsForPair": snapshot.pendingRequestIdsForPair,
|
|
164
|
+
])
|
|
165
|
+
} catch {
|
|
166
|
+
reject("SNI_INVALID_CONFIG", "\(error)", error)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
@objc
|
|
137
171
|
public func isProxyActiveForUrl(
|
|
138
172
|
_ url: String,
|
|
@@ -83,7 +83,7 @@ final class SniConnectPinnedResolverLease {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
var isReleased: Bool {
|
|
87
87
|
queue.sync {
|
|
88
88
|
didRelease
|
|
89
89
|
}
|
|
@@ -186,8 +186,9 @@ final class SniConnectClient {
|
|
|
186
186
|
// token so cancelAllRequests() also covers requests that do not have a requestId.
|
|
187
187
|
private var activeTasksByToken: [UUID: Task<Response, Error>] = [:]
|
|
188
188
|
private var requestTokensById: [String: UUID] = [:]
|
|
189
|
+
private var invalidated = false
|
|
189
190
|
private let tasksQueue = DispatchQueue(label: "com.onekey.sni.connect.tasks", attributes: .concurrent)
|
|
190
|
-
private let requestLimiter = SniConnectRequestLimiter
|
|
191
|
+
private let requestLimiter = SniConnectRequestLimiter.shared
|
|
191
192
|
|
|
192
193
|
private struct SessionKey: Hashable {
|
|
193
194
|
let hostname: String
|
|
@@ -215,10 +216,13 @@ final class SniConnectClient {
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
private static let maxCachedSessions = SniConnectPinnedResolverRegistry.defaultMaxEntries
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
private var
|
|
221
|
-
private
|
|
219
|
+
// The resolver registry is process-shared, so the cache and its slot accounting
|
|
220
|
+
// must have the same owner across every RN runtime.
|
|
221
|
+
private static var sessionCache: [SessionKey: ManagedSession] = [:]
|
|
222
|
+
private static var sessionAccessOrder: [SessionKey] = []
|
|
223
|
+
private static var activeSessionCounts: [ObjectIdentifier: Int] = [:]
|
|
224
|
+
private static var pendingResolverSlots: [SniConnectPinnedResolverLease] = []
|
|
225
|
+
private static let sessionsQueue = DispatchQueue(label: "com.onekey.sni.connect.sessions")
|
|
222
226
|
|
|
223
227
|
// Token for the memory-warning observer (block-based observers are not removed
|
|
224
228
|
// by `removeObserver(self)`, so the token must be retained and removed explicitly).
|
|
@@ -428,21 +432,26 @@ final class SniConnectClient {
|
|
|
428
432
|
private func sessionLease(for config: RequestConfig) async throws -> SessionLease {
|
|
429
433
|
let key = SessionKey(hostname: config.hostname.lowercased(), ip: config.ip)
|
|
430
434
|
while true {
|
|
431
|
-
let acquisition = try sessionsQueue.sync { () throws -> SessionAcquisition in
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
let acquisition = try Self.sessionsQueue.sync { () throws -> SessionAcquisition in
|
|
436
|
+
Self.pendingResolverSlots.removeAll { $0.isReleased }
|
|
437
|
+
if let managedSession = Self.sessionCache[key] {
|
|
438
|
+
Self.markSessionUsed(key)
|
|
439
|
+
return .ready(Self.retainSession(managedSession.session))
|
|
440
|
+
}
|
|
441
|
+
if !Self.pendingResolverSlots.isEmpty {
|
|
442
|
+
return .waitForResolverSlots(Self.pendingResolverSlots)
|
|
435
443
|
}
|
|
436
444
|
|
|
437
|
-
let pendingResolverSlots = evictSessionsIfNeeded(forPendingInsert: true)
|
|
445
|
+
let pendingResolverSlots = Self.evictSessionsIfNeeded(forPendingInsert: true)
|
|
438
446
|
if !pendingResolverSlots.isEmpty {
|
|
447
|
+
Self.pendingResolverSlots.append(contentsOf: pendingResolverSlots)
|
|
439
448
|
return .waitForResolverSlots(pendingResolverSlots)
|
|
440
449
|
}
|
|
441
450
|
|
|
442
451
|
let managedSession = try Self.makeURLSession(for: key)
|
|
443
|
-
sessionCache[key] = managedSession
|
|
444
|
-
markSessionUsed(key)
|
|
445
|
-
return .ready(retainSession(managedSession.session))
|
|
452
|
+
Self.sessionCache[key] = managedSession
|
|
453
|
+
Self.markSessionUsed(key)
|
|
454
|
+
return .ready(Self.retainSession(managedSession.session))
|
|
446
455
|
}
|
|
447
456
|
|
|
448
457
|
switch acquisition {
|
|
@@ -457,35 +466,40 @@ final class SniConnectClient {
|
|
|
457
466
|
for resolverLease in resolverLeases {
|
|
458
467
|
try await resolverLease.waitUntilReleased()
|
|
459
468
|
}
|
|
469
|
+
Self.sessionsQueue.sync {
|
|
470
|
+
Self.pendingResolverSlots.removeAll { $0.isReleased }
|
|
471
|
+
}
|
|
460
472
|
}
|
|
461
473
|
}
|
|
462
474
|
}
|
|
463
475
|
|
|
464
|
-
private func retainSession(_ session: URLSession) -> SessionLease {
|
|
476
|
+
private static func retainSession(_ session: URLSession) -> SessionLease {
|
|
465
477
|
let sessionID = ObjectIdentifier(session)
|
|
466
478
|
activeSessionCounts[sessionID, default: 0] += 1
|
|
467
479
|
return SessionLease(session: session, sessionID: sessionID)
|
|
468
480
|
}
|
|
469
481
|
|
|
470
482
|
private func releaseSessionLease(_ lease: SessionLease) {
|
|
471
|
-
sessionsQueue.sync {
|
|
472
|
-
guard let count = activeSessionCounts[lease.sessionID] else {
|
|
483
|
+
Self.sessionsQueue.sync {
|
|
484
|
+
guard let count = Self.activeSessionCounts[lease.sessionID] else {
|
|
473
485
|
return
|
|
474
486
|
}
|
|
475
487
|
if count > 1 {
|
|
476
|
-
activeSessionCounts[lease.sessionID] = count - 1
|
|
488
|
+
Self.activeSessionCounts[lease.sessionID] = count - 1
|
|
477
489
|
} else {
|
|
478
|
-
activeSessionCounts.removeValue(forKey: lease.sessionID)
|
|
490
|
+
Self.activeSessionCounts.removeValue(forKey: lease.sessionID)
|
|
479
491
|
}
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
494
|
|
|
483
|
-
private func markSessionUsed(_ key: SessionKey) {
|
|
495
|
+
private static func markSessionUsed(_ key: SessionKey) {
|
|
484
496
|
sessionAccessOrder.removeAll { $0 == key }
|
|
485
497
|
sessionAccessOrder.append(key)
|
|
486
498
|
}
|
|
487
499
|
|
|
488
|
-
private func evictSessionsIfNeeded(
|
|
500
|
+
private static func evictSessionsIfNeeded(
|
|
501
|
+
forPendingInsert: Bool = false
|
|
502
|
+
) -> [SniConnectPinnedResolverLease] {
|
|
489
503
|
var pendingResolverSlots: [SniConnectPinnedResolverLease] = []
|
|
490
504
|
let limit = forPendingInsert ? Self.maxCachedSessions - 1 : Self.maxCachedSessions
|
|
491
505
|
while sessionCache.count > limit, let evictedKey = sessionAccessOrder.first {
|
|
@@ -506,7 +520,7 @@ final class SniConnectClient {
|
|
|
506
520
|
return pendingResolverSlots
|
|
507
521
|
}
|
|
508
522
|
|
|
509
|
-
private func invalidateSession(
|
|
523
|
+
private static func invalidateSession(
|
|
510
524
|
_ managedSession: ManagedSession,
|
|
511
525
|
for key: SessionKey
|
|
512
526
|
) -> SniConnectPinnedResolverLease? {
|
|
@@ -524,15 +538,21 @@ final class SniConnectClient {
|
|
|
524
538
|
/// slot immediately; sessions with active requests release it when invalidation
|
|
525
539
|
/// completes.
|
|
526
540
|
func clearDNSCache() {
|
|
527
|
-
let invalidations = sessionsQueue.sync { () -> [SessionInvalidation] in
|
|
528
|
-
let invalidations = sessionCache.map { _, managedSession in
|
|
541
|
+
let invalidations = Self.sessionsQueue.sync { () -> [SessionInvalidation] in
|
|
542
|
+
let invalidations = Self.sessionCache.map { _, managedSession in
|
|
529
543
|
SessionInvalidation(
|
|
530
544
|
managedSession: managedSession,
|
|
531
|
-
releaseResolverImmediately:
|
|
545
|
+
releaseResolverImmediately:
|
|
546
|
+
Self.activeSessionCounts[ObjectIdentifier(managedSession.session)] == nil
|
|
532
547
|
)
|
|
533
548
|
}
|
|
534
|
-
sessionCache.removeAll()
|
|
535
|
-
sessionAccessOrder.removeAll()
|
|
549
|
+
Self.sessionCache.removeAll()
|
|
550
|
+
Self.sessionAccessOrder.removeAll()
|
|
551
|
+
// Block new cache inserts until every registry entry removed by this
|
|
552
|
+
// process-wide clear has actually released its resolver slot.
|
|
553
|
+
Self.pendingResolverSlots.append(contentsOf: invalidations.map {
|
|
554
|
+
$0.managedSession.resolverLease
|
|
555
|
+
})
|
|
536
556
|
return invalidations
|
|
537
557
|
}
|
|
538
558
|
var releasedResolverCount = 0
|
|
@@ -552,6 +572,10 @@ final class SniConnectClient {
|
|
|
552
572
|
]))
|
|
553
573
|
}
|
|
554
574
|
|
|
575
|
+
func debugSnapshot(hostname: String, ip: String) -> SniConnectRequestLimiter.Snapshot {
|
|
576
|
+
requestLimiter.snapshot(hostname: hostname, ip: ip)
|
|
577
|
+
}
|
|
578
|
+
|
|
555
579
|
/// Cancel a request by ID
|
|
556
580
|
func cancelRequest(requestId: String) -> Bool {
|
|
557
581
|
return tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
@@ -574,15 +598,27 @@ final class SniConnectClient {
|
|
|
574
598
|
|
|
575
599
|
/// Cancel all active requests
|
|
576
600
|
func cancelAllRequests() {
|
|
601
|
+
cancelOwnedRequests(markInvalidated: false, logAction: "sni_cancel_all")
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Permanently stop this runtime's client and cancel work racing with teardown.
|
|
605
|
+
func invalidate() {
|
|
606
|
+
cancelOwnedRequests(markInvalidated: true, logAction: "sni_runtime_invalidate")
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private func cancelOwnedRequests(markInvalidated: Bool, logAction: String) {
|
|
577
610
|
tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
578
611
|
guard let self = self else { return }
|
|
612
|
+
if markInvalidated {
|
|
613
|
+
self.invalidated = true
|
|
614
|
+
}
|
|
579
615
|
let tasks = Array(self.activeTasksByToken.values)
|
|
580
616
|
for task in tasks {
|
|
581
617
|
task.cancel()
|
|
582
618
|
}
|
|
583
619
|
self.activeTasksByToken.removeAll()
|
|
584
620
|
self.requestTokensById.removeAll()
|
|
585
|
-
SniConnectLog.info(SniConnectLog.event(
|
|
621
|
+
SniConnectLog.info(SniConnectLog.event(logAction, [
|
|
586
622
|
("cancelledCount", tasks.count),
|
|
587
623
|
("success", true),
|
|
588
624
|
]))
|
|
@@ -592,8 +628,13 @@ final class SniConnectClient {
|
|
|
592
628
|
/// Register an active task immediately after creation, before JS can cancel it.
|
|
593
629
|
func registerTask(_ task: Task<Response, Error>, for requestId: String?) -> UUID {
|
|
594
630
|
let token = UUID()
|
|
631
|
+
var shouldCancel = false
|
|
595
632
|
tasksQueue.sync(flags: .barrier) { [weak self] in
|
|
596
633
|
guard let self = self else { return }
|
|
634
|
+
if self.invalidated {
|
|
635
|
+
shouldCancel = true
|
|
636
|
+
return
|
|
637
|
+
}
|
|
597
638
|
if let requestId = requestId, let previousToken = self.requestTokensById[requestId],
|
|
598
639
|
let previousTask = self.activeTasksByToken.removeValue(forKey: previousToken) {
|
|
599
640
|
previousTask.cancel()
|
|
@@ -607,6 +648,9 @@ final class SniConnectClient {
|
|
|
607
648
|
self.requestTokensById[requestId] = token
|
|
608
649
|
}
|
|
609
650
|
}
|
|
651
|
+
if shouldCancel {
|
|
652
|
+
task.cancel()
|
|
653
|
+
}
|
|
610
654
|
return token
|
|
611
655
|
}
|
|
612
656
|
|
|
@@ -656,11 +700,24 @@ final class SniConnectClient {
|
|
|
656
700
|
throw sniError
|
|
657
701
|
}
|
|
658
702
|
|
|
703
|
+
let deadline = SniConnectWallClockDeadline.makeDeadline(
|
|
704
|
+
timeoutMilliseconds: config.effectiveTotalTimeout
|
|
705
|
+
)
|
|
659
706
|
let requestSlot: SniConnectRequestLimiter.Token
|
|
660
707
|
do {
|
|
661
|
-
|
|
708
|
+
// Admission wait and transport share one total wall-clock deadline.
|
|
709
|
+
let limiter = requestLimiter
|
|
710
|
+
requestSlot = try await SniConnectWallClockDeadline.run(
|
|
711
|
+
until: deadline
|
|
712
|
+
) {
|
|
713
|
+
try await limiter.acquire(
|
|
714
|
+
hostname: config.hostname,
|
|
715
|
+
ip: config.ip,
|
|
716
|
+
requestId: config.requestId
|
|
717
|
+
)
|
|
718
|
+
}
|
|
662
719
|
} catch {
|
|
663
|
-
let sniError = SniConnectError.
|
|
720
|
+
let sniError = SniConnectError.from(error)
|
|
664
721
|
SniConnectLog.error(SniConnectLog.event("sni_request_result", [
|
|
665
722
|
("result", "error"),
|
|
666
723
|
("code", sniError.code),
|
|
@@ -678,6 +735,14 @@ final class SniConnectClient {
|
|
|
678
735
|
defer {
|
|
679
736
|
requestSlot.release()
|
|
680
737
|
}
|
|
738
|
+
try Task.checkCancellation()
|
|
739
|
+
|
|
740
|
+
let queueWaitMilliseconds = SniConnectLog.elapsedMs(since: startedAt)
|
|
741
|
+
let remainingTimeoutMilliseconds =
|
|
742
|
+
SniConnectWallClockDeadline.remainingMilliseconds(until: deadline)
|
|
743
|
+
guard remainingTimeoutMilliseconds > 0 else {
|
|
744
|
+
throw SniConnectError.requestTimeout
|
|
745
|
+
}
|
|
681
746
|
|
|
682
747
|
let url = try Self.buildURL(hostname: config.hostname, normalizedPath: normalizedPath)
|
|
683
748
|
|
|
@@ -685,8 +750,9 @@ final class SniConnectClient {
|
|
|
685
750
|
mutableRequest.httpMethod = method
|
|
686
751
|
|
|
687
752
|
// Convert milliseconds to seconds for timeout values
|
|
688
|
-
let totalTimeoutSeconds =
|
|
689
|
-
let connectTimeoutSeconds =
|
|
753
|
+
let totalTimeoutSeconds = remainingTimeoutMilliseconds / 1000.0
|
|
754
|
+
let connectTimeoutSeconds =
|
|
755
|
+
min(config.effectiveConnectTimeout, remainingTimeoutMilliseconds) / 1000.0
|
|
690
756
|
|
|
691
757
|
// URLRequest.timeoutInterval is not a full request deadline. Keep it aligned
|
|
692
758
|
// with the caller timeout as a transport guard; the wall-clock deadline below
|
|
@@ -716,12 +782,15 @@ final class SniConnectClient {
|
|
|
716
782
|
("method", method),
|
|
717
783
|
("timeoutMs", Int(config.effectiveTotalTimeout)),
|
|
718
784
|
("connectTimeoutMs", Int(config.effectiveConnectTimeout)),
|
|
785
|
+
("queueWaitMs", queueWaitMilliseconds),
|
|
719
786
|
("headerCount", normalizedHeaders.count),
|
|
720
787
|
("bodyBytes", config.body?.data(using: .utf8)?.count ?? 0),
|
|
721
788
|
]))
|
|
722
789
|
|
|
723
790
|
do {
|
|
724
|
-
return try await SniConnectWallClockDeadline.run(
|
|
791
|
+
return try await SniConnectWallClockDeadline.run(
|
|
792
|
+
until: deadline
|
|
793
|
+
) {
|
|
725
794
|
let lease = try await self.sessionLease(for: config)
|
|
726
795
|
defer {
|
|
727
796
|
self.releaseSessionLease(lease)
|
package/ios/SniConnectCore.swift
CHANGED
|
@@ -160,11 +160,37 @@ enum SniConnectTimeout: Error, Equatable {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
enum SniConnectWallClockDeadline {
|
|
163
|
+
struct Deadline: Sendable {
|
|
164
|
+
fileprivate let uptimeNanoseconds: UInt64
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static func makeDeadline(timeoutMilliseconds: TimeInterval) -> Deadline {
|
|
168
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
169
|
+
let (deadline, overflow) = now.addingReportingOverflow(
|
|
170
|
+
timeoutNanoseconds(milliseconds: timeoutMilliseconds)
|
|
171
|
+
)
|
|
172
|
+
return Deadline(uptimeNanoseconds: overflow ? UInt64.max : deadline)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static func remainingMilliseconds(until deadline: Deadline) -> TimeInterval {
|
|
176
|
+
Double(remainingNanoseconds(until: deadline)) / 1_000_000.0
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
static func run<T>(
|
|
164
180
|
timeoutMilliseconds: TimeInterval,
|
|
165
181
|
operation: @escaping @Sendable () async throws -> T
|
|
166
182
|
) async throws -> T {
|
|
167
|
-
|
|
183
|
+
try await run(
|
|
184
|
+
until: makeDeadline(timeoutMilliseconds: timeoutMilliseconds),
|
|
185
|
+
operation: operation
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static func run<T>(
|
|
190
|
+
until deadline: Deadline,
|
|
191
|
+
operation: @escaping @Sendable () async throws -> T
|
|
192
|
+
) async throws -> T {
|
|
193
|
+
let timeoutNanoseconds = remainingNanoseconds(until: deadline)
|
|
168
194
|
return try await withThrowingTaskGroup(of: T.self) { group in
|
|
169
195
|
group.addTask {
|
|
170
196
|
try await operation()
|
|
@@ -174,18 +200,38 @@ enum SniConnectWallClockDeadline {
|
|
|
174
200
|
throw SniConnectTimeout.deadlineExceeded
|
|
175
201
|
}
|
|
176
202
|
|
|
203
|
+
defer {
|
|
204
|
+
group.cancelAll()
|
|
205
|
+
}
|
|
177
206
|
do {
|
|
178
207
|
guard let result = try await group.next() else {
|
|
179
208
|
throw SniConnectTimeout.deadlineExceeded
|
|
180
209
|
}
|
|
181
|
-
|
|
210
|
+
guard !hasExpired(deadline) else {
|
|
211
|
+
throw SniConnectTimeout.deadlineExceeded
|
|
212
|
+
}
|
|
182
213
|
return result
|
|
183
214
|
} catch {
|
|
184
|
-
|
|
215
|
+
if hasExpired(deadline) {
|
|
216
|
+
throw SniConnectTimeout.deadlineExceeded
|
|
217
|
+
}
|
|
185
218
|
throw error
|
|
186
219
|
}
|
|
187
220
|
}
|
|
188
221
|
}
|
|
222
|
+
|
|
223
|
+
private static func remainingNanoseconds(until deadline: Deadline) -> UInt64 {
|
|
224
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
225
|
+
return deadline.uptimeNanoseconds > now ? deadline.uptimeNanoseconds - now : 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private static func hasExpired(_ deadline: Deadline) -> Bool {
|
|
229
|
+
DispatchTime.now().uptimeNanoseconds >= deadline.uptimeNanoseconds
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static func timeoutNanoseconds(milliseconds: TimeInterval) -> UInt64 {
|
|
233
|
+
UInt64(max(1.0, milliseconds) * 1_000_000.0)
|
|
234
|
+
}
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
struct SniConnectHeaderMaps: Equatable {
|