Merge branch 'manager' into development

* manager:
  Add SocketManager for multiplexing namespaces
This commit is contained in:
Erik Little 2017-10-21 17:28:52 -04:00
commit 5a85c97f3d
No known key found for this signature in database
GPG Key ID: 4930B7C5FBC1A69D
28 changed files with 1488 additions and 778 deletions

18
CHANGELOG.md Normal file
View File

@ -0,0 +1,18 @@
# v13.0.0
What's new:
---
-Adds a new `SocketManager` class that multiplexes multiple namespaces through a single engine.
- Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs.
- watchOS support.
Important API changes
---
- Many properties that were previously on `SocketIOClient` have been moved to the `SocketManager`.
- `SocketIOClientOption.nsp` has been removed. Use `SocketManager.socket(forNamespace:)` to create/get a socket attached to a specific namespace.
- Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs.
- Makes the framework a single target.
- Updates Starscream to 3.0

View File

@ -7,21 +7,22 @@ Socket.IO-client for iOS/OS X.
```swift ```swift
import SocketIO import SocketIO
let socket = SocketIOClient(socketURL: URL(string: "http://localhost:8080")!, config: [.log(true), .compress]) let manager = SocketManager(socketURL: URL(string: "http://localhost:8080")!, config: [.log(true), .compress])
let socket = manager.defaultSocket
socket.on(clientEvent: .connect) {data, ack in socket.on(clientEvent: .connect) {data, ack in
print("socket connected") print("socket connected")
} }
socket.on("currentAmount") {data, ack in socket.on("currentAmount") {data, ack in
if let cur = data[0] as? Double { guard let cur = data[0] as? Double else { return }
socket.emitWithAck("canUpdate", cur).timingOut(after: 0) {data in socket.emitWithAck("canUpdate", cur).timingOut(after: 0) {data in
socket.emit("update", ["amount": cur + 2.50]) socket.emit("update", ["amount": cur + 2.50])
} }
ack.with("Got your currentAmount", "dude") ack.with("Got your currentAmount", "dude")
} }
}
socket.connect() socket.connect()
``` ```
@ -29,8 +30,10 @@ socket.connect()
## Objective-C Example ## Objective-C Example
```objective-c ```objective-c
@import SocketIO; @import SocketIO;
NSURL* url = [[NSURL alloc] initWithString:@"http://localhost:8080"]; NSURL* url = [[NSURL alloc] initWithString:@"http://localhost:8080"];
SocketIOClient* socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @YES, @"compress": @YES}]; SocketManager* manager = [[SocketManager alloc] initWithSocketURL:url config:@{@"log": @YES, @"compress": @YES}];
SocketIOClient* socket = manager.defaultSocket;
[socket on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) { [socket on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) {
NSLog(@"socket connected"); NSLog(@"socket connected");
@ -134,6 +137,7 @@ Objective-C:
# [Docs](https://nuclearace.github.io/Socket.IO-Client-Swift/index.html) # [Docs](https://nuclearace.github.io/Socket.IO-Client-Swift/index.html)
- [Client](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketIOClient.html) - [Client](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketIOClient.html)
- [Manager](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketManager.html)
- [Engine](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketEngine.html) - [Engine](https://nuclearace.github.io/Socket.IO-Client-Swift/Classes/SocketEngine.html)
- [Options](https://nuclearace.github.io/Socket.IO-Client-Swift/Enums/SocketIOClientOption.html) - [Options](https://nuclearace.github.io/Socket.IO-Client-Swift/Enums/SocketIOClientOption.html)

View File

@ -7,6 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1C6572803D7E252A77A86E5F /* SocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C65763817782DFAC67BE05C /* SocketManager.swift */; };
1C657FBB3F670261780FD72E /* SocketManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */; };
1C686BE21F869AFD007D8627 /* SocketIOClientConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */; }; 1C686BE21F869AFD007D8627 /* SocketIOClientConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */; };
1C686BE31F869AFD007D8627 /* SocketEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */; }; 1C686BE31F869AFD007D8627 /* SocketEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */; };
1C686BE41F869AFD007D8627 /* SocketSideEffectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */; }; 1C686BE41F869AFD007D8627 /* SocketSideEffectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */; };
@ -30,17 +32,18 @@
DD52B3A6C1E082841C35C85D /* SocketEngineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */; }; DD52B3A6C1E082841C35C85D /* SocketEngineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */; };
DD52B44AE56F2E07F3F3F991 /* SocketAckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */; }; DD52B44AE56F2E07F3F3F991 /* SocketAckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */; };
DD52B4DFA12F2599410205D9 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE9AD8B2BD7F841CD1D4 /* SocketEngineWebsocket.swift */; }; DD52B4DFA12F2599410205D9 /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BE9AD8B2BD7F841CD1D4 /* SocketEngineWebsocket.swift */; };
DD52B53F2609D91A683DFCDD /* ManagerObjectiveCTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */; };
DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B471D780013E18DF9335 /* SocketExtensions.swift */; }; DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B471D780013E18DF9335 /* SocketExtensions.swift */; };
DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B59C11D3D2BC63612E50 /* SocketPacket.swift */; }; DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B59C11D3D2BC63612E50 /* SocketPacket.swift */; };
DD52B660D63B6A25C3755AA7 /* SocketClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B282975446C9A9C56D7B /* SocketClientManager.swift */; };
DD52B883F942CD5A9D29892B /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */; }; DD52B883F942CD5A9D29892B /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */; };
DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */; }; DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */; };
DD52BB69B6D260035B652CA4 /* SocketAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B5A9DE10C7A8AD35617F /* SocketAnyEvent.swift */; }; DD52BB69B6D260035B652CA4 /* SocketAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B5A9DE10C7A8AD35617F /* SocketAnyEvent.swift */; };
DD52BB82239886CF6ADD642C /* SocketEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */; }; DD52BB82239886CF6ADD642C /* SocketEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */; };
DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */; }; DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */; };
DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BED81BF312B0E90E92AC /* SocketLogger.swift */; }; DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BED81BF312B0E90E92AC /* SocketLogger.swift */; };
DD52BCCD25EFA76E0F9B313C /* SocketMangerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */; };
DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */; }; DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */; };
DD52BE4D1E6BB752CD9614A6 /* SocketIOClientStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */; }; DD52BE4D1E6BB752CD9614A6 /* SocketIOStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */; };
DD52BF924BEF05E1235CFD29 /* SocketIOClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */; }; DD52BF924BEF05E1235CFD29 /* SocketIOClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */; };
DD52BFBC9E7CC32D3515AC80 /* SocketEngineSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */; }; DD52BFBC9E7CC32D3515AC80 /* SocketEngineSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */; };
DD52BFEB4DBD3BF8D93DAEFF /* SocketEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */; }; DD52BFEB4DBD3BF8D93DAEFF /* SocketEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */; };
@ -57,6 +60,8 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManagerSpec.swift; sourceTree = "<group>"; };
1C65763817782DFAC67BE05C /* SocketManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManager.swift; sourceTree = "<group>"; };
1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOClientConfigurationTest.swift; sourceTree = "<group>"; }; 1C686BD21F869AF1007D8627 /* SocketIOClientConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOClientConfigurationTest.swift; sourceTree = "<group>"; };
1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketEngineTest.swift; sourceTree = "<group>"; }; 1C686BD31F869AF1007D8627 /* SocketEngineTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketEngineTest.swift; sourceTree = "<group>"; };
1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSideEffectTest.swift; sourceTree = "<group>"; }; 1C686BD41F869AF1007D8627 /* SocketSideEffectTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSideEffectTest.swift; sourceTree = "<group>"; };
@ -83,8 +88,8 @@
9432E00D1F77F889006AF628 /* Starscream.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Starscream.framework; path = Carthage/Build/tvOS/Starscream.framework; sourceTree = "<group>"; }; 9432E00D1F77F889006AF628 /* Starscream.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Starscream.framework; path = Carthage/Build/tvOS/Starscream.framework; sourceTree = "<group>"; };
DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientOption.swift; sourceTree = "<group>"; }; DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientOption.swift; sourceTree = "<group>"; };
DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAckManager.swift; sourceTree = "<group>"; }; DD52B09F7984E730513AB7E5 /* SocketAckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAckManager.swift; sourceTree = "<group>"; };
DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientStatus.swift; sourceTree = "<group>"; }; DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOStatus.swift; sourceTree = "<group>"; };
DD52B282975446C9A9C56D7B /* SocketClientManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketClientManager.swift; sourceTree = "<group>"; }; DD52B2C54A6ADF3371C13DCB /* SocketObjectiveCTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SocketObjectiveCTest.h; sourceTree = "<group>"; };
DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEnginePollable.swift; sourceTree = "<group>"; }; DD52B2D110F55723F82B108E /* SocketEnginePollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEnginePollable.swift; sourceTree = "<group>"; };
DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketParsable.swift; sourceTree = "<group>"; }; DD52B31D0E6815F5F10CEFB6 /* SocketParsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketParsable.swift; sourceTree = "<group>"; };
DD52B471D780013E18DF9335 /* SocketExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketExtensions.swift; sourceTree = "<group>"; }; DD52B471D780013E18DF9335 /* SocketExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketExtensions.swift; sourceTree = "<group>"; };
@ -95,8 +100,11 @@
DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineSpec.swift; sourceTree = "<group>"; }; DD52B645273A873667BC2D43 /* SocketEngineSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineSpec.swift; sourceTree = "<group>"; };
DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEventHandler.swift; sourceTree = "<group>"; }; DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEventHandler.swift; sourceTree = "<group>"; };
DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngine.swift; sourceTree = "<group>"; }; DD52B7A9779A2E08075E5AAC /* SocketEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngine.swift; sourceTree = "<group>"; };
DD52B8396C7DEE7BFD6A985A /* ManagerObjectiveCTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ManagerObjectiveCTest.h; sourceTree = "<group>"; };
DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClient.swift; sourceTree = "<group>"; }; DD52BA1F41F2E4B3DC20260E /* SocketIOClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClient.swift; sourceTree = "<group>"; };
DD52BA240D139F72633D4159 /* SocketStringReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketStringReader.swift; sourceTree = "<group>"; }; DD52BA240D139F72633D4159 /* SocketStringReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketStringReader.swift; sourceTree = "<group>"; };
DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ManagerObjectiveCTest.m; sourceTree = "<group>"; };
DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketMangerTest.swift; sourceTree = "<group>"; };
DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientSpec.swift; sourceTree = "<group>"; }; DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientSpec.swift; sourceTree = "<group>"; };
DD52BDC9E66AADA2CC5E8246 /* SocketTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketTypes.swift; sourceTree = "<group>"; }; DD52BDC9E66AADA2CC5E8246 /* SocketTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketTypes.swift; sourceTree = "<group>"; };
DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineClient.swift; sourceTree = "<group>"; }; DD52BE5FDCE1D684132E897C /* SocketEngineClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineClient.swift; sourceTree = "<group>"; };
@ -128,6 +136,16 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
1C657951DEA2E0293D0FD1B6 /* Manager */ = {
isa = PBXGroup;
children = (
1C65763817782DFAC67BE05C /* SocketManager.swift */,
1C6574AF9687A213814753E4 /* SocketManagerSpec.swift */,
);
name = Manager;
path = Source/SocketIO/Manager;
sourceTree = "<group>";
};
1C686BD11F869AF1007D8627 /* TestSocketIO */ = { 1C686BD11F869AF1007D8627 /* TestSocketIO */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -138,6 +156,7 @@
1C686BD61F869AF1007D8627 /* SocketAckManagerTest.swift */, 1C686BD61F869AF1007D8627 /* SocketAckManagerTest.swift */,
1C686BD71F869AF1007D8627 /* SocketParserTest.swift */, 1C686BD71F869AF1007D8627 /* SocketParserTest.swift */,
1C686BD81F869AF1007D8627 /* SocketNamespacePacketTest.swift */, 1C686BD81F869AF1007D8627 /* SocketNamespacePacketTest.swift */,
DD52BBAC5FAA7730D32CD5BF /* SocketMangerTest.swift */,
); );
name = TestSocketIO; name = TestSocketIO;
path = Tests/TestSocketIO; path = Tests/TestSocketIO;
@ -147,6 +166,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1C686BFE1F869E9D007D8627 /* SocketObjectiveCTest.m */, 1C686BFE1F869E9D007D8627 /* SocketObjectiveCTest.m */,
DD52BB5E907D283ACC31E17F /* ManagerObjectiveCTest.m */,
DD52B8396C7DEE7BFD6A985A /* ManagerObjectiveCTest.h */,
DD52B2C54A6ADF3371C13DCB /* SocketObjectiveCTest.h */,
); );
name = TestSocketIOObjc; name = TestSocketIOObjc;
path = Tests/TestSocketIOObjc; path = Tests/TestSocketIOObjc;
@ -198,6 +220,7 @@
DD52B6A0966AF71393777311 /* Client */, DD52B6A0966AF71393777311 /* Client */,
DD52B1D10D761CEF3944A6BC /* Util */, DD52B1D10D761CEF3944A6BC /* Util */,
DD52B647ED881F3FF6EEC617 /* Parse */, DD52B647ED881F3FF6EEC617 /* Parse */,
1C657951DEA2E0293D0FD1B6 /* Manager */,
); );
name = Source; name = Source;
sourceTree = "<group>"; sourceTree = "<group>";
@ -251,7 +274,6 @@
DD52BED81BF312B0E90E92AC /* SocketLogger.swift */, DD52BED81BF312B0E90E92AC /* SocketLogger.swift */,
DD52B471D780013E18DF9335 /* SocketExtensions.swift */, DD52B471D780013E18DF9335 /* SocketExtensions.swift */,
DD52BA240D139F72633D4159 /* SocketStringReader.swift */, DD52BA240D139F72633D4159 /* SocketStringReader.swift */,
DD52B282975446C9A9C56D7B /* SocketClientManager.swift */,
9432E0061F77F7CA006AF628 /* SSLSecurity.swift */, 9432E0061F77F7CA006AF628 /* SSLSecurity.swift */,
); );
name = Util; name = Util;
@ -276,7 +298,7 @@
DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */, DD52B6DCCBBAC6BE9C22568D /* SocketEventHandler.swift */,
DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */, DD52BCAF915A546288664346 /* SocketIOClientSpec.swift */,
DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */, DD52B078DB0A3C3D1BB507CD /* SocketIOClientOption.swift */,
DD52B1D9BC4AE46D38D827DE /* SocketIOClientStatus.swift */, DD52B1D9BC4AE46D38D827DE /* SocketIOStatus.swift */,
DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */, DD52B57FFEE8560CFFD793B3 /* SocketIOClientConfiguration.swift */,
); );
name = Client; name = Client;
@ -448,15 +470,16 @@
9432E00F1F77F8C4006AF628 /* SSLSecurity.swift in Sources */, 9432E00F1F77F8C4006AF628 /* SSLSecurity.swift in Sources */,
DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */, DD52BB9A3E42FF2DD6BE7C2F /* SocketIOClientSpec.swift in Sources */,
DD52B2AFE7D46039C7AE4D19 /* SocketIOClientOption.swift in Sources */, DD52B2AFE7D46039C7AE4D19 /* SocketIOClientOption.swift in Sources */,
DD52BE4D1E6BB752CD9614A6 /* SocketIOClientStatus.swift in Sources */, DD52BE4D1E6BB752CD9614A6 /* SocketIOStatus.swift in Sources */,
DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */, DD52BD065B74AC5B77BAEFAA /* SocketIOClientConfiguration.swift in Sources */,
DD52B048C71D724ABBD18C71 /* SocketTypes.swift in Sources */, DD52B048C71D724ABBD18C71 /* SocketTypes.swift in Sources */,
DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */, DD52BC3F1F880820E8FDFD0C /* SocketLogger.swift in Sources */,
DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */, DD52B56DE03CDB4F40BD1A23 /* SocketExtensions.swift in Sources */,
DD52B11AF936352BAE30B2C8 /* SocketStringReader.swift in Sources */, DD52B11AF936352BAE30B2C8 /* SocketStringReader.swift in Sources */,
DD52B660D63B6A25C3755AA7 /* SocketClientManager.swift in Sources */,
DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */, DD52B57E7ABC61B57EE2A4B8 /* SocketPacket.swift in Sources */,
DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */, DD52B9412F660F828B683422 /* SocketParsable.swift in Sources */,
1C6572803D7E252A77A86E5F /* SocketManager.swift in Sources */,
1C657FBB3F670261780FD72E /* SocketManagerSpec.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -472,6 +495,8 @@
1C686BE61F869AFD007D8627 /* SocketAckManagerTest.swift in Sources */, 1C686BE61F869AFD007D8627 /* SocketAckManagerTest.swift in Sources */,
1C686BE71F869AFD007D8627 /* SocketParserTest.swift in Sources */, 1C686BE71F869AFD007D8627 /* SocketParserTest.swift in Sources */,
1C686BE81F869AFD007D8627 /* SocketNamespacePacketTest.swift in Sources */, 1C686BE81F869AFD007D8627 /* SocketNamespacePacketTest.swift in Sources */,
DD52BCCD25EFA76E0F9B313C /* SocketMangerTest.swift in Sources */,
DD52B53F2609D91A683DFCDD /* ManagerObjectiveCTest.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -120,8 +120,10 @@ public final class OnAckCallback : NSObject {
guard seconds != 0 else { return } guard seconds != 0 else { return }
socket.handleQueue.asyncAfter(deadline: DispatchTime.now() + seconds) { socket.manager?.handleQueue.asyncAfter(deadline: DispatchTime.now() + seconds) {[weak socket] in
socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: socket.handleQueue) guard let socket = socket, let manager = socket.manager else { return }
socket.ackHandlers.timeoutAck(self.ackNumber, onQueue: manager.handleQueue)
} }
} }

View File

@ -25,110 +25,52 @@
import Dispatch import Dispatch
import Foundation import Foundation
/// The main class for SocketIOClientSwift. /// Represents a socket.io-client.
/// ///
/// **NOTE**: The client is not thread/queue safe, all interaction with the socket should be done on the `handleQueue` /// Clients are created through a `SocketManager`, which owns the `SocketEngineSpec` that controls the connection to the server.
/// ///
/// Represents a socket.io-client. Most interaction with socket.io will be through this class. /// For example:
open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, SocketParsable { ///
/// ```swift
/// // Create a socket for the /swift namespace
/// let socket = manager.socket(forNamespace: "/swift")
///
/// // Add some handlers and connect
/// ```
///
/// **NOTE**: The client is not thread/queue safe, all interaction with the socket should be done on the `manager.handleQueue`
///
open class SocketIOClient : NSObject, SocketIOClientSpec {
// MARK: Properties // MARK: Properties
private static let logType = "SocketIOClient"
/// If `true` then every time `connect` is called, a new engine will be created.
@objc
public var forceNew = false
/// The queue that all interaction with the client should occur on. This is the queue that event handlers are
/// called on.
///
/// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**.
@objc
public var handleQueue = DispatchQueue.main
/// The namespace that this socket is currently connected to. /// The namespace that this socket is currently connected to.
/// ///
/// **Must** start with a `/`. /// **Must** start with a `/`.
@objc @objc
public var nsp = "/" public var nsp = "/"
/// The configuration for this client.
///
/// **This cannot be set after calling one of the connect methods**.
public var config: SocketIOClientConfiguration {
get {
return _config
}
set {
guard status == .notConnected else {
DefaultSocketLogger.Logger.error("Tried setting config after calling connect",
type: SocketIOClient.logType)
return
}
_config = newValue
if socketURL.absoluteString.hasPrefix("https://") {
_config.insert(.secure(true))
}
_config.insert(.path("/socket.io/"), replacing: false)
setConfigs()
}
}
/// If `true`, this client will try and reconnect on any disconnects.
@objc
public var reconnects = true
/// The number of seconds to wait before attempting to reconnect.
@objc
public var reconnectWait = 10
/// The session id of this client. /// The session id of this client.
@objc @objc
public var sid: String? { public var sid: String {
return engine?.sid guard let engine = manager?.engine else { return "" }
return nsp == "/" ? engine.sid : "\(nsp)#\(engine.sid)"
} }
/// The URL of the socket.io server.
///
/// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the
/// init.
@objc
public var socketURL: URL
/// A list of packets that are waiting for binary data.
///
/// The way that socket.io works all data should be sent directly after each packet.
/// So this should ideally be an array of one packet waiting for data.
///
/// **This should not be modified directly.**
public var waitingPackets = [SocketPacket]()
/// A handler that will be called on any event. /// A handler that will be called on any event.
public private(set) var anyHandler: ((SocketAnyEvent) -> ())? public private(set) var anyHandler: ((SocketAnyEvent) -> ())?
/// The engine for this client.
@objc
public internal(set) var engine: SocketEngineSpec?
/// The array of handlers for this socket. /// The array of handlers for this socket.
public private(set) var handlers = [SocketEventHandler]() public private(set) var handlers = [SocketEventHandler]()
/// The manager for this socket.
@objc
public private(set) weak var manager: SocketManagerSpec?
/// The status of this client. /// The status of this client.
@objc @objc
public private(set) var status = SocketIOClientStatus.notConnected { public private(set) var status = SocketIOStatus.notConnected {
didSet { didSet {
switch status {
case .connected:
reconnecting = false
currentReconnectAttempt = 0
default:
break
}
handleClientEvent(.statusChange, data: [status]) handleClientEvent(.statusChange, data: [status])
} }
} }
@ -136,60 +78,29 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
var ackHandlers = SocketAckManager() var ackHandlers = SocketAckManager()
private(set) var currentAck = -1 private(set) var currentAck = -1
private(set) var reconnectAttempts = -1
private var _config: SocketIOClientConfiguration private lazy var logType = "SocketIOClient{\(nsp)}"
private var currentReconnectAttempt = 0
private var reconnecting = false
// MARK: Initializers // MARK: Initializers
/// Type safe way to create a new SocketIOClient. `opts` can be omitted. /// Type safe way to create a new SocketIOClient. `opts` can be omitted.
/// ///
/// - parameter manager: The manager for this socket.
/// - parameter socketURL: The url of the socket.io server. /// - parameter socketURL: The url of the socket.io server.
/// - parameter config: The config for this socket. @objc
public init(socketURL: URL, config: SocketIOClientConfiguration = []) { public init(manager: SocketManagerSpec, nsp: String) {
self._config = config self.manager = manager
self.socketURL = socketURL self.nsp = nsp
if socketURL.absoluteString.hasPrefix("https://") {
self._config.insert(.secure(true))
}
self._config.insert(.path("/socket.io/"), replacing: false)
super.init() super.init()
setConfigs()
}
/// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity.
/// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set<SocketIOClientOption>)`
///
/// - parameter socketURL: The url of the socket.io server.
/// - parameter config: The config for this socket.
@objc
public convenience init(socketURL: NSURL, config: NSDictionary?) {
self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? [])
} }
deinit { deinit {
DefaultSocketLogger.Logger.log("Client is being released", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Client is being released", type: logType)
engine?.disconnect(reason: "Client Deinit")
} }
// MARK: Methods // MARK: Methods
private func addEngine() {
DefaultSocketLogger.Logger.log("Adding engine", type: SocketIOClient.logType)
engine?.engineQueue.sync {
self.engine?.client = nil
}
engine = SocketEngine(client: self, url: socketURL, config: config)
}
/// Connect to the server. The same as calling `connect(timeoutAfter:withHandler:)` with a timeout of 0. /// Connect to the server. The same as calling `connect(timeoutAfter:withHandler:)` with a timeout of 0.
/// ///
/// Only call after adding your event listeners, unless you know what you're doing. /// Only call after adding your event listeners, unless you know what you're doing.
@ -209,27 +120,22 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) { open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) {
assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)") assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)")
guard status != .connected else { guard let manager = self.manager, status != .connected else {
DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType)
type: SocketIOClient.logType)
return return
} }
status = .connecting status = .connecting
if engine == nil || forceNew { manager.connectSocket(self)
addEngine()
}
engine?.connect()
guard timeoutAfter != 0 else { return } guard timeoutAfter != 0 else { return }
handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in
guard let this = self, this.status == .connecting || this.status == .notConnected else { return } guard let this = self, this.status == .connecting || this.status == .notConnected else { return }
this.status = .disconnected this.status = .disconnected
this.engine?.disconnect(reason: "Connect timeout") this.leaveNamespace()
handler?() handler?()
} }
@ -248,7 +154,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
open func didConnect(toNamespace namespace: String) { open func didConnect(toNamespace namespace: String) {
guard status != .connected else { return } guard status != .connected else { return }
DefaultSocketLogger.Logger.log("Socket connected", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Socket connected", type: logType)
status = .connected status = .connected
@ -261,21 +167,22 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
open func didDisconnect(reason: String) { open func didDisconnect(reason: String) {
guard status != .disconnected else { return } guard status != .disconnected else { return }
DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: logType)
reconnecting = false
status = .disconnected status = .disconnected
// Make sure the engine is actually dead.
engine?.disconnect(reason: reason)
handleClientEvent(.disconnect, data: [reason]) handleClientEvent(.disconnect, data: [reason])
} }
/// Disconnects the socket. /// Disconnects the socket.
///
/// This will cause the socket to leave the namespace it is associated to, as well as remove itself from the
/// `manager`.
@objc @objc
open func disconnect() { open func disconnect() {
DefaultSocketLogger.Logger.log("Closing socket", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Closing socket", type: logType)
leaveNamespace()
didDisconnect(reason: "Disconnect") didDisconnect(reason: "Disconnect")
} }
@ -291,7 +198,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
try emit(event, with: items.map({ try $0.socketRepresentation() })) try emit(event, with: items.map({ try $0.socketRepresentation() }))
} catch let err { } catch let err {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: SocketIOClient.logType) type: logType)
handleClientEvent(.error, data: [event, items, err]) handleClientEvent(.error, data: [event, items, err])
} }
@ -335,7 +242,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() })) return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() }))
} catch let err { } catch let err {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)", DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: SocketIOClient.logType) type: logType)
handleClientEvent(.error, data: [event, items, err]) handleClientEvent(.error, data: [event, items, err])
@ -373,9 +280,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: false) let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: false)
let str = packet.packetString let str = packet.packetString
DefaultSocketLogger.Logger.log("Emitting: \(str)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Emitting: \(str)", type: logType)
engine?.send(str, withData: packet.binary) manager?.engine?.send(str, withData: packet.binary)
} }
/// Call when you wish to tell the server that you've received the event for `ack`. /// Call when you wish to tell the server that you've received the event for `ack`.
@ -390,91 +297,9 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: nsp, ack: true) let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: nsp, ack: true)
let str = packet.packetString let str = packet.packetString
DefaultSocketLogger.Logger.log("Emitting Ack: \(str)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Emitting Ack: \(str)", type: logType)
engine?.send(str, withData: packet.binary) manager?.engine?.send(str, withData: packet.binary)
}
/// Called when the engine closes.
///
/// - parameter reason: The reason that the engine closed.
open func engineDidClose(reason: String) {
handleQueue.async {
self._engineDidClose(reason: reason)
}
}
private func _engineDidClose(reason: String) {
waitingPackets.removeAll()
if status != .disconnected {
status = .notConnected
}
if status == .disconnected || !reconnects {
didDisconnect(reason: reason)
} else if !reconnecting {
reconnecting = true
tryReconnect(reason: reason)
}
}
/// Called when the engine errors.
///
/// - parameter reason: The reason the engine errored.
open func engineDidError(reason: String) {
handleQueue.async {
self._engineDidError(reason: reason)
}
}
private func _engineDidError(reason: String) {
DefaultSocketLogger.Logger.error("\(reason)", type: SocketIOClient.logType)
handleClientEvent(.error, data: [reason])
}
/// Called when the engine opens.
///
/// - parameter reason: The reason the engine opened.
open func engineDidOpen(reason: String) {
handleQueue.async {
self._engineDidOpen(reason: reason)
}
}
private func _engineDidOpen(reason: String) {
DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketIOClient.logType)
guard nsp != "/" else {
didConnect(toNamespace: "/")
return
}
joinNamespace(nsp)
}
/// Called when the engine receives a pong message.
open func engineDidReceivePong() {
handleQueue.async {
self._engineDidReceivePong()
}
}
private func _engineDidReceivePong() {
handleClientEvent(.pong, data: [])
}
/// Called when the sends a ping to the server.
open func engineDidSendPing() {
handleQueue.async {
self._engineDidSendPing()
}
}
private func _engineDidSendPing() {
handleClientEvent(.ping, data: [])
} }
/// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called. /// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called.
@ -483,11 +308,19 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
/// - parameter data: The data sent back with this ack. /// - parameter data: The data sent back with this ack.
@objc @objc
open func handleAck(_ ack: Int, data: [Any]) { open func handleAck(_ ack: Int, data: [Any]) {
guard status == .connected else { return } guard status == .connected, let manager = self.manager else { return }
DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: logType)
ackHandlers.executeAck(ack, with: data, onQueue: handleQueue) ackHandlers.executeAck(ack, with: data, onQueue: manager.handleQueue)
}
/// Called on socket.io specific events.
///
/// - parameter event: The `SocketClientEvent`.
/// - parameter data: The data for this event.
open func handleClientEvent(_ event: SocketClientEvent, data: [Any]) {
handleEvent(event.rawValue, data: data, isInternalMessage: true)
} }
/// Called when we get an event from socket.io. /// Called when we get an event from socket.io.
@ -500,7 +333,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) { open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) {
guard status == .connected || isInternalMessage else { return } guard status == .connected || isInternalMessage else { return }
DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: logType)
anyHandler?(SocketAnyEvent(event: event, items: data)) anyHandler?(SocketAnyEvent(event: event, items: data))
@ -509,36 +342,49 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
} }
} }
/// Called on socket.io specific events. /// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the
/// socket.
/// ///
/// - parameter event: The `SocketClientEvent`. /// - parameter pack: The packet to handle.
/// - parameter data: The data for this event. open func handlePacket(_ pack: SocketPacket) {
open func handleClientEvent(_ event: SocketClientEvent, data: [Any]) { func handleConnect(_ packetNamespace: String) {
handleEvent(event.rawValue, data: data, isInternalMessage: true) guard packetNamespace == nsp else { return }
didConnect(toNamespace: packetNamespace)
} }
/// Call when you wish to leave a namespace and return to the default namespace. switch pack.type {
case .event, .binaryEvent:
handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id)
case .ack, .binaryAck:
handleAck(pack.id, data: pack.data)
case .connect:
handleConnect(pack.nsp)
case .disconnect:
didDisconnect(reason: "Got Disconnect")
case .error:
handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id)
}
}
/// Call when you wish to leave a namespace and disconnect this socket.
@objc @objc
open func leaveNamespace() { open func leaveNamespace() {
guard nsp != "/" else { return } guard nsp != "/" else { return }
engine?.send("1\(nsp)", withData: []) status = .disconnected
nsp = "/"
manager?.disconnectSocket(self)
} }
/// Joins `namespace`. /// Joins `nsp`.
///
/// **Do not use this to join the default namespace.** Instead call `leaveNamespace`.
///
/// - parameter namespace: The namespace to join.
@objc @objc
open func joinNamespace(_ namespace: String) { open func joinNamespace() {
guard namespace != "/" else { return } guard nsp != "/" else { return }
DefaultSocketLogger.Logger.log("Joining namespace \(namespace)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Joining namespace \(nsp)", type: logType)
nsp = namespace manager?.engine?.send("0\(nsp)", withData: [])
engine?.send("0\(nsp)", withData: [])
} }
/// Removes handler(s) for a client event. /// Removes handler(s) for a client event.
@ -557,7 +403,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
/// - parameter event: The event to remove handlers for. /// - parameter event: The event to remove handlers for.
@objc @objc
open func off(_ event: String) { open func off(_ event: String) {
DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: logType)
handlers = handlers.filter({ $0.event != event }) handlers = handlers.filter({ $0.event != event })
} }
@ -569,7 +415,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
/// - parameter id: The UUID of the handler you wish to remove. /// - parameter id: The UUID of the handler you wish to remove.
@objc @objc
open func off(id: UUID) { open func off(id: UUID) {
DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: logType)
handlers = handlers.filter({ $0.id != id }) handlers = handlers.filter({ $0.id != id })
} }
@ -582,7 +428,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
@objc @objc
@discardableResult @discardableResult
open func on(_ event: String, callback: @escaping NormalCallback) -> UUID { open func on(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: logType)
let handler = SocketEventHandler(event: event, id: UUID(), callback: callback) let handler = SocketEventHandler(event: event, id: UUID(), callback: callback)
handlers.append(handler) handlers.append(handler)
@ -626,7 +472,7 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
@objc @objc
@discardableResult @discardableResult
open func once(_ event: String, callback: @escaping NormalCallback) -> UUID { open func once(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: SocketIOClient.logType) DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: logType)
let id = UUID() let id = UUID()
@ -649,31 +495,10 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
anyHandler = handler anyHandler = handler
} }
/// Called when the engine has a message that must be parsed.
///
/// - parameter msg: The message that needs parsing.
public func parseEngineMessage(_ msg: String) {
DefaultSocketLogger.Logger.log("Should parse message: \(msg)", type: SocketIOClient.logType)
handleQueue.async { self.parseSocketMessage(msg) }
}
/// Called when the engine receives binary data.
///
/// - parameter data: The data the engine received.
public func parseEngineBinaryData(_ data: Data) {
handleQueue.async { self.parseBinaryData(data) }
}
/// Tries to reconnect to the server. /// Tries to reconnect to the server.
///
/// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event.
@objc @objc
open func reconnect() { @available(*, unavailable, message: "Call the manager's reconnect method")
guard !reconnecting else { return } open func reconnect() { }
engine?.disconnect(reason: "manual reconnect")
}
/// Removes all handlers. /// Removes all handlers.
/// ///
@ -683,54 +508,15 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
handlers.removeAll(keepingCapacity: false) handlers.removeAll(keepingCapacity: false)
} }
private func tryReconnect(reason: String) { /// Puts the socket back into the connecting state.
guard reconnecting else { return } /// Called when the manager detects a broken connection, or when a manual reconnect is triggered.
///
/// - parameter reason: The reason this socket is reconnecting.
@objc
open func setReconnecting(reason: String) {
status = .connecting
DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketIOClient.logType)
handleClientEvent(.reconnect, data: [reason]) handleClientEvent(.reconnect, data: [reason])
_tryReconnect()
}
private func _tryReconnect() {
guard reconnects && reconnecting && status != .disconnected else { return }
if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts {
return didDisconnect(reason: "Reconnect Failed")
}
DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketIOClient.logType)
handleClientEvent(.reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)])
currentReconnectAttempt += 1
connect()
handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect)
}
private func setConfigs() {
for option in config {
switch option {
case let .reconnects(reconnects):
self.reconnects = reconnects
case let .reconnectAttempts(attempts):
reconnectAttempts = attempts
case let .reconnectWait(wait):
reconnectWait = abs(wait)
case let .nsp(nsp):
self.nsp = nsp
case let .log(log):
DefaultSocketLogger.Logger.log = log
case let .logger(logger):
DefaultSocketLogger.Logger = logger
case let .handleQueue(queue):
handleQueue = queue
case let .forceNew(force):
forceNew = force
default:
continue
}
}
} }
// Test properties // Test properties
@ -743,14 +529,10 @@ open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, So
status = .connected status = .connected
} }
func setTestStatus(_ status: SocketIOClientStatus) { func setTestStatus(_ status: SocketIOStatus) {
self.status = status self.status = status
} }
func setTestEngine(_ engine: SocketEngineSpec?) {
self.engine = engine
}
func emitTest(event: String, _ data: Any...) { func emitTest(event: String, _ data: Any...) {
emit([event] + data) emit([event] + data)
} }

View File

@ -125,5 +125,12 @@ public struct SocketIOClientConfiguration : ExpressibleByArrayLiteral, Collectio
backingArray.append(element) backingArray.append(element)
} }
}
/// Declares that a type can set configs from a `SocketIOClientConfiguration`.
public protocol ConfigSettable {
/// Called when an `ConfigSettable` should set/update its configs from a given configuration.
///
/// - parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs.
mutating func setConfigs(_ config: SocketIOClientConfiguration)
} }

View File

@ -40,10 +40,6 @@ public enum SocketIOClientOption : ClientOption {
/// An array of cookies that will be sent during the initial connection. /// An array of cookies that will be sent during the initial connection.
case cookies([HTTPCookie]) case cookies([HTTPCookie])
/// Deprecated
@available(*, deprecated, message: "No longer needed in socket.io 2.0+")
case doubleEncodeUTF8(Bool)
/// Any extra HTTP headers that should be sent during the initial connection. /// Any extra HTTP headers that should be sent during the initial connection.
case extraHeaders([String: String]) case extraHeaders([String: String])
@ -67,10 +63,6 @@ public enum SocketIOClientOption : ClientOption {
/// Used to pass in a custom logger. /// Used to pass in a custom logger.
case logger(SocketLogger) case logger(SocketLogger)
/// The namespace that this client should connect to. Can be changed during use using the `joinNamespace`
/// and `leaveNamespace` methods on `SocketIOClient`.
case nsp(String)
/// A custom path to socket.io. Only use this if the socket.io server is configured to look for this path. /// A custom path to socket.io. Only use this if the socket.io server is configured to look for this path.
case path(String) case path(String)
@ -96,11 +88,6 @@ public enum SocketIOClientOption : ClientOption {
/// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs. /// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs.
case sessionDelegate(URLSessionDelegate) case sessionDelegate(URLSessionDelegate)
/// If passed `true`, the WebSocket transport will try and use voip logic to keep network connections open in
/// the background. **This option is experimental as socket.io shouldn't be used for background communication.**
@available(*, deprecated, message: "No longer has any effect, and will be removed in v11.0")
case voipEnabled(Bool)
// MARK: Properties // MARK: Properties
/// The description of this option. /// The description of this option.
@ -114,8 +101,6 @@ public enum SocketIOClientOption : ClientOption {
description = "connectParams" description = "connectParams"
case .cookies: case .cookies:
description = "cookies" description = "cookies"
case .doubleEncodeUTF8:
description = "doubleEncodeUTF8"
case .extraHeaders: case .extraHeaders:
description = "extraHeaders" description = "extraHeaders"
case .forceNew: case .forceNew:
@ -130,8 +115,6 @@ public enum SocketIOClientOption : ClientOption {
description = "log" description = "log"
case .logger: case .logger:
description = "logger" description = "logger"
case .nsp:
description = "nsp"
case .path: case .path:
description = "path" description = "path"
case .reconnects: case .reconnects:
@ -148,8 +131,6 @@ public enum SocketIOClientOption : ClientOption {
description = "security" description = "security"
case .sessionDelegate: case .sessionDelegate:
description = "sessionDelegate" description = "sessionDelegate"
case .voipEnabled:
description = "voipEnabled"
} }
return description return description
@ -165,8 +146,6 @@ public enum SocketIOClientOption : ClientOption {
value = params value = params
case let .cookies(cookies): case let .cookies(cookies):
value = cookies value = cookies
case let .doubleEncodeUTF8(encode):
value = encode
case let .extraHeaders(headers): case let .extraHeaders(headers):
value = headers value = headers
case let .forceNew(force): case let .forceNew(force):
@ -181,8 +160,6 @@ public enum SocketIOClientOption : ClientOption {
value = log value = log
case let .logger(logger): case let .logger(logger):
value = logger value = logger
case let .nsp(nsp):
value = nsp
case let .path(path): case let .path(path):
value = path value = path
case let .reconnects(reconnects): case let .reconnects(reconnects):
@ -199,8 +176,6 @@ public enum SocketIOClientOption : ClientOption {
value = signed value = signed
case let .sessionDelegate(delegate): case let .sessionDelegate(delegate):
value = delegate value = delegate
case let .voipEnabled(enabled):
value = enabled
} }
return value return value

View File

@ -32,22 +32,19 @@ public protocol SocketIOClientSpec : class {
/// A handler that will be called on any event. /// A handler that will be called on any event.
var anyHandler: ((SocketAnyEvent) -> ())? { get } var anyHandler: ((SocketAnyEvent) -> ())? { get }
/// The configuration for this client.
var config: SocketIOClientConfiguration { get set }
/// The queue that all interaction with the client must be on.
var handleQueue: DispatchQueue { get set }
/// The array of handlers for this socket. /// The array of handlers for this socket.
var handlers: [SocketEventHandler] { get } var handlers: [SocketEventHandler] { get }
/// The manager for this socket.
var manager: SocketManagerSpec? { get }
/// The namespace that this socket is currently connected to. /// The namespace that this socket is currently connected to.
/// ///
/// **Must** start with a `/`. /// **Must** start with a `/`.
var nsp: String { get set } var nsp: String { get set }
/// The status of this client. /// The status of this client.
var status: SocketIOClientStatus { get } var status: SocketIOStatus { get }
// MARK: Methods // MARK: Methods
@ -126,6 +123,12 @@ public protocol SocketIOClientSpec : class {
/// - parameter data: The data sent back with this ack. /// - parameter data: The data sent back with this ack.
func handleAck(_ ack: Int, data: [Any]) func handleAck(_ ack: Int, data: [Any])
/// Called on socket.io specific events.
///
/// - parameter event: The `SocketClientEvent`.
/// - parameter data: The data for this event.
func handleClientEvent(_ event: SocketClientEvent, data: [Any])
/// Called when we get an event from socket.io. /// Called when we get an event from socket.io.
/// ///
/// - parameter event: The name of the event. /// - parameter event: The name of the event.
@ -134,21 +137,17 @@ public protocol SocketIOClientSpec : class {
/// - parameter withAck: If > 0 then this event expects to get an ack back from the client. /// - parameter withAck: If > 0 then this event expects to get an ack back from the client.
func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int) func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int)
/// Called on socket.io specific events. /// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the
/// socket.
/// ///
/// - parameter event: The `SocketClientEvent`. /// - parameter pack: The packet to handle.
/// - parameter data: The data for this event. func handlePacket(_ pack: SocketPacket)
func handleClientEvent(_ event: SocketClientEvent, data: [Any])
/// Call when you wish to leave a namespace and return to the default namespace. /// Call when you wish to leave a namespace and disconnect this socket.
func leaveNamespace() func leaveNamespace()
/// Joins `namespace`. /// Joins `nsp`.
/// func joinNamespace()
/// **Do not use this to join the default namespace.** Instead call `leaveNamespace`.
///
/// - parameter namespace: The namespace to join.
func joinNamespace(_ namespace: String)
/// Removes handler(s) for a client event. /// Removes handler(s) for a client event.
/// ///
@ -212,13 +211,16 @@ public protocol SocketIOClientSpec : class {
/// - parameter handler: The callback that will execute whenever an event is received. /// - parameter handler: The callback that will execute whenever an event is received.
func onAny(_ handler: @escaping (SocketAnyEvent) -> ()) func onAny(_ handler: @escaping (SocketAnyEvent) -> ())
/// Tries to reconnect to the server.
func reconnect()
/// Removes all handlers. /// Removes all handlers.
/// ///
/// Can be used after disconnecting to break any potential remaining retain cycles. /// Can be used after disconnecting to break any potential remaining retain cycles.
func removeAllHandlers() func removeAllHandlers()
/// Puts the socket back into the connecting state.
/// Called when the manager detects a broken connection, or when a manual reconnect is triggered.
///
/// parameter reason: The reason this socket is going reconnecting.
func setReconnecting(reason: String)
} }
public extension SocketIOClientSpec { public extension SocketIOClientSpec {

View File

@ -1,5 +1,5 @@
// //
// SocketIOClientStatus.swift // SocketIOStatus.swift
// Socket.IO-Client-Swift // Socket.IO-Client-Swift
// //
// Created by Erik Little on 8/14/15. // Created by Erik Little on 8/14/15.
@ -24,17 +24,36 @@
import Foundation import Foundation
/// Represents the state of the client. /// Represents state of a manager or client.
@objc public enum SocketIOClientStatus : Int { @objc
/// The client has never been connected. Or the client has been reset. public enum SocketIOStatus : Int, CustomStringConvertible {
// MARK: Cases
/// The client/manager has never been connected. Or the client has been reset.
case notConnected case notConnected
/// The client was once connected, but not anymore. /// The client/manager was once connected, but not anymore.
case disconnected case disconnected
/// The client is in the process of connecting. /// The client/manager is in the process of connecting.
case connecting case connecting
/// The client is currently connected. /// The client/manager is currently connected.
case connected case connected
// MARK: Properties
/// - returns: True if this client/manager is connected/connecting to a server.
public var active: Bool {
return self == .connected || self == .connecting
}
public var description: String {
switch self {
case .connected: return "connected"
case .connecting: return "connecting"
case .disconnected: return "disconnected"
case .notConnected: return "notConnected"
}
}
} }

View File

@ -28,7 +28,8 @@ import Starscream
/// The class that handles the engine.io protocol and transports. /// The class that handles the engine.io protocol and transports.
/// See `SocketEnginePollable` and `SocketEngineWebsocket` for transport specific methods. /// See `SocketEnginePollable` and `SocketEngineWebsocket` for transport specific methods.
public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket { public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket,
ConfigSettable {
// MARK: Properties // MARK: Properties
private static let logType = "SocketEngine" private static let logType = "SocketEngine"
@ -147,41 +148,11 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll
public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) { public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) {
self.client = client self.client = client
self.url = url self.url = url
for option in config {
switch option {
case let .connectParams(params):
connectParams = params
case let .cookies(cookies):
self.cookies = cookies
case let .extraHeaders(headers):
extraHeaders = headers
case let .sessionDelegate(delegate):
sessionDelegate = delegate
case let .forcePolling(force):
forcePolling = force
case let .forceWebsockets(force):
forceWebsockets = force
case let .path(path):
socketPath = path
if !socketPath.hasSuffix("/") {
socketPath += "/"
}
case let .secure(secure):
self.secure = secure
case let .selfSigned(selfSigned):
self.selfSigned = selfSigned
case let .security(security):
self.security = security
case .compress:
self.compress = true
default:
continue
}
}
super.init() super.init()
setConfigs(config)
sessionDelegate = sessionDelegate ?? self sessionDelegate = sessionDelegate ?? self
(urlPolling, urlWebSocket) = createURLs() (urlPolling, urlWebSocket) = createURLs()
@ -572,6 +543,44 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll
client?.engineDidSendPing() client?.engineDidSendPing()
} }
/// Called when the engine should set/update its configs from a given configuration.
///
/// parameter config: The `SocketIOClientConfiguration` that should be used to set/update configs.
open func setConfigs(_ config: SocketIOClientConfiguration) {
for option in config {
switch option {
case let .connectParams(params):
connectParams = params
case let .cookies(cookies):
self.cookies = cookies
case let .extraHeaders(headers):
extraHeaders = headers
case let .sessionDelegate(delegate):
sessionDelegate = delegate
case let .forcePolling(force):
forcePolling = force
case let .forceWebsockets(force):
forceWebsockets = force
case let .path(path):
socketPath = path
if !socketPath.hasSuffix("/") {
socketPath += "/"
}
case let .secure(secure):
self.secure = secure
case let .selfSigned(selfSigned):
self.selfSigned = selfSigned
case let .security(security):
self.security = security
case .compress:
self.compress = true
default:
continue
}
}
}
// Moves from long-polling to websockets // Moves from long-polling to websockets
private func upgradeTransport() { private func upgradeTransport() {
if ws?.isConnected ?? false { if ws?.isConnected ?? false {
@ -645,6 +654,12 @@ public final class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePoll
client?.engineDidClose(reason: "Socket Disconnected") client?.engineDidClose(reason: "Socket Disconnected")
} }
} }
// Test Properties
func setConnected(_ value: Bool) {
connected = value
}
} }
extension SocketEngine { extension SocketEngine {

View File

@ -34,6 +34,9 @@ import Starscream
/// `true` if this engine is closed. /// `true` if this engine is closed.
var closed: Bool { get } var closed: Bool { get }
/// If `true` the engine will attempt to use WebSocket compression.
var compress: Bool { get }
/// `true` if this engine is connected. Connected means that the initial poll connect has succeeded. /// `true` if this engine is connected. Connected means that the initial poll connect has succeeded.
var connected: Bool { get } var connected: Bool { get }

View File

@ -0,0 +1,519 @@
//
// Created by Erik Little on 10/14/17.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Dispatch
import Foundation
///
/// A manager for a socket.io connection.
///
/// A `SocketManager` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`.
///
/// Example:
///
/// ```swift
/// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!)
/// let defaultNamespaceSocket = manager.defaultSocket
/// let swiftSocket = manager.socket(forNamespace: "/swift")
///
/// // defaultNamespaceSocket and swiftSocket both share a single connection to the server
/// ```
///
/// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference
/// to the manager must be maintained to keep sockets alive.
///
/// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket,
/// or call one of the `disconnectSocket` methods on this class.
///
open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDataBufferable, ConfigSettable {
private static let logType = "SocketManager"
// MARK Properties
/// The socket associated with the default namespace ("/").
public var defaultSocket: SocketIOClient? {
return socket(forNamespace: "/")
}
/// The URL of the socket.io server.
///
/// If changed after calling `init`, `forceNew` must be set to `true`, or it will only connect to the url set in the
/// init.
public let socketURL: URL
/// The configuration for this client.
///
/// **Some configs will not take affect until after a reconnect if set after calling a connect method**.
public var config: SocketIOClientConfiguration {
get {
return _config
}
set {
if status.active {
DefaultSocketLogger.Logger.log("Setting configs on active manager. Some configs may not be applied until reconnect",
type: SocketManager.logType)
}
setConfigs(newValue)
}
}
/// The engine for this manager.
public var engine: SocketEngineSpec?
/// If `true` then every time `connect` is called, a new engine will be created.
public var forceNew = false
/// The queue that all interaction with the client should occur on. This is the queue that event handlers are
/// called on.
///
/// **This should be a serial queue! Concurrent queues are not supported and might cause crashes and races**.
public var handleQueue = DispatchQueue.main
/// The sockets in this manager indexed by namespace.
public var nsps = [String: SocketIOClient]()
/// If `true`, this client will try and reconnect on any disconnects.
public var reconnects = true
/// The number of seconds to wait before attempting to reconnect.
public var reconnectWait = 10
/// The status of this manager.
public private(set) var status: SocketIOStatus = .notConnected {
didSet {
switch status {
case .connected:
reconnecting = false
currentReconnectAttempt = 0
default:
break
}
}
}
/// A list of packets that are waiting for binary data.
///
/// The way that socket.io works all data should be sent directly after each packet.
/// So this should ideally be an array of one packet waiting for data.
///
/// **This should not be modified directly.**
public var waitingPackets = [SocketPacket]()
private(set) var reconnectAttempts = -1
private var _config: SocketIOClientConfiguration
private var currentReconnectAttempt = 0
private var reconnecting = false
/// Type safe way to create a new SocketIOClient. `opts` can be omitted.
///
/// - parameter socketURL: The url of the socket.io server.
/// - parameter config: The config for this socket.
public init(socketURL: URL, config: SocketIOClientConfiguration = []) {
self._config = config
self.socketURL = socketURL
if socketURL.absoluteString.hasPrefix("https://") {
self._config.insert(.secure(true))
}
self._config.insert(.path("/socket.io/"), replacing: false)
super.init()
setConfigs(_config)
}
/// Not so type safe way to create a SocketIOClient, meant for Objective-C compatiblity.
/// If using Swift it's recommended to use `init(socketURL: NSURL, options: Set<SocketIOClientOption>)`
///
/// - parameter socketURL: The url of the socket.io server.
/// - parameter config: The config for this socket.
@objc
public convenience init(socketURL: NSURL, config: NSDictionary?) {
self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? [])
}
deinit {
DefaultSocketLogger.Logger.log("Manager is being released", type: SocketManager.logType)
engine?.disconnect(reason: "Manager Deinit")
}
// MARK: Methods
private func addEngine() {
DefaultSocketLogger.Logger.log("Adding engine", type: SocketManager.logType)
engine?.engineQueue.sync {
self.engine?.client = nil
}
engine = SocketEngine(client: self, url: socketURL, config: config)
}
/// Connects the underlying transport and the default namespace socket.
///
/// Override if you wish to attach a custom `SocketEngineSpec`.
open func connect() {
guard !status.active else {
DefaultSocketLogger.Logger.log("Tried connecting an already active socket", type: SocketManager.logType)
return
}
if engine == nil || forceNew {
addEngine()
}
status = .connecting
engine?.connect()
}
/// Connects a socket through this manager's engine.
///
/// - parameter socket: The socket who we should connect through this manager.
open func connectSocket(_ socket: SocketIOClient) {
guard status == .connected else {
DefaultSocketLogger.Logger.log("Tried connecting socket when engine isn't open. Connecting",
type: SocketManager.logType)
connect()
return
}
engine?.send("0\(socket.nsp)", withData: [])
}
/// Called when the manager has disconnected from socket.io.
///
/// - parameter reason: The reason for the disconnection.
open func didDisconnect(reason: String) {
forAll {socket in
socket.didDisconnect(reason: reason)
}
}
/// Disconnects the manager and all associated sockets.
open func disconnect() {
DefaultSocketLogger.Logger.log("Manager closing", type: SocketManager.logType)
status = .disconnected
engine?.disconnect(reason: "Disconnect")
}
/// Disconnects the given socket.
///
/// This will remove the socket for the manager's control, and make the socket instance useless and ready for
/// releasing.
///
/// - parameter socket: The socket to disconnect.
open func disconnectSocket(_ socket: SocketIOClient) {
// Make sure we remove socket from nsps
nsps.removeValue(forKey: socket.nsp)
engine?.send("1\(socket.nsp)", withData: [])
socket.didDisconnect(reason: "Namespace leave")
}
/// Disconnects the socket associated with `forNamespace`.
///
/// This will remove the socket for the manager's control, and make the socket instance useless and ready for
/// releasing.
///
/// - parameter forNamespace: The namespace to disconnect from.
open func disconnectSocket(forNamespace nsp: String) {
guard let socket = nsps.removeValue(forKey: nsp) else {
DefaultSocketLogger.Logger.log("Could not find socket for \(nsp) to disconnect",
type: SocketManager.logType)
return
}
disconnectSocket(socket)
}
/// Sends a client event to all sockets in `nsps`
///
/// - parameter clientEvent: The event to emit.
open func emitAll(clientEvent event: SocketClientEvent, data: [Any]) {
forAll {socket in
socket.handleClientEvent(event, data: data)
}
}
/// Sends an event to the server on all namespaces in this manager.
///
/// - parameter event: The event to send.
/// - parameter items: The data to send with this event.
open func emitAll(_ event: String, _ items: SocketData...) {
guard let emitData = try? items.map({ try $0.socketRepresentation() }) else {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: SocketManager.logType)
return
}
emitAll(event, withItems: emitData)
}
/// Sends an event to the server on all namespaces in this manager.
///
/// Same as `emitAll(_:_:)`, but meant for Objective-C.
///
/// - parameter event: The event to send.
/// - parameter withItems: The data to send with this event.
open func emitAll(_ event: String, withItems items: [Any]) {
forAll {socket in
socket.emit(event, with: items)
}
}
/// Called when the engine closes.
///
/// - parameter reason: The reason that the engine closed.
open func engineDidClose(reason: String) {
handleQueue.async {
self._engineDidClose(reason: reason)
}
}
private func _engineDidClose(reason: String) {
waitingPackets.removeAll()
if status != .disconnected {
status = .notConnected
}
if status == .disconnected || !reconnects {
didDisconnect(reason: reason)
} else if !reconnecting {
reconnecting = true
tryReconnect(reason: reason)
}
}
/// Called when the engine errors.
///
/// - parameter reason: The reason the engine errored.
open func engineDidError(reason: String) {
handleQueue.async {
self._engineDidError(reason: reason)
}
}
private func _engineDidError(reason: String) {
DefaultSocketLogger.Logger.error("\(reason)", type: SocketManager.logType)
emitAll(clientEvent: .error, data: [reason])
}
/// Called when the engine opens.
///
/// - parameter reason: The reason the engine opened.
open func engineDidOpen(reason: String) {
handleQueue.async {
self._engineDidOpen(reason: reason)
}
}
private func _engineDidOpen(reason: String) {
DefaultSocketLogger.Logger.log("Engine opened \(reason)", type: SocketManager.logType)
status = .connected
nsps["/"]?.didConnect(toNamespace: "/")
for (nsp, socket) in nsps where nsp != "/" && socket.status == .connecting {
connectSocket(socket)
}
}
/// Called when the engine receives a pong message.
open func engineDidReceivePong() {
handleQueue.async {
self._engineDidReceivePong()
}
}
private func _engineDidReceivePong() {
emitAll(clientEvent: .pong, data: [])
}
/// Called when the sends a ping to the server.
open func engineDidSendPing() {
handleQueue.async {
self._engineDidSendPing()
}
}
private func _engineDidSendPing() {
emitAll(clientEvent: .ping, data: [])
}
private func forAll(do: (SocketIOClient) throws -> ()) rethrows {
for (_, socket) in nsps {
try `do`(socket)
}
}
/// Called when the engine has a message that must be parsed.
///
/// - parameter msg: The message that needs parsing.
open func parseEngineMessage(_ msg: String) {
handleQueue.async {
self._parseEngineMessage(msg)
}
}
private func _parseEngineMessage(_ msg: String) {
guard let packet = parseSocketMessage(msg) else { return }
guard packet.type != .binaryAck && packet.type != .binaryEvent else {
waitingPackets.append(packet)
return
}
nsps[packet.nsp]?.handlePacket(packet)
}
/// Called when the engine receives binary data.
///
/// - parameter data: The data the engine received.
open func parseEngineBinaryData(_ data: Data) {
handleQueue.async {
self._parseEngineBinaryData(data)
}
}
private func _parseEngineBinaryData(_ data: Data) {
guard let packet = parseBinaryData(data) else { return }
nsps[packet.nsp]?.handlePacket(packet)
}
/// Tries to reconnect to the server.
///
/// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event.
open func reconnect() {
guard !reconnecting else { return }
engine?.disconnect(reason: "manual reconnect")
}
private func tryReconnect(reason: String) {
guard reconnecting else { return }
DefaultSocketLogger.Logger.log("Starting reconnect", type: SocketManager.logType)
// Set status to connecting and emit reconnect for all sockets
forAll {socket in
guard socket.status == .connected else { return }
socket.setReconnecting(reason: reason)
}
_tryReconnect()
}
private func _tryReconnect() {
guard reconnects && reconnecting && status != .disconnected else { return }
if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts {
return didDisconnect(reason: "Reconnect Failed")
}
DefaultSocketLogger.Logger.log("Trying to reconnect", type: SocketManager.logType)
emitAll(clientEvent: .reconnectAttempt, data: [(reconnectAttempts - currentReconnectAttempt)])
currentReconnectAttempt += 1
connect()
handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect)
}
/// Sets manager specific configs.
///
/// parameter config: The configs that should be set.
open func setConfigs(_ config: SocketIOClientConfiguration) {
for option in config {
switch option {
case let .forceNew(new):
self.forceNew = new
case let .reconnects(reconnects):
self.reconnects = reconnects
case let .reconnectWait(wait):
reconnectWait = abs(wait)
case let .log(log):
DefaultSocketLogger.Logger.log = log
case let .logger(logger):
DefaultSocketLogger.Logger = logger
default:
continue
}
}
_config = config
_config.insert(.path("/socket.io/"), replacing: false)
// If `ConfigSettable` & `SocketEngineSpec`, update its configs.
if var settableEngine = engine as? ConfigSettable & SocketEngineSpec {
settableEngine.engineQueue.sync {
settableEngine.setConfigs(self._config)
}
engine = settableEngine
}
}
/// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager.
///
/// Calling multiple times returns the same socket.
///
/// Sockets created from this method are retained by the manager.
/// Call one of the `disconnectSocket` methods on this class to remove the socket from manager control.
/// Or call `SocketIOClient.disconnect()` on the client.
///
/// - parameter forNamespace: The namespace for the socket.
/// - returns: A `SocketIOClient` for the given namespace.
open func socket(forNamespace nsp: String) -> SocketIOClient {
assert(nsp.hasPrefix("/"), "forNamespace must have a leading /")
if let socket = nsps[nsp] {
return socket
}
let client = SocketIOClient(manager: self, nsp: nsp)
nsps[nsp] = client
return client
}
// Test properties
func setTestStatus(_ status: SocketIOStatus) {
self.status = status
}
}

View File

@ -0,0 +1,128 @@
//
// Created by Erik Little on 10/18/17.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Dispatch
import Foundation
// TODO Fix the types so that we aren't using concrete types
///
/// A manager for a socket.io connection.
///
/// A `SocketManagerSpec` is responsible for multiplexing multiple namespaces through a single `SocketEngineSpec`.
///
/// Example with `SocketManager`:
///
/// ```swift
/// let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!)
/// let defaultNamespaceSocket = manager.defaultSocket
/// let swiftSocket = manager.socket(forNamespace: "/swift")
///
/// // defaultNamespaceSocket and swiftSocket both share a single connection to the server
/// ```
///
/// Sockets created through the manager are retained by the manager. So at the very least, a single strong reference
/// to the manager must be maintained to keep sockets alive.
///
/// To disconnect a socket and remove it from the manager, either call `SocketIOClient.disconnect()` on the socket,
/// or call one of the `disconnectSocket` methods on this class.
///
@objc
public protocol SocketManagerSpec : class, SocketEngineClient {
// MARK: Properties
/// Returns the socket associated with the default namespace ("/").
var defaultSocket: SocketIOClient? { get }
/// The engine for this manager.
var engine: SocketEngineSpec? { get set }
/// If `true` then every time `connect` is called, a new engine will be created.
var forceNew: Bool { get set }
// TODO Per socket queues?
/// The queue that all interaction with the client should occur on. This is the queue that event handlers are
/// called on.
var handleQueue: DispatchQueue { get set }
/// If `true`, this manager will try and reconnect on any disconnects.
var reconnects: Bool { get set }
/// The number of seconds to wait before attempting to reconnect.
var reconnectWait: Int { get set }
/// The URL of the socket.io server.
var socketURL: URL { get }
/// The status of this manager.
var status: SocketIOStatus { get }
// MARK: Methods
/// Connects the underlying transport.
func connect()
/// Connects a socket through this manager's engine.
///
/// - parameter socket: The socket who we should connect through this manager.
func connectSocket(_ socket: SocketIOClient)
/// Called when the manager has disconnected from socket.io.
///
/// - parameter reason: The reason for the disconnection.
func didDisconnect(reason: String)
/// Disconnects the manager and all associated sockets.
func disconnect()
/// Disconnects the given socket.
///
/// - parameter socket: The socket to disconnect.
func disconnectSocket(_ socket: SocketIOClient)
/// Disconnects the socket associated with `forNamespace`.
///
/// - parameter forNamespace: The namespace to disconnect from.
func disconnectSocket(forNamespace nsp: String)
/// Sends an event to the server on all namespaces in this manager.
///
/// - parameter event: The event to send.
/// - parameter withItems: The data to send with this event.
func emitAll(_ event: String, withItems items: [Any])
/// Tries to reconnect to the server.
///
/// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event.
func reconnect()
/// Returns a `SocketIOClient` for the given namespace. This socket shares a transport with the manager.
///
/// Calling multiple times returns the same socket.
///
/// Sockets created from this method are retained by the manager.
/// Call one of the `disconnectSocket` methods on the implementing class to remove the socket from manager control.
/// Or call `SocketIOClient.disconnect()` on the client.
///
/// - parameter forNamespace: The namespace for the socket.
/// - returns: A `SocketIOClient` for the given namespace.
func socket(forNamespace nsp: String) -> SocketIOClient
}

View File

@ -142,12 +142,8 @@ public struct SocketPacket : CustomStringConvertible {
if dict["_placeholder"] as? Bool ?? false { if dict["_placeholder"] as? Bool ?? false {
return binary[dict["num"] as! Int] return binary[dict["num"] as! Int]
} else { } else {
return dict.reduce(JSON(), {cur, keyValue in return dict.reduce(into: JSON(), {cur, keyValue in
var cur = cur
cur[keyValue.0] = _fillInPlaceholders(keyValue.1) cur[keyValue.0] = _fillInPlaceholders(keyValue.1)
return cur
}) })
} }
case let arr as [Any]: case let arr as [Any]:
@ -225,12 +221,8 @@ private extension SocketPacket {
case let arr as [Any]: case let arr as [Any]:
return arr.map({shred($0, binary: &binary)}) return arr.map({shred($0, binary: &binary)})
case let dict as JSON: case let dict as JSON:
return dict.reduce(JSON(), {cur, keyValue in return dict.reduce(into: JSON(), {cur, keyValue in
var mutCur = cur cur[keyValue.0] = shred(keyValue.1, binary: &binary)
mutCur[keyValue.0] = shred(keyValue.1, binary: &binary)
return mutCur
}) })
default: default:
return data return data

View File

@ -26,14 +26,6 @@ import Foundation
public protocol SocketParsable : class { public protocol SocketParsable : class {
// MARK: Properties // MARK: Properties
/// A list of packets that are waiting for binary data.
///
/// The way that socket.io works all data should be sent directly after each packet.
/// So this should ideally be an array of one packet waiting for data.
///
/// **This should not be modified directly.**
var waitingPackets: [SocketPacket] { get set }
// MARK: Methods // MARK: Methods
/// Called when the engine has received some binary data that should be attached to a packet. /// Called when the engine has received some binary data that should be attached to a packet.
@ -43,12 +35,13 @@ public protocol SocketParsable : class {
/// into the correct placeholder. /// into the correct placeholder.
/// ///
/// - parameter data: The data that should be attached to a packet. /// - parameter data: The data that should be attached to a packet.
func parseBinaryData(_ data: Data) func parseBinaryData(_ data: Data) -> SocketPacket?
/// Called when the engine has received a string that should be parsed into a socket.io packet. /// Called when the engine has received a string that should be parsed into a socket.io packet.
/// ///
/// - parameter message: The string that needs parsing. /// - parameter message: The string that needs parsing.
func parseSocketMessage(_ message: String) /// - returns: A completed socket packet if there is no more data left to collect.
func parseSocketMessage(_ message: String) -> SocketPacket?
} }
/// Errors that can be thrown during parsing. /// Errors that can be thrown during parsing.
@ -65,38 +58,18 @@ public enum SocketParsableError : Error {
case invalidPacketType case invalidPacketType
} }
public extension SocketParsable where Self: SocketIOClientSpec { /// Says that a type will be able to buffer binary data before all data for an event has come in.
private func isCorrectNamespace(_ nsp: String) -> Bool { public protocol SocketDataBufferable : class {
return nsp == self.nsp /// A list of packets that are waiting for binary data.
} ///
/// The way that socket.io works all data should be sent directly after each packet.
private func handleConnect(_ packetNamespace: String) { /// So this should ideally be an array of one packet waiting for data.
guard packetNamespace == nsp else { return } ///
/// **This should not be modified directly.**
didConnect(toNamespace: packetNamespace) var waitingPackets: [SocketPacket] { get set }
}
private func handlePacket(_ pack: SocketPacket) {
switch pack.type {
case .event where isCorrectNamespace(pack.nsp):
handleEvent(pack.event, data: pack.args, isInternalMessage: false, withAck: pack.id)
case .ack where isCorrectNamespace(pack.nsp):
handleAck(pack.id, data: pack.data)
case .binaryEvent where isCorrectNamespace(pack.nsp):
waitingPackets.append(pack)
case .binaryAck where isCorrectNamespace(pack.nsp):
waitingPackets.append(pack)
case .connect:
handleConnect(pack.nsp)
case .disconnect:
didDisconnect(reason: "Got Disconnect")
case .error:
handleEvent("error", data: pack.data, isInternalMessage: true, withAck: pack.id)
default:
DefaultSocketLogger.Logger.log("Got invalid packet: \(pack.description)", type: "SocketParser")
}
} }
public extension SocketParsable where Self: SocketManagerSpec & SocketDataBufferable {
/// Parses a message from the engine, returning a complete SocketPacket or throwing. /// Parses a message from the engine, returning a complete SocketPacket or throwing.
/// ///
/// - parameter message: The message to parse. /// - parameter message: The message to parse.
@ -169,8 +142,9 @@ public extension SocketParsable where Self: SocketIOClientSpec {
/// Called when the engine has received a string that should be parsed into a socket.io packet. /// Called when the engine has received a string that should be parsed into a socket.io packet.
/// ///
/// - parameter message: The string that needs parsing. /// - parameter message: The string that needs parsing.
public func parseSocketMessage(_ message: String) { /// - returns: A completed socket packet or nil if the packet is invalid.
guard !message.isEmpty else { return } public func parseSocketMessage(_ message: String) -> SocketPacket? {
guard !message.isEmpty else { return nil }
DefaultSocketLogger.Logger.log("Parsing \(message)", type: "SocketParser") DefaultSocketLogger.Logger.log("Parsing \(message)", type: "SocketParser")
@ -179,9 +153,11 @@ public extension SocketParsable where Self: SocketIOClientSpec {
DefaultSocketLogger.Logger.log("Decoded packet as: \(packet.description)", type: "SocketParser") DefaultSocketLogger.Logger.log("Decoded packet as: \(packet.description)", type: "SocketParser")
handlePacket(packet) return packet
} catch { } catch {
DefaultSocketLogger.Logger.error("\(error): \(message)", type: "SocketParser") DefaultSocketLogger.Logger.error("\(error): \(message)", type: "SocketParser")
return nil
} }
} }
@ -192,21 +168,17 @@ public extension SocketParsable where Self: SocketIOClientSpec {
/// into the correct placeholder. /// into the correct placeholder.
/// ///
/// - parameter data: The data that should be attached to a packet. /// - parameter data: The data that should be attached to a packet.
public func parseBinaryData(_ data: Data) { /// - returns: A completed socket packet if there is no more data left to collect.
public func parseBinaryData(_ data: Data) -> SocketPacket? {
guard !waitingPackets.isEmpty else { guard !waitingPackets.isEmpty else {
DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser") DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser")
return
return nil
} }
// Should execute event? // Should execute event?
guard waitingPackets[waitingPackets.count - 1].addData(data) else { return } guard waitingPackets[waitingPackets.count - 1].addData(data) else { return nil }
let packet = waitingPackets.removeLast() return waitingPackets.removeLast()
if packet.type != .binaryAck {
handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id)
} else {
handleAck(packet.id, data: packet.args)
}
} }
} }

View File

@ -1,110 +0,0 @@
//
// SocketClientManager.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 6/11/16.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
/**
Experimental socket manager.
API subject to change.
Can be used to persist sockets across ViewControllers.
Sockets are strongly stored, so be sure to remove them once they are no
longer needed.
Example usage:
```
let manager = SocketClientManager.sharedManager
manager["room1"] = socket1
manager["room2"] = socket2
manager.removeSocket(socket: socket2)
manager["room1"]?.emit("hello")
```
*/
open class SocketClientManager : NSObject {
// MARK: Properties.
/// The shared manager.
@objc
open static let sharedManager = SocketClientManager()
private var sockets = [String: SocketIOClient]()
/// Gets a socket by its name.
///
/// - returns: The socket, if one had the given name.
open subscript(string: String) -> SocketIOClient? {
get {
return sockets[string]
}
set(socket) {
sockets[string] = socket
}
}
// MARK: Methods.
/// Adds a socket.
///
/// - parameter socket: The socket to add.
/// - parameter labeledAs: The label for this socket.
@objc
open func addSocket(_ socket: SocketIOClient, labeledAs label: String) {
sockets[label] = socket
}
/// Removes a socket by a given name.
///
/// - parameter withLabel: The label of the socket to remove.
/// - returns: The socket for the given label, if one was present.
@objc
@discardableResult
open func removeSocket(withLabel label: String) -> SocketIOClient? {
return sockets.removeValue(forKey: label)
}
/// Removes a socket.
///
/// - parameter socket: The socket to remove.
/// - returns: The socket if it was in the manager.
@objc
@discardableResult
open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? {
var returnSocket: SocketIOClient?
for (label, dictSocket) in sockets where dictSocket === socket {
returnSocket = sockets.removeValue(forKey: label)
}
return returnSocket
}
/// Removes all the sockets in the manager.
@objc
open func removeSockets() {
sockets.removeAll()
}
}

