websocket and user center

This commit is contained in:
lianxiang
2018-09-03 14:43:21 +08:00
parent e64e011d8f
commit 3a3e6eeee9
92 changed files with 11993 additions and 2376 deletions
@@ -0,0 +1,145 @@
//
// SocketAckEmitter.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 9/16/15.
//
// 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 class that represents a waiting ack call.
///
/// **NOTE**: You should not store this beyond the life of the event handler.
public final class SocketAckEmitter : NSObject {
private unowned let socket: SocketIOClient
private let ackNum: Int
/// A view into this emitter where emits do not check for binary data.
///
/// Usage:
///
/// ```swift
/// ack.rawEmitView.with(myObject)
/// ```
///
/// **NOTE**: It is not safe to hold on to this view beyond the life of the socket.
@objc
public private(set) lazy var rawEmitView = SocketRawAckView(socket: socket, ackNum: ackNum)
// MARK: Properties
/// If true, this handler is expecting to be acked. Call `with(_: SocketData...)` to ack.
public var expected: Bool {
return ackNum != -1
}
// MARK: Initializers
/// Creates a new `SocketAckEmitter`.
///
/// - parameter socket: The socket for this emitter.
/// - parameter ackNum: The ack number for this emitter.
public init(socket: SocketIOClient, ackNum: Int) {
self.socket = socket
self.ackNum = ackNum
}
// MARK: Methods
/// Call to ack receiving this event.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[ackNum, items, theError]`
///
/// - parameter items: A variable number of items to send when acking.
public func with(_ items: SocketData...) {
guard ackNum != -1 else { return }
do {
socket.emitAck(ackNum, with: try items.map({ try $0.socketRepresentation() }))
} catch {
socket.handleClientEvent(.error, data: [ackNum, items, error])
}
}
/// Call to ack receiving this event.
///
/// - parameter items: An array of items to send when acking. Use `[]` to send nothing.
@objc
public func with(_ items: [Any]) {
guard ackNum != -1 else { return }
socket.emitAck(ackNum, with: items)
}
}
/// A class that represents an emit that will request an ack that has not yet been sent.
/// Call `timingOut(after:callback:)` to complete the emit
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent").timingOut(after: 1) {data in
/// ...
/// }
/// ```
public final class OnAckCallback : NSObject {
private let ackNumber: Int
private let binary: Bool
private let items: [Any]
private weak var socket: SocketIOClient?
init(ackNumber: Int, items: [Any], socket: SocketIOClient, binary: Bool = true) {
self.ackNumber = ackNumber
self.items = items
self.socket = socket
self.binary = binary
}
deinit {
DefaultSocketLogger.Logger.log("OnAckCallback for \(ackNumber) being released", type: "OnAckCallback")
}
// MARK: Methods
/// Completes an emitWithAck. If this isn't called, the emit never happens.
///
/// - parameter seconds: The number of seconds before this emit times out if an ack hasn't been received.
/// - parameter callback: The callback called when an ack is received, or when a timeout happens.
/// To check for timeout, use `SocketAckStatus`'s `noAck` case.
@objc
public func timingOut(after seconds: Double, callback: @escaping AckCallback) {
guard let socket = self.socket, ackNumber != -1 else { return }
socket.ackHandlers.addAck(ackNumber, callback: callback)
socket.emit(items, ack: ackNumber, binary: binary)
guard seconds != 0 else { return }
socket.manager?.handleQueue.asyncAfter(deadline: DispatchTime.now() + seconds) {[weak socket] in
guard let socket = socket else { return }
socket.ackHandlers.timeoutAck(self.ackNumber)
}
}
}
@@ -0,0 +1,77 @@
//
// SocketAckManager.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 4/3/15.
//
// 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
/// The status of an ack.
public enum SocketAckStatus : String {
// MARK: Cases
/// The ack timed out.
case noAck = "NO ACK"
}
private struct SocketAck : Hashable {
let ack: Int
var callback: AckCallback!
var hashValue: Int {
return ack.hashValue
}
init(ack: Int) {
self.ack = ack
}
init(ack: Int, callback: @escaping AckCallback) {
self.ack = ack
self.callback = callback
}
fileprivate static func <(lhs: SocketAck, rhs: SocketAck) -> Bool {
return lhs.ack < rhs.ack
}
fileprivate static func ==(lhs: SocketAck, rhs: SocketAck) -> Bool {
return lhs.ack == rhs.ack
}
}
class SocketAckManager {
private var acks = Set<SocketAck>(minimumCapacity: 1)
func addAck(_ ack: Int, callback: @escaping AckCallback) {
acks.insert(SocketAck(ack: ack, callback: callback))
}
/// Should be called on handle queue
func executeAck(_ ack: Int, with items: [Any]) {
acks.remove(SocketAck(ack: ack))?.callback(items)
}
/// Should be called on handle queue
func timeoutAck(_ ack: Int) {
acks.remove(SocketAck(ack: ack))?.callback?([SocketAckStatus.noAck.rawValue])
}
}
@@ -0,0 +1,48 @@
//
// SocketAnyEvent.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 3/28/15.
//
// 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
/// Represents some event that was received.
public final class SocketAnyEvent : NSObject {
// MARK: Properties
/// The event name.
@objc
public let event: String
/// The data items for this event.
@objc
public let items: [Any]?
/// The description of this event.
override public var description: String {
return "SocketAnyEvent: Event: \(event) items: \(String(describing: items))"
}
init(event: String, items: [Any]?) {
self.event = event
self.items = items
}
}
@@ -0,0 +1,50 @@
//
// EventHandler.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 1/18/15.
//
// 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
/// A wrapper around a handler.
public struct SocketEventHandler {
// MARK: Properties
/// The event for this handler.
public let event: String
/// A unique identifier for this handler.
public let id: UUID
/// The actual handler function.
public let callback: NormalCallback
// MARK: Methods
/// Causes this handler to be executed.
///
/// - parameter with: The data that this handler should be called with.
/// - parameter withAck: The ack number that this event expects. Pass -1 to say this event doesn't expect an ack.
/// - parameter withSocket: The socket that is calling this event.
public func executeCallback(with items: [Any], withAck ack: Int, withSocket socket: SocketIOClient) {
callback(items, SocketAckEmitter(socket: socket, ackNum: ack))
}
}
@@ -0,0 +1,535 @@
//
// SocketIOClient.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 11/23/14.
//
// 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
/// Represents a socket.io-client.
///
/// Clients are created through a `SocketManager`, which owns the `SocketEngineSpec` that controls the connection to the server.
///
/// For example:
///
/// ```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
/// The namespace that this socket is currently connected to.
///
/// **Must** start with a `/`.
@objc
public let nsp: String
/// The session id of this client.
@objc
public var sid: String {
guard let engine = manager?.engine else { return "" }
return nsp == "/" ? engine.sid : "\(nsp)#\(engine.sid)"
}
/// A handler that will be called on any event.
public private(set) var anyHandler: ((SocketAnyEvent) -> ())?
/// The array of handlers for this socket.
public private(set) var handlers = [SocketEventHandler]()
/// The manager for this socket.
@objc
public private(set) weak var manager: SocketManagerSpec?
/// A view into this socket where emits do not check for binary data.
///
/// Usage:
///
/// ```swift
/// socket.rawEmitView.emit("myEvent", myObject)
/// ```
///
/// **NOTE**: It is not safe to hold on to this view beyond the life of the socket.
@objc
public private(set) lazy var rawEmitView = SocketRawView(socket: self)
/// The status of this client.
@objc
public private(set) var status = SocketIOStatus.notConnected {
didSet {
handleClientEvent(.statusChange, data: [status])
}
}
let ackHandlers = SocketAckManager()
private(set) var currentAck = -1
private lazy var logType = "SocketIOClient{\(nsp)}"
// MARK: Initializers
/// Type safe way to create a new SocketIOClient. `opts` can be omitted.
///
/// - parameter manager: The manager for this socket.
/// - parameter nsp: The namespace of the socket.
@objc
public init(manager: SocketManagerSpec, nsp: String) {
self.manager = manager
self.nsp = nsp
super.init()
}
deinit {
DefaultSocketLogger.Logger.log("Client is being released", type: logType)
}
// MARK: Methods
/// 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.
@objc
open func connect() {
connect(timeoutAfter: 0, withHandler: nil)
}
/// Connect to the server. If we aren't connected after `timeoutAfter` seconds, then `withHandler` is called.
///
/// Only call after adding your event listeners, unless you know what you're doing.
///
/// - parameter timeoutAfter: The number of seconds after which if we are not connected we assume the connection
/// has failed. Pass 0 to never timeout.
/// - parameter handler: The handler to call when the client fails to connect.
@objc
open func connect(timeoutAfter: Double, withHandler handler: (() -> ())?) {
assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)")
guard let manager = self.manager, status != .connected else {
DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType)
return
}
status = .connecting
joinNamespace()
if manager.status == .connected && nsp == "/" {
// We might not get a connect event for the default nsp, fire immediately
didConnect(toNamespace: nsp)
return
}
guard timeoutAfter != 0 else { return }
manager.handleQueue.asyncAfter(deadline: DispatchTime.now() + timeoutAfter) {[weak self] in
guard let this = self, this.status == .connecting || this.status == .notConnected else { return }
this.status = .disconnected
this.leaveNamespace()
handler?()
}
}
func createOnAck(_ items: [Any], binary: Bool = true) -> OnAckCallback {
currentAck += 1
return OnAckCallback(ackNumber: currentAck, items: items, socket: self)
}
/// Called when the client connects to a namespace. If the client was created with a namespace upfront,
/// then this is only called when the client connects to that namespace.
///
/// - parameter toNamespace: The namespace that was connected to.
open func didConnect(toNamespace namespace: String) {
guard status != .connected else { return }
DefaultSocketLogger.Logger.log("Socket connected", type: logType)
status = .connected
handleClientEvent(.connect, data: [namespace])
}
/// Called when the client has disconnected from socket.io.
///
/// - parameter reason: The reason for the disconnection.
open func didDisconnect(reason: String) {
guard status != .disconnected else { return }
DefaultSocketLogger.Logger.log("Disconnected: \(reason)", type: logType)
status = .disconnected
handleClientEvent(.disconnect, data: [reason])
}
/// 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
open func disconnect() {
DefaultSocketLogger.Logger.log("Closing socket", type: logType)
leaveNamespace()
}
/// Send an event to the server, with optional data items.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
open func emit(_ event: String, _ items: SocketData...) {
do {
try emit(event, with: items.map({ try $0.socketRepresentation() }))
} catch {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: logType)
handleClientEvent(.error, data: [event, items, error])
}
}
/// Same as emit, but meant for Objective-C
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. Send an empty array to send no data.
@objc
open func emit(_ event: String, with items: [Any]) {
emit([event] + items)
}
/// Sends a message to the server, requesting an ack.
///
/// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack.
/// Check that your server's api will ack the event being sent.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent", 1).timingOut(after: 1) {data in
/// ...
/// }
/// ```
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
/// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent.
open func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback {
do {
return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() }))
} catch {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: logType)
handleClientEvent(.error, data: [event, items, error])
return OnAckCallback(ackNumber: -1, items: [], socket: self)
}
}
/// Same as emitWithAck, but for Objective-C
///
/// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack.
/// Check that your server's api will ack the event being sent.
///
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent", with: [1]).timingOut(after: 1) {data in
/// ...
/// }
/// ```
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. Use `[]` to send nothing.
/// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent.
@objc
open func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback {
return createOnAck([event] + items)
}
func emit(_ data: [Any], ack: Int? = nil, binary: Bool = true, isAck: Bool = false) {
guard status == .connected else {
handleClientEvent(.error, data: ["Tried emitting when not connected"])
return
}
let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: isAck, checkForBinary: binary)
let str = packet.packetString
DefaultSocketLogger.Logger.log("Emitting: \(str), Ack: \(isAck)", type: logType)
manager?.engine?.send(str, withData: packet.binary)
}
/// Call when you wish to tell the server that you've received the event for `ack`.
///
/// **You shouldn't need to call this directly.** Instead use an `SocketAckEmitter` that comes in an event callback.
///
/// - parameter ack: The ack number.
/// - parameter with: The data for this ack.
open func emitAck(_ ack: Int, with items: [Any]) {
emit(items, ack: ack, binary: true, isAck: true)
}
/// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called.
///
/// - parameter ack: The number for this ack.
/// - parameter data: The data sent back with this ack.
@objc
open func handleAck(_ ack: Int, data: [Any]) {
guard status == .connected else { return }
DefaultSocketLogger.Logger.log("Handling ack: \(ack) with data: \(data)", type: logType)
ackHandlers.executeAck(ack, with: data)
}
/// 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.
///
/// - parameter event: The name of the event.
/// - parameter data: The data that was sent with this event.
/// - parameter isInternalMessage: Whether this event was sent internally. If `true` it is always sent to handlers.
/// - parameter ack: If > 0 then this event expects to get an ack back from the client.
@objc
open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) {
guard status == .connected || isInternalMessage else { return }
DefaultSocketLogger.Logger.log("Handling event: \(event) with data: \(data)", type: logType)
anyHandler?(SocketAnyEvent(event: event, items: data))
for handler in handlers where handler.event == event {
handler.executeCallback(with: data, withAck: ack, withSocket: self)
}
}
/// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the
/// socket.
///
/// - parameter packet: The packet to handle.
open func handlePacket(_ packet: SocketPacket) {
guard packet.nsp == nsp else { return }
switch packet.type {
case .event, .binaryEvent:
handleEvent(packet.event, data: packet.args, isInternalMessage: false, withAck: packet.id)
case .ack, .binaryAck:
handleAck(packet.id, data: packet.data)
case .connect:
didConnect(toNamespace: nsp)
case .disconnect:
didDisconnect(reason: "Got Disconnect")
case .error:
handleEvent("error", data: packet.data, isInternalMessage: true, withAck: packet.id)
}
}
/// Call when you wish to leave a namespace and disconnect this socket.
@objc
open func leaveNamespace() {
manager?.disconnectSocket(self)
}
/// Joins `nsp`.
@objc
open func joinNamespace() {
DefaultSocketLogger.Logger.log("Joining namespace \(nsp)", type: logType)
manager?.connectSocket(self)
}
/// Removes handler(s) for a client event.
///
/// If you wish to remove a client event handler, call the `off(id:)` with the UUID received from its `on` call.
///
/// - parameter clientEvent: The event to remove handlers for.
open func off(clientEvent event: SocketClientEvent) {
off(event.rawValue)
}
/// Removes handler(s) based on an event name.
///
/// If you wish to remove a specific event, call the `off(id:)` with the UUID received from its `on` call.
///
/// - parameter event: The event to remove handlers for.
@objc
open func off(_ event: String) {
DefaultSocketLogger.Logger.log("Removing handler for event: \(event)", type: logType)
handlers = handlers.filter({ $0.event != event })
}
/// Removes a handler with the specified UUID gotten from an `on` or `once`
///
/// If you want to remove all events for an event, call the off `off(_:)` method with the event name.
///
/// - parameter id: The UUID of the handler you wish to remove.
@objc
open func off(id: UUID) {
DefaultSocketLogger.Logger.log("Removing handler with id: \(id)", type: logType)
handlers = handlers.filter({ $0.id != id })
}
/// Adds a handler for an event.
///
/// - parameter event: The event name for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
@objc
@discardableResult
open func on(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding handler for event: \(event)", type: logType)
let handler = SocketEventHandler(event: event, id: UUID(), callback: callback)
handlers.append(handler)
return handler.id
}
/// Adds a handler for a client event.
///
/// Example:
///
/// ```swift
/// socket.on(clientEvent: .connect) {data, ack in
/// ...
/// }
/// ```
///
/// - parameter event: The event for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
@discardableResult
open func on(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID {
return on(event.rawValue, callback: callback)
}
/// Adds a single-use handler for a client event.
///
/// - parameter clientEvent: The event for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
@discardableResult
open func once(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID {
return once(event.rawValue, callback: callback)
}
/// Adds a single-use handler for an event.
///
/// - parameter event: The event name for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
@objc
@discardableResult
open func once(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding once handler for event: \(event)", type: logType)
let id = UUID()
let handler = SocketEventHandler(event: event, id: id) {[weak self] data, ack in
guard let this = self else { return }
this.off(id: id)
callback(data, ack)
}
handlers.append(handler)
return handler.id
}
/// Adds a handler that will be called on every event.
///
/// - parameter handler: The callback that will execute whenever an event is received.
@objc
open func onAny(_ handler: @escaping (SocketAnyEvent) -> ()) {
anyHandler = handler
}
/// Tries to reconnect to the server.
@objc
@available(*, unavailable, message: "Call the manager's reconnect method")
open func reconnect() { }
/// Removes all handlers.
///
/// Can be used after disconnecting to break any potential remaining retain cycles.
@objc
open func removeAllHandlers() {
handlers.removeAll(keepingCapacity: false)
}
/// 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 reconnecting.
@objc
open func setReconnecting(reason: String) {
status = .connecting
handleClientEvent(.reconnect, data: [reason])
}
// Test properties
var testHandlers: [SocketEventHandler] {
return handlers
}
func setTestable() {
status = .connected
}
func setTestStatus(_ status: SocketIOStatus) {
self.status = status
}
func emitTest(event: String, _ data: Any...) {
emit([event] + data)
}
}
@@ -0,0 +1,138 @@
//
// SocketIOClientConfiguration.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 8/13/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.
/// An array-like type that holds `SocketIOClientOption`s
public struct SocketIOClientConfiguration : ExpressibleByArrayLiteral, Collection, MutableCollection {
// MARK: Typealiases
/// Type of element stored.
public typealias Element = SocketIOClientOption
/// Index type.
public typealias Index = Array<SocketIOClientOption>.Index
/// Iterator type.
public typealias Iterator = Array<SocketIOClientOption>.Iterator
/// SubSequence type.
public typealias SubSequence = Array<SocketIOClientOption>.SubSequence
// MARK: Properties
private var backingArray = [SocketIOClientOption]()
/// The start index of this collection.
public var startIndex: Index {
return backingArray.startIndex
}
/// The end index of this collection.
public var endIndex: Index {
return backingArray.endIndex
}
/// Whether this collection is empty.
public var isEmpty: Bool {
return backingArray.isEmpty
}
/// The number of elements stored in this collection.
public var count: Index.Stride {
return backingArray.count
}
/// The first element in this collection.
public var first: Element? {
return backingArray.first
}
public subscript(position: Index) -> Element {
get {
return backingArray[position]
}
set {
backingArray[position] = newValue
}
}
public subscript(bounds: Range<Index>) -> SubSequence {
get {
return backingArray[bounds]
}
set {
backingArray[bounds] = newValue
}
}
// MARK: Initializers
/// Creates a new `SocketIOClientConfiguration` from an array literal.
///
/// - parameter arrayLiteral: The elements.
public init(arrayLiteral elements: Element...) {
backingArray = elements
}
// MARK: Methods
/// Creates an iterator for this collection.
///
/// - returns: An iterator over this collection.
public func makeIterator() -> Iterator {
return backingArray.makeIterator()
}
/// - returns: The index after index.
public func index(after i: Index) -> Index {
return backingArray.index(after: i)
}
/// Special method that inserts `element` into the collection, replacing any other instances of `element`.
///
/// - parameter element: The element to insert.
/// - parameter replacing: Whether to replace any occurrences of element to the new item. Default is `true`.
public mutating func insert(_ element: Element, replacing replace: Bool = true) {
for i in 0..<backingArray.count where backingArray[i] == element {
guard replace else { return }
backingArray[i] = element
return
}
backingArray.append(element)
}
}
/// Declares that a type can set configs from a `SocketIOClientConfiguration`.
public protocol ConfigSettable {
// MARK: Methods
/// 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)
}
@@ -0,0 +1,197 @@
//
// SocketIOClientOption .swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 10/17/15.
//
// 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
import Starscream
protocol ClientOption : CustomStringConvertible, Equatable {
func getSocketIOOptionValue() -> Any
}
/// The options for a client.
public enum SocketIOClientOption : ClientOption {
/// If given, the WebSocket transport will attempt to use compression.
case compress
/// A dictionary of GET parameters that will be included in the connect url.
case connectParams([String: Any])
/// An array of cookies that will be sent during the initial connection.
case cookies([HTTPCookie])
/// Any extra HTTP headers that should be sent during the initial connection.
case extraHeaders([String: String])
/// If passed `true`, will cause the client to always create a new engine. Useful for debugging,
/// or when you want to be sure no state from previous engines is being carried over.
case forceNew(Bool)
/// If passed `true`, the only transport that will be used will be HTTP long-polling.
case forcePolling(Bool)
/// If passed `true`, the only transport that will be used will be WebSockets.
case forceWebsockets(Bool)
/// 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**.
case handleQueue(DispatchQueue)
/// If passed `true`, the client will log debug information. This should be turned off in production code.
case log(Bool)
/// Used to pass in a custom logger.
case logger(SocketLogger)
/// A custom path to socket.io. Only use this if the socket.io server is configured to look for this path.
case path(String)
/// If passed `false`, the client will not reconnect when it loses connection. Useful if you want full control
/// over when reconnects happen.
case reconnects(Bool)
/// The number of times to try and reconnect before giving up. Pass `-1` to [never give up](https://www.youtube.com/watch?v=dQw4w9WgXcQ).
case reconnectAttempts(Int)
/// The number of seconds to wait before reconnect attempts.
case reconnectWait(Int)
/// Set `true` if your server is using secure transports.
case secure(Bool)
/// Allows you to set which certs are valid. Useful for SSL pinning.
case security(SSLSecurity)
/// If you're using a self-signed set. Only use for development.
case selfSigned(Bool)
/// Sets an NSURLSessionDelegate for the underlying engine. Useful if you need to handle self-signed certs.
case sessionDelegate(URLSessionDelegate)
// MARK: Properties
/// The description of this option.
public var description: String {
let description: String
switch self {
case .compress:
description = "compress"
case .connectParams:
description = "connectParams"
case .cookies:
description = "cookies"
case .extraHeaders:
description = "extraHeaders"
case .forceNew:
description = "forceNew"
case .forcePolling:
description = "forcePolling"
case .forceWebsockets:
description = "forceWebsockets"
case .handleQueue:
description = "handleQueue"
case .log:
description = "log"
case .logger:
description = "logger"
case .path:
description = "path"
case .reconnects:
description = "reconnects"
case .reconnectAttempts:
description = "reconnectAttempts"
case .reconnectWait:
description = "reconnectWait"
case .secure:
description = "secure"
case .selfSigned:
description = "selfSigned"
case .security:
description = "security"
case .sessionDelegate:
description = "sessionDelegate"
}
return description
}
func getSocketIOOptionValue() -> Any {
let value: Any
switch self {
case .compress:
value = true
case let .connectParams(params):
value = params
case let .cookies(cookies):
value = cookies
case let .extraHeaders(headers):
value = headers
case let .forceNew(force):
value = force
case let .forcePolling(force):
value = force
case let .forceWebsockets(force):
value = force
case let .handleQueue(queue):
value = queue
case let .log(log):
value = log
case let .logger(logger):
value = logger
case let .path(path):
value = path
case let .reconnects(reconnects):
value = reconnects
case let .reconnectAttempts(attempts):
value = attempts
case let .reconnectWait(wait):
value = wait
case let .secure(secure):
value = secure
case let .security(security):
value = security
case let .selfSigned(signed):
value = signed
case let .sessionDelegate(delegate):
value = delegate
}
return value
}
// MARK: Operators
/// Compares whether two options are the same.
///
/// - parameter lhs: Left operand to compare.
/// - parameter rhs: Right operand to compare.
/// - returns: `true` if the two are the same option.
public static func ==(lhs: SocketIOClientOption, rhs: SocketIOClientOption) -> Bool {
return lhs.description == rhs.description
}
}
@@ -0,0 +1,337 @@
//
// SocketIOClientSpec.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 1/3/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 Dispatch
import Foundation
/// Defines the interface for a SocketIOClient.
public protocol SocketIOClientSpec : AnyObject {
// MARK: Properties
/// A handler that will be called on any event.
var anyHandler: ((SocketAnyEvent) -> ())? { get }
/// The array of handlers for this socket.
var handlers: [SocketEventHandler] { get }
/// The manager for this socket.
var manager: SocketManagerSpec? { get }
/// The namespace that this socket is currently connected to.
///
/// **Must** start with a `/`.
var nsp: String { get }
/// A view into this socket where emits do not check for binary data.
///
/// Usage:
///
/// ```swift
/// socket.rawEmitView.emit("myEvent", myObject)
/// ```
///
/// **NOTE**: It is not safe to hold on to this view beyond the life of the socket.
var rawEmitView: SocketRawView { get }
/// The status of this client.
var status: SocketIOStatus { get }
// MARK: Methods
/// 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.
func connect()
/// Connect to the server. If we aren't connected after `timeoutAfter` seconds, then `withHandler` is called.
///
/// Only call after adding your event listeners, unless you know what you're doing.
///
/// - parameter timeoutAfter: The number of seconds after which if we are not connected we assume the connection
/// has failed. Pass 0 to never timeout.
/// - parameter handler: The handler to call when the client fails to connect.
func connect(timeoutAfter: Double, withHandler handler: (() -> ())?)
/// Called when the client connects to a namespace. If the client was created with a namespace upfront,
/// then this is only called when the client connects to that namespace.
///
/// - parameter toNamespace: The namespace that was connected to.
func didConnect(toNamespace namespace: String)
/// Called when the client has disconnected from socket.io.
///
/// - parameter reason: The reason for the disconnection.
func didDisconnect(reason: String)
/// Called when the client encounters an error.
///
/// - parameter reason: The reason for the disconnection.
func didError(reason: String)
/// Disconnects the socket.
func disconnect()
/// Send an event to the server, with optional data items.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
func emit(_ event: String, _ items: SocketData...)
/// Call when you wish to tell the server that you've received the event for `ack`.
///
/// - parameter ack: The ack number.
/// - parameter with: The data for this ack.
func emitAck(_ ack: Int, with items: [Any])
/// Sends a message to the server, requesting an ack.
///
/// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack.
/// Check that your server's api will ack the event being sent.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent", 1).timingOut(after: 1) {data in
/// ...
/// }
/// ```
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
/// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent.
func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback
/// Called when socket.io has acked one of our emits. Causes the corresponding ack callback to be called.
///
/// - parameter ack: The number for this ack.
/// - parameter data: The data sent back with this ack.
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.
///
/// - parameter event: The name of the event.
/// - parameter data: The data that was sent with this event.
/// - parameter isInternalMessage: Whether this event was sent internally. If `true` it is always sent to handlers.
/// - parameter ack: 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)
/// Causes a client to handle a socket.io packet. The namespace for the packet must match the namespace of the
/// socket.
///
/// - parameter packet: The packet to handle.
func handlePacket(_ packet: SocketPacket)
/// Call when you wish to leave a namespace and disconnect this socket.
func leaveNamespace()
/// Joins `nsp`.
func joinNamespace()
/// Removes handler(s) for a client event.
///
/// If you wish to remove a client event handler, call the `off(id:)` with the UUID received from its `on` call.
///
/// - parameter clientEvent: The event to remove handlers for.
func off(clientEvent event: SocketClientEvent)
/// Removes handler(s) based on an event name.
///
/// If you wish to remove a specific event, call the `off(id:)` with the UUID received from its `on` call.
///
/// - parameter event: The event to remove handlers for.
func off(_ event: String)
/// Removes a handler with the specified UUID gotten from an `on` or `once`
///
/// If you want to remove all events for an event, call the off `off(_:)` method with the event name.
///
/// - parameter id: The UUID of the handler you wish to remove.
func off(id: UUID)
/// Adds a handler for an event.
///
/// - parameter event: The event name for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
func on(_ event: String, callback: @escaping NormalCallback) -> UUID
/// Adds a handler for a client event.
///
/// Example:
///
/// ```swift
/// socket.on(clientEvent: .connect) {data, ack in
/// ...
/// }
/// ```
///
/// - parameter event: The event for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
func on(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID
/// Adds a single-use handler for a client event.
///
/// - parameter clientEvent: The event for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
func once(clientEvent event: SocketClientEvent, callback: @escaping NormalCallback) -> UUID
/// Adds a single-use handler for an event.
///
/// - parameter event: The event name for this handler.
/// - parameter callback: The callback that will execute when this event is received.
/// - returns: A unique id for the handler that can be used to remove it.
func once(_ event: String, callback: @escaping NormalCallback) -> UUID
/// Adds a handler that will be called on every event.
///
/// - parameter handler: The callback that will execute whenever an event is received.
func onAny(_ handler: @escaping (SocketAnyEvent) -> ())
/// Removes all handlers.
///
/// Can be used after disconnecting to break any potential remaining retain cycles.
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 {
/// Default implementation.
public func didError(reason: String) {
DefaultSocketLogger.Logger.error("\(reason)", type: "SocketIOClient")
handleClientEvent(.error, data: [reason])
}
}
/// The set of events that are generated by the client.
public enum SocketClientEvent : String {
// MARK: Cases
/// Emitted when the client connects. This is also called on a successful reconnection. A connect event gets one
/// data item: the namespace that was connected to.
///
/// ```swift
/// socket.on(clientEvent: .connect) {data, ack in
/// guard let nsp = data[0] as? String else { return }
/// // Some logic using the nsp
/// }
/// ```
case connect
/// Emitted when the socket has disconnected and will not attempt to try to reconnect.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .disconnect) {data, ack in
/// // Some cleanup logic
/// }
/// ```
case disconnect
/// Emitted when an error occurs.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .error) {data, ack in
/// // Some logging
/// }
/// ```
case error
/// Emitted whenever the engine sends a ping.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .ping) {_, _ in
/// // Maybe keep track of latency?
/// }
/// ```
case ping
/// Emitted whenever the engine gets a pong.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .pong) {_, _ in
/// // Maybe keep track of latency?
/// }
/// ```
case pong
/// Emitted when the client begins the reconnection process.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .reconnect) {data, ack in
/// // Some reconnect event logic
/// }
/// ```
case reconnect
/// Emitted each time the client tries to reconnect to the server.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .reconnectAttempt) {data, ack in
/// // Some reconnect attempt logging
/// }
/// ```
case reconnectAttempt
/// Emitted every time there is a change in the client's status.
///
/// Usage:
///
/// ```swift
/// socket.on(clientEvent: .statusChange) {data, ack in
/// // Some status changing logging
/// }
/// ```
case statusChange
}
@@ -0,0 +1,59 @@
//
// SocketIOStatus.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 8/14/15.
//
// 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
/// Represents state of a manager or client.
@objc
public enum SocketIOStatus : Int, CustomStringConvertible {
// MARK: Cases
/// The client/manager has never been connected. Or the client has been reset.
case notConnected
/// The client/manager was once connected, but not anymore.
case disconnected
/// The client/manager is in the process of connecting.
case connecting
/// The client/manager is currently 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"
}
}
}
@@ -0,0 +1,163 @@
//
// SocketRawView.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 3/30/18.
//
// 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
/// Class that gives a backwards compatible way to cause an emit not to recursively check for Data objects.
///
/// Usage:
///
/// ```swift
/// socket.rawEmitView.emit("myEvent", myObject)
/// ```
public final class SocketRawView : NSObject {
private unowned let socket: SocketIOClient
init(socket: SocketIOClient) {
self.socket = socket
}
/// Send an event to the server, with optional data items.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
public func emit(_ event: String, _ items: SocketData...) {
do {
try emit(event, with: items.map({ try $0.socketRepresentation() }))
} catch {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: "SocketIOClient")
socket.handleClientEvent(.error, data: [event, items, error])
}
}
/// Same as emit, but meant for Objective-C
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. Send an empty array to send no data.
@objc
public func emit(_ event: String, with items: [Any]) {
socket.emit([event] + items, binary: false)
}
/// Sends a message to the server, requesting an ack.
///
/// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack.
/// Check that your server's api will ack the event being sent.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[eventName, items, theError]`
///
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent", 1).timingOut(after: 1) {data in
/// ...
/// }
/// ```
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. May be left out.
/// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent.
public func emitWithAck(_ event: String, _ items: SocketData...) -> OnAckCallback {
do {
return emitWithAck(event, with: try items.map({ try $0.socketRepresentation() }))
} catch {
DefaultSocketLogger.Logger.error("Error creating socketRepresentation for emit: \(event), \(items)",
type: "SocketIOClient")
socket.handleClientEvent(.error, data: [event, items, error])
return OnAckCallback(ackNumber: -1, items: [], socket: socket)
}
}
/// Same as emitWithAck, but for Objective-C
///
/// **NOTE**: It is up to the server send an ack back, just calling this method does not mean the server will ack.
/// Check that your server's api will ack the event being sent.
///
/// Example:
///
/// ```swift
/// socket.emitWithAck("myEvent", with: [1]).timingOut(after: 1) {data in
/// ...
/// }
/// ```
///
/// - parameter event: The event to send.
/// - parameter items: The items to send with this event. Use `[]` to send nothing.
/// - returns: An `OnAckCallback`. You must call the `timingOut(after:)` method before the event will be sent.
@objc
public func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback {
return socket.createOnAck([event] + items, binary: false)
}
}
/// Class that gives a backwards compatible way to cause an emit not to recursively check for Data objects.
///
/// Usage:
///
/// ```swift
/// ack.rawEmitView.with(myObject)
/// ```
public final class SocketRawAckView : NSObject {
private unowned let socket: SocketIOClient
private let ackNum: Int
init(socket: SocketIOClient, ackNum: Int) {
self.socket = socket
self.ackNum = ackNum
}
/// Call to ack receiving this event.
///
/// If an error occurs trying to transform `items` into their socket representation, a `SocketClientEvent.error`
/// will be emitted. The structure of the error data is `[ackNum, items, theError]`
///
/// - parameter items: A variable number of items to send when acking.
public func with(_ items: SocketData...) {
guard ackNum != -1 else { return }
do {
socket.emit(try items.map({ try $0.socketRepresentation() }), ack: ackNum, binary: false, isAck: true)
} catch {
socket.handleClientEvent(.error, data: [ackNum, items, error])
}
}
/// Call to ack receiving this event.
///
/// - parameter items: An array of items to send when acking. Use `[]` to send nothing.
@objc
public func with(_ items: [Any]) {
guard ackNum != -1 else { return }
socket.emit(items, ack: ackNum, binary: false, isAck: true)
}
}
@@ -0,0 +1,688 @@
//
// SocketEngine.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 3/3/15.
//
// 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
import Starscream
/// The class that handles the engine.io protocol and transports.
/// See `SocketEnginePollable` and `SocketEngineWebsocket` for transport specific methods.
open class SocketEngine : NSObject, URLSessionDelegate, SocketEnginePollable, SocketEngineWebsocket, ConfigSettable {
// MARK: Properties
private static let logType = "SocketEngine"
/// The queue that all engine actions take place on.
public let engineQueue = DispatchQueue(label: "com.socketio.engineHandleQueue")
/// The connect parameters sent during a connect.
public var connectParams: [String: Any]? {
didSet {
(urlPolling, urlWebSocket) = createURLs()
}
}
/// A dictionary of extra http headers that will be set during connection.
public var extraHeaders: [String: String]?
/// A queue of engine.io messages waiting for POSTing
///
/// **You should not touch this directly**
public var postWait = [String]()
/// `true` if there is an outstanding poll. Trying to poll before the first is done will cause socket.io to
/// disconnect us.
///
/// **Do not touch this directly**
public var waitingForPoll = false
/// `true` if there is an outstanding post. Trying to post before the first is done will cause socket.io to
/// disconnect us.
///
/// **Do not touch this directly**
public var waitingForPost = false
/// `true` if this engine is closed.
public private(set) var closed = false
/// If `true` the engine will attempt to use WebSocket compression.
public private(set) var compress = false
/// `true` if this engine is connected. Connected means that the initial poll connect has succeeded.
public private(set) var connected = false
/// An array of HTTPCookies that are sent during the connection.
public private(set) var cookies: [HTTPCookie]?
/// When `true`, the engine is in the process of switching to WebSockets.
///
/// **Do not touch this directly**
public private(set) var fastUpgrade = false
/// When `true`, the engine will only use HTTP long-polling as a transport.
public private(set) var forcePolling = false
/// When `true`, the engine will only use WebSockets as a transport.
public private(set) var forceWebsockets = false
/// `true` If engine's session has been invalidated.
public private(set) var invalidated = false
/// If `true`, the engine is currently in HTTP long-polling mode.
public private(set) var polling = true
/// If `true`, the engine is currently seeing whether it can upgrade to WebSockets.
public private(set) var probing = false
/// The URLSession that will be used for polling.
public private(set) var session: URLSession?
/// The session id for this engine.
public private(set) var sid = ""
/// The path to engine.io.
public private(set) var socketPath = "/engine.io/"
/// The url for polling.
public private(set) var urlPolling = URL(string: "http://localhost/")!
/// The url for WebSockets.
public private(set) var urlWebSocket = URL(string: "http://localhost/")!
/// If `true`, then the engine is currently in WebSockets mode.
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
public private(set) var websocket = false
/// The WebSocket for this engine.
public private(set) var ws: WebSocket?
/// The client for this engine.
public weak var client: SocketEngineClient?
private weak var sessionDelegate: URLSessionDelegate?
private let url: URL
private var pingInterval: Int?
private var pingTimeout = 0 {
didSet {
pongsMissedMax = Int(pingTimeout / (pingInterval ?? 25000))
}
}
private var pongsMissed = 0
private var pongsMissedMax = 0
private var probeWait = ProbeWaitQueue()
private var secure = false
private var security: SocketIO.SSLSecurity?
private var selfSigned = false
// MARK: Initializers
/// Creates a new engine.
///
/// - parameter client: The client for this engine.
/// - parameter url: The url for this engine.
/// - parameter config: An array of configuration options for this engine.
public init(client: SocketEngineClient, url: URL, config: SocketIOClientConfiguration) {
self.client = client
self.url = url
super.init()
setConfigs(config)
sessionDelegate = sessionDelegate ?? self
(urlPolling, urlWebSocket) = createURLs()
}
/// Creates a new engine.
///
/// - parameter client: The client for this engine.
/// - parameter url: The url for this engine.
/// - parameter options: The options for this engine.
public required convenience init(client: SocketEngineClient, url: URL, options: [String: Any]?) {
self.init(client: client, url: url, config: options?.toSocketConfiguration() ?? [])
}
deinit {
DefaultSocketLogger.Logger.log("Engine is being released", type: SocketEngine.logType)
closed = true
stopPolling()
}
// MARK: Methods
private func checkAndHandleEngineError(_ msg: String) {
do {
let dict = try msg.toDictionary()
guard let error = dict["message"] as? String else { return }
/*
0: Unknown transport
1: Unknown sid
2: Bad handshake request
3: Bad request
*/
didError(reason: error)
} catch {
client?.engineDidError(reason: "Got unknown error from server \(msg)")
}
}
private func handleBase64(message: String) {
// binary in base64 string
let noPrefix = String(message[message.index(message.startIndex, offsetBy: 2)..<message.endIndex])
if let data = Data(base64Encoded: noPrefix, options: .ignoreUnknownCharacters) {
client?.parseEngineBinaryData(data)
}
}
private func closeOutEngine(reason: String) {
sid = ""
closed = true
invalidated = true
connected = false
ws?.disconnect()
stopPolling()
client?.engineDidClose(reason: reason)
}
/// Starts the connection to the server.
open func connect() {
engineQueue.async {
self._connect()
}
}
private func _connect() {
if connected {
DefaultSocketLogger.Logger.error("Engine tried opening while connected. Assuming this was a reconnect",
type: SocketEngine.logType)
disconnect(reason: "reconnect")
}
DefaultSocketLogger.Logger.log("Starting engine. Server: \(url)", type: SocketEngine.logType)
DefaultSocketLogger.Logger.log("Handshaking", type: SocketEngine.logType)
resetEngine()
if forceWebsockets {
polling = false
createWebSocketAndConnect()
return
}
var reqPolling = URLRequest(url: urlPolling, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
addHeaders(to: &reqPolling)
doLongPoll(for: reqPolling)
}
private func createURLs() -> (URL, URL) {
if client == nil {
return (URL(string: "http://localhost/")!, URL(string: "http://localhost/")!)
}
var urlPolling = URLComponents(string: url.absoluteString)!
var urlWebSocket = URLComponents(string: url.absoluteString)!
var queryString = ""
urlWebSocket.path = socketPath
urlPolling.path = socketPath
if secure {
urlPolling.scheme = "https"
urlWebSocket.scheme = "wss"
} else {
urlPolling.scheme = "http"
urlWebSocket.scheme = "ws"
}
if let connectParams = self.connectParams {
for (key, value) in connectParams {
let keyEsc = key.urlEncode()!
let valueEsc = "\(value)".urlEncode()!
queryString += "&\(keyEsc)=\(valueEsc)"
}
}
urlWebSocket.percentEncodedQuery = "transport=websocket" + queryString
urlPolling.percentEncodedQuery = "transport=polling&b64=1" + queryString
return (urlPolling.url!, urlWebSocket.url!)
}
private func createWebSocketAndConnect() {
var req = URLRequest(url: urlWebSocketWithSid)
addHeaders(to: &req, includingCookies: session?.configuration.httpCookieStorage?.cookies)
ws = WebSocket(request: req)
ws?.callbackQueue = engineQueue
ws?.enableCompression = compress
ws?.disableSSLCertValidation = selfSigned
ws?.security = security?.security
ws?.onConnect = {[weak self] in
guard let this = self else { return }
this.websocketDidConnect()
}
ws?.onDisconnect = {[weak self] error in
guard let this = self else { return }
this.websocketDidDisconnect(error: error)
}
ws?.onData = {[weak self] data in
guard let this = self else { return }
this.parseEngineData(data)
}
ws?.onText = {[weak self] message in
guard let this = self else { return }
this.parseEngineMessage(message)
}
ws?.connect()
}
/// Called when an error happens during execution. Causes a disconnection.
open func didError(reason: String) {
DefaultSocketLogger.Logger.error("\(reason)", type: SocketEngine.logType)
client?.engineDidError(reason: reason)
disconnect(reason: reason)
}
/// Disconnects from the server.
///
/// - parameter reason: The reason for the disconnection. This is communicated up to the client.
open func disconnect(reason: String) {
engineQueue.async {
self._disconnect(reason: reason)
}
}
private func _disconnect(reason: String) {
guard connected && !closed else { return closeOutEngine(reason: reason) }
DefaultSocketLogger.Logger.log("Engine is being closed.", type: SocketEngine.logType)
if polling {
disconnectPolling(reason: reason)
} else {
sendWebSocketMessage("", withType: .close, withData: [])
closeOutEngine(reason: reason)
}
}
// We need to take special care when we're polling that we send it ASAP
// Also make sure we're on the emitQueue since we're touching postWait
private func disconnectPolling(reason: String) {
postWait.append(String(SocketEnginePacketType.close.rawValue))
doRequest(for: createRequestForPostWithPostWait()) {_, _, _ in }
closeOutEngine(reason: reason)
}
/// Called to switch from HTTP long-polling to WebSockets. After calling this method the engine will be in
/// WebSocket mode.
///
/// **You shouldn't call this directly**
open func doFastUpgrade() {
if waitingForPoll {
DefaultSocketLogger.Logger.error("Outstanding poll when switched to WebSockets," +
"we'll probably disconnect soon. You should report this.", type: SocketEngine.logType)
}
DefaultSocketLogger.Logger.log("Switching to WebSockets", type: SocketEngine.logType)
sendWebSocketMessage("", withType: .upgrade, withData: [])
polling = false
fastUpgrade = false
probing = false
flushProbeWait()
// Need to flush postWait to socket since it connected successfully
// moved from flushProbeWait() since it is also called on connected failure, and we don't want to try and send
// packets through WebSockets when WebSockets has failed!
if !postWait.isEmpty {
flushWaitingForPostToWebSocket()
}
}
private func flushProbeWait() {
DefaultSocketLogger.Logger.log("Flushing probe wait", type: SocketEngine.logType)
for waiter in probeWait {
write(waiter.msg, withType: waiter.type, withData: waiter.data)
}
probeWait.removeAll(keepingCapacity: false)
}
/// Causes any packets that were waiting for POSTing to be sent through the WebSocket. This happens because when
/// the engine is attempting to upgrade to WebSocket it does not do any POSTing.
///
/// **You shouldn't call this directly**
open func flushWaitingForPostToWebSocket() {
guard let ws = self.ws else { return }
for msg in postWait {
ws.write(string: msg)
}
postWait.removeAll(keepingCapacity: false)
}
private func handleClose(_ reason: String) {
client?.engineDidClose(reason: reason)
}
private func handleMessage(_ message: String) {
client?.parseEngineMessage(message)
}
private func handleNOOP() {
doPoll()
}
private func handleOpen(openData: String) {
guard let json = try? openData.toDictionary() else {
didError(reason: "Error parsing open packet")
return
}
guard let sid = json["sid"] as? String else {
didError(reason: "Open packet contained no sid")
return
}
let upgradeWs: Bool
self.sid = sid
connected = true
pongsMissed = 0
if let upgrades = json["upgrades"] as? [String] {
upgradeWs = upgrades.contains("websocket")
} else {
upgradeWs = false
}
if let pingInterval = json["pingInterval"] as? Int, let pingTimeout = json["pingTimeout"] as? Int {
self.pingInterval = pingInterval
self.pingTimeout = pingTimeout
}
if !forcePolling && !forceWebsockets && upgradeWs {
createWebSocketAndConnect()
}
sendPing()
if !forceWebsockets {
doPoll()
}
client?.engineDidOpen(reason: "Connect")
}
private func handlePong(with message: String) {
pongsMissed = 0
// We should upgrade
if message == "3probe" {
DefaultSocketLogger.Logger.log("Received probe response, should upgrade to WebSockets",
type: SocketEngine.logType)
upgradeTransport()
}
client?.engineDidReceivePong()
}
/// Parses raw binary received from engine.io.
///
/// - parameter data: The data to parse.
open func parseEngineData(_ data: Data) {
DefaultSocketLogger.Logger.log("Got binary data: \(data)", type: SocketEngine.logType)
client?.parseEngineBinaryData(data.subdata(in: 1..<data.endIndex))
}
/// Parses a raw engine.io packet.
///
/// - parameter message: The message to parse.
open func parseEngineMessage(_ message: String) {
DefaultSocketLogger.Logger.log("Got message: \(message)", type: SocketEngine.logType)
let reader = SocketStringReader(message: message)
if message.hasPrefix("b4") {
return handleBase64(message: message)
}
guard let type = SocketEnginePacketType(rawValue: Int(reader.currentCharacter) ?? -1) else {
checkAndHandleEngineError(message)
return
}
switch type {
case .message:
handleMessage(String(message.dropFirst()))
case .noop:
handleNOOP()
case .pong:
handlePong(with: message)
case .open:
handleOpen(openData: String(message.dropFirst()))
case .close:
handleClose(message)
default:
DefaultSocketLogger.Logger.log("Got unknown packet type", type: SocketEngine.logType)
}
}
// Puts the engine back in its default state
private func resetEngine() {
let queue = OperationQueue()
queue.underlyingQueue = engineQueue
closed = false
connected = false
fastUpgrade = false
polling = true
probing = false
invalidated = false
session = Foundation.URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: queue)
sid = ""
waitingForPoll = false
waitingForPost = false
}
private func sendPing() {
guard connected, let pingInterval = pingInterval else { return }
// Server is not responding
if pongsMissed > pongsMissedMax {
client?.engineDidClose(reason: "Ping timeout")
return
}
pongsMissed += 1
write("", withType: .ping, withData: [])
engineQueue.asyncAfter(deadline: DispatchTime.now() + .milliseconds(pingInterval)) {[weak self, id = self.sid] in
// Make sure not to ping old connections
guard let this = self, this.sid == id else { return }
this.sendPing()
}
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
private func upgradeTransport() {
if ws?.isConnected ?? false {
DefaultSocketLogger.Logger.log("Upgrading transport to WebSockets", type: SocketEngine.logType)
fastUpgrade = true
sendPollMessage("", withType: .noop, withData: [])
// After this point, we should not send anymore polling messages
}
}
/// Writes a message to engine.io, independent of transport.
///
/// - parameter msg: The message to send.
/// - parameter type: The type of this message.
/// - parameter data: Any data that this message has.
open func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data]) {
engineQueue.async {
guard self.connected else { return }
guard !self.probing else {
self.probeWait.append((msg, type, data))
return
}
if self.polling {
DefaultSocketLogger.Logger.log("Writing poll: \(msg) has data: \(data.count != 0)",
type: SocketEngine.logType)
self.sendPollMessage(msg, withType: type, withData: data)
} else {
DefaultSocketLogger.Logger.log("Writing ws: \(msg) has data: \(data.count != 0)",
type: SocketEngine.logType)
self.sendWebSocketMessage(msg, withType: type, withData: data)
}
}
}
// WebSocket Methods
private func websocketDidConnect() {
if !forceWebsockets {
probing = true
probeWebSocket()
} else {
connected = true
probing = false
polling = false
}
}
private func websocketDidDisconnect(error: Error?) {
probing = false
if closed {
client?.engineDidClose(reason: "Disconnect")
return
}
guard !polling else {
flushProbeWait()
return
}
connected = false
polling = true
if let reason = error?.localizedDescription {
didError(reason: reason)
} else {
client?.engineDidClose(reason: "Socket Disconnected")
}
}
// Test Properties
func setConnected(_ value: Bool) {
connected = value
}
}
extension SocketEngine {
// MARK: URLSessionDelegate methods
/// Delegate called when the session becomes invalid.
public func URLSession(session: URLSession, didBecomeInvalidWithError error: NSError?) {
DefaultSocketLogger.Logger.error("Engine URLSession became invalid", type: "SocketEngine")
didError(reason: "Engine URLSession became invalid")
}
}
@@ -0,0 +1,62 @@
//
// SocketEngineClient.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 3/19/15.
//
// 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
/// Declares that a type will be a delegate to an engine.
@objc public protocol SocketEngineClient {
// MARK: Methods
/// Called when the engine errors.
///
/// - parameter reason: The reason the engine errored.
func engineDidError(reason: String)
/// Called when the engine closes.
///
/// - parameter reason: The reason that the engine closed.
func engineDidClose(reason: String)
/// Called when the engine opens.
///
/// - parameter reason: The reason the engine opened.
func engineDidOpen(reason: String)
/// Called when the engine receives a pong message.
func engineDidReceivePong()
/// Called when the engine sends a ping to the server.
func engineDidSendPing()
/// Called when the engine has a message that must be parsed.
///
/// - parameter msg: The message that needs parsing.
func parseEngineMessage(_ msg: String)
/// Called when the engine receives binary data.
///
/// - parameter data: The data the engine received.
func parseEngineBinaryData(_ data: Data)
}
@@ -0,0 +1,50 @@
//
// SocketEnginePacketType.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 10/7/15.
//
// 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
/// Represents the type of engine.io packet types.
@objc public enum SocketEnginePacketType : Int {
/// Open message.
case open
/// Close message.
case close
/// Ping message.
case ping
/// Pong message.
case pong
/// Regular message.
case message
/// Upgrade message.
case upgrade
/// NOOP.
case noop
}
@@ -0,0 +1,234 @@
//
// SocketEnginePollable.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 1/15/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
/// Protocol that is used to implement socket.io polling support
public protocol SocketEnginePollable : SocketEngineSpec {
// MARK: Properties
/// `true` If engine's session has been invalidated.
var invalidated: Bool { get }
/// A queue of engine.io messages waiting for POSTing
///
/// **You should not touch this directly**
var postWait: [String] { get set }
/// The URLSession that will be used for polling.
var session: URLSession? { get }
/// `true` if there is an outstanding poll. Trying to poll before the first is done will cause socket.io to
/// disconnect us.
///
/// **Do not touch this directly**
var waitingForPoll: Bool { get set }
/// `true` if there is an outstanding post. Trying to post before the first is done will cause socket.io to
/// disconnect us.
///
/// **Do not touch this directly**
var waitingForPost: Bool { get set }
// MARK: Methods
/// Call to send a long-polling request.
///
/// You shouldn't need to call this directly, the engine should automatically maintain a long-poll request.
func doPoll()
/// Sends an engine.io message through the polling transport.
///
/// You shouldn't call this directly, instead call the `write` method on `SocketEngine`.
///
/// - parameter message: The message to send.
/// - parameter withType: The type of message to send.
/// - parameter withData: The data associated with this message.
func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data])
/// Call to stop polling and invalidate the URLSession.
func stopPolling()
}
// Default polling methods
extension SocketEnginePollable {
func createRequestForPostWithPostWait() -> URLRequest {
defer { postWait.removeAll(keepingCapacity: true) }
var postStr = ""
for packet in postWait {
postStr += "\(packet.utf16.count):\(packet)"
}
DefaultSocketLogger.Logger.log("Created POST string: \(postStr)", type: "SocketEnginePolling")
var req = URLRequest(url: urlPollingWithSid)
let postData = postStr.data(using: .utf8, allowLossyConversion: false)!
addHeaders(to: &req)
req.httpMethod = "POST"
req.setValue("text/plain; charset=UTF-8", forHTTPHeaderField: "Content-Type")
req.httpBody = postData
req.setValue(String(postData.count), forHTTPHeaderField: "Content-Length")
return req
}
/// Call to send a long-polling request.
///
/// You shouldn't need to call this directly, the engine should automatically maintain a long-poll request.
public func doPoll() {
guard polling && !waitingForPoll && connected && !closed else { return }
var req = URLRequest(url: urlPollingWithSid)
addHeaders(to: &req)
doLongPoll(for: req)
}
func doRequest(for req: URLRequest, callbackWith callback: @escaping (Data?, URLResponse?, Error?) -> ()) {
guard polling && !closed && !invalidated && !fastUpgrade else { return }
DefaultSocketLogger.Logger.log("Doing polling \(req.httpMethod ?? "") \(req)", type: "SocketEnginePolling")
session?.dataTask(with: req, completionHandler: callback).resume()
}
func doLongPoll(for req: URLRequest) {
waitingForPoll = true
doRequest(for: req) {[weak self] data, res, err in
guard let this = self, this.polling else { return }
if err != nil || data == nil {
DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling")
if this.polling {
this.didError(reason: err?.localizedDescription ?? "Error")
}
return
}
DefaultSocketLogger.Logger.log("Got polling response", type: "SocketEnginePolling")
if let str = String(data: data!, encoding: .utf8) {
this.parsePollingMessage(str)
}
this.waitingForPoll = false
if this.fastUpgrade {
this.doFastUpgrade()
} else if !this.closed && this.polling {
this.doPoll()
}
}
}
private func flushWaitingForPost() {
guard postWait.count != 0 && connected else { return }
guard polling else {
flushWaitingForPostToWebSocket()
return
}
let req = createRequestForPostWithPostWait()
waitingForPost = true
DefaultSocketLogger.Logger.log("POSTing", type: "SocketEnginePolling")
doRequest(for: req) {[weak self] data, res, err in
guard let this = self else { return }
if err != nil {
DefaultSocketLogger.Logger.error(err?.localizedDescription ?? "Error", type: "SocketEnginePolling")
if this.polling {
this.didError(reason: err?.localizedDescription ?? "Error")
}
return
}
this.waitingForPost = false
if !this.fastUpgrade {
this.flushWaitingForPost()
this.doPoll()
}
}
}
func parsePollingMessage(_ str: String) {
guard str.count != 1 else { return }
DefaultSocketLogger.Logger.log("Got poll message: \(str)", type: "SocketEnginePolling")
var reader = SocketStringReader(message: str)
while reader.hasNext {
if let n = Int(reader.readUntilOccurence(of: ":")) {
parseEngineMessage(reader.read(count: n))
} else {
parseEngineMessage(str)
break
}
}
}
/// Sends an engine.io message through the polling transport.
///
/// You shouldn't call this directly, instead call the `write` method on `SocketEngine`.
///
/// - parameter message: The message to send.
/// - parameter withType: The type of message to send.
/// - parameter withData: The data associated with this message.
public func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data]) {
DefaultSocketLogger.Logger.log("Sending poll: \(message) as type: \(type.rawValue)", type: "SocketEnginePolling")
postWait.append(String(type.rawValue) + message)
for data in datas {
if case let .right(bin) = createBinaryDataForSend(using: data) {
postWait.append(bin)
}
}
if !waitingForPost {
flushWaitingForPost()
}
}
/// Call to stop polling and invalidate the URLSession.
public func stopPolling() {
waitingForPoll = false
waitingForPost = false
session?.finishTasksAndInvalidate()
}
}
@@ -0,0 +1,185 @@
//
// SocketEngineSpec.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 10/7/15.
//
// 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
import Starscream
/// Specifies a SocketEngine.
@objc public protocol SocketEngineSpec {
// MARK: Properties
/// The client for this engine.
var client: SocketEngineClient? { get set }
/// `true` if this engine is closed.
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.
var connected: Bool { get }
/// The connect parameters sent during a connect.
var connectParams: [String: Any]? { get set }
/// An array of HTTPCookies that are sent during the connection.
var cookies: [HTTPCookie]? { get }
/// The queue that all engine actions take place on.
var engineQueue: DispatchQueue { get }
/// A dictionary of extra http headers that will be set during connection.
var extraHeaders: [String: String]? { get set }
/// When `true`, the engine is in the process of switching to WebSockets.
var fastUpgrade: Bool { get }
/// When `true`, the engine will only use HTTP long-polling as a transport.
var forcePolling: Bool { get }
/// When `true`, the engine will only use WebSockets as a transport.
var forceWebsockets: Bool { get }
/// If `true`, the engine is currently in HTTP long-polling mode.
var polling: Bool { get }
/// If `true`, the engine is currently seeing whether it can upgrade to WebSockets.
var probing: Bool { get }
/// The session id for this engine.
var sid: String { get }
/// The path to engine.io.
var socketPath: String { get }
/// The url for polling.
var urlPolling: URL { get }
/// The url for WebSockets.
var urlWebSocket: URL { get }
/// If `true`, then the engine is currently in WebSockets mode.
@available(*, deprecated, message: "No longer needed, if we're not polling, then we must be doing websockets")
var websocket: Bool { get }
/// The WebSocket for this engine.
var ws: WebSocket? { get }
// MARK: Initializers
/// Creates a new engine.
///
/// - parameter client: The client for this engine.
/// - parameter url: The url for this engine.
/// - parameter options: The options for this engine.
init(client: SocketEngineClient, url: URL, options: [String: Any]?)
// MARK: Methods
/// Starts the connection to the server.
func connect()
/// Called when an error happens during execution. Causes a disconnection.
func didError(reason: String)
/// Disconnects from the server.
///
/// - parameter reason: The reason for the disconnection. This is communicated up to the client.
func disconnect(reason: String)
/// Called to switch from HTTP long-polling to WebSockets. After calling this method the engine will be in
/// WebSocket mode.
///
/// **You shouldn't call this directly**
func doFastUpgrade()
/// Causes any packets that were waiting for POSTing to be sent through the WebSocket. This happens because when
/// the engine is attempting to upgrade to WebSocket it does not do any POSTing.
///
/// **You shouldn't call this directly**
func flushWaitingForPostToWebSocket()
/// Parses raw binary received from engine.io.
///
/// - parameter data: The data to parse.
func parseEngineData(_ data: Data)
/// Parses a raw engine.io packet.
///
/// - parameter message: The message to parse.
func parseEngineMessage(_ message: String)
/// Writes a message to engine.io, independent of transport.
///
/// - parameter msg: The message to send.
/// - parameter type: The type of this message.
/// - parameter data: Any data that this message has.
func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data])
}
extension SocketEngineSpec {
var urlPollingWithSid: URL {
var com = URLComponents(url: urlPolling, resolvingAgainstBaseURL: false)!
com.percentEncodedQuery = com.percentEncodedQuery! + "&sid=\(sid.urlEncode()!)"
return com.url!
}
var urlWebSocketWithSid: URL {
var com = URLComponents(url: urlWebSocket, resolvingAgainstBaseURL: false)!
com.percentEncodedQuery = com.percentEncodedQuery! + (sid == "" ? "" : "&sid=\(sid.urlEncode()!)")
return com.url!
}
func addHeaders(to req: inout URLRequest, includingCookies additionalCookies: [HTTPCookie]? = nil) {
var cookiesToAdd: [HTTPCookie] = cookies ?? []
cookiesToAdd += additionalCookies ?? []
if !cookiesToAdd.isEmpty {
req.allHTTPHeaderFields = HTTPCookie.requestHeaderFields(with: cookiesToAdd)
}
if let extraHeaders = extraHeaders {
for (headerName, value) in extraHeaders {
req.setValue(value, forHTTPHeaderField: headerName)
}
}
}
func createBinaryDataForSend(using data: Data) -> Either<Data, String> {
if polling {
return .right("b4" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)))
} else {
return .left(Data(bytes: [0x4]) + data)
}
}
/// Send an engine message (4)
func send(_ msg: String, withData datas: [Data]) {
write(msg, withType: .message, withData: datas)
}
}
@@ -0,0 +1,69 @@
//
// SocketEngineWebsocket.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 1/15/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
import Starscream
/// Protocol that is used to implement socket.io WebSocket support
public protocol SocketEngineWebsocket : SocketEngineSpec {
// MARK: Methods
/// Sends an engine.io message through the WebSocket transport.
///
/// You shouldn't call this directly, instead call the `write` method on `SocketEngine`.
///
/// - parameter message: The message to send.
/// - parameter withType: The type of message to send.
/// - parameter withData: The data associated with this message.
func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data])
}
// WebSocket methods
extension SocketEngineWebsocket {
func probeWebSocket() {
if ws?.isConnected ?? false {
sendWebSocketMessage("probe", withType: .ping, withData: [])
}
}
/// Sends an engine.io message through the WebSocket transport.
///
/// You shouldn't call this directly, instead call the `write` method on `SocketEngine`.
///
/// - parameter message: The message to send.
/// - parameter withType: The type of message to send.
/// - parameter withData: The data associated with this message.
public func sendWebSocketMessage(_ str: String, withType type: SocketEnginePacketType, withData datas: [Data]) {
DefaultSocketLogger.Logger.log("Sending ws: \(str) as type: \(type.rawValue)", type: "SocketEngineWebSocket")
ws?.write(string: "\(type.rawValue)\(str)")
for data in datas {
if case let .left(bin) = createBinaryDataForSend(using: data) {
ws?.write(data: bin)
}
}
}
}
@@ -0,0 +1,539 @@
//
// 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.
///
/// **NOTE**: The manager is not thread/queue safe, all interaction with the manager should be done on the `handleQueue`
///
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
// MARK: Initializers
/// 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))
}
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: URL, config: [String: Any]?) {
self.init(socketURL: socketURL, 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
// Close old engine so it will not leak because of URLSession if in polling mode
self.engine?.disconnect(reason: "Adding new engine")
}
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) {
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 nsp: 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 items: 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.isBinary 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 `SocketClientEvent.reconnect` event to be emitted, as well as
/// `SocketClientEvent.reconnectAttempt` events.
open func reconnect() {
guard !reconnecting else { return }
engine?.disconnect(reason: "manual reconnect")
}
/// Removes the socket from the manager's control. One of the disconnect methods should be called before calling this
/// method.
///
/// After calling this method the socket should no longer be considered usable.
///
/// - parameter socket: The socket to remove.
/// - returns: The socket removed, if it was owned by the manager.
@discardableResult
open func removeSocket(_ socket: SocketIOClient) -> SocketIOClient? {
return nsps.removeValue(forKey: socket.nsp)
}
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 .handleQueue(queue):
self.handleQueue = queue
case let .reconnects(reconnects):
self.reconnects = reconnects
case let .reconnectAttempts(attempts):
self.reconnectAttempts = attempts
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 nsp: 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
}
}
@@ -0,0 +1,139 @@
//
// 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 : AnyObject, 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 }
/// The sockets in this manager indexed by namespace.
var nsps: [String: SocketIOClient] { 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 nsp: 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 items: 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()
/// Removes the socket from the manager's control.
/// After calling this method the socket should no longer be considered usable.
///
/// - parameter socket: The socket to remove.
/// - returns: The socket removed, if it was owned by the manager.
@discardableResult
func removeSocket(_ socket: SocketIOClient) -> SocketIOClient?
/// 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 nsp: The namespace for the socket.
/// - returns: A `SocketIOClient` for the given namespace.
func socket(forNamespace nsp: String) -> SocketIOClient
}
@@ -0,0 +1,250 @@
//
// SocketPacket.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 1/18/15.
//
// 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
/// A struct that represents a socket.io packet.
public struct SocketPacket : CustomStringConvertible {
// MARK: Properties
private static let logType = "SocketPacket"
/// The namespace for this packet.
public let nsp: String
/// If > 0 then this packet is using acking.
public let id: Int
/// The type of this packet.
public let type: PacketType
/// An array of binary data for this packet.
public internal(set) var binary: [Data]
/// The data for this event.
///
/// Note: This includes all data inside of the socket.io packet payload array, which includes the event name for
/// event type packets.
public internal(set) var data: [Any]
/// Returns the payload for this packet, minus the event name if this is an event or binaryEvent type packet.
public var args: [Any] {
if type == .event || type == .binaryEvent && data.count != 0 {
return Array(data.dropFirst())
} else {
return data
}
}
private let placeholders: Int
/// A string representation of this packet.
public var description: String {
return "SocketPacket {type: \(String(type.rawValue)); data: " +
"\(String(describing: data)); id: \(id); placeholders: \(placeholders); nsp: \(nsp)}"
}
/// The event name for this packet.
public var event: String {
return String(describing: data[0])
}
/// A string representation of this packet.
public var packetString: String {
return createPacketString()
}
init(type: PacketType, data: [Any] = [Any](), id: Int = -1, nsp: String, placeholders: Int = 0,
binary: [Data] = [Data]()) {
self.data = data
self.id = id
self.nsp = nsp
self.type = type
self.placeholders = placeholders
self.binary = binary
}
mutating func addData(_ data: Data) -> Bool {
if placeholders == binary.count {
return true
}
binary.append(data)
if placeholders == binary.count {
fillInPlaceholders()
return true
} else {
return false
}
}
private func completeMessage(_ message: String) -> String {
guard data.count != 0 else { return message + "[]" }
guard let jsonSend = try? data.toJSON(), let jsonString = String(data: jsonSend, encoding: .utf8) else {
DefaultSocketLogger.Logger.error("Error creating JSON object in SocketPacket.completeMessage",
type: SocketPacket.logType)
return message + "[]"
}
return message + jsonString
}
private func createPacketString() -> String {
let typeString = String(type.rawValue)
// Binary count?
let binaryCountString = typeString + (type.isBinary ? "\(String(binary.count))-" : "")
// Namespace?
let nspString = binaryCountString + (nsp != "/" ? "\(nsp)," : "")
// Ack number?
let idString = nspString + (id != -1 ? String(id) : "")
return completeMessage(idString)
}
// Called when we have all the binary data for a packet
// calls _fillInPlaceholders, which replaces placeholders with the
// corresponding binary
private mutating func fillInPlaceholders() {
data = data.map(_fillInPlaceholders)
}
// Helper method that looks for placeholders
// If object is a collection it will recurse
// Returns the object if it is not a placeholder or the corresponding
// binary data
private func _fillInPlaceholders(_ object: Any) -> Any {
switch object {
case let dict as JSON:
if dict["_placeholder"] as? Bool ?? false {
return binary[dict["num"] as! Int]
} else {
return dict.reduce(into: JSON(), {cur, keyValue in
cur[keyValue.0] = _fillInPlaceholders(keyValue.1)
})
}
case let arr as [Any]:
return arr.map(_fillInPlaceholders)
default:
return object
}
}
}
public extension SocketPacket {
// MARK: PacketType enum
/// The type of packets.
public enum PacketType: Int {
// MARK: Cases
/// Connect: 0
case connect
/// Disconnect: 1
case disconnect
/// Event: 2
case event
/// Ack: 3
case ack
/// Error: 4
case error
/// Binary Event: 5
case binaryEvent
/// Binary Ack: 6
case binaryAck
// MARK: Properties
/// Whether or not this type is binary
public var isBinary: Bool {
return self == .binaryAck || self == .binaryEvent
}
}
}
extension SocketPacket {
private static func findType(_ binCount: Int, ack: Bool) -> PacketType {
switch binCount {
case 0 where !ack:
return .event
case 0 where ack:
return .ack
case _ where !ack:
return .binaryEvent
case _ where ack:
return .binaryAck
default:
return .error
}
}
static func packetFromEmit(_ items: [Any], id: Int, nsp: String, ack: Bool, checkForBinary: Bool = true) -> SocketPacket {
if checkForBinary {
let (parsedData, binary) = deconstructData(items)
return SocketPacket(type: findType(binary.count, ack: ack), data: parsedData, id: id, nsp: nsp,
binary: binary)
} else {
return SocketPacket(type: findType(0, ack: ack), data: items, id: id, nsp: nsp)
}
}
}
private extension SocketPacket {
// Recursive function that looks for NSData in collections
static func shred(_ data: Any, binary: inout [Data]) -> Any {
let placeholder = ["_placeholder": true, "num": binary.count] as JSON
switch data {
case let bin as Data:
binary.append(bin)
return placeholder
case let arr as [Any]:
return arr.map({shred($0, binary: &binary)})
case let dict as JSON:
return dict.reduce(into: JSON(), {cur, keyValue in
cur[keyValue.0] = shred(keyValue.1, binary: &binary)
})
default:
return data
}
}
// Removes binary data from emit data
// Returns a type containing the de-binaryed data and the binary
static func deconstructData(_ data: [Any]) -> ([Any], [Data]) {
var binary = [Data]()
return (data.map({ shred($0, binary: &binary) }), binary)
}
}
@@ -0,0 +1,184 @@
//
// SocketParsable.swift
// Socket.IO-Client-Swift
//
// 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
/// Defines that a type will be able to parse socket.io-protocol messages.
public protocol SocketParsable : AnyObject {
// MARK: Methods
/// Called when the engine has received some binary data that should be attached to a packet.
///
/// Packets binary data should be sent directly after the packet that expects it, so there's confusion over
/// where the data should go. Data should be received in the order it is sent, so that the correct data is put
/// into the correct placeholder.
///
/// - parameter data: The data that should be attached to a packet.
func parseBinaryData(_ data: Data) -> SocketPacket?
/// Called when the engine has received a string that should be parsed into a socket.io packet.
///
/// - parameter message: The string that needs parsing.
/// - 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.
public enum SocketParsableError : Error {
// MARK: Cases
/// Thrown when a packet received has an invalid data array, or is missing the data array.
case invalidDataArray
/// Thrown when an malformed packet is received.
case invalidPacket
/// Thrown when the parser receives an unknown packet type.
case invalidPacketType
}
/// Says that a type will be able to buffer binary data before all data for an event has come in.
public protocol SocketDataBufferable : AnyObject {
// 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 }
}
public extension SocketParsable where Self: SocketManagerSpec & SocketDataBufferable {
/// Parses a message from the engine, returning a complete SocketPacket or throwing.
///
/// - parameter message: The message to parse.
/// - returns: A completed packet, or throwing.
internal func parseString(_ message: String) throws -> SocketPacket {
var reader = SocketStringReader(message: message)
guard let type = Int(reader.read(count: 1)).flatMap({ SocketPacket.PacketType(rawValue: $0) }) else {
throw SocketParsableError.invalidPacketType
}
if !reader.hasNext {
return SocketPacket(type: type, nsp: "/")
}
var namespace = "/"
var placeholders = -1
if type.isBinary {
if let holders = Int(reader.readUntilOccurence(of: "-")) {
placeholders = holders
} else {
throw SocketParsableError.invalidPacket
}
}
if reader.currentCharacter == "/" {
namespace = reader.readUntilOccurence(of: ",")
}
if !reader.hasNext {
return SocketPacket(type: type, nsp: namespace, placeholders: placeholders)
}
var idString = ""
if type == .error {
reader.advance(by: -1)
} else {
while reader.hasNext {
if let int = Int(reader.read(count: 1)) {
idString += String(int)
} else {
reader.advance(by: -2)
break
}
}
}
var dataArray = String(message.utf16[message.utf16.index(reader.currentIndex, offsetBy: 1)...])!
if type == .error && !dataArray.hasPrefix("[") && !dataArray.hasSuffix("]") {
dataArray = "[" + dataArray + "]"
}
let data = try parseData(dataArray)
return SocketPacket(type: type, data: data, id: Int(idString) ?? -1, nsp: namespace, placeholders: placeholders)
}
// Parses data for events
private func parseData(_ data: String) throws -> [Any] {
do {
return try data.toArray()
} catch {
throw SocketParsableError.invalidDataArray
}
}
/// Called when the engine has received a string that should be parsed into a socket.io packet.
///
/// - parameter message: The string that needs parsing.
/// - returns: A completed socket packet or nil if the packet is invalid.
public func parseSocketMessage(_ message: String) -> SocketPacket? {
guard !message.isEmpty else { return nil }
DefaultSocketLogger.Logger.log("Parsing \(message)", type: "SocketParser")
do {
let packet = try parseString(message)
DefaultSocketLogger.Logger.log("Decoded packet as: \(packet.description)", type: "SocketParser")
return packet
} catch {
DefaultSocketLogger.Logger.error("\(error): \(message)", type: "SocketParser")
return nil
}
}
/// Called when the engine has received some binary data that should be attached to a packet.
///
/// Packets binary data should be sent directly after the packet that expects it, so there's confusion over
/// where the data should go. Data should be received in the order it is sent, so that the correct data is put
/// into the correct placeholder.
///
/// - parameter data: The data that should be attached to a packet.
/// - returns: A completed socket packet if there is no more data left to collect.
public func parseBinaryData(_ data: Data) -> SocketPacket? {
guard !waitingPackets.isEmpty else {
DefaultSocketLogger.Logger.error("Got data when not remaking packet", type: "SocketParser")
return nil
}
// Should execute event?
guard waitingPackets[waitingPackets.count - 1].addData(data) else { return nil }
return waitingPackets.removeLast()
}
}
@@ -0,0 +1,72 @@
//
// SSLSecurity.swift
// SocketIO-iOS
//
// Created by Lukas Schmidt on 24.09.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 Foundation
import Starscream
/// A wrapper around Starscream's SSLSecurity that provides a minimal Objective-C interface.
open class SSLSecurity : NSObject {
// MARK: Properties
/// The internal Starscream SSLSecurity.
public let security: Starscream.SSLSecurity
init(security: Starscream.SSLSecurity) {
self.security = security
}
// MARK: Methods
/// Creates a new SSLSecurity that specifies whether to use publicKeys or certificates should be used for SSL
/// pinning validation
///
/// - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning
/// validation
@objc
public convenience init(usePublicKeys: Bool = true) {
let security = Starscream.SSLSecurity(usePublicKeys: usePublicKeys)
self.init(security: security)
}
/// Designated init
///
/// - parameter certs: is the certificates or public keys to use
/// - parameter usePublicKeys: is to specific if the publicKeys or certificates should be used for SSL pinning
/// validation
/// - returns: a representation security object to be used with
public convenience init(certs: [SSLCert], usePublicKeys: Bool) {
let security = Starscream.SSLSecurity(certs: certs, usePublicKeys: usePublicKeys)
self.init(security: security)
}
/// Returns whether or not the given trust is valid.
///
/// - parameter trust: The trust to validate.
/// - parameter domain: The CN domain to validate.
/// - returns: Whether or not this is valid.
public func isValid(_ trust: SecTrust, domain: String?) -> Bool {
return security.isValid(trust, domain: domain)
}
}
@@ -0,0 +1,124 @@
//
// SocketExtensions.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 7/1/2016.
//
// 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
import Starscream
enum JSONError : Error {
case notArray
case notNSDictionary
}
extension Array {
func toJSON() throws -> Data {
return try JSONSerialization.data(withJSONObject: self, options: JSONSerialization.WritingOptions(rawValue: 0))
}
}
extension CharacterSet {
static var allowedURLCharacterSet: CharacterSet {
return CharacterSet(charactersIn: "!*'();:@&=+$,/?%#[]\" {}^").inverted
}
}
extension Dictionary where Key == String, Value == Any {
private static func keyValueToSocketIOClientOption(key: String, value: Any) -> SocketIOClientOption? {
switch (key, value) {
case let ("connectParams", params as [String: Any]):
return .connectParams(params)
case let ("cookies", cookies as [HTTPCookie]):
return .cookies(cookies)
case let ("extraHeaders", headers as [String: String]):
return .extraHeaders(headers)
case let ("forceNew", force as Bool):
return .forceNew(force)
case let ("forcePolling", force as Bool):
return .forcePolling(force)
case let ("forceWebsockets", force as Bool):
return .forceWebsockets(force)
case let ("handleQueue", queue as DispatchQueue):
return .handleQueue(queue)
case let ("log", log as Bool):
return .log(log)
case let ("logger", logger as SocketLogger):
return .logger(logger)
case let ("path", path as String):
return .path(path)
case let ("reconnects", reconnects as Bool):
return .reconnects(reconnects)
case let ("reconnectAttempts", attempts as Int):
return .reconnectAttempts(attempts)
case let ("reconnectWait", wait as Int):
return .reconnectWait(wait)
case let ("secure", secure as Bool):
return .secure(secure)
case let ("security", security as SSLSecurity):
return .security(security)
case let ("selfSigned", selfSigned as Bool):
return .selfSigned(selfSigned)
case let ("sessionDelegate", delegate as URLSessionDelegate):
return .sessionDelegate(delegate)
case let ("compress", compress as Bool):
return compress ? .compress : nil
default:
return nil
}
}
func toSocketConfiguration() -> SocketIOClientConfiguration {
var options = [] as SocketIOClientConfiguration
for (rawKey, value) in self {
if let opt = Dictionary.keyValueToSocketIOClientOption(key: rawKey, value: value) {
options.insert(opt)
}
}
return options
}
}
extension String {
func toArray() throws -> [Any] {
guard let stringData = data(using: .utf16, allowLossyConversion: false) else { return [] }
guard let array = try JSONSerialization.jsonObject(with: stringData, options: .mutableContainers) as? [Any] else {
throw JSONError.notArray
}
return array
}
func toDictionary() throws -> [String: Any] {
guard let binData = data(using: .utf16, allowLossyConversion: false) else { return [:] }
guard let json = try JSONSerialization.jsonObject(with: binData, options: .allowFragments) as? [String: Any] else {
throw JSONError.notNSDictionary
}
return json
}
func urlEncode() -> String? {
return addingPercentEncoding(withAllowedCharacters: .allowedURLCharacterSet)
}
}
@@ -0,0 +1,73 @@
//
// SocketLogger.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 4/11/15.
//
// 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
/// Represents a class will log client events.
public protocol SocketLogger : AnyObject {
// MARK: Properties
/// Whether to log or not
var log: Bool { get set }
// MARK: Methods
/// Normal log messages
///
/// - parameter message: The message being logged. Can include `%@` that will be replaced with `args`
/// - parameter type: The type of entity that called for logging.
/// - parameter args: Any args that should be inserted into the message. May be left out.
func log(_ message: @autoclosure () -> String, type: String)
/// Error Messages
///
/// - parameter message: The message being logged. Can include `%@` that will be replaced with `args`
/// - parameter type: The type of entity that called for logging.
/// - parameter args: Any args that should be inserted into the message. May be left out.
func error(_ message: @autoclosure () -> String, type: String)
}
public extension SocketLogger {
/// Default implementation.
func log(_ message: @autoclosure () -> String, type: String) {
abstractLog("LOG", message: message, type: type)
}
/// Default implementation.
func error(_ message: @autoclosure () -> String, type: String) {
abstractLog("ERROR", message: message, type: type)
}
private func abstractLog(_ logType: String, message: @autoclosure () -> String, type: String) {
guard log else { return }
NSLog("\(logType) \(type): %@", message())
}
}
class DefaultSocketLogger : SocketLogger {
static var Logger: SocketLogger = DefaultSocketLogger()
var log = false
}
@@ -0,0 +1,73 @@
//
// SocketStringReader.swift
// Socket.IO-Client-Swift
//
// Created by Lukas Schmidt on 07.09.15.
//
// 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.
struct SocketStringReader {
let message: String
var currentIndex: String.UTF16View.Index
var hasNext: Bool {
return currentIndex != message.utf16.endIndex
}
var currentCharacter: String {
return String(UnicodeScalar(message.utf16[currentIndex])!)
}
init(message: String) {
self.message = message
currentIndex = message.utf16.startIndex
}
@discardableResult
mutating func advance(by: Int) -> String.UTF16View.Index {
currentIndex = message.utf16.index(currentIndex, offsetBy: by)
return currentIndex
}
mutating func read(count: Int) -> String {
let readString = String(message.utf16[currentIndex..<message.utf16.index(currentIndex, offsetBy: count)])!
advance(by: count)
return readString
}
mutating func readUntilOccurence(of string: String) -> String {
let substring = message.utf16[currentIndex...]
guard let foundIndex = substring.index(of: string.utf16.first!) else {
currentIndex = message.utf16.endIndex
return String(substring)!
}
advance(by: substring.distance(from: substring.startIndex, to: foundIndex) + 1)
return String(substring[substring.startIndex..<foundIndex])!
}
mutating func readUntilEnd() -> String {
return read(count: message.utf16.distance(from: currentIndex, to: message.utf16.endIndex))
}
}
@@ -0,0 +1,83 @@
//
// SocketTypes.swift
// Socket.IO-Client-Swift
//
// Created by Erik Little on 4/8/15.
//
// 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
/// A marking protocol that says a type can be represented in a socket.io packet.
///
/// Example:
///
/// ```swift
/// struct CustomData : SocketData {
/// let name: String
/// let age: Int
///
/// func socketRepresentation() -> SocketData {
/// return ["name": name, "age": age]
/// }
/// }
///
/// socket.emit("myEvent", CustomData(name: "Erik", age: 24))
/// ```
public protocol SocketData {
// MARK: Methods
/// A representation of self that can sent over socket.io.
func socketRepresentation() throws -> SocketData
}
public extension SocketData {
/// Default implementation. Only works for native Swift types and a few Foundation types.
func socketRepresentation() -> SocketData {
return self
}
}
extension Array : SocketData { }
extension Bool : SocketData { }
extension Dictionary : SocketData { }
extension Double : SocketData { }
extension Int : SocketData { }
extension NSArray : SocketData { }
extension Data : SocketData { }
extension NSData : SocketData { }
extension NSDictionary : SocketData { }
extension NSString : SocketData { }
extension NSNull : SocketData { }
extension String : SocketData { }
/// A typealias for an ack callback.
public typealias AckCallback = ([Any]) -> ()
/// A typealias for a normal callback.
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()
typealias JSON = [String: Any]
typealias Probe = (msg: String, type: SocketEnginePacketType, data: [Data])
typealias ProbeWaitQueue = [Probe]
enum Either<E, V> {
case left(E)
case right(V)
}