diff --git a/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource-watchOS.xcscheme b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource-watchOS.xcscheme index 1b4607d..ae87957 100644 --- a/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource-watchOS.xcscheme +++ b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource-watchOS.xcscheme @@ -15,7 +15,7 @@ @@ -29,8 +29,6 @@ shouldUseLaunchSchemeArgsEnv = "YES"> - - - - diff --git a/EventSource/EventSource.h b/EventSource/EventSource.h index 7752b64..3692f3f 100644 --- a/EventSource/EventSource.h +++ b/EventSource/EventSource.h @@ -9,9 +9,9 @@ #import typedef enum { - kEventStateConnecting = 0, - kEventStateOpen = 1, - kEventStateClosed = 2, + kEventStateConnecting = 0, + kEventStateOpen = 1, + kEventStateClosed = 2, } EventState; // --------------------------------------------------------------------------------------------------------------------- @@ -42,27 +42,12 @@ typedef void (^EventSourceEventHandler)(Event *event); /// Connect to and receive Server-Sent Events (SSEs). @interface EventSource : NSObject -/// Returns a new instance of EventSource with the specified URL. -/// -/// @param URL The URL of the EventSource. -+ (instancetype)eventSourceWithURL:(NSURL *)URL; - -/// Returns a new instance of EventSource with the specified URL. -/// -/// @param URL The URL of the EventSource. -/// @param timeoutInterval The request timeout interval in seconds. See NSURLRequest for more details. Default: 5 minutes. -+ (instancetype)eventSourceWithURL:(NSURL *)URL timeoutInterval:(NSTimeInterval)timeoutInterval; - -/// Creates a new instance of EventSource with the specified URL. -/// -/// @param URL The URL of the EventSource. -- (instancetype)initWithURL:(NSURL *)URL; - /// Creates a new instance of EventSource with the specified URL. /// /// @param URL The URL of the EventSource. +/// @param authorization HTTP authorization header parameter. /// @param timeoutInterval The request timeout interval in seconds. See NSURLRequest for more details. Default: 5 minutes. -- (instancetype)initWithURL:(NSURL *)URL timeoutInterval:(NSTimeInterval)timeoutInterval; +- (instancetype)initWithURL:(NSURL *)URL authorization:(NSString *)authorization timeoutInterval:(NSTimeInterval)timeoutInterval; /// Registers an event handler for the Message event. /// diff --git a/EventSource/EventSource.m b/EventSource/EventSource.m index cd9883e..0908c4c 100644 --- a/EventSource/EventSource.m +++ b/EventSource/EventSource.m @@ -9,8 +9,7 @@ #import "EventSource.h" #import -static CGFloat const ES_RETRY_INTERVAL = 1.0; -static CGFloat const ES_DEFAULT_TIMEOUT = 300.0; +static CGFloat const ES_RETRY_INTERVAL = 40.0; static NSString *const ESKeyValueDelimiter = @":"; static NSString *const ESEventSeparatorLFLF = @"\n\n"; @@ -24,18 +23,21 @@ static NSString *const ESEventRetryKey = @"retry"; @interface EventSource () { - BOOL wasClosed; - dispatch_queue_t messageQueue; - dispatch_queue_t connectionQueue; + BOOL wasClosed; + dispatch_queue_t messageQueue; + dispatch_queue_t connectionQueue; } @property (nonatomic, strong) NSURL *eventURL; +@property (nonatomic, strong) NSString *authorization; @property (nonatomic, strong) NSURLSessionDataTask *eventSourceTask; @property (nonatomic, strong) NSMutableDictionary *listeners; @property (nonatomic, assign) NSTimeInterval timeoutInterval; @property (nonatomic, assign) NSTimeInterval retryInterval; @property (nonatomic, strong) id lastEventID; +@property (nonatomic) NSMutableString *buffer; + - (void)_open; - (void)_dispatchEvent:(Event *)e; @@ -43,74 +45,59 @@ - (void)_dispatchEvent:(Event *)e; @implementation EventSource -+ (instancetype)eventSourceWithURL:(NSURL *)URL -{ - return [[EventSource alloc] initWithURL:URL]; -} - -+ (instancetype)eventSourceWithURL:(NSURL *)URL timeoutInterval:(NSTimeInterval)timeoutInterval -{ - return [[EventSource alloc] initWithURL:URL timeoutInterval:timeoutInterval]; -} - -- (instancetype)initWithURL:(NSURL *)URL -{ - return [self initWithURL:URL timeoutInterval:ES_DEFAULT_TIMEOUT]; -} - -- (instancetype)initWithURL:(NSURL *)URL timeoutInterval:(NSTimeInterval)timeoutInterval +- (instancetype)initWithURL:(NSURL *)URL authorization:(NSString *)authorization timeoutInterval:(NSTimeInterval)timeoutInterval { - self = [super init]; - if (self) { - _listeners = [NSMutableDictionary dictionary]; - _eventURL = URL; - _timeoutInterval = timeoutInterval; - _retryInterval = ES_RETRY_INTERVAL; - - messageQueue = dispatch_queue_create("co.cwbrn.eventsource-queue", DISPATCH_QUEUE_SERIAL); - connectionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); - - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_retryInterval * NSEC_PER_SEC)); - dispatch_after(popTime, connectionQueue, ^(void){ - [self _open]; - }); - } - return self; + self = [super init]; + if (self) { + _listeners = [NSMutableDictionary dictionary]; + _eventURL = URL; + _authorization = authorization; + _timeoutInterval = timeoutInterval; + _retryInterval = ES_RETRY_INTERVAL; + + messageQueue = dispatch_queue_create("co.cwbrn.eventsource-queue", DISPATCH_QUEUE_SERIAL); + connectionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); + + dispatch_async(connectionQueue, ^{ + [self _open]; + }); + } + return self; } - (void)addEventListener:(NSString *)eventName handler:(EventSourceEventHandler)handler { - if (self.listeners[eventName] == nil) { - [self.listeners setObject:[NSMutableArray array] forKey:eventName]; - } - - [self.listeners[eventName] addObject:handler]; + if (self.listeners[eventName] == nil) { + [self.listeners setObject:[NSMutableArray array] forKey:eventName]; + } + + [self.listeners[eventName] addObject:handler]; } - (void)onMessage:(EventSourceEventHandler)handler { - [self addEventListener:MessageEvent handler:handler]; + [self addEventListener:MessageEvent handler:handler]; } - (void)onError:(EventSourceEventHandler)handler { - [self addEventListener:ErrorEvent handler:handler]; + [self addEventListener:ErrorEvent handler:handler]; } - (void)onOpen:(EventSourceEventHandler)handler { - [self addEventListener:OpenEvent handler:handler]; + [self addEventListener:OpenEvent handler:handler]; } - (void)onReadyStateChanged:(EventSourceEventHandler)handler { - [self addEventListener:ReadyStateEvent handler:handler]; + [self addEventListener:ReadyStateEvent handler:handler]; } - (void)close { - wasClosed = YES; - [self.eventSourceTask cancel]; + wasClosed = YES; + [self.eventSourceTask cancel]; } // ----------------------------------------------------------------------------------------------------------------------------------------- @@ -118,144 +105,163 @@ - (void)close - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; - if (httpResponse.statusCode == 200) { - // Opened - Event *e = [Event new]; - e.readyState = kEventStateOpen; - - [self _dispatchEvent:e type:ReadyStateEvent]; - [self _dispatchEvent:e type:OpenEvent]; - } - - if (completionHandler) { - completionHandler(NSURLSessionResponseAllow); - } + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + // Opened + Event *e = [Event new]; + e.readyState = kEventStateOpen; + + [self _dispatchEvent:e type:ReadyStateEvent]; + [self _dispatchEvent:e type:OpenEvent]; + } + + if (completionHandler) { + completionHandler(NSURLSessionResponseAllow); + } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { - NSString *eventString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSArray *lines = [eventString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - - Event *event = [Event new]; - event.readyState = kEventStateOpen; - - for (NSString *line in lines) { - if ([line hasPrefix:ESKeyValueDelimiter]) { - continue; - } - - if (!line || line.length == 0) { - if (event.data != nil) { - dispatch_async(messageQueue, ^{ - [self _dispatchEvent:event]; - }); - - event = [Event new]; - event.readyState = kEventStateOpen; - } - continue; - } - - @autoreleasepool { - NSScanner *scanner = [NSScanner scannerWithString:line]; - scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet]; - - NSString *key, *value; - [scanner scanUpToString:ESKeyValueDelimiter intoString:&key]; - [scanner scanString:ESKeyValueDelimiter intoString:nil]; - [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet] intoString:&value]; - - if (key && value) { - if ([key isEqualToString:ESEventEventKey]) { - event.event = value; - } else if ([key isEqualToString:ESEventDataKey]) { - if (event.data != nil) { - event.data = [event.data stringByAppendingFormat:@"\n%@", value]; - } else { - event.data = value; - } - } else if ([key isEqualToString:ESEventIDKey]) { - event.id = value; - self.lastEventID = event.id; - } else if ([key isEqualToString:ESEventRetryKey]) { - self.retryInterval = [value doubleValue]; - } - } - } - } + NSString * const string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if(string) + { + [_buffer appendString:string]; + + while(1) + { + NSRange range; + if((range = [_buffer rangeOfString:ESEventSeparatorLFLF]).location != NSNotFound || + (range = [_buffer rangeOfString:ESEventSeparatorCRCR]).location != NSNotFound || + (range = [_buffer rangeOfString:ESEventSeparatorCRLFCRLF]).location != NSNotFound) + { + [self didReceiveString:[_buffer substringToIndex:range.location]]; + + [_buffer deleteCharactersInRange:NSMakeRange(0, NSMaxRange(range))]; + } + else + break; + } + } } -- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error +- (void)didReceiveString:(NSString *)string { - self.eventSourceTask = nil; - - if (wasClosed) { - return; - } - - Event *e = [Event new]; - e.readyState = kEventStateClosed; - e.error = error ?: [NSError errorWithDomain:@"" - code:e.readyState - userInfo:@{ NSLocalizedDescriptionKey: @"Connection with the event source was closed." }]; - - [self _dispatchEvent:e type:ReadyStateEvent]; - [self _dispatchEvent:e type:ErrorEvent]; + Event * const event = [Event new]; + event.readyState = kEventStateOpen; + + for(NSString * const line in [string componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet]) + { + @autoreleasepool + { + NSScanner * const scanner = [NSScanner scannerWithString:line]; + scanner.charactersToBeSkipped = NSCharacterSet.whitespaceCharacterSet; + + NSString *key; + if([scanner scanUpToString:ESKeyValueDelimiter intoString:&key]) + { + [scanner scanString:ESKeyValueDelimiter intoString:nil]; + + NSString *value; + if([scanner scanUpToCharactersFromSet:NSCharacterSet.newlineCharacterSet intoString:&value]) + { + if([key isEqualToString:ESEventEventKey]) + event.event = value; + else if([key isEqualToString:ESEventDataKey]) + event.data = (event.data ? [event.data stringByAppendingFormat:@"\n%@", value] : value); + else if([key isEqualToString:ESEventIDKey]) + _lastEventID = event.id = value; + else if([key isEqualToString:ESEventRetryKey]) + _retryInterval = value.doubleValue; + } + } + } + } + + if(event.data) + dispatch_async(messageQueue, ^{ [self _dispatchEvent:event]; }); +} - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_retryInterval * NSEC_PER_SEC)); - dispatch_after(popTime, connectionQueue, ^(void){ - [self _open]; - }); +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error +{ + self.eventSourceTask = nil; + + if (wasClosed) { + return; + } + + Event *e = [Event new]; + e.readyState = kEventStateClosed; + e.error = error ?: [NSError errorWithDomain:@"" + code:e.readyState + userInfo:@{ NSLocalizedDescriptionKey: @"Connection with the event source was closed." }]; + + [self _dispatchEvent:e type:ReadyStateEvent]; + [self _dispatchEvent:e type:ErrorEvent]; + + __weak __typeof(self) const weakSelf = self; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_retryInterval * NSEC_PER_SEC)); + dispatch_after(popTime, connectionQueue, ^(void){ + __typeof(self) const strongSelf = weakSelf; + if(strongSelf && !strongSelf->wasClosed) + [strongSelf _open]; + }); } // ------------------------------------------------------------------------------------------------------------------------------------- - (void)_open { - wasClosed = NO; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.eventURL - cachePolicy:NSURLRequestReloadIgnoringCacheData - timeoutInterval:self.timeoutInterval]; - if (self.lastEventID) { - [request setValue:self.lastEventID forHTTPHeaderField:@"Last-Event-ID"]; - } - - NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:self - delegateQueue:[NSOperationQueue currentQueue]]; - - self.eventSourceTask = [session dataTaskWithRequest:request]; - [self.eventSourceTask resume]; - - Event *e = [Event new]; - e.readyState = kEventStateConnecting; - - [self _dispatchEvent:e type:ReadyStateEvent]; - - if (![NSThread isMainThread]) { - CFRunLoopRun(); - } + wasClosed = NO; + + _buffer = [NSMutableString new]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.eventURL + cachePolicy:NSURLRequestReloadIgnoringCacheData + timeoutInterval:self.timeoutInterval]; + if (self.lastEventID) { + [request setValue:self.lastEventID forHTTPHeaderField:@"Last-Event-ID"]; + } + + NSURLSessionConfiguration * const sessionContiguration = NSURLSessionConfiguration.defaultSessionConfiguration; + if(_authorization) + sessionContiguration.HTTPAdditionalHeaders = @{ @"Authorization": _authorization }; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionContiguration + delegate:self + delegateQueue:[NSOperationQueue currentQueue]]; + + self.eventSourceTask = [session dataTaskWithRequest:request]; + [self.eventSourceTask resume]; + + [session finishTasksAndInvalidate]; + + Event *e = [Event new]; + e.readyState = kEventStateConnecting; + + [self _dispatchEvent:e type:ReadyStateEvent]; + + if (![NSThread isMainThread]) { + CFRunLoopRun(); + } } - (void)_dispatchEvent:(Event *)event type:(NSString * const)type { - NSArray *errorHandlers = self.listeners[type]; - for (EventSourceEventHandler handler in errorHandlers) { - dispatch_async(connectionQueue, ^{ - handler(event); - }); - } + NSArray *errorHandlers = self.listeners[type]; + for (EventSourceEventHandler handler in errorHandlers) { + dispatch_async(connectionQueue, ^{ + handler(event); + }); + } } - (void)_dispatchEvent:(Event *)event { - [self _dispatchEvent:event type:MessageEvent]; - - if (event.event != nil) { - [self _dispatchEvent:event type:event.event]; - } + [self _dispatchEvent:event type:MessageEvent]; + + if (event.event != nil) { + [self _dispatchEvent:event type:event.event]; + } } @end @@ -266,25 +272,25 @@ @implementation Event - (NSString *)description { - NSString *state = nil; - switch (self.readyState) { - case kEventStateConnecting: - state = @"CONNECTING"; - break; - case kEventStateOpen: - state = @"OPEN"; - break; - case kEventStateClosed: - state = @"CLOSED"; - break; - } - - return [NSString stringWithFormat:@"<%@: readyState: %@, id: %@; event: %@; data: %@>", - [self class], - state, - self.id, - self.event, - self.data]; + NSString *state = nil; + switch (self.readyState) { + case kEventStateConnecting: + state = @"CONNECTING"; + break; + case kEventStateOpen: + state = @"OPEN"; + break; + case kEventStateClosed: + state = @"CLOSED"; + break; + } + + return [NSString stringWithFormat:@"<%@: readyState: %@, id: %@; event: %@; data: %@>", + [self class], + state, + self.id, + self.event, + self.data]; } @end