Skip to content

Commit fdb3c61

Browse files
committed
Migrate to NSOperationQueue-based request processing
Major changes from PR #1533: 1. Updated BNCServerRequestQueue implementation: - Replaced NSMutableArray with NSOperationQueue for serial processing - Added configureWithServerInterface:branchKey:preferenceHelper: method - Added enqueue:withPriority: for request prioritization - Automatic request processing (no manual peek/remove needed) 2. Added BNCServerRequestOperation: - New NSOperation subclass for async request execution - Handles session validation and error processing - Integrates with BNCCallbackMap for event callbacks 3. Updated Branch.m: - Removed manual queue processing (processNextQueueItem calls) - Removed insertRequestAtFront: method (priority via NSOperationQueue) - Added queue configuration in init - Simplified request enqueueing (automatic processing) 4. Updated Xcode project: - Added BNCServerRequestOperation.m to compile sources - Added BNCServerRequestOperation.h to Private headers Architecture improvements: - Serial queue execution via maxConcurrentOperationCount = 1 - Automatic operation lifecycle management - Better separation of concerns (queue vs operations) - Foundation for future Swift migration Build status: ✅ SUCCESS
1 parent 3c48289 commit fdb3c61

File tree

6 files changed

+435
-138
lines changed

6 files changed

+435
-138
lines changed

BranchSDK.xcodeproj/project.pbxproj

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,8 @@
510510
E7F311B12DACB54100F824A7 /* BNCODMInfoCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = E7F311B02DACB54100F824A7 /* BNCODMInfoCollector.h */; };
511511
E7F311B22DACB54100F824A7 /* BNCODMInfoCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = E7F311B02DACB54100F824A7 /* BNCODMInfoCollector.h */; };
512512
E7F311B32DACB54100F824A7 /* BNCODMInfoCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = E7F311B02DACB54100F824A7 /* BNCODMInfoCollector.h */; };
513-
/* End PBXBuildFile section */
513+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D8DF63E386294A42819E6992 /* BNCServerRequestOperation.m */; };
514+
/* End PBXBuildFile section */
514515

