Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@elsurudo
Last active July 18, 2016 08:59
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save elsurudo/6039065 to your computer and use it in GitHub Desktop.
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
#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
#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
@getaaron
Copy link

You might want to replace https://gist.github.com/elsurudo/6039065#file-eventsource-m-L80 with

if ([[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)] containsIndex:[httpResponse statusCode]]) {

to account for all possible HTTP success codes, instead of just 200.

@getaaron
Copy link

You also might consider ensuring the HTTP content-type is text/event-stream before parsing.

@elsurudo
Copy link
Author

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.

@seandong
Copy link

seandong commented Feb 9, 2014

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