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