websocket and user center
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
+138
@@ -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)
|
||||
}
|
||||
+197
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
+337
@@ -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)
|
||||
}
|
||||
+50
@@ -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
|
||||
}
|
||||
+234
@@ -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)
|
||||
}
|
||||
}
|
||||
+69
@@ -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
|
||||
}
|
||||
}
|
||||
+139
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user