Skip to content

Instantly share code, notes, and snippets.

@elsurudo elsurudo/EventSource.h
Last active Jul 18, 2016

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

commented Jul 19, 2013

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

This comment has been minimized.

Copy link

commented Jul 19, 2013

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

@elsurudo

This comment has been minimized.

Copy link
Owner Author

commented Jul 19, 2013

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

This comment has been minimized.

Copy link

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
You can’t perform that action at this time.