aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Realtime
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase/Database/Realtime
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Firebase/Database/Realtime')
-rw-r--r--Firebase/Database/Realtime/FConnection.h52
-rw-r--r--Firebase/Database/Realtime/FConnection.m211
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.h46
-rw-r--r--Firebase/Database/Realtime/FWebSocketConnection.m305
4 files changed, 614 insertions, 0 deletions
diff --git a/Firebase/Database/Realtime/FConnection.h b/Firebase/Database/Realtime/FConnection.h
new file mode 100644
index 0000000..ed4879a
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FWebSocketConnection.h"
+#import "FTypedefs.h"
+
+@protocol FConnectionDelegate;
+
+@interface FConnection : NSObject <FWebSocketDelegate>
+
+@property (nonatomic, weak) id <FConnectionDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void)open;
+- (void)close;
+- (void)sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive;
+
+// FWebSocketDelegate delegate methods
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
+
+typedef enum {
+ DISCONNECT_REASON_SERVER_RESET = 0,
+ DISCONNECT_REASON_OTHER = 1
+} FDisconnectReason;
+
+@protocol FConnectionDelegate <NSObject>
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID;
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason;
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason;
+
+@end
+
diff --git a/Firebase/Database/Realtime/FConnection.m b/Firebase/Database/Realtime/FConnection.m
new file mode 100644
index 0000000..1550bfc
--- /dev/null
+++ b/Firebase/Database/Realtime/FConnection.m
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FConnection.h"
+#import "FConstants.h"
+
+typedef enum {
+ REALTIME_STATE_CONNECTING = 0,
+ REALTIME_STATE_CONNECTED = 1,
+ REALTIME_STATE_DISCONNECTED = 2,
+} FConnectionState;
+
+@interface FConnection () {
+ FConnectionState state;
+}
+
+@property (nonatomic, strong) FWebSocketConnection* conn;
+@property (nonatomic, strong) FRepoInfo* repoInfo;
+
+@end
+
+#pragma mark -
+#pragma mark FConnection implementation
+
+@implementation FConnection
+
+@synthesize delegate;
+@synthesize conn;
+@synthesize repoInfo;
+
+#pragma mark -
+#pragma mark Initializers
+
+- (id)initWith:(FRepoInfo *)aRepoInfo andDispatchQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID{
+ self = [super init];
+ if (self) {
+ state = REALTIME_STATE_CONNECTING;
+ self.repoInfo = aRepoInfo;
+ self.conn = [[FWebSocketConnection alloc] initWith:self.repoInfo andQueue:queue lastSessionID:lastSessionID];
+ self.conn.delegate = self;
+ }
+ return self;
+}
+
+#pragma mark -
+#pragma mark Public method implementation
+
+- (void)open {
+ FFLog(@"I-RDB082001", @"Calling open in FConnection");
+ [self.conn open];
+}
+
+- (void) closeWithReason:(FDisconnectReason)reason {
+ if (state != REALTIME_STATE_DISCONNECTED) {
+ FFLog(@"I-RDB082002", @"Closing realtime connection.");
+ state = REALTIME_STATE_DISCONNECTED;
+
+ if (self.conn) {
+ FFLog(@"I-RDB082003", @"Calling close again.");
+ [self.conn close];
+ self.conn = nil;
+ }
+
+ [self.delegate onDisconnect:self withReason:reason];
+ }
+}
+
+- (void) close {
+ [self closeWithReason:DISCONNECT_REASON_OTHER];
+}
+
+- (void) sendRequest:(NSDictionary *)dataMsg sensitive:(BOOL)sensitive {
+ // since this came from the persistent connection, wrap it in a data message envelope
+ NSDictionary* msg = @{
+ kFWPRequestType: kFWPRequestTypeData,
+ kFWPRequestDataPayload: dataMsg
+ };
+ [self sendData:msg sensitive:sensitive];
+}
+
+#pragma mark -
+#pragma mark Helpers
+
+
+- (void) sendData:(NSDictionary *)data sensitive:(BOOL)sensitive {
+ if (state != REALTIME_STATE_CONNECTED) {
+ @throw [[NSException alloc] initWithName:@"InvalidConnectionState" reason:@"Tried to send data on an unconnected FConnection" userInfo:nil];
+ } else {
+ if (sensitive) {
+ FFLog(@"I-RDB082004", @"Sending data (contents hidden)");
+ } else {
+ FFLog(@"I-RDB082005", @"Sending: %@", data);
+ }
+ [self.conn send:data];
+ }
+}
+
+#pragma mark -
+#pragma mark FWebSocketConnectinDelegate implementation
+
+// Corresponds to onConnectionLost in JS
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected {
+
+ self.conn = nil;
+ if (!everConnected && state == REALTIME_STATE_CONNECTING) {
+ FFLog(@"I-RDB082006", @"Realtime connection failed.");
+
+ // Since we failed to connect at all, clear any cached entry for this namespace in case the machine went away
+ [self.repoInfo clearInternalHostCache];
+ } else if (state == REALTIME_STATE_CONNECTED) {
+ FFLog(@"I-RDB082007", @"Realtime connection lost.");
+ }
+
+ [self close];
+}
+
+// Corresponds to onMessageReceived in JS
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message {
+ NSString* rawMessageType = [message objectForKey:kFWPAsyncServerEnvelopeType];
+ if(rawMessageType != nil) {
+ if([rawMessageType isEqualToString:kFWPAsyncServerDataMessage]) {
+ [self onDataMessage:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else if ([rawMessageType isEqualToString:kFWPAsyncServerControlMessage]) {
+ [self onControl:[message objectForKey:kFWPAsyncServerEnvelopeData]];
+ }
+ else {
+ FFLog(@"I-RDB082008", @"Unrecognized server packet type: %@", rawMessageType);
+ }
+ }
+ else {
+ FFLog(@"I-RDB082009", @"Unrecognized raw server packet received: %@", message);
+ }
+}
+
+- (void) onDataMessage:(NSDictionary *)message {
+ // we don't do anything with data messages, just kick them up a level
+ FFLog(@"I-RDB082010", @"Got data message: %@", message);
+ [self.delegate onDataMessage:self withMessage:message];
+}
+
+- (void) onControl:(NSDictionary *)message {
+ FFLog(@"I-RDB082011", @"Got control message: %@", message);
+ NSString* type = [message objectForKey:kFWPAsyncServerControlMessageType];
+ if([type isEqualToString:kFWPAsyncServerControlMessageShutdown]) {
+ NSString* reason = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onConnectionShutdownWithReason:reason];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerControlMessageReset]) {
+ NSString* host = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onReset:host];
+ }
+ else if ([type isEqualToString:kFWPAsyncServerHello]) {
+ NSDictionary* handshakeData = [message objectForKey:kFWPAsyncServerControlMessageData];
+ [self onHandshake:handshakeData];
+ }
+ else {
+ FFLog(@"I-RDB082012", @"Unknown control message returned from server: %@", message);
+ }
+}
+
+- (void) onConnectionShutdownWithReason:(NSString *)reason {
+ FFLog(@"I-RDB082013", @"Connection shutdown command received. Shutting down...");
+
+ [self.delegate onKill:self withReason:reason];
+ [self close];
+}
+
+- (void) onHandshake:(NSDictionary *)handshake {
+ NSNumber* timestamp = [handshake objectForKey:kFWPAsyncServerHelloTimestamp];
+// NSString* version = [handshake objectForKey:kFWPAsyncServerHelloVersion];
+ NSString* host = [handshake objectForKey:kFWPAsyncServerHelloConnectedHost];
+ NSString* sessionID = [handshake objectForKey:kFWPAsyncServerHelloSession];
+
+ self.repoInfo.internalHost = host;
+
+ if (state == REALTIME_STATE_CONNECTING) {
+ [self.conn start];
+ [self onConnection:self.conn readyAtTime:timestamp sessionID:sessionID];
+ }
+}
+
+- (void) onConnection:(FWebSocketConnection *)conn readyAtTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID {
+ FFLog(@"I-RDB082014", @"Realtime connection established");
+ state = REALTIME_STATE_CONNECTED;
+
+ [self.delegate onReady:self atTime:timestamp sessionID:sessionID];
+}
+
+- (void) onReset:(NSString *)host {
+ FFLog(@"I-RDB082015", @"Got a reset; killing connection to: %@; Updating internalHost to: %@", repoInfo.internalHost, host);
+ self.repoInfo.internalHost = host;
+
+ // Explicitly close the connection with SERVER_RESET so calling code knows to reconnect immediately.
+ [self closeWithReason:DISCONNECT_REASON_SERVER_RESET];
+}
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.h b/Firebase/Database/Realtime/FWebSocketConnection.h
new file mode 100644
index 0000000..6a14d47
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FSRWebSocket.h"
+#import "FUtilities.h"
+
+@protocol FWebSocketDelegate;
+
+@interface FWebSocketConnection : NSObject <FSRWebSocketDelegate>
+
+@property (nonatomic, weak) id <FWebSocketDelegate> delegate;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID;
+
+- (void) open;
+- (void) close;
+- (void) start;
+- (void) send:(NSDictionary *)dictionary;
+
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message;
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket;
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error;
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
+
+@end
+
+@protocol FWebSocketDelegate <NSObject>
+
+- (void)onMessage:(FWebSocketConnection *)fwebSocket withMessage:(NSDictionary *)message;
+- (void)onDisconnect:(FWebSocketConnection *)fwebSocket wasEverConnected:(BOOL)everConnected;
+
+@end
diff --git a/Firebase/Database/Realtime/FWebSocketConnection.m b/Firebase/Database/Realtime/FWebSocketConnection.m
new file mode 100644
index 0000000..52e2296
--- /dev/null
+++ b/Firebase/Database/Realtime/FWebSocketConnection.m
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+#import "FIRDatabaseReference.h"
+#import "FStringUtilities.h"
+#import "FIRDatabase_Private.h"
+
+#if TARGET_OS_IPHONE
+#import <UIKit/UIKit.h>
+#endif
+
+@interface FWebSocketConnection () {
+ NSMutableString* frame;
+ BOOL everConnected;
+ BOOL isClosed;
+ NSTimer* keepAlive;
+}
+
+- (void) shutdown;
+- (void) onClosed;
+- (void) closeIfNeverConnected;
+
+@property (nonatomic, strong) FSRWebSocket* webSocket;
+@property (nonatomic, strong) NSNumber* connectionId;
+@property (nonatomic, readwrite) int totalFrames;
+@property (nonatomic, readonly) BOOL buffering;
+@property (nonatomic, readonly) NSString* userAgent;
+@property (nonatomic) dispatch_queue_t dispatchQueue;
+
+- (void)nop:(NSTimer *)timer;
+
+@end
+
+@implementation FWebSocketConnection
+
+@synthesize delegate;
+@synthesize webSocket;
+@synthesize connectionId;
+
+- (id)initWith:(FRepoInfo *)repoInfo andQueue:(dispatch_queue_t)queue lastSessionID:(NSString *)lastSessionID {
+ self = [super init];
+ if (self) {
+ everConnected = NO;
+ isClosed = NO;
+ self.connectionId = [FUtilities LUIDGenerator];
+ self.totalFrames = 0;
+ self.dispatchQueue = queue;
+ frame = nil;
+
+ NSString* connectionUrl = [repoInfo connectionURLWithLastSessionID:lastSessionID];
+ NSString* ua = [self userAgent];
+ FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@", self.connectionId, connectionUrl, ua);
+
+ NSURLRequest* req = [[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
+ self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req queue:queue andUserAgent:ua];
+ [self.webSocket setDelegateDispatchQueue:queue];
+ self.webSocket.delegate = self;
+ }
+ return self;
+}
+
+- (NSString *) userAgent {
+ NSString* systemVersion;
+ NSString* deviceName;
+ BOOL hasUiDeviceClass = NO;
+
+ // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build.
+ #if TARGET_OS_IPHONE
+ Class uiDeviceClass = NSClassFromString(@"UIDevice");
+ if (uiDeviceClass) {
+ systemVersion = [uiDeviceClass currentDevice].systemVersion;
+ deviceName = [uiDeviceClass currentDevice].model;
+ hasUiDeviceClass = YES;
+ }
+ #endif
+
+ if (!hasUiDeviceClass) {
+ NSDictionary *systemVersionDictionary = [NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"];
+ systemVersion = [systemVersionDictionary objectForKey:@"ProductVersion"];
+ deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
+ }
+
+ NSString* bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+
+ // Sanitize '/'s in deviceName and bundleIdentifier for stats
+ deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
+ bundleIdentifier = [FStringUtilities sanitizedForUserAgent:bundleIdentifier];
+
+ // Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model / os (Mac OS X, iPhone, etc.}_<bundle id>
+ NSString* ua = [NSString stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion, [FIRDatabase buildVersion], systemVersion, deviceName, bundleIdentifier];
+ return ua;
+}
+
+- (BOOL) buffering {
+ return frame != nil;
+}
+
+#pragma mark -
+#pragma mark Public FWebSocketConnection methods
+
+- (void) open {
+ FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.", self.connectionId);
+ assert(delegate);
+ everConnected = NO;
+ // TODO Assert url
+ [self.webSocket open];
+ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
+ dispatch_after(when, self.dispatchQueue, ^{
+ [self closeIfNeverConnected];
+ });
+}
+
+- (void) close {
+ FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.", self.connectionId);
+ isClosed = YES;
+ [self.webSocket close];
+}
+
+- (void) start {
+ // Start is a no-op for websockets.
+}
+
+- (void) send:(NSDictionary *)dictionary {
+
+ [self resetKeepAlive];
+
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
+ options:kNilOptions error:nil];
+
+ NSString* data = [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+
+ NSArray* dataSegs = [FUtilities splitString:data intoMaxSize:kWebsocketMaxFrameSize];
+
+ // First send the header so the server knows how many segments are forthcoming
+ if (dataSegs.count > 1) {
+ [self.webSocket send:[NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
+ }
+
+ // Then, actually send the segments.
+ for(NSString * segment in dataSegs) {
+ [self.webSocket send:segment];
+ }
+}
+
+- (void) nop:(NSTimer *)timer {
+ if(self.webSocket) {
+ FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
+ [self.webSocket send:@"0"];
+ }
+ else {
+ FFLog(@"I-RDB083005", @"(wsc:%@) No more websocket; invalidating nop timer.", self.connectionId);
+ [timer invalidate];
+ }
+}
+
+- (void) handleNewFrameCount:(int) numFrames {
+ self.totalFrames = numFrames;
+ frame = [[NSMutableString alloc] initWithString:@""];
+ FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d", self.connectionId, self.totalFrames);
+}
+
+- (NSString *) extractFrameCount:(NSString *) message {
+ if ([message length] <= 4) {
+ int frameCount = [message intValue];
+ if (frameCount > 0) {
+ [self handleNewFrameCount:frameCount];
+ return nil;
+ }
+ }
+ [self handleNewFrameCount:1];
+ return message;
+}
+
+- (void) appendFrame:(NSString *) message {
+ [frame appendString:message];
+ self.totalFrames = self.totalFrames - 1;
+
+ if (self.totalFrames == 0) {
+ // Call delegate and pass an immutable version of the frame
+ NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
+ options:kNilOptions
+ error:nil];
+ frame = nil;
+ FFLog(@"I-RDB083007", @"(wsc:%@) handleIncomingFrame sending complete frame: %d", self.connectionId, self.totalFrames);
+
+ @autoreleasepool {
+ [self.delegate onMessage:self withMessage:json];
+ }
+ }
+}
+
+- (void) handleIncomingFrame:(NSString *) message {
+ [self resetKeepAlive];
+ if (self.buffering) {
+ [self appendFrame:message];
+ } else {
+ NSString *remaining = [self extractFrameCount:message];
+ if (remaining) {
+ [self appendFrame:remaining];
+ }
+ }
+}
+
+#pragma mark -
+#pragma mark SRWebSocketDelegate implementation
+- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message
+{
+ [self handleIncomingFrame:message];
+}
+
+- (void)webSocketDidOpen:(FSRWebSocket *)webSocket
+{
+ FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
+
+ everConnected = YES;
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ self->keepAlive = [NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
+ target:self
+ selector:@selector(nop:)
+ userInfo:nil
+ repeats:YES];
+ FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off", self.connectionId);
+ });
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error
+{
+ FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@", self.connectionId, [error description]);
+ [self onClosed];
+}
+
+- (void)webSocket:(FSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
+{
+ FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@", self.connectionId, (long)code, reason);
+ [self onClosed];
+}
+
+#pragma mark -
+#pragma mark Private methods
+
+/**
+ * Note that the close / onClosed / shutdown cycle here is a little different from the javascript client.
+ * In order to properly handle deallocation, no close-related action is taken at a higher level until we
+ * have received notification from the websocket itself that it is closed. Otherwise, we end up deallocating
+ * this class and the FConnection class before the websocket has a change to call some of its delegate methods.
+ * So, since close is the external close handler, we just set a flag saying not to call our own delegate method
+ * and close the websocket. That will trigger a callback into this class that can then do things like clean up
+ * the keepalive timer.
+ */
+
+- (void) closeIfNeverConnected {
+ if (!everConnected) {
+ FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect", self.connectionId);
+ [self.webSocket close];
+ }
+}
+
+- (void) shutdown {
+ isClosed = YES;
+
+ // Call delegate methods
+ [self.delegate onDisconnect:self wasEverConnected:everConnected];
+
+}
+
+- (void) onClosed {
+ if (!isClosed) {
+ FFLog(@"I-RDB083013", @"Websocket is closing itself");
+ [self shutdown];
+ }
+ self.webSocket = nil;
+ if (keepAlive.isValid) {
+ [keepAlive invalidate];
+ }
+}
+
+- (void) resetKeepAlive {
+ NSDate* newTime = [NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
+ // Calling setFireDate is actually kinda' expensive, so wait at least 5 seconds before updating it.
+ if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
+ FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@", self.connectionId, newTime, [keepAlive fireDate]);
+ [keepAlive setFireDate:newTime];
+ }
+}
+
+@end