socket.io-client-swift/Source/SocketIOClient.swift
Erik 404fa8cdfa
Merge branch 'development' into swift2.3
* 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
2016-09-02 17:40:58 -04:00

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)
}
}