View File

@ -63,8 +63,6 @@ extension NSDictionary {
return .log(log) return .log(log)
case let ("logger", logger as SocketLogger): case let ("logger", logger as SocketLogger):
return .logger(logger) return .logger(logger)
case let ("nsp", nsp as String):
return .nsp(nsp)
case let ("path", path as String): case let ("path", path as String):
return .path(path) return .path(path)
case let ("reconnects", reconnects as Bool): case let ("reconnects", reconnects as Bool):

View File

@ -150,10 +150,9 @@ class SocketBasicPacketTest: XCTestCase {
func testBinaryStringPlaceholderInMessage() { func testBinaryStringPlaceholderInMessage() {
let engineString = "52-[\"test\",\"~~0\",{\"num\":0,\"_placeholder\":true},{\"_placeholder\":true,\"num\":1}]" let engineString = "52-[\"test\",\"~~0\",{\"num\":0,\"_placeholder\":true},{\"_placeholder\":true,\"num\":1}]"
let socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!) let manager = SocketManager(socketURL: URL(string: "http://localhost/")!)
socket.setTestable()
var packet = try! socket.parseString(engineString) var packet = try! manager.parseString(engineString)
XCTAssertEqual(packet.event, "test") XCTAssertEqual(packet.event, "test")
_ = packet.addData(data) _ = packet.addData(data)

View File

@ -10,20 +10,9 @@ import XCTest
@testable import SocketIO @testable import SocketIO
class SocketEngineTest: XCTestCase { class SocketEngineTest: XCTestCase {
var client: SocketIOClient!
var engine: SocketEngine!
override func setUp() {
super.setUp()
client = SocketIOClient(socketURL: URL(string: "http://localhost")!)
engine = SocketEngine(client: client, url: URL(string: "http://localhost")!, options: nil)
client.setTestable()
}
func testBasicPollingMessage() { func testBasicPollingMessage() {
let expect = expectation(description: "Basic polling test") let expect = expectation(description: "Basic polling test")
client.on("blankTest") {data, ack in socket.on("blankTest") {data, ack in
expect.fulfill() expect.fulfill()
} }
@ -35,11 +24,11 @@ class SocketEngineTest: XCTestCase {
let finalExpectation = expectation(description: "Final packet in poll test") let finalExpectation = expectation(description: "Final packet in poll test")
var gotBlank = false var gotBlank = false
client.on("blankTest") {data, ack in socket.on("blankTest") {data, ack in
gotBlank = true gotBlank = true
} }
client.on("stringTest") {data, ack in socket.on("stringTest") {data, ack in
if let str = data[0] as? String, gotBlank { if let str = data[0] as? String, gotBlank {
if str == "hello" { if str == "hello" {
finalExpectation.fulfill() finalExpectation.fulfill()
@ -54,7 +43,7 @@ class SocketEngineTest: XCTestCase {
func testEngineDoesErrorOnUnknownTransport() { func testEngineDoesErrorOnUnknownTransport() {
let finalExpectation = expectation(description: "Unknown Transport") let finalExpectation = expectation(description: "Unknown Transport")
client.on("error") {data, ack in socket.on("error") {data, ack in
if let error = data[0] as? String, error == "Unknown transport" { if let error = data[0] as? String, error == "Unknown transport" {
finalExpectation.fulfill() finalExpectation.fulfill()
} }
@ -67,7 +56,7 @@ class SocketEngineTest: XCTestCase {
func testEngineDoesErrorOnUnknownMessage() { func testEngineDoesErrorOnUnknownMessage() {
let finalExpectation = expectation(description: "Engine Errors") let finalExpectation = expectation(description: "Engine Errors")
client.on("error") {data, ack in socket.on("error") {data, ack in
finalExpectation.fulfill() finalExpectation.fulfill()
} }
@ -78,7 +67,7 @@ class SocketEngineTest: XCTestCase {
func testEngineDecodesUTF8Properly() { func testEngineDecodesUTF8Properly() {
let expect = expectation(description: "Engine Decodes utf8") let expect = expectation(description: "Engine Decodes utf8")
client.on("stringTest") {data, ack in socket.on("stringTest") {data, ack in
XCTAssertEqual(data[0] as? String, "lïne one\nlīne \rtwo𦅙𦅛", "Failed string test") XCTAssertEqual(data[0] as? String, "lïne one\nlīne \rtwo𦅙𦅛", "Failed string test")
expect.fulfill() expect.fulfill()
} }
@ -110,7 +99,7 @@ class SocketEngineTest: XCTestCase {
let b64String = "b4aGVsbG8NCg==" let b64String = "b4aGVsbG8NCg=="
let packetString = "451-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]" let packetString = "451-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]"
client.on("test") {data, ack in socket.on("test") {data, ack in
if let data = data[0] as? Data, let string = String(data: data, encoding: .utf8) { if let data = data[0] as? Data, let string = String(data: data, encoding: .utf8) {
XCTAssertEqual(string, "hello") XCTAssertEqual(string, "hello")
} }
@ -123,4 +112,97 @@ class SocketEngineTest: XCTestCase {
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
func testSettingExtraHeadersBeforeConnectSetsEngineExtraHeaders() {
let newValue = ["hello": "world"]
manager.engine = engine
manager.setTestStatus(.notConnected)
manager.config = [.extraHeaders(["new": "value"])]
manager.config.insert(.extraHeaders(newValue), replacing: true)
XCTAssertEqual(2, manager.config.count)
XCTAssertEqual(manager.engine!.extraHeaders!, newValue)
for config in manager.config {
switch config {
case let .extraHeaders(headers):
XCTAssertTrue(headers.keys.contains("hello"), "It should contain hello header key")
XCTAssertFalse(headers.keys.contains("new"), "It should not contain old data")
case .path:
continue
default:
XCTFail("It should only have two configs")
}
}
}
func testSettingExtraHeadersAfterConnectDoesNotIgnoreChanges() {
let newValue = ["hello": "world"]
manager.engine = engine
manager.setTestStatus(.connected)
engine.setConnected(true)
manager.config = [.extraHeaders(["new": "value"])]
manager.config.insert(.extraHeaders(["hello": "world"]), replacing: true)
XCTAssertEqual(2, manager.config.count)
XCTAssertEqual(manager.engine!.extraHeaders!, newValue)
}
func testSettingPathAfterConnectDoesNotIgnoreChanges() {
let newValue = "/newpath/"
manager.engine = engine
manager.setTestStatus(.connected)
engine.setConnected(true)
manager.config.insert(.path(newValue))
XCTAssertEqual(1, manager.config.count)
XCTAssertEqual(manager.engine!.socketPath, newValue)
}
func testSettingCompressAfterConnectDoesNotIgnoreChanges() {
manager.engine = engine
manager.setTestStatus(.connected)
engine.setConnected(true)
manager.config.insert(.compress)
XCTAssertEqual(2, manager.config.count)
XCTAssertTrue(manager.engine!.compress)
}
func testSettingForcePollingAfterConnectDoesNotIgnoreChanges() {
manager.engine = engine
manager.setTestStatus(.connected)
engine.setConnected(true)
manager.config.insert(.forcePolling(true))
XCTAssertEqual(2, manager.config.count)
XCTAssertTrue(manager.engine!.forcePolling)
}
func testSettingForceWebSocketsAfterConnectDoesNotIgnoreChanges() {
manager.engine = engine
manager.setTestStatus(.connected)
engine.setConnected(true)
manager.config.insert(.forceWebsockets(true))
XCTAssertEqual(2, manager.config.count)
XCTAssertTrue(manager.engine!.forceWebsockets)
}
var manager: SocketManager!
var socket: SocketIOClient!
var engine: SocketEngine!
override func setUp() {
super.setUp()
manager = SocketManager(socketURL: URL(string: "http://localhost")!)
socket = manager.defaultSocket
engine = SocketEngine(client: manager, url: URL(string: "http://localhost")!, options: nil)
socket.setTestable()
}
} }

View File

@ -10,14 +10,6 @@ import XCTest
import SocketIO import SocketIO
class TestSocketIOClientConfiguration : XCTestCase { class TestSocketIOClientConfiguration : XCTestCase {
var config = [] as SocketIOClientConfiguration
override func setUp() {
super.setUp()
config = [.log(false), .forceNew(true)]
}
func testReplaceSameOption() { func testReplaceSameOption() {
config.insert(.log(true)) config.insert(.log(true))
@ -43,4 +35,12 @@ class TestSocketIOClientConfiguration: XCTestCase {
XCTFail() XCTFail()
} }
} }
var config = [] as SocketIOClientConfiguration
override func setUp() {
config = [.log(false), .forceNew(true)]
super.setUp()
}
} }

View File

@ -0,0 +1,174 @@
//
// Created by Erik Little on 10/21/17.
//
import Dispatch
import Foundation
@testable import SocketIO
import XCTest
class SocketMangerTest : XCTestCase {
func testManagerProperties() {
XCTAssertNotNil(manager.defaultSocket)
XCTAssertNil(manager.engine)
XCTAssertFalse(manager.forceNew)
XCTAssertEqual(manager.handleQueue, DispatchQueue.main)
XCTAssertTrue(manager.reconnects)
XCTAssertEqual(manager.reconnectWait, 10)
XCTAssertEqual(manager.status, .notConnected)
}
func testManagerCallsConnect() {
setUpSockets()
socket.expectations[ManagerExpectation.didConnectCalled] = expectation(description: "The manager should call connect on the default socket")
socket2.expectations[ManagerExpectation.didConnectCalled] = expectation(description: "The manager should call connect on the socket")
socket.connect()
socket2.connect()
manager.fakeConnecting()
manager.fakeConnecting(toNamespace: "/swift")
waitForExpectations(timeout: 0.3)
}
func testManagerCallsDisconnect() {
setUpSockets()
socket.expectations[ManagerExpectation.didDisconnectCalled] = expectation(description: "The manager should call disconnect on the default socket")
socket2.expectations[ManagerExpectation.didDisconnectCalled] = expectation(description: "The manager should call disconnect on the socket")
socket2.on(clientEvent: .connect) {data, ack in
self.manager.disconnect()
self.manager.fakeDisconnecting()
}
socket.connect()
socket2.connect()
manager.fakeConnecting()
manager.fakeConnecting(toNamespace: "/swift")
waitForExpectations(timeout: 0.3)
}
func testManagerEmitAll() {
setUpSockets()
socket.expectations[ManagerExpectation.emitAllEventCalled] = expectation(description: "The manager should emit an event to the default socket")
socket2.expectations[ManagerExpectation.emitAllEventCalled] = expectation(description: "The manager should emit an event to the socket")
socket2.on(clientEvent: .connect) {data, ack in
self.manager.emitAll("event", "testing")
}
socket.connect()
socket2.connect()
manager.fakeConnecting()
manager.fakeConnecting(toNamespace: "/swift")
waitForExpectations(timeout: 0.3)
}
private func setUpSockets() {
socket = manager.testSocket(forNamespace: "/")
socket2 = manager.testSocket(forNamespace: "/swift")
}
private var manager: TestManager!
private var socket: TestSocket!
private var socket2: TestSocket!
override func setUp() {
super.setUp()
manager = TestManager(socketURL: URL(string: "http://localhost/")!)
socket = nil
socket2 = nil
}
}
public enum ManagerExpectation : String {
case didConnectCalled
case didDisconnectCalled
case emitAllEventCalled
}
public class TestManager : SocketManager {
public override func disconnect() {
setTestStatus(.disconnected)
}
@objc
public func testSocket(forNamespace nsp: String) -> TestSocket {
return socket(forNamespace: nsp) as! TestSocket
}
@objc
public func fakeConnecting(toNamespace nsp: String) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
// Fake connecting
self.parseEngineMessage("0\(nsp)")
}
}
@objc
public func fakeDisconnecting() {
engineDidClose(reason: "")
}
@objc
public func fakeConnecting() {
engineDidOpen(reason: "")
}
public override func socket(forNamespace nsp: String) -> SocketIOClient {
// set socket to our test socket, the superclass method will get this from nsps
nsps[nsp] = TestSocket(manager: self, nsp: nsp)
return super.socket(forNamespace: nsp)
}
}
public class TestSocket : SocketIOClient {
public var expectations = [ManagerExpectation: XCTestExpectation]()
@objc
public var expects = NSMutableDictionary()
public override func didConnect(toNamespace nsp: String) {
expectations[ManagerExpectation.didConnectCalled]?.fulfill()
expectations[ManagerExpectation.didConnectCalled] = nil
if let expect = expects[ManagerExpectation.didConnectCalled.rawValue] as? XCTestExpectation {
expect.fulfill()
expects[ManagerExpectation.didConnectCalled.rawValue] = nil
}
super.didConnect(toNamespace: nsp)
}
public override func didDisconnect(reason: String) {
expectations[ManagerExpectation.didDisconnectCalled]?.fulfill()
expectations[ManagerExpectation.didDisconnectCalled] = nil
if let expect = expects[ManagerExpectation.didDisconnectCalled.rawValue] as? XCTestExpectation {
expect.fulfill()
expects[ManagerExpectation.didDisconnectCalled.rawValue] = nil
}
super.didDisconnect(reason: reason)
}
public override func emit(_ event: String, with items: [Any]) {
expectations[ManagerExpectation.emitAllEventCalled]?.fulfill()
expectations[ManagerExpectation.emitAllEventCalled] = nil
if let expect = expects[ManagerExpectation.emitAllEventCalled.rawValue] as? XCTestExpectation {
expect.fulfill()
expects[ManagerExpectation.emitAllEventCalled.rawValue] = nil
}
}
}

View File

@ -10,26 +10,6 @@ import XCTest
@testable import SocketIO @testable import SocketIO
class SocketParserTest: XCTestCase { class SocketParserTest: XCTestCase {
let testSocket = SocketIOClient(socketURL: URL(string: "http://localhost/")!)
//Format key: message; namespace-data-binary-id
static let packetTypes: [String: (String, [Any], [Data], Int)] = [
"0": ("/", [], [], -1), "1": ("/", [], [], -1),
"25[\"test\"]": ("/", ["test"], [], 5),
"2[\"test\",\"~~0\"]": ("/", ["test", "~~0"], [], -1),
"2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]": ("/swift", ["testArrayEmitReturn", ["test3", "test4"] as NSArray], [], -1),
"51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": ("/swift", ["testMultipleItemsWithBufferEmitReturn", [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], -1),
"3/swift,0[[\"test3\",\"test4\"]]": ("/swift", [["test3", "test4"] as NSArray], [], 0),
"61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]":
("/swift", [ [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], 19),
"4/swift,": ("/swift", [], [], -1),
"0/swift": ("/swift", [], [], -1),
"1/swift": ("/swift", [], [], -1),
"4\"ERROR\"": ("/", ["ERROR"], [], -1),
"4{\"test\":2}": ("/", [["test": 2]], [], -1),
"41": ("/", [1], [], -1),
"4[1, \"hello\"]": ("/", [1, "hello"], [], -1)]
func testDisconnect() { func testDisconnect() {
let message = "1" let message = "1"
validateParseResult(message) validateParseResult(message)
@ -108,7 +88,7 @@ class SocketParserTest: XCTestCase {
func testInvalidInput() { func testInvalidInput() {
let message = "8" let message = "8"
do { do {
let _ = try testSocket.parseString(message) let _ = try testManager.parseString(message)
XCTFail() XCTFail()
} catch { } catch {
@ -125,7 +105,7 @@ class SocketParserTest: XCTestCase {
func validateParseResult(_ message: String) { func validateParseResult(_ message: String) {
let validValues = SocketParserTest.packetTypes[message]! let validValues = SocketParserTest.packetTypes[message]!
let packet = try! testSocket.parseString(message) let packet = try! testManager.parseString(message)
let type = String(message.characters.prefix(1)) let type = String(message.characters.prefix(1))
XCTAssertEqual(packet.type, SocketPacket.PacketType(rawValue: Int(type) ?? -1)!) XCTAssertEqual(packet.type, SocketPacket.PacketType(rawValue: Int(type) ?? -1)!)
@ -139,8 +119,29 @@ class SocketParserTest: XCTestCase {
let keys = Array(SocketParserTest.packetTypes.keys) let keys = Array(SocketParserTest.packetTypes.keys)
measure { measure {
for item in keys.enumerated() { for item in keys.enumerated() {
_ = try! self.testSocket.parseString(item.element) _ = try! self.testManager.parseString(item.element)
} }
} }
} }
let testManager = SocketManager(socketURL: URL(string: "http://localhost/")!)
//Format key: message; namespace-data-binary-id
static let packetTypes: [String: (String, [Any], [Data], Int)] = [
"0": ("/", [], [], -1), "1": ("/", [], [], -1),
"25[\"test\"]": ("/", ["test"], [], 5),
"2[\"test\",\"~~0\"]": ("/", ["test", "~~0"], [], -1),
"2/swift,[\"testArrayEmitReturn\",[\"test3\",\"test4\"]]": ("/swift", ["testArrayEmitReturn", ["test3", "test4"] as NSArray], [], -1),
"51-/swift,[\"testMultipleItemsWithBufferEmitReturn\",[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]": ("/swift", ["testMultipleItemsWithBufferEmitReturn", [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], -1),
"3/swift,0[[\"test3\",\"test4\"]]": ("/swift", [["test3", "test4"] as NSArray], [], 0),
"61-/swift,19[[1,2],{\"test\":\"bob\"},25,\"polo\",{\"_placeholder\":true,\"num\":0}]":
("/swift", [ [1, 2] as NSArray, ["test": "bob"] as NSDictionary, 25, "polo", ["_placeholder": true, "num": 0] as NSDictionary], [], 19),
"4/swift,": ("/swift", [], [], -1),
"0/swift": ("/swift", [], [], -1),
"1/swift": ("/swift", [], [], -1),
"4\"ERROR\"": ("/", ["ERROR"], [], -1),
"4{\"test\":2}": ("/", [["test": 2]], [], -1),
"41": ("/", [1], [], -1),
"4[1, \"hello\"]": ("/", [1, "hello"], [], -1)
]
} }

View File

@ -34,7 +34,7 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.parseSocketMessage("30[\"hello world\"]") manager.parseEngineMessage("30[\"hello world\"]")
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
@ -45,8 +45,8 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.parseSocketMessage("61-0[{\"_placeholder\":true,\"num\":0},{\"test\":true}]") manager.parseEngineMessage("61-0[{\"_placeholder\":true,\"num\":0},{\"test\":true}]")
socket.parseBinaryData(Data()) manager.parseEngineBinaryData(Data())
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
@ -57,7 +57,7 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.parseSocketMessage("2[\"test\",\"hello world\"]") manager.parseEngineMessage("2[\"test\",\"hello world\"]")
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
@ -68,7 +68,7 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.parseSocketMessage("2[\"test\",\"\\\"hello world\\\"\"]") manager.parseEngineMessage("2[\"test\",\"\\\"hello world\\\"\"]")
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
@ -80,7 +80,7 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.parseSocketMessage("2[\"test\",\"hello world\"]") manager.parseEngineMessage("2[\"test\",\"hello world\"]")
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
@ -96,7 +96,7 @@ class SocketSideEffectTest: XCTestCase {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
// Fake connecting // Fake connecting
self.socket.parseEngineMessage("0/") self.manager.parseEngineMessage("0/")
} }
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
@ -136,59 +136,47 @@ class SocketSideEffectTest: XCTestCase {
} }
} }
socket.parseSocketMessage("4\"test error\"") manager.parseEngineMessage("4\"test error\"")
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
func testHandleBinaryEvent() { func testHandleBinaryEvent() {
let expect = expectation(description: "handled binary event") let expect = expectation(description: "handled binary event")
socket.on("test") {data, ack in socket.on("test") {data, ack in
if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData { if let dict = data[0] as? [String: Any], let data = dict["test"] as? Data {
XCTAssertEqual(data as Data, self.data) XCTAssertEqual(data as Data, self.data)
expect.fulfill() expect.fulfill()
} }
} }
socket.parseSocketMessage("51-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]") manager.parseEngineMessage("51-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0}}]")
socket.parseBinaryData(data) manager.parseEngineBinaryData(data)
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
func testHandleMultipleBinaryEvent() { func testHandleMultipleBinaryEvent() {
let expect = expectation(description: "handled multiple binary event") let expect = expectation(description: "handled multiple binary event")
socket.on("test") {data, ack in socket.on("test") {data, ack in
if let dict = data[0] as? NSDictionary, let data = dict["test"] as? NSData, if let dict = data[0] as? [String: Any], let data = dict["test"] as? Data,
let data2 = dict["test2"] as? NSData { let data2 = dict["test2"] as? Data {
XCTAssertEqual(data as Data, self.data) XCTAssertEqual(data as Data, self.data)
XCTAssertEqual(data2 as Data, self.data2) XCTAssertEqual(data2 as Data, self.data2)
expect.fulfill() expect.fulfill()
} }
} }
socket.parseSocketMessage("52-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0},\"test2\":{\"_placeholder\":true,\"num\":1}}]") manager.parseEngineMessage("52-[\"test\",{\"test\":{\"_placeholder\":true,\"num\":0},\"test2\":{\"_placeholder\":true,\"num\":1}}]")
socket.parseBinaryData(data) manager.parseEngineBinaryData(data)
socket.parseBinaryData(data2) manager.parseEngineBinaryData(data2)
waitForExpectations(timeout: 3, handler: nil) waitForExpectations(timeout: 3, handler: nil)
} }
func testSocketManager() {
let manager = SocketClientManager.sharedManager
manager["test"] = socket
XCTAssert(manager["test"] === socket, "failed to get socket")
manager["test"] = nil
XCTAssert(manager["test"] == nil, "socket not removed")
}
func testChangingStatusCallsStatusChangeHandler() { func testChangingStatusCallsStatusChangeHandler() {
let expect = expectation(description: "The client should announce when the status changes") let expect = expectation(description: "The client should announce when the status changes")
let statusChange = SocketIOClientStatus.connecting let statusChange = SocketIOStatus.connecting
socket.on("statusChange") {data, ack in socket.on("statusChange") {data, ack in
guard let status = data[0] as? SocketIOClientStatus else { guard let status = data[0] as? SocketIOStatus else {
XCTFail("Status should be one of the defined statuses") XCTFail("Status should be one of the defined statuses")
return return
@ -251,9 +239,9 @@ class SocketSideEffectTest: XCTestCase {
func testConnectTimesOutIfNotConnected() { func testConnectTimesOutIfNotConnected() {
let expect = expectation(description: "The client should call the timeout function") let expect = expectation(description: "The client should call the timeout function")
socket = manager.socket(forNamespace: "/someNamespace")
socket.setTestStatus(.notConnected) socket.setTestStatus(.notConnected)
socket.nsp = "/someNamespace" manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil)
socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil)
socket.connect(timeoutAfter: 0.5, withHandler: { socket.connect(timeoutAfter: 0.5, withHandler: {
expect.fulfill() expect.fulfill()
@ -266,7 +254,7 @@ class SocketSideEffectTest: XCTestCase {
let expect = expectation(description: "The client should not call the timeout function") let expect = expectation(description: "The client should not call the timeout function")
socket.setTestStatus(.notConnected) socket.setTestStatus(.notConnected)
socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil)
socket.on(clientEvent: .connect) {data, ack in socket.on(clientEvent: .connect) {data, ack in
expect.fulfill() expect.fulfill()
@ -278,7 +266,7 @@ class SocketSideEffectTest: XCTestCase {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
// Fake connecting // Fake connecting
self.socket.parseEngineMessage("0/") self.manager.parseEngineMessage("0/")
} }
waitForExpectations(timeout: 2) waitForExpectations(timeout: 2)
@ -288,7 +276,7 @@ class SocketSideEffectTest: XCTestCase {
let expect = expectation(description: "The client call the connect handler") let expect = expectation(description: "The client call the connect handler")
socket.setTestStatus(.notConnected) socket.setTestStatus(.notConnected)
socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil) manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil)
socket.on(clientEvent: .connect) {data, ack in socket.on(clientEvent: .connect) {data, ack in
expect.fulfill() expect.fulfill()
@ -305,9 +293,9 @@ class SocketSideEffectTest: XCTestCase {
let expect = expectation(description: "The client should not call the timeout function") let expect = expectation(description: "The client should not call the timeout function")
let nspString = "/swift" let nspString = "/swift"
socket = manager.socket(forNamespace: "/swift")
socket.setTestStatus(.notConnected) socket.setTestStatus(.notConnected)
socket.nsp = nspString manager.engine = TestEngine(client: manager, url: manager.socketURL, options: nil)
socket.engine = TestEngine(client: socket, url: socket.socketURL, options: nil)
socket.on(clientEvent: .connect) {data, ack in socket.on(clientEvent: .connect) {data, ack in
guard let nsp = data[0] as? String else { guard let nsp = data[0] as? String else {
@ -327,7 +315,7 @@ class SocketSideEffectTest: XCTestCase {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
// Fake connecting // Fake connecting
self.socket.parseEngineMessage("0/swift") self.manager.parseEngineMessage("0/swift")
} }
waitForExpectations(timeout: 2) waitForExpectations(timeout: 2)
@ -377,39 +365,31 @@ class SocketSideEffectTest: XCTestCase {
func testSettingConfigAfterInit() { func testSettingConfigAfterInit() {
socket.setTestStatus(.notConnected) socket.setTestStatus(.notConnected)
socket.config.insert(.log(true)) manager.config.insert(.log(true))
XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to true after creation") XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to true after creation")
socket.config = [.log(false), .nsp("/test")] manager.config = [.log(false)]
XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation") XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation")
XCTAssertEqual(socket.nsp, "/test", "It should set the namespace after creation")
} }
func testSettingExtraHeadersAfterInit() { func testSettingConfigAfterDisconnect() {
socket.setTestStatus(.notConnected) socket.setTestStatus(.disconnected)
socket.config = [.extraHeaders(["new": "value"])] manager.config.insert(.log(true))
socket.config.insert(.extraHeaders(["hello": "world"]), replacing: true)
for config in socket.config { XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to true after creation")
switch config {
case let .extraHeaders(headers):
XCTAssertTrue(headers.keys.contains("hello"), "It should contain hello header key")
XCTAssertFalse(headers.keys.contains("new"), "It should not contain old data")
case .path:
continue
default:
XCTFail("It should only have two configs")
}
}
}
func testSettingConfigAfterInitWhenConnectedIgnoresChanges() { manager.config = [.log(false)]
socket.config = [.log(true), .nsp("/test")]
XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation") XCTAssertFalse(DefaultSocketLogger.Logger.log, "It should set logging to false after creation")
XCTAssertEqual(socket.nsp, "/", "It should set the namespace after creation") }
func testSettingConfigAfterInitWhenConnectedDoesNotIgnoreChanges() {
manager.connect()
manager.config = [.log(true)]
XCTAssertTrue(DefaultSocketLogger.Logger.log, "It should set logging to false after creation")
} }
func testClientCallsSentPingHandler() { func testClientCallsSentPingHandler() {
@ -419,7 +399,7 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.engineDidSendPing() manager.engineDidSendPing()
waitForExpectations(timeout: 0.2) waitForExpectations(timeout: 0.2)
} }
@ -431,18 +411,22 @@ class SocketSideEffectTest: XCTestCase {
expect.fulfill() expect.fulfill()
} }
socket.engineDidReceivePong() manager.engineDidReceivePong()
waitForExpectations(timeout: 0.2) waitForExpectations(timeout: 0.2)
} }
let data = "test".data(using: String.Encoding.utf8)! let data = "test".data(using: String.Encoding.utf8)!
let data2 = "test2".data(using: String.Encoding.utf8)! let data2 = "test2".data(using: String.Encoding.utf8)!
private var manager: SocketManager!
private var socket: SocketIOClient! private var socket: SocketIOClient!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
socket = SocketIOClient(socketURL: URL(string: "http://localhost/")!)
manager = SocketManager(socketURL: URL(string: "http://localhost/")!)
socket = manager.defaultSocket
socket.setTestable() socket.setTestable()
} }
} }
@ -461,6 +445,7 @@ struct ThrowingData : SocketData {
class TestEngine : SocketEngineSpec { class TestEngine : SocketEngineSpec {
weak var client: SocketEngineClient? weak var client: SocketEngineClient?
private(set) var closed = false private(set) var closed = false
private(set) var compress = false
private(set) var connected = false private(set) var connected = false
var connectParams: [String: Any]? = nil var connectParams: [String: Any]? = nil
private(set) var cookies: [HTTPCookie]? = nil private(set) var cookies: [HTTPCookie]? = nil

View File

@ -0,0 +1,16 @@
//
// Created by Erik Little on 10/21/17.
//
#import "SocketIO_Tests-Swift.h"
@import XCTest;
@import SocketIO;
@interface ManagerObjectiveCTest : XCTestCase
@property TestSocket* socket;
@property TestSocket* socket2;
@property TestManager* manager;
@end

View File

@ -0,0 +1,115 @@
//
// Created by Erik Little on 10/21/17.
//
#import "ManagerObjectiveCTest.h"
@import Dispatch;
@import Foundation;
@import XCTest;
@import SocketIO;
@implementation ManagerObjectiveCTest
- (void)testManagerProperties {
XCTAssertNotNil(self.manager.defaultSocket);
XCTAssertNil(self.manager.engine);
XCTAssertFalse(self.manager.forceNew);
XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue());
XCTAssertTrue(self.manager.reconnects);
XCTAssertEqual(self.manager.reconnectWait, 10);
XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected);
}
- (void)testConnectSocketSyntax {
[self setUpSockets];
[self.manager connectSocket:self.socket];
}
- (void)testDisconnectSocketSyntax {
[self setUpSockets];
[self.manager disconnectSocket:self.socket];
}
- (void)testSocketForNamespaceSyntax {
SocketIOClient* client = [self.manager socketForNamespace:@"/swift"];
client = nil;
}
- (void)testManagerCallsConnect {
[self setUpSockets];
XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call connect on the default socket"];
XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call connect on the socket"];
self.socket.expects[@"didConnectCalled"] = expect;
self.socket2.expects[@"didConnectCalled"] = expect2;
[self.socket connect];
[self.socket2 connect];
[self.manager fakeConnecting];
[self.manager fakeConnectingToNamespace:@"/swift"];
[self waitForExpectationsWithTimeout:0.3 handler:nil];
}
- (void)testManagerCallsDisconnect {
[self setUpSockets];
XCTestExpectation* expect = [self expectationWithDescription:@"The manager should call disconnect on the default socket"];
XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should call disconnect on the socket"];
self.socket.expects[@"didDisconnectCalled"] = expect;
self.socket2.expects[@"didDisconnectCalled"] = expect2;
[self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) {
[self.manager disconnect];
[self.manager fakeDisconnecting];
}];
[self.socket connect];
[self.socket2 connect];
[self.manager fakeConnecting];
[self.manager fakeConnectingToNamespace:@"/swift"];
[self waitForExpectationsWithTimeout:0.3 handler:nil];
}
- (void)testManagerEmitAll {
[self setUpSockets];
XCTestExpectation* expect = [self expectationWithDescription:@"The manager should emit an event to the default socket"];
XCTestExpectation* expect2 = [self expectationWithDescription:@"The manager should emit an event to the socket"];
self.socket.expects[@"emitAllEventCalled"] = expect;
self.socket2.expects[@"emitAllEventCalled"] = expect2;
[self.socket2 on:@"connect" callback:^(NSArray* data, SocketAckEmitter* ack) {
[self.manager emitAll:@"event" withItems:@[@"testing"]];
}];
[self.socket connect];
[self.socket2 connect];
[self.manager fakeConnecting];
[self.manager fakeConnectingToNamespace:@"/swift"];
[self waitForExpectationsWithTimeout:0.3 handler:nil];
}
- (void)setUpSockets {
self.socket = [self.manager testSocketForNamespace:@"/"];
self.socket2 = [self.manager testSocketForNamespace:@"/swift"];
}
- (void)setUp {
[super setUp];
NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"];
self.manager = [[TestManager alloc] initWithSocketURL:url config:nil];
self.socket = nil;
self.socket2 = nil;
}
@end

View File

@ -0,0 +1,16 @@
//
// Created by Erik Little on 10/21/17.
//
@import Dispatch;
@import Foundation;
@import XCTest;
@import SocketIO;
@interface SocketObjectiveCTest : XCTestCase
@property SocketIOClient* socket;
@property SocketManager* manager;
@end

View File

@ -7,36 +7,20 @@
// Merely tests whether the Objective-C api breaks // Merely tests whether the Objective-C api breaks
// //
#import "SocketObjectiveCTest.h"
@import Dispatch; @import Dispatch;
@import Foundation; @import Foundation;
@import XCTest; @import XCTest;
@import SocketIO; @import SocketIO;
@interface SocketObjectiveCTest : XCTestCase // TODO Manager interface tests
@property SocketIOClient* socket;
@end
@implementation SocketObjectiveCTest @implementation SocketObjectiveCTest
- (void)setUp {
[super setUp];
NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"];
self.socket = [[SocketIOClient alloc] initWithSocketURL:url config:@{@"log": @NO, @"forcePolling": @YES}];
}
- (void)testProperties { - (void)testProperties {
NSURL* url = nil;
url = self.socket.socketURL;
self.socket.forceNew = false;
self.socket.handleQueue = dispatch_get_main_queue();
self.socket.nsp = @"/objective-c"; self.socket.nsp = @"/objective-c";
self.socket.reconnects = false; if (self.socket.status == SocketIOStatusConnected) { }
self.socket.reconnectWait = 1;
if (self.socket.status == SocketIOClientStatusConnected) { }
if (self.socket.engine == NULL) { }
} }
- (void)testOnSyntax { - (void)testOnSyntax {
@ -62,7 +46,7 @@
} }
- (void)testJoinNamespaceSyntax { - (void)testJoinNamespaceSyntax {
[self.socket joinNamespace:@"/objective-c"]; [self.socket joinNamespace];
} }
- (void)testOnAnySyntax { - (void)testOnAnySyntax {
@ -74,10 +58,6 @@
}]; }];
} }
- (void)testReconnectSyntax {
[self.socket reconnect];
}
- (void)testRemoveAllHandlersSyntax { - (void)testRemoveAllHandlersSyntax {
[self.socket removeAllHandlers]; [self.socket removeAllHandlers];
} }
@ -94,15 +74,16 @@
[self.socket off:@"test"]; [self.socket off:@"test"];
} }
- (void)testSocketManager {
SocketClientManager* manager = [SocketClientManager sharedManager];
[manager addSocket:self.socket labeledAs:@"test"];
[manager removeSocketWithLabel:@"test"];
}
- (void)testSSLSecurity { - (void)testSSLSecurity {
SSLSecurity* sec = [[SSLSecurity alloc] initWithUsePublicKeys:0]; SSLSecurity* sec = [[SSLSecurity alloc] initWithUsePublicKeys:0];
sec = nil; sec = nil;
} }
- (void)setUp {
[super setUp];
NSURL* url = [[NSURL alloc] initWithString:@"http://localhost"];
self.manager = [[SocketManager alloc] initWithSocketURL:url config:nil];
self.socket = [self.manager defaultSocket];
}
@end @end

View File

@ -13,11 +13,11 @@ One of the most common reasons your event might not be called is if the client i
Take this code for example: Take this code for example:
```swift ```swift
class SocketManager { class Manager {
func addHandlers() { func addHandlers() {
let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!) let manager = SocketManager(socketURL: URL(string: "http://somesocketioserver.com")!)
socket.on("myEvent") {data, ack in manager.defaultSocket.on("myEvent") {data, ack in
print(data) print(data)
} }
} }
@ -25,30 +25,20 @@ class SocketManager {
} }
``` ```
This code is **incorrect**, and the event handler will never be called. Because as soon as this method is called `socket` This code is **incorrect**, and the event handler will never be called. Because as soon as this method is called `manager`
will be released and its memory reclaimed. will be released, along with the socket, and its memory reclaimed.
A correct way would be: A correct way would be:
```swift ```swift
class SocketManager { class Manager {
let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!) let manager = SocketManager(socketURL: URL(string: "http://somesocketioserver.com")!)
func addHandlers() { func addHandlers() {
socket.on("myEvent") {data, ack in manager.defaultSocket.on("myEvent") {data, ack in
print(data) print(data)
} }
} }
} }
``` ```
------
Another case where this might happen is if you use namespaces in your socket.io application.
In the JavaScript client a url that looks like `http://somesocketioserver.com/client` would be done with the `nsp` config.
```swift
let socket = SocketIOClient(socketURL: URL(string: "http://somesocketioserver.com")!, config: [.nsp("/client")])
```