* development: change spm name Modify mac version base mac os 10.9 Fix cocoapods name fix sid bug Fix socketio/socket.io-client-swift#472 bump version Refactor engine. Fix infinite recursion in configuration Don't send whole packet to handleConnect Update readme bump version update readme Renamed modules to make then consistent. rename engine init Revert "travis plz" travis plz
462 lines
16 KiB
Swift
462 lines
16 KiB
Swift
//
|
|
// 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 Foundation
|
|
|
|
public final class SocketIOClient : NSObject, SocketEngineClient, SocketParsable {
|
|
public let socketURL: NSURL
|
|
|
|
public private(set) var engine: SocketEngineSpec?
|
|
public private(set) var status = SocketIOClientStatus.NotConnected {
|
|
didSet {
|
|
switch status {
|
|
case .Connected:
|
|
reconnecting = false
|
|
currentReconnectAttempt = 0
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
public var forceNew = false
|
|
public var nsp = "/"
|
|
public var config: SocketIOClientConfiguration
|
|
public var reconnects = true
|
|
public var reconnectWait = 10
|
|
|
|
private let ackQueue = dispatch_queue_create("com.socketio.ackQueue", DISPATCH_QUEUE_SERIAL)
|
|
private let emitQueue = dispatch_queue_create("com.socketio.emitQueue", DISPATCH_QUEUE_SERIAL)
|
|
private let logType = "SocketIOClient"
|
|
private let parseQueue = dispatch_queue_create("com.socketio.parseQueue", DISPATCH_QUEUE_SERIAL)
|
|
|
|
private var anyHandler: ((SocketAnyEvent) -> Void)?
|
|
private var currentReconnectAttempt = 0
|
|
private var handlers = [SocketEventHandler]()
|
|
private var ackHandlers = SocketAckManager()
|
|
private var reconnecting = false
|
|
|
|
private(set) var currentAck = -1
|
|
private(set) var handleQueue = dispatch_get_main_queue()
|
|
private(set) var reconnectAttempts = -1
|
|
|
|
var waitingPackets = [SocketPacket]()
|
|
|
|
public var sid: String? {
|
|
return nsp + "#" + (engine?.sid ?? "")
|
|
}
|
|
|
|
/// Type safe way to create a new SocketIOClient. config can be omitted
|
|
public init(socketURL: NSURL, config: SocketIOClientConfiguration = []) {
|
|
self.config = config
|
|
self.socketURL = socketURL
|
|
|
|
if socketURL.absoluteString?.hasPrefix("https://") ?? false {
|
|
self.config.insert(.Secure(true))
|
|
}
|
|
|
|
for option in config {
|
|
switch option {
|
|
case let .Reconnects(reconnects):
|
|
self.reconnects = reconnects
|
|
case let .ReconnectAttempts(attempts):
|
|
reconnectAttempts = attempts
|
|
case let .ReconnectWait(wait):
|
|
reconnectWait = abs(wait)
|
|
case let .Nsp(nsp):
|
|
self.nsp = nsp
|
|
case let .Log(log):
|
|
DefaultSocketLogger.Logger.log = log
|
|
case let .Logger(logger):
|
|
DefaultSocketLogger.Logger = logger
|
|
case let .HandleQueue(queue):
|
|
handleQueue = queue
|
|
case let .ForceNew(force):
|
|
forceNew = force
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
self.config.insert(.Path("/socket.io/"), replacing: false)
|
|
|
|
super.init()
|
|
}
|
|
|
|
/// 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>)`
|
|
public convenience init(socketURL: NSURL, config: NSDictionary?) {
|
|
self.init(socketURL: socketURL, config: config?.toSocketConfiguration() ?? [])
|
|
}
|
|
|
|
deinit {
|
|
DefaultSocketLogger.Logger.log("Client is being released", type: logType)
|
|
engine?.disconnect("Client Deinit")
|
|
}
|
|
|
|
private func addEngine() -> SocketEngineSpec {
|
|
DefaultSocketLogger.Logger.log("Adding engine", type: logType)
|
|
|
|
engine = SocketEngine(client: self, url: socketURL, config: config)
|
|
|
|
return engine!
|
|
}
|
|
|
|
/// Connect to the server.
|
|
public func connect() {
|
|
connect(timeoutAfter: 0, withTimeoutHandler: nil)
|
|
}
|
|
|
|
/// Connect to the server. If we aren't connected after timeoutAfter, call handler
|
|
public func connect(timeoutAfter timeoutAfter: Int, withTimeoutHandler handler: (() -> Void)?) {
|
|
assert(timeoutAfter >= 0, "Invalid timeout: \(timeoutAfter)")
|
|
|
|
guard status != .Connected else {
|
|
DefaultSocketLogger.Logger.log("Tried connecting on an already connected socket", type: logType)
|
|
return
|
|
}
|
|
|
|
status = .Connecting
|
|
|
|
if engine == nil || forceNew {
|
|
addEngine().connect()
|
|
} else {
|
|
engine?.connect()
|
|
}
|
|
|
|
guard timeoutAfter != 0 else { return }
|
|
|
|
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeoutAfter) * Int64(NSEC_PER_SEC))
|
|
|
|
dispatch_after(time, handleQueue) {[weak self] in
|
|
guard let this = self where this.status != .Connected && this.status != .Disconnected else { return }
|
|
|
|
this.status = .Disconnected
|
|
this.engine?.disconnect("Connect timeout")
|
|
|
|
handler?()
|
|
}
|
|
}
|
|
|
|
private func createOnAck(items: [AnyObject]) -> OnAckCallback {
|
|
currentAck += 1
|
|
|
|
return {[weak self, ack = currentAck] timeout, callback in
|
|
guard let this = self else { return }
|
|
|
|
dispatch_sync(this.ackQueue) {
|
|
this.ackHandlers.addAck(ack, callback: callback)
|
|
}
|
|
|
|
|
|
this._emit(items, ack: ack)
|
|
|
|
if timeout != 0 {
|
|
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(timeout * NSEC_PER_SEC))
|
|
|
|
dispatch_after(time, this.ackQueue) {
|
|
this.ackHandlers.timeoutAck(ack, onQueue: this.handleQueue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func didConnect() {
|
|
DefaultSocketLogger.Logger.log("Socket connected", type: logType)
|
|
status = .Connected
|
|
|
|
// Don't handle as internal because something crazy could happen where
|
|
// we disconnect before it's handled
|
|
handleEvent("connect", data: [], isInternalMessage: false)
|
|
}
|
|
|
|
func didDisconnect(reason: String) {
|
|
guard status != .Disconnected else { return }
|
|
|
|
DefaultSocketLogger.Logger.log("Disconnected: %@", type: logType, args: reason)
|
|
|
|
reconnecting = false
|
|
status = .Disconnected
|
|
|
|
// Make sure the engine is actually dead.
|
|
engine?.disconnect(reason)
|
|
handleEvent("disconnect", data: [reason], isInternalMessage: true)
|
|
}
|
|
|
|
/// Disconnects the socket.
|
|
public func disconnect() {
|
|
DefaultSocketLogger.Logger.log("Closing socket", type: logType)
|
|
|
|
didDisconnect("Disconnect")
|
|
}
|
|
|
|
/// Send a message to the server
|
|
public func emit(event: String, _ items: AnyObject...) {
|
|
emit(event, withItems: items)
|
|
}
|
|
|
|
/// Same as emit, but meant for Objective-C
|
|
public func emit(event: String, withItems items: [AnyObject]) {
|
|
guard status == .Connected else {
|
|
handleEvent("error", data: ["Tried emitting \(event) when not connected"], isInternalMessage: true)
|
|
return
|
|
}
|
|
|
|
_emit([event] + items)
|
|
}
|
|
|
|
/// Sends a message to the server, requesting an ack. Use the onAck method of SocketAckHandler to add
|
|
/// an ack.
|
|
public func emitWithAck(event: String, _ items: AnyObject...) -> OnAckCallback {
|
|
return emitWithAck(event, withItems: items)
|
|
}
|
|
|
|
/// Same as emitWithAck, but for Objective-C
|
|
public func emitWithAck(event: String, withItems items: [AnyObject]) -> OnAckCallback {
|
|
return createOnAck([event] + items)
|
|
}
|
|
|
|
private func _emit(data: [AnyObject], ack: Int? = nil) {
|
|
dispatch_async(emitQueue) {
|
|
guard self.status == .Connected else {
|
|
self.handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true)
|
|
return
|
|
}
|
|
|
|
let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: self.nsp, ack: false)
|
|
let str = packet.packetString
|
|
|
|
DefaultSocketLogger.Logger.log("Emitting: %@", type: self.logType, args: str)
|
|
|
|
self.engine?.send(str, withData: packet.binary)
|
|
}
|
|
}
|
|
|
|
// If the server wants to know that the client received data
|
|
func emitAck(ack: Int, withItems items: [AnyObject]) {
|
|
dispatch_async(emitQueue) {
|
|
if self.status == .Connected {
|
|
let packet = SocketPacket.packetFromEmit(items, id: ack ?? -1, nsp: self.nsp, ack: true)
|
|
let str = packet.packetString
|
|
|
|
DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: self.logType, args: str)
|
|
|
|
self.engine?.send(str, withData: packet.binary)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func engineDidClose(reason: String) {
|
|
waitingPackets.removeAll()
|
|
|
|
if status != .Disconnected {
|
|
status = .NotConnected
|
|
}
|
|
|
|
if status == .Disconnected || !reconnects {
|
|
didDisconnect(reason)
|
|
} else if !reconnecting {
|
|
reconnecting = true
|
|
tryReconnectWithReason(reason)
|
|
}
|
|
}
|
|
|
|
/// error
|
|
public func engineDidError(reason: String) {
|
|
DefaultSocketLogger.Logger.error("%@", type: logType, args: reason)
|
|
|
|
handleEvent("error", data: [reason], isInternalMessage: true)
|
|
}
|
|
|
|
public func engineDidOpen(reason: String) {
|
|
DefaultSocketLogger.Logger.log(reason, type: "SocketEngineClient")
|
|
}
|
|
|
|
// Called when the socket gets an ack for something it sent
|
|
func handleAck(ack: Int, data: [AnyObject]) {
|
|
guard status == .Connected else { return }
|
|
|
|
DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data ?? "")
|
|
|
|
dispatch_async(ackQueue) {
|
|
self.ackHandlers.executeAck(ack, items: data, onQueue: self.handleQueue)
|
|
}
|
|
}
|
|
|
|
/// Causes an event to be handled. Only use if you know what you're doing.
|
|
public func handleEvent(event: String, data: [AnyObject], isInternalMessage: Bool, withAck ack: Int = -1) {
|
|
guard status == .Connected || isInternalMessage else { return }
|
|
|
|
DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data ?? "")
|
|
|
|
dispatch_async(handleQueue) {
|
|
self.anyHandler?(SocketAnyEvent(event: event, items: data))
|
|
|
|
for handler in self.handlers where handler.event == event {
|
|
handler.executeCallback(data, withAck: ack, withSocket: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Leaves nsp and goes back to /
|
|
public func leaveNamespace() {
|
|
if nsp != "/" {
|
|
engine?.send("1\(nsp)", withData: [])
|
|
nsp = "/"
|
|
}
|
|
}
|
|
|
|
/// Joins namespace
|
|
public func joinNamespace(namespace: String) {
|
|
nsp = namespace
|
|
|
|
if nsp != "/" {
|
|
DefaultSocketLogger.Logger.log("Joining namespace", type: logType)
|
|
engine?.send("0\(nsp)", withData: [])
|
|
}
|
|
}
|
|
|
|
/// Removes handler(s) based on name
|
|
public func off(event: String) {
|
|
DefaultSocketLogger.Logger.log("Removing handler for event: %@", type: logType, args: event)
|
|
|
|
handlers = handlers.filter({ $0.event != event })
|
|
}
|
|
|
|
/// Removes a handler with the specified UUID gotten from an `on` or `once`
|
|
public func off(id id: NSUUID) {
|
|
DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id)
|
|
|
|
handlers = handlers.filter({ $0.id != id })
|
|
}
|
|
|
|
/// Adds a handler for an event.
|
|
/// Returns: A unique id for the handler
|
|
public func on(event: String, callback: NormalCallback) -> NSUUID {
|
|
DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event)
|
|
|
|
let handler = SocketEventHandler(event: event, id: NSUUID(), callback: callback)
|
|
handlers.append(handler)
|
|
|
|
return handler.id
|
|
}
|
|
|
|
/// Adds a single-use handler for an event.
|
|
/// Returns: A unique id for the handler
|
|
public func once(event: String, callback: NormalCallback) -> NSUUID {
|
|
DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event)
|
|
|
|
let id = NSUUID()
|
|
|
|
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.
|
|
public func onAny(handler: (SocketAnyEvent) -> Void) {
|
|
anyHandler = handler
|
|
}
|
|
|
|
public func parseEngineMessage(msg: String) {
|
|
DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg)
|
|
|
|
dispatch_async(parseQueue) {
|
|
self.parseSocketMessage(msg)
|
|
}
|
|
}
|
|
|
|
public func parseEngineBinaryData(data: NSData) {
|
|
dispatch_async(parseQueue) {
|
|
self.parseBinaryData(data)
|
|
}
|
|
}
|
|
|
|
/// Tries to reconnect to the server.
|
|
public func reconnect() {
|
|
guard !reconnecting else { return }
|
|
|
|
engine?.disconnect("manual reconnect")
|
|
}
|
|
|
|
/// Removes all handlers.
|
|
/// Can be used after disconnecting to break any potential remaining retain cycles.
|
|
public func removeAllHandlers() {
|
|
handlers.removeAll(keepCapacity: false)
|
|
}
|
|
|
|
private func tryReconnectWithReason(reason: String) {
|
|
guard reconnecting else { return }
|
|
|
|
DefaultSocketLogger.Logger.log("Starting reconnect", type: logType)
|
|
handleEvent("reconnect", data: [reason], isInternalMessage: true)
|
|
|
|
_tryReconnect()
|
|
}
|
|
|
|
private func _tryReconnect() {
|
|
guard reconnecting else { return }
|
|
|
|
if reconnectAttempts != -1 && currentReconnectAttempt + 1 > reconnectAttempts || !reconnects {
|
|
return didDisconnect("Reconnect Failed")
|
|
}
|
|
|
|
DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType)
|
|
handleEvent("reconnectAttempt", data: [reconnectAttempts - currentReconnectAttempt],
|
|
isInternalMessage: true)
|
|
|
|
currentReconnectAttempt += 1
|
|
connect()
|
|
|
|
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(UInt64(reconnectWait) * NSEC_PER_SEC))
|
|
|
|
dispatch_after(time, dispatch_get_main_queue(), _tryReconnect)
|
|
}
|
|
}
|
|
|
|
// Test extensions
|
|
extension SocketIOClient {
|
|
var testHandlers: [SocketEventHandler] {
|
|
return handlers
|
|
}
|
|
|
|
func setTestable() {
|
|
status = .Connected
|
|
}
|
|
|
|
func setTestEngine(engine: SocketEngineSpec?) {
|
|
self.engine = engine
|
|
}
|
|
|
|
func emitTest(event: String, _ data: AnyObject...) {
|
|
_emit([event] + data)
|
|
}
|
|
}
|