Last active
July 18, 2016 08:59
-
-
Save elsurudo/6039065 to your computer and use it in GitHub Desktop.
Class to handle Server Sent Events in a similar way to browser EventSource
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import <Foundation/Foundation.h> | |
@class EventSource; | |
@protocol EventSourceDelegate <NSObject> | |
@optional | |
- (void)eventSourceDidOpenConnection:(EventSource*)eventSource; | |
- (void)eventSource:(EventSource*)eventSource didFailWithError:(NSError*)error; | |
@required | |
- (void)eventSource:(EventSource *)eventSource didReceiveMessage:(NSString*)message | |
eventID:(NSString*)eventID | |
type:(NSString*)type; | |
@end | |
@interface EventSource : NSObject | |
- (id)initWithURL:(NSURL*)url delegate:(NSObject<EventSourceDelegate>*)delegate; | |
- (void)cancel; | |
@property (weak, nonatomic) NSObject <EventSourceDelegate> *delegate; | |
@property (nonatomic, readonly) NSURL *url; | |
@property (nonatomic, readonly) NSString *lastEventID; | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "EventSource.h" | |
#define kErrorCodeBadResponseStatusCode 1 | |
#define kErrorCodeBadResponseContentType 2 | |
@interface EventSource () <NSURLConnectionDelegate, NSURLConnectionDataDelegate> | |
@property (nonatomic, strong) NSURL *url; | |
@property (nonatomic, strong) NSURLConnection *connection; | |
@property (nonatomic, strong) NSMutableString *currentMessage; | |
@end | |
@implementation EventSource | |
- (id)initWithURL:(NSURL*)url delegate:(NSObject<EventSourceDelegate>*)delegate | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_url = url; | |
_delegate = delegate; | |
// start connection | |
_currentMessage = [NSMutableString string]; | |
NSURLRequest *request = [NSURLRequest requestWithURL:url]; | |
_connection = [NSURLConnection connectionWithRequest:request delegate:self]; | |
} | |
return self; | |
} | |
- (void)cancel | |
{ | |
[_connection cancel]; | |
} | |
#pragma mark - NSURLConnectionDelegate methods | |
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error | |
{ | |
if ([_delegate respondsToSelector:@selector(eventSource:didFailWithError:)]) { | |
[_delegate eventSource:self didFailWithError:error]; | |
} | |
} | |
#pragma mark - NSURLConnectionDataDelegate methods | |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response | |
{ | |
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; | |
// ensure we've got a successful response | |
if ([[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)] containsIndex:[httpResponse statusCode]]) | |
{ | |
// ensure HTTP content-type is text/event-stream | |
if ([httpResponse.allHeaderFields[@"content-type"] isEqualToString:@"text/event-stream"]) | |
{ | |
if ([_delegate respondsToSelector:@selector(eventSourceDidOpenConnection:)]) { | |
[_delegate eventSourceDidOpenConnection:self]; | |
} | |
} | |
else | |
{ | |
if ([_delegate respondsToSelector:@selector(eventSource:didFailWithError:)]) | |
{ | |
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain | |
code:kErrorCodeBadResponseContentType | |
userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"Server must respond with a content-type of 'text/event-stream'.", nil) }]; | |
[_delegate eventSource:self didFailWithError:error]; | |
} | |
[_connection cancel]; | |
} | |
} | |
else | |
{ | |
if ([_delegate respondsToSelector:@selector(eventSource:didFailWithError:)]) | |
{ | |
NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain | |
code:kErrorCodeBadResponseStatusCode | |
userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"Server must respond with a status code in the 200-299 range.", nil) }]; | |
[_delegate eventSource:self didFailWithError:error]; | |
} | |
[_connection cancel]; | |
} | |
} | |
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data | |
{ | |
NSString *receivedString = [[NSString alloc] initWithData:data | |
encoding:NSUTF8StringEncoding]; | |
[_currentMessage appendString:receivedString]; | |
NSArray *messages = [_currentMessage componentsSeparatedByString:@"\n\n"]; | |
for (int i = 0; i < messages.count - 1; i++) | |
{ | |
[self processMessage:messages[i]]; | |
} | |
_currentMessage = [NSMutableString stringWithString:[messages lastObject]]; | |
} | |
#pragma mark - Privates | |
- (void)processMessage:(NSString*)message | |
{ | |
NSMutableString *eventMessage = [NSMutableString string]; | |
NSString *eventType = nil; | |
NSString *eventID = nil; | |
NSArray *lines = [message componentsSeparatedByString:@"\n"]; | |
for (NSString *line in lines) | |
{ | |
if ([line hasPrefix:@":"]) | |
{ | |
// comment; do nothing | |
} | |
else if ([line hasPrefix:@"id:"]) | |
{ | |
// id | |
NSString *value = [line substringFromIndex:3]; | |
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
eventID = value; | |
_lastEventID = value; | |
} | |
else if ([line hasPrefix:@"event:"]) | |
{ | |
// event | |
NSString *value = [line substringFromIndex:6]; | |
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
eventType = value; | |
} | |
else if ([line hasPrefix:@"data:"]) | |
{ | |
// data | |
NSString *value = [line substringFromIndex:5]; | |
value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
[eventMessage appendFormat:@"%@\n", value]; | |
} | |
} | |
[_delegate eventSource:self didReceiveMessage:eventMessage eventID:eventType type:eventType]; | |
} | |
@end |
You also might consider ensuring the HTTP content-type is text/event-stream
before parsing.
Thanks for the tips! Will include those in the next revision. I just posted an update that gets rid of the unnecessary EventSourceMessage class (seemed un-Cocoa-like to do that, instead of just passing those fields to the delegate).
Also, I refactored to ensure that we can handle the case where the server might buffer several full messages, plus, say, only the first half of the last message. Should be more robust now; let me know what you think.
change [httpResponse.allHeaderFields[@"content-type"] isEqualToString:@"text/event-stream"] to [httpResponse.allHeaderFields[@"content-type"] hasPrefix:@"text/event-stream"] may be better.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You might want to replace https://gist.github.com/elsurudo/6039065#file-eventsource-m-L80 with
to account for all possible HTTP success codes, instead of just 200.