diff --git a/SPTDataLoader/SPTDataLoader.m b/SPTDataLoader/SPTDataLoader.m index 2d01c962..61067761 100644 --- a/SPTDataLoader/SPTDataLoader.m +++ b/SPTDataLoader/SPTDataLoader.m @@ -242,6 +242,42 @@ - (void)needsNewBodyStream:(void (^)(NSInputStream *))completionHandler } } +- (void)updatedCountOfBytesReceived:(int64_t)countOfBytesReceived + countOfBytesExpectedToReceive:(int64_t)countOfBytesExpectedToReceive + forRequest:(SPTDataLoaderRequest *)request +{ + if (![self isRequestExpected:request]) { + return; + } + + if ([self.delegate respondsToSelector:@selector(dataLoader:updatedCountOfBytesReceived:countOfBytesExpectedToReceive:forRequest:)]) { + [self executeDelegateBlock:^{ + [self.delegate dataLoader:self + updatedCountOfBytesReceived:countOfBytesReceived + countOfBytesExpectedToReceive:countOfBytesExpectedToReceive + forRequest:request]; + }]; + } +} + +- (void)updatedCountOfBytesSent:(int64_t)countOfBytesSent + countOfBytesExpectedToSend:(int64_t)countOfBytesExpectedToSend + forRequest:(SPTDataLoaderRequest *)request +{ + if (![self isRequestExpected:request]) { + return; + } + + if ([self.delegate respondsToSelector:@selector(dataLoader:updatedCountOfBytesSent:countOfBytesExpectedToSend:forRequest:)]) { + [self executeDelegateBlock:^{ + [self.delegate dataLoader:self + updatedCountOfBytesSent:countOfBytesSent + countOfBytesExpectedToSend:countOfBytesExpectedToSend + forRequest:request]; + }]; + } +} + #pragma mark SPTDataLoaderCancellationTokenDelegate - (void)cancellationTokenDidCancel:(id)cancellationToken diff --git a/SPTDataLoader/SPTDataLoaderFactory.m b/SPTDataLoader/SPTDataLoaderFactory.m index 7a736c92..e3f604fb 100644 --- a/SPTDataLoader/SPTDataLoaderFactory.m +++ b/SPTDataLoader/SPTDataLoaderFactory.m @@ -172,6 +172,32 @@ - (void)needsNewBodyStream:(void (^)(NSInputStream * _Nonnull))completionHandler [requestResponseHandler needsNewBodyStream:completionHandler forRequest:request]; } +- (void)updatedCountOfBytesReceived:(int64_t)countOfBytesReceived + countOfBytesExpectedToReceive:(int64_t)countOfBytesExpectedToReceive + forRequest:(SPTDataLoaderRequest *)request +{ + id requestResponseHandler = nil; + @synchronized(self.requestToRequestResponseHandler) { + requestResponseHandler = [self.requestToRequestResponseHandler objectForKey:request]; + } + [requestResponseHandler updatedCountOfBytesReceived:countOfBytesReceived + countOfBytesExpectedToReceive:countOfBytesExpectedToReceive + forRequest:request]; +} + +- (void)updatedCountOfBytesSent:(int64_t)countOfBytesSent + countOfBytesExpectedToSend:(int64_t)countOfBytesExpectedToSend + forRequest:(SPTDataLoaderRequest *)request +{ + id requestResponseHandler = nil; + @synchronized(self.requestToRequestResponseHandler) { + requestResponseHandler = [self.requestToRequestResponseHandler objectForKey:request]; + } + [requestResponseHandler updatedCountOfBytesSent:countOfBytesSent + countOfBytesExpectedToSend:countOfBytesExpectedToSend + forRequest:request]; +} + #pragma mark SPTDataLoaderRequestResponseHandlerDelegate - (void)requestResponseHandler:(id)requestResponseHandler diff --git a/SPTDataLoader/SPTDataLoaderRequestResponseHandler.h b/SPTDataLoader/SPTDataLoaderRequestResponseHandler.h index 689fd72f..c4c2acba 100644 --- a/SPTDataLoader/SPTDataLoaderRequestResponseHandler.h +++ b/SPTDataLoader/SPTDataLoaderRequestResponseHandler.h @@ -113,6 +113,25 @@ NS_ASSUME_NONNULL_BEGIN - (void)needsNewBodyStream:(void (^)(NSInputStream *))completionHandler forRequest:(SPTDataLoaderRequest *)request; +/** + * Called when the number of bytes that received from the server has changed + * @param countOfBytesReceived The number of bytes that received from the server in the response body + * @param countOfBytesExpectedToReceive The number of bytes that expects to receive in the response body + * @param request The object describing the request + */ +- (void)updatedCountOfBytesReceived:(int64_t)countOfBytesReceived + countOfBytesExpectedToReceive:(int64_t)countOfBytesExpectedToReceive + forRequest:(SPTDataLoaderRequest *)request; +/** + * Called when the number of bytes that sent to the server has changed + * @param countOfBytesSent The number of bytes that sent to the server in the request body + * @param countOfBytesExpectedToSend The number of bytes that the task expects to send in the request body + * @param request The object describing the request + */ +- (void)updatedCountOfBytesSent:(int64_t)countOfBytesSent + countOfBytesExpectedToSend:(int64_t)countOfBytesExpectedToSend + forRequest:(SPTDataLoaderRequest *)request; + @optional /** diff --git a/SPTDataLoader/SPTDataLoaderRequestTaskHandler.m b/SPTDataLoader/SPTDataLoaderRequestTaskHandler.m index b41a6120..2d3c4e30 100644 --- a/SPTDataLoader/SPTDataLoaderRequestTaskHandler.m +++ b/SPTDataLoader/SPTDataLoaderRequestTaskHandler.m @@ -33,6 +33,12 @@ static NSUInteger const SPTDataLoaderRequestTaskHandlerMaxRedirects = 10; +static void *SPTDataLoaderRequestTaskHandlerContext = &SPTDataLoaderRequestTaskHandlerContext; +static NSString *const kCountOfBytesReceived = @"countOfBytesReceived"; +static NSString *const kCountOfBytesSent = @"countOfBytesSent"; +static NSString *const kCountOfBytesExpectedToReceive = @"countOfBytesExpectedToReceive"; +static NSString *const kCountOfBytesExpectedToSend = @"countOfBytesExpectedToSend"; + @interface SPTDataLoaderRequestTaskHandler () @property (nonatomic, assign, readwrite, getter = isCancelled) BOOL cancelled; @@ -53,6 +59,7 @@ @interface SPTDataLoaderRequestTaskHandler () @property (nonatomic, assign) BOOL calledFailedResponse; @property (nonatomic, assign) BOOL calledCancelledRequest; @property (nonatomic, assign) BOOL started; +@property (nonatomic, assign, getter = isObserving) BOOL observing; @property (nonatomic, strong, readwrite) dispatch_queue_t retryQueue; @end @@ -83,6 +90,7 @@ - (instancetype)initWithTask:(NSURLSessionTask *)task self = [super init]; if (self) { _task = task; + [self addObserverForTask:_task]; _request = request; _requestResponseHandler = requestResponseHandler; _rateLimiter = rateLimiter; @@ -99,6 +107,30 @@ - (instancetype)initWithTask:(NSURLSessionTask *)task return self; } +@synthesize task = _task; + +- (NSURLSessionTask *)task { + @synchronized(self) { + return _task; + } +} + +- (void)setTask:(NSURLSessionTask * _Nonnull)task { + @synchronized(self) { + if (_task != task) { + if (_task) { + [self removeObserverForTask:_task]; + } + + _task = task; + + if (_task) { + [self addObserverForTask:_task]; + } + } + } +} + - (void)receiveData:(NSData *)data { [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) { @@ -239,11 +271,86 @@ - (void)completeIfInFlight } } +#pragma mark KVO + +- (void)observeValueForKeyPath:(nullable NSString *)keyPath + ofObject:(nullable id)object + change:(nullable NSDictionary *)change + context:(nullable void *)context +{ + if (context == SPTDataLoaderRequestTaskHandlerContext) { + NSURLSessionTask *task = (NSURLSessionTask *)object; + if ([task isKindOfClass:[NSURLSessionTask class]]) { + id requestResponseHandler = self.requestResponseHandler; + if ([keyPath isEqualToString:kCountOfBytesReceived] || [keyPath isEqualToString:kCountOfBytesExpectedToReceive]) { + [requestResponseHandler updatedCountOfBytesReceived:self.task.countOfBytesReceived + countOfBytesExpectedToReceive:self.task.countOfBytesExpectedToReceive + forRequest:self.request]; + } else if ([keyPath isEqualToString:kCountOfBytesSent] || [keyPath isEqualToString:kCountOfBytesExpectedToSend]) { + [requestResponseHandler updatedCountOfBytesSent:self.task.countOfBytesSent + countOfBytesExpectedToSend:self.task.countOfBytesExpectedToSend + forRequest:self.request]; + } + } + } +} + +- (void)addObserverForTask:(NSURLSessionTask *)task +{ + NSAssert(!self.isObserving, @"Observer is already registered for task"); + if (self.isObserving) { + return; + } + + self.observing = YES; + + [task addObserver:self + forKeyPath:kCountOfBytesReceived + options:NSKeyValueObservingOptionNew + context:SPTDataLoaderRequestTaskHandlerContext]; + [task addObserver:self + forKeyPath:kCountOfBytesSent + options:NSKeyValueObservingOptionNew + context:SPTDataLoaderRequestTaskHandlerContext]; + [task addObserver:self + forKeyPath:kCountOfBytesExpectedToReceive + options:NSKeyValueObservingOptionNew + context:SPTDataLoaderRequestTaskHandlerContext]; + [task addObserver:self + forKeyPath:kCountOfBytesExpectedToSend + options:NSKeyValueObservingOptionNew + context:SPTDataLoaderRequestTaskHandlerContext]; +} + +- (void)removeObserverForTask:(NSURLSessionTask *)task +{ + NSAssert(self.isObserving, @"Observer is not registered"); + if (!self.isObserving) { + return; + } + + self.observing = NO; + + [task removeObserver:self + forKeyPath:kCountOfBytesReceived + context:SPTDataLoaderRequestTaskHandlerContext]; + [task removeObserver:self + forKeyPath:kCountOfBytesSent + context:SPTDataLoaderRequestTaskHandlerContext]; + [task removeObserver:self + forKeyPath:kCountOfBytesExpectedToReceive + context:SPTDataLoaderRequestTaskHandlerContext]; + [task removeObserver:self + forKeyPath:kCountOfBytesExpectedToSend + context:SPTDataLoaderRequestTaskHandlerContext]; +} + #pragma mark NSObject - (void)dealloc { [self completeIfInFlight]; + [self removeObserverForTask:_task]; } @end diff --git a/SPTDataLoaderTests/NSURLSessionTaskMock.h b/SPTDataLoaderTests/NSURLSessionTaskMock.h index 3b4bea47..bf5efa2c 100644 --- a/SPTDataLoaderTests/NSURLSessionTaskMock.h +++ b/SPTDataLoaderTests/NSURLSessionTaskMock.h @@ -29,8 +29,10 @@ #pragma mark NSURLSessionTask -@property (atomic, readonly) int64_t countOfBytesSent; -@property (atomic, readonly) int64_t countOfBytesReceived; +@property (atomic, assign) int64_t countOfBytesSent; +@property (atomic, assign) int64_t countOfBytesReceived; +@property (atomic, assign) int64_t countOfBytesExpectedToSend; +@property (atomic, assign) int64_t countOfBytesExpectedToReceive; @property (atomic, nullable, readonly, copy) NSURLRequest *currentRequest; @end diff --git a/SPTDataLoaderTests/NSURLSessionTaskMock.m b/SPTDataLoaderTests/NSURLSessionTaskMock.m index 5282e24e..52876a1c 100644 --- a/SPTDataLoaderTests/NSURLSessionTaskMock.m +++ b/SPTDataLoaderTests/NSURLSessionTaskMock.m @@ -46,6 +46,8 @@ - (NSURLResponse *)response @synthesize countOfBytesSent; @synthesize countOfBytesReceived; +@synthesize countOfBytesExpectedToSend; +@synthesize countOfBytesExpectedToReceive; @synthesize currentRequest; @end diff --git a/SPTDataLoaderTests/SPTDataLoaderFactoryTest.m b/SPTDataLoaderTests/SPTDataLoaderFactoryTest.m index 0f5f2bc2..a45b9450 100644 --- a/SPTDataLoaderTests/SPTDataLoaderFactoryTest.m +++ b/SPTDataLoaderTests/SPTDataLoaderFactoryTest.m @@ -226,4 +226,24 @@ - (void)testRequestingNewBodyStream XCTAssertEqual(requestResponseHandler.numberOfNewBodyStreamCalls, 1u, @"The factory did not relay a prompt for delivering a new body stream to the correct handler"); } +- (void)testUpdatedCountOfBytesReceived +{ + SPTDataLoaderRequestResponseHandlerMock *requestResponseHandler = [SPTDataLoaderRequestResponseHandlerMock new]; + SPTDataLoaderRequest *request = [SPTDataLoaderRequest new]; + [self.factory requestResponseHandler:requestResponseHandler performRequest:request]; + [self.factory updatedCountOfBytesReceived:1u countOfBytesExpectedToReceive:2u forRequest:request]; + + XCTAssertEqual(requestResponseHandler.numberOfUpdatedCountOfBytesReceived, 1u, @"The factory did not relay an update of the number of bytes that received from the server"); +} + +- (void)testUpdatedCountOfBytesSent +{ + SPTDataLoaderRequestResponseHandlerMock *requestResponseHandler = [SPTDataLoaderRequestResponseHandlerMock new]; + SPTDataLoaderRequest *request = [SPTDataLoaderRequest new]; + [self.factory requestResponseHandler:requestResponseHandler performRequest:request]; + [self.factory updatedCountOfBytesSent:1u countOfBytesExpectedToSend:2u forRequest:request]; + + XCTAssertEqual(requestResponseHandler.numberOfUpdatedCountOfBytesSent, 1u, @"The factory did not relay an update of the number of bytes that sent to the server"); +} + @end diff --git a/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.h b/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.h index e2c488f6..84014c0e 100644 --- a/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.h +++ b/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.h @@ -32,6 +32,8 @@ @property (nonatomic, assign, readonly) NSUInteger numberOfSuccessfulDataResponseCalls; @property (nonatomic, assign, readonly) NSUInteger numberOfReceivedInitialResponseCalls; @property (nonatomic, assign, readonly) NSUInteger numberOfNewBodyStreamCalls; +@property (nonatomic, assign, readonly) NSUInteger numberOfUpdatedCountOfBytesReceived; +@property (nonatomic, assign, readonly) NSUInteger numberOfUpdatedCountOfBytesSent; @property (nonatomic, strong, readonly) SPTDataLoaderResponse *lastReceivedResponse; @property (nonatomic, assign, readwrite, getter = isAuthorising) BOOL authorising; @property (nonatomic, strong, readwrite) dispatch_block_t failedResponseBlock; diff --git a/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.m b/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.m index 3c0b356a..885cdd25 100644 --- a/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.m +++ b/SPTDataLoaderTests/SPTDataLoaderRequestResponseHandlerMock.m @@ -28,6 +28,8 @@ @interface SPTDataLoaderRequestResponseHandlerMock () @property (nonatomic, assign, readwrite) NSUInteger numberOfSuccessfulDataResponseCalls; @property (nonatomic, assign, readwrite) NSUInteger numberOfReceivedInitialResponseCalls; @property (nonatomic, assign, readwrite) NSUInteger numberOfNewBodyStreamCalls; +@property (nonatomic, assign, readwrite) NSUInteger numberOfUpdatedCountOfBytesReceived; +@property (nonatomic, assign, readwrite) NSUInteger numberOfUpdatedCountOfBytesSent; @property (nonatomic, strong, readwrite) SPTDataLoaderResponse *lastReceivedResponse; @end @@ -83,4 +85,18 @@ - (void)needsNewBodyStream:(void (^)(NSInputStream * _Nonnull))completionHandler self.numberOfNewBodyStreamCalls++; } +- (void)updatedCountOfBytesReceived:(int64_t)countOfBytesReceived + countOfBytesExpectedToReceive:(int64_t)countOfBytesExpectedToReceive + forRequest:(SPTDataLoaderRequest *)request +{ + self.numberOfUpdatedCountOfBytesReceived++; +} + +- (void)updatedCountOfBytesSent:(int64_t)countOfBytesSent + countOfBytesExpectedToSend:(int64_t)countOfBytesExpectedToSend + forRequest:(SPTDataLoaderRequest *)request +{ + self.numberOfUpdatedCountOfBytesSent++; +} + @end diff --git a/SPTDataLoaderTests/SPTDataLoaderRequestTaskHandlerTest.m b/SPTDataLoaderTests/SPTDataLoaderRequestTaskHandlerTest.m index a1f0e09f..c61ea2ef 100644 --- a/SPTDataLoaderTests/SPTDataLoaderRequestTaskHandlerTest.m +++ b/SPTDataLoaderTests/SPTDataLoaderRequestTaskHandlerTest.m @@ -204,4 +204,20 @@ - (void)testCancelledWhenReceivingCancelError XCTAssertTrue(self.handler.cancelled); } +- (void)testRelayUpdatedCountOfBytesSent +{ + self.task.countOfBytesSent = 1u; + self.task.countOfBytesExpectedToSend = 2u; + + XCTAssertEqual(self.requestResponseHandler.numberOfUpdatedCountOfBytesSent, 2u, @"The handler did not relay the updated count of bytes sent its request response handler"); +} + +- (void)testRelayUpdatedCountOfBytesReceived +{ + self.task.countOfBytesReceived = 1u; + self.task.countOfBytesExpectedToReceive = 2u; + + XCTAssertEqual(self.requestResponseHandler.numberOfUpdatedCountOfBytesReceived, 2u, @"The handler did not relay the updated count of bytes received its request response handler"); +} + @end diff --git a/include/SPTDataLoader/SPTDataLoaderDelegate.h b/include/SPTDataLoader/SPTDataLoaderDelegate.h index 7165e5b7..5467441e 100644 --- a/include/SPTDataLoader/SPTDataLoaderDelegate.h +++ b/include/SPTDataLoader/SPTDataLoaderDelegate.h @@ -90,6 +90,29 @@ didReceiveDataChunk:(NSData *)data needsNewBodyStream:(void (^)(NSInputStream *))completionHandler forRequest:(SPTDataLoaderRequest *)request; +/** + * Called when the data loader receives an update of the number of bytes that received from the server for the request + * @param dataLoader The data loader that received an update + * @param countOfBytesReceived The number of bytes that received from the server in the response body + * @param countOfBytesExpectedToReceive The number of bytes that expects to receive in the response body + * @param request The object describing the request that was updated + */ +- (void)dataLoader:(SPTDataLoader *)dataLoader +updatedCountOfBytesReceived:(int64_t)countOfBytesReceived +countOfBytesExpectedToReceive:(int64_t)countOfBytesExpectedToReceive + forRequest:(SPTDataLoaderRequest *)request; +/** + * Called when the data loader receives an update of the number of bytes that sent to the server for request + * @param dataLoader The data loader that received an update + * @param countOfBytesSent The number of bytes that sent to the server in the request body + * @param countOfBytesExpectedToSend The number of bytes that the task expects to send in the request body + * @param request The object describing the request + */ +- (void)dataLoader:(SPTDataLoader *)dataLoader +updatedCountOfBytesSent:(int64_t)countOfBytesSent +countOfBytesExpectedToSend:(int64_t)countOfBytesExpectedToSend + forRequest:(SPTDataLoaderRequest *)request; + @end NS_ASSUME_NONNULL_END