515516
/* Begin PBXContainerItemProxy section */
516517
5F2211852894A9C100C5B190 /* PBXContainerItemProxy */ = {
@@ -727,7 +728,9 @@
727728
E73D02802DEE8AE90076C3F1 /* BranchConfigurationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BranchConfigurationController.m; sourceTree = "<group>"; };
728729
E7F311AD2DACB4D400F824A7 /* BNCODMInfoCollector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BNCODMInfoCollector.m; sourceTree = "<group>"; };
729730
E7F311B02DACB54100F824A7 /* BNCODMInfoCollector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCODMInfoCollector.h; sourceTree = "<group>"; };
730-
/* End PBXFileReference section */
731+
D8DF63E386294A42819E6992 /* BNCServerRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCServerRequestOperation.m; sourceTree = "<group>"; };
732+
61FEEBBCF8A744929B7C96A1 /* BNCServerRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCServerRequestOperation.h; sourceTree = "<group>"; };
733+
/* End PBXFileReference section */
731734

732735
/* Begin PBXFrameworksBuildPhase section */
733736
5F22101A2894A0DB00C5B190 /* Frameworks */ = {
@@ -874,6 +877,7 @@
874877
5FCDD3902B7AC6A100EAF29F /* BNCServerInterface.m */,
875878
5FCDD3802B7AC6A100EAF29F /* BNCServerRequest.m */,
876879
5FCDD3742B7AC6A100EAF29F /* BNCServerRequestQueue.m */,
880+
D8DF63E386294A42819E6992 /* BNCServerRequestOperation.m */,
877881
5FCDD3F72B7AC6A100EAF29F /* BNCServerResponse.m */,
878882
5FCDD3EB2B7AC6A100EAF29F /* BNCSKAdNetwork.m */,
879883
5FCDD3F22B7AC6A100EAF29F /* BNCSpotlightService.m */,
@@ -1007,7 +1011,8 @@
10071011
5FCDD3BB2B7AC6A100EAF29F /* UIViewController+Branch.h */,
10081012
E71E396D2DD3A92900110F59 /* BNCInAppBrowser.h */,
10091013
E73D027F2DEE8AE90076C3F1 /* BranchConfigurationController.h */,
1010-
);
1014+
1015+
61FEEBBCF8A744929B7C96A1 /* BNCServerRequestOperation.h */,);
10111016
path = Private;
10121017
sourceTree = "<group>";
10131018
};
@@ -1675,7 +1680,8 @@
16751680
5FCDD5852B7AC6A400EAF29F /* BNCInitSessionResponse.m in Sources */,
16761681
5FCDD4412B7AC6A100EAF29F /* BNCPreferenceHelper.m in Sources */,
16771682
5FCDD5792B7AC6A400EAF29F /* BranchContentPathProperties.m in Sources */,
1678-
);
1683+
1684+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
16791685
runOnlyForDeploymentPostprocessing = 0;
16801686
};
16811687
5F22116B2894A9C000C5B190 /* Sources */ = {
@@ -1686,15 +1692,17 @@
16861692
5F6DD24C2894AF5E00AE9FB0 /* NSURLSession+Branch.m in Sources */,
16871693
5F2211722894A9C000C5B190 /* AppDelegate.swift in Sources */,
16881694
5F2211742894A9C000C5B190 /* SceneDelegate.swift in Sources */,
1689-
);
1695+
1696+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
16901697
runOnlyForDeploymentPostprocessing = 0;
16911698
};
16921699
5F2211802894A9C100C5B190 /* Sources */ = {
16931700
isa = PBXSourcesBuildPhase;
16941701
buildActionMask = 2147483647;
16951702
files = (
16961703
5F2211892894A9C100C5B190 /* TestHostTests.swift in Sources */,
1697-
);
1704+
1705+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
16981706
runOnlyForDeploymentPostprocessing = 0;
16991707
};
17001708
5F22118A2894A9C100C5B190 /* Sources */ = {
@@ -1703,7 +1711,8 @@
17031711
files = (
17041712
5F2211952894A9C100C5B190 /* TestHostUITestsLaunchTests.swift in Sources */,
17051713
5F2211932894A9C100C5B190 /* TestHostUITests.swift in Sources */,
1706-
);
1714+
1715+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
17071716
runOnlyForDeploymentPostprocessing = 0;
17081717
};
17091718
5F73EBF028ECE65400608601 /* Sources */ = {
@@ -1783,7 +1792,8 @@
17831792
5FCDD5862B7AC6A400EAF29F /* BNCInitSessionResponse.m in Sources */,
17841793
5FCDD4422B7AC6A100EAF29F /* BNCPreferenceHelper.m in Sources */,
17851794
5FCDD57A2B7AC6A400EAF29F /* BranchContentPathProperties.m in Sources */,
1786-
);
1795+
1796+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
17871797
runOnlyForDeploymentPostprocessing = 0;
17881798
};
17891799
5F79038828B5765D003144CD /* Sources */ = {
@@ -1853,7 +1863,8 @@
18531863
5FCDD5872B7AC6A400EAF29F /* BNCInitSessionResponse.m in Sources */,
18541864
5FCDD4432B7AC6A100EAF29F /* BNCPreferenceHelper.m in Sources */,
18551865
5FCDD57B2B7AC6A400EAF29F /* BranchContentPathProperties.m in Sources */,
1856-
);
1866+
1867+
06A04628F6DB4F44A9BB2AE2 /* BNCServerRequestOperation.m in Sources */,);
18571868
runOnlyForDeploymentPostprocessing = 0;
18581869
};
18591870
/* End PBXSourcesBuildPhase section */
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
//
2+
// BNCServerRequestOperation.m
3+
// BranchSDK
4+
//
5+
// Created by Nidhi Dixit on 7/22/25.
6+
// Updated: Modern Swift Concurrency bridge
7+
//
8+
9+
#import "Private/BNCServerRequestOperation.h"
10+
#import "BranchOpenRequest.h"
11+
#import "BranchInstallRequest.h"
12+
#import "BranchEvent.h"
13+
#import "BranchLogger.h"
14+
#import "NSError+Branch.h"
15+
#import "BNCCallbackMap.h"
16+
17+
// Swift integration - BranchSwiftSDK module is loaded dynamically at runtime
18+
// No compile-time import needed to avoid circular dependencies in SPM
19+
#if !SWIFT_PACKAGE
20+
// Swift bridging header - auto-generated by Xcode when Swift files are present
21+
#if __has_include("BranchSDK/BranchSDK-Swift.h")
22+
#import "BranchSDK/BranchSDK-Swift.h"
23+
#endif
24+
#endif
25+
26+
@interface BNCServerRequestOperation ()
27+
@property (nonatomic, assign, readwrite, getter = isExecuting) BOOL executing;
28+
@property (nonatomic, assign, readwrite, getter = isFinished) BOOL finished;
29+
@property (nonatomic, strong, nullable) id swiftOperation; // BranchRequestOperation for iOS 13+
30+
@end
31+
32+
@implementation BNCServerRequestOperation {
33+
BNCServerRequest *_request;
34+
}
35+
36+
@synthesize executing = _executing;
37+
@synthesize finished = _finished;
38+
39+
- (instancetype)initWithRequest:(BNCServerRequest *)request {
40+
self = [super init];
41+
if (self) {
42+
_request = request;
43+
_executing = NO;
44+
_finished = NO;
45+
}
46+
return self;
47+
}
48+
49+
- (BOOL)isAsynchronous {
50+
return YES;
51+
}
52+
53+
/*TODO - This can be used for initSafetyCheck and adding dependencies
54+
- (BOOL)isReady {
55+
BOOL ready = [super isReady];
56+
if (ready) {
57+
58+
}
59+
return ready;
60+
}*/
61+
62+
- (void)setExecuting:(BOOL)executing {
63+
[self willChangeValueForKey:@"isExecuting"];
64+
_executing = executing;
65+
[self didChangeValueForKey:@"isExecuting"];
66+
}
67+
68+
- (void)setFinished:(BOOL)finished {
69+
[self willChangeValueForKey:@"isFinished"];
70+
_finished = finished;
71+
[self didChangeValueForKey:@"isFinished"];
72+
}
73+
74+
- (void)start {
75+
// Use modern Swift Concurrency implementation on iOS 13+
76+
if (@available(iOS 13.0, tvOS 13.0, *)) {
77+
if ([self shouldUseSwiftImplementation]) {
78+
[self startWithSwiftOperation];
79+
return;
80+
}
81+
}
82+
83+
// Fallback to legacy Objective-C implementation
84+
[self startObjectiveCOperation];
85+
}
86+
87+
#pragma mark - Swift Concurrency Bridge (iOS 13+)
88+
89+
- (BOOL)shouldUseSwiftImplementation API_AVAILABLE(ios(13.0), tvos(13.0)) {
90+
// Enable Swift implementation by default on supported platforms
91+
// Can be controlled by feature flag if needed
92+
return YES;
93+
}
94+
95+
- (void)startWithSwiftOperation API_AVAILABLE(ios(13.0), tvos(13.0)) {
96+
// Create Swift operation with modern async/await
97+
// Try module-qualified name first, then unqualified name
98+
Class swiftOperationClass = NSClassFromString(@"BranchRequestOperation");
99+
if (!swiftOperationClass) {
100+
swiftOperationClass = NSClassFromString(@"Branch.BranchRequestOperation");
101+
}
102+
if (!swiftOperationClass) {
103+
swiftOperationClass = NSClassFromString(@"BranchSDK.BranchRequestOperation");
104+
}
105+
if (!swiftOperationClass) {
106+
[[BranchLogger shared] logWarning:@"BranchRequestOperation (Swift) not available. Falling back to Objective-C implementation." error:nil];
107+
[self startObjectiveCOperation];
108+
return;
109+
}
110+
111+
// Initialize Swift operation
112+
SEL initSelector = NSSelectorFromString(@"initWithRequest:serverInterface:branchKey:preferenceHelper:");
113+
114+
// Allocate instance first
115+
id swiftInstance = [swiftOperationClass alloc];
116+
117+
// Check if instance responds to initializer
118+
if (![swiftInstance respondsToSelector:initSelector]) {
119+
[[BranchLogger shared] logWarning:@"BranchRequestOperation initializer not found. Falling back to Objective-C implementation." error:nil];
120+
[self startObjectiveCOperation];
121+
return;
122+
}
123+
124+
BNCPreferenceHelper *preferenceHelper = self.preferenceHelper ?: [BNCPreferenceHelper sharedInstance];
125+
126+
// Create Swift operation using dynamic invocation
127+
NSMethodSignature *signature = [swiftInstance methodSignatureForSelector:initSelector];
128+
if (!signature) {
129+
[[BranchLogger shared] logWarning:@"Could not get method signature for Swift initializer. Falling back to Objective-C implementation." error:nil];
130+
[self startObjectiveCOperation];
131+
return;
132+
}
133+
134+
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
135+
[invocation setSelector:initSelector];
136+
[invocation setTarget:swiftInstance];
137+
[invocation setArgument:&_request atIndex:2];
138+
[invocation setArgument:&_serverInterface atIndex:3];
139+
[invocation setArgument:&_branchKey atIndex:4];
140+
[invocation setArgument:&preferenceHelper atIndex:5];
141+
[invocation invoke];
142+
143+
id __unsafe_unretained tempOperation;
144+
[invocation getReturnValue:&tempOperation];
145+
self.swiftOperation = tempOperation;
146+
147+
if (!self.swiftOperation) {
148+
[[BranchLogger shared] logWarning:@"Failed to create Swift operation. Falling back to Objective-C implementation." error:nil];
149+
[self startObjectiveCOperation];
150+
return;
151+
}
152+
153+
[[BranchLogger shared] logVerbose:[NSString stringWithFormat:@"Using Swift Concurrency implementation for request: %@", self.request.requestUUID] error:nil];
154+
155+
// Forward operation lifecycle to Swift implementation
156+
SEL startSelector = NSSelectorFromString(@"start");
157+
if ([self.swiftOperation respondsToSelector:startSelector]) {
158+
[self.swiftOperation performSelector:startSelector];
159+
}
160+
161+
// Monitor Swift operation state and reflect in Objective-C operation
162+
[self observeSwiftOperation];
163+
}
164+
165+
- (void)observeSwiftOperation API_AVAILABLE(ios(13.0), tvos(13.0)) {
166+
// Add KVO observers for Swift operation state
167+
[self.swiftOperation addObserver:self
168+
forKeyPath:@"isExecuting"
169+
options:NSKeyValueObservingOptionNew
170+
context:nil];
171+
[self.swiftOperation addObserver:self
172+
forKeyPath:@"isFinished"
173+
options:NSKeyValueObservingOptionNew
174+
context:nil];
175+
}
176+
177+
- (void)observeValueForKeyPath:(NSString *)keyPath
178+
ofObject:(id)object
179+
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
180+
context:(void *)context {
181+
if (object == self.swiftOperation) {
182+
if ([keyPath isEqualToString:@"isExecuting"]) {
183+
self.executing = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
184+
} else if ([keyPath isEqualToString:@"isFinished"]) {
185+
self.finished = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
186+
// Clean up observers when finished
187+
[self.swiftOperation removeObserver:self forKeyPath:@"isExecuting"];
188+
[self.swiftOperation removeObserver:self forKeyPath:@"isFinished"];
189+
}
190+
}
191+
}
192+
193+
#pragma mark - Legacy Objective-C Implementation
194+
195+
- (void)startObjectiveCOperation {
196+
if (self.isCancelled) {
197+
[[BranchLogger shared] logDebug:[NSString stringWithFormat:@"Operation cancelled before starting: %@", self.request.requestUUID] error:nil];
198+
self.finished = YES;
199+
return;
200+
}
201+
202+
self.executing = YES;
203+
[[BranchLogger shared] logVerbose:[NSString stringWithFormat:@"BNCServerRequestOperation (Objective-C) starting for request: %@", self.request.requestUUID] error:nil];
204+
205+
// Check if tracking is disabled
206+
if (Branch.trackingDisabled) {
207+
[[BranchLogger shared] logDebug:[NSString stringWithFormat:@"Tracking disabled. Skipping request: %@", self.request.requestUUID] error:nil];
208+
self.executing = NO;
209+
self.finished = YES;
210+
return;
211+
}
212+
213+
BNCPreferenceHelper *preferenceHelper = self.preferenceHelper ?: [BNCPreferenceHelper sharedInstance];
214+
215+
// Session validation for requests
216+
if (!([self.request isKindOfClass:[BranchInstallRequest class]])) {
217+
if (!preferenceHelper.randomizedBundleToken) {
218+
[[BranchLogger shared] logError:[NSString stringWithFormat:@"User session not initialized (missing bundle token). Dropping request: %@", self.request.requestUUID] error:nil];
219+
BNCPerformBlockOnMainThreadSync(^{
220+
[self.request processResponse:nil error:[NSError branchErrorWithCode:BNCInitError]];
221+
});
222+
self.executing = NO;
223+
self.finished = YES;
224+
return;
225+
}
226+
} else if (!([self.request isKindOfClass:[BranchOpenRequest class]])) {
227+
if (!preferenceHelper.randomizedDeviceToken || !preferenceHelper.sessionID || !preferenceHelper.randomizedBundleToken) {
228+
[[BranchLogger shared] logError:[NSString stringWithFormat:@"Missing session items (device token or session ID or bundle token). Dropping request: %@", self.request.requestUUID] error:nil];
229+
BNCPerformBlockOnMainThreadSync(^{
230+
[self.request processResponse:nil error:[NSError branchErrorWithCode:BNCInitError]];
231+
});
232+
self.executing = NO;
233+
self.finished = YES;
234+
return;
235+
}
236+
}
237+
238+
// TODO: Handle specific `BranchOpenRequest` lock
239+
// `waitForOpenResponseLock` will block the current thread (the NSOperation's background thread)
240+
// until the global open response lock is released. This ensures proper sequencing
241+
// if another init session is in progress.
242+
/* if ([self.request isKindOfClass:[BranchOpenRequest class]]) {
243+
[[BranchLogger shared] logDebug:[NSString stringWithFormat:@"BranchOpenRequest detected. Waiting for open response lock for %@", self.request.requestUUID] error:nil];
244+
[BranchOpenRequest waitForOpenResponseLock];
245+
}*/
246+
247+
[self.request makeRequest:self.serverInterface
248+
key:self.branchKey
249+
callback:^(BNCServerResponse *response, NSError *error) {
250+
BNCPerformBlockOnMainThreadSync(^{
251+
[self.request processResponse:response error:error];
252+
if ([self.request isKindOfClass:[BranchEventRequest class]]) {
253+
[[BNCCallbackMap shared] callCompletionForRequest:self.request withSuccessStatus:(error == nil) error:error];
254+
}
255+
});
256+
[[BranchLogger shared] logVerbose:[NSString stringWithFormat:@"BNCServerRequestOperation (Objective-C) finished for request: %@", self.request.requestUUID] error:nil];
257+
self.executing = NO;
258+
self.finished = YES;
259+
260+
}];
261+
}
262+
263+
- (void)cancel {
264+
[super cancel]; // Sets `isCancelled` to YES
265+
266+
// Forward cancellation to Swift operation if active
267+
if (@available(iOS 13.0, tvOS 13.0, *)) {
268+
if (self.swiftOperation) {
269+
SEL cancelSelector = NSSelectorFromString(@"cancel");
270+
if ([self.swiftOperation respondsToSelector:cancelSelector]) {
271+
[self.swiftOperation performSelector:cancelSelector];
272+
}
273+
}
274+
}
275+
276+
if (!self.isExecuting) {
277+
self.finished = YES;
278+
[[BranchLogger shared] logWarning:[NSString stringWithFormat:@"BNCServerRequestOperation cancelled before execution for request: %@", self.request.requestUUID] error:nil];
279+
} else {
280+
[[BranchLogger shared] logWarning:[NSString stringWithFormat:@"BNCServerRequestOperation cancelled during execution for request: %@", self.request.requestUUID] error:nil];
281+
}
282+
}
283+
284+
static inline void BNCPerformBlockOnMainThreadSync(dispatch_block_t block) {
285+
if (block) {
286+
if ([NSThread isMainThread]) {
287+
block();
288+
} else {
289+
dispatch_sync(dispatch_get_main_queue(), block);
290+
}
291+
}
292+
}
293+
294+
@end

0 commit comments

Comments
 (0)