socket.io-client-swift/Source/SocketIOClient.swift
Erik 1ff8e11a1f
Document SocketIOClientSwift
add docs

Start documenting engine

More engine documentation

Document engine

document SocketEngineClient

Document SocketEnginePollable and SocketEnginePacketType

Document SocketEngineWebsocket

Document SocketIOClient

Document SocketIOClientStatus

Document SocketLogger

Document some typealiases

Document SocketIOClientOption

Document SocketIOClientConfiguration
2017-05-06 14:41:50 -04:00

554 lines
19 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 Dispatch
import Foundation
/// The main class for SocketIOClientSwift.
///
/// Represents a socket.io-client. Most interaction with socket.io will be through this class.
open class SocketIOClient : NSObject, SocketIOClientSpec, SocketEngineClient, SocketParsable {
// MARK: Properties
/// The URL of the socket.io server. This is set in the initializer.
public let socketURL: URL
/// The engine for this client.
public private(set) var engine: SocketEngineSpec?
/// The status of this client.
public private(set) var status = SocketIOClientStatus.notConnected {
didSet {
switch status {
case .connected:
reconnecting = false
currentReconnectAttempt = 0
default:
break
}
}
}
/// 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.
public var handleQueue = DispatchQueue.main
/// The namespace for this client.
public var nsp = "/"
/// The configuration for this client.
public var config: SocketIOClientConfiguration
/// 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 session id of this client.
public var sid: String? {
return engine?.sid
}
private let logType = "SocketIOClient"
private var anyHandler: ((SocketAnyEvent) -> Void)?
private var currentReconnectAttempt = 0
private var handlers = [SocketEventHandler]()
private var reconnecting = false
private(set) var currentAck = -1
private(set) var reconnectAttempts = -1
var ackHandlers = SocketAckManager()
var waitingPackets = [SocketPacket]()
// 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))
}
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>)`
///
/// - parameter socketURL: The url of the socket.io server.
/// - parameter config: The config for this socket.
public convenience init(socketURL: NSURL, config: NSDictionary?) {
self.init(socketURL: socketURL as URL, config: config?.toSocketConfiguration() ?? [])
}
deinit {
DefaultSocketLogger.Logger.log("Client is being released", type: logType)
engine?.disconnect(reason: "Client Deinit")
}
// MARK: Methods
private func addEngine() -> SocketEngineSpec {
DefaultSocketLogger.Logger.log("Adding engine", type: logType, args: "")
engine?.client = nil
engine = SocketEngine(client: self, url: socketURL, config: config)
return engine!
}
/// Connect to the server.
open func connect() {
connect(timeoutAfter: 0, withHandler: nil)
}
/// Connect to the server. If we aren't connected after `timeoutAfter` seconds, then `withHandler` is called.
///
/// - 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 withHandler: The handler to call when the client fails to connect.
open func connect(timeoutAfter: Int, withHandler 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 = DispatchTime.now() + Double(UInt64(timeoutAfter) * NSEC_PER_SEC) / Double(NSEC_PER_SEC)
handleQueue.asyncAfter(deadline: time) {[weak self] in
guard let this = self, this.status != .connected && this.status != .disconnected else { return }
this.status = .disconnected
this.engine?.disconnect(reason: "Connect timeout")
handler?()
}
}
private func createOnAck(_ items: [Any]) -> OnAckCallback {
currentAck += 1
return OnAckCallback(ackNumber: currentAck, items: items, socket: self)
}
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: reason)
handleEvent("disconnect", data: [reason], isInternalMessage: true)
}
/// Disconnects the socket.
open func disconnect() {
DefaultSocketLogger.Logger.log("Closing socket", type: logType)
didDisconnect(reason: "Disconnect")
}
/// Send an event to the server, with optional data items.
///
/// - 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...) {
emit(event, with: items)
}
/// Same as emit, but meant for Objective-C
///
/// - parameter event: The event to send.
/// - parameter with: The items to send with this event. May be left out.
open func emit(_ event: String, with items: [Any]) {
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.
///
/// **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", 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 {
return emitWithAck(event, with: items)
}
/// 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 with: 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.
open func emitWithAck(_ event: String, with items: [Any]) -> OnAckCallback {
return createOnAck([event] + items)
}
func _emit(_ data: [Any], ack: Int? = nil) {
guard status == .connected else {
handleEvent("error", data: ["Tried emitting when not connected"], isInternalMessage: true)
return
}
let packet = SocketPacket.packetFromEmit(data, id: ack ?? -1, nsp: nsp, ack: false)
let str = packet.packetString
DefaultSocketLogger.Logger.log("Emitting: %@", type: logType, args: str)
engine?.send(str, withData: packet.binary)
}
// If the server wants to know that the client received data
func emitAck(_ ack: Int, with items: [Any]) {
guard status == .connected else { return }
let packet = SocketPacket.packetFromEmit(items, id: ack, nsp: nsp, ack: true)
let str = packet.packetString
DefaultSocketLogger.Logger.log("Emitting Ack: %@", type: logType, args: str)
engine?.send(str, withData: packet.binary)
}
/// Called when the engine closes.
///
/// - parameter reason: The reason that the engine closed.
open func engineDidClose(reason: String) {
handleQueue.async {
self._engineDidClose(reason: reason)
}
}
private func _engineDidClose(reason: String) {
waitingPackets.removeAll()
if status != .disconnected {
status = .notConnected
}
if status == .disconnected || !reconnects {
didDisconnect(reason: reason)
} else if !reconnecting {
reconnecting = true
tryReconnect(reason: reason)
}
}
/// Called when the engine errors.
///
/// - parameter reason: The reason the engine errored.
open func engineDidError(reason: String) {
handleQueue.async {
self._engineDidError(reason: reason)
}
}
private func _engineDidError(reason: String) {
DefaultSocketLogger.Logger.error("%@", type: logType, args: reason)
handleEvent("error", data: [reason], isInternalMessage: true)
}
/// Called when the engine opens.
///
/// - parameter reason: The reason the engine opened.
open 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: [Any]) {
guard status == .connected else { return }
DefaultSocketLogger.Logger.log("Handling ack: %@ with data: %@", type: logType, args: ack, data)
ackHandlers.executeAck(ack, with: data, onQueue: handleQueue)
}
/// Causes an event to be handled, and any event handlers for that event to be called.
///
/// - parameter event: The event that is to be handled.
/// - parameter data: the data associated with this event.
/// - parameter isInternalMessage: If `true` event handlers for this event will be called regardless of status.
/// - parameter withAck: The ack number for this event. May be left out.
open func handleEvent(_ event: String, data: [Any], isInternalMessage: Bool, withAck ack: Int = -1) {
guard status == .connected || isInternalMessage else { return }
DefaultSocketLogger.Logger.log("Handling event: %@ with data: %@", type: logType, args: event, data)
anyHandler?(SocketAnyEvent(event: event, items: data))
for handler in handlers where handler.event == event {
handler.executeCallback(with: data, withAck: ack, withSocket: self)
}
}
/// Leaves nsp and goes back to the default namespace.
open func leaveNamespace() {
if nsp != "/" {
engine?.send("1\(nsp)", withData: [])
nsp = "/"
}
}
/// Joins `namespace`.
///
/// **Do not use this to join the default namespace.** Instead call `leaveNamespace`.
///
/// - parameter namespace: The namespace to join.
open 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 an event name.
///
/// If you wish to remove a specific event, call the `off(if:)` with the UUID received from its `on` call.
///
/// - parameter event: The event to remove handlers for.
open 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`
///
/// 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.
open func off(id: UUID) {
DefaultSocketLogger.Logger.log("Removing handler with id: %@", type: logType, args: id)
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.
@discardableResult
open func on(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding handler for event: %@", type: logType, args: event)
let handler = SocketEventHandler(event: event, id: UUID(), callback: callback)
handlers.append(handler)
return handler.id
}
/// 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.
@discardableResult
open func once(_ event: String, callback: @escaping NormalCallback) -> UUID {
DefaultSocketLogger.Logger.log("Adding once handler for event: %@", type: logType, args: event)
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.
open func onAny(_ handler: @escaping (SocketAnyEvent) -> Void) {
anyHandler = handler
}
/// Called when the engine has a message that must be parsed.
///
/// - parameter msg: The message that needs parsing.
public func parseEngineMessage(_ msg: String) {
DefaultSocketLogger.Logger.log("Should parse message: %@", type: "SocketIOClient", args: msg)
handleQueue.async { self.parseSocketMessage(msg) }
}
/// Called when the engine receives binary data.
///
/// - parameter data: The data the engine received.
public func parseEngineBinaryData(_ data: Data) {
handleQueue.async { self.parseBinaryData(data) }
}
/// Tries to reconnect to the server.
///
/// This will cause a `disconnect` event to be emitted, as well as an `reconnectAttempt` event.
open func reconnect() {
guard !reconnecting else { return }
engine?.disconnect(reason: "manual reconnect")
}
/// Removes all handlers.
/// Can be used after disconnecting to break any potential remaining retain cycles.
open func removeAllHandlers() {
handlers.removeAll(keepingCapacity: false)
}
private func tryReconnect(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(reason: "Reconnect Failed")
}
DefaultSocketLogger.Logger.log("Trying to reconnect", type: logType)
handleEvent("reconnectAttempt", data: [(reconnectAttempts - currentReconnectAttempt)], isInternalMessage: true)
currentReconnectAttempt += 1
connect()
handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect)
}
// Test properties
var testHandlers: [SocketEventHandler] {
return handlers
}
func setTestable() {
status = .connected
}
func setTestEngine(_ engine: SocketEngineSpec?) {
self.engine = engine
}
func emitTest(event: String, _ data: Any...) {
_emit([event] + data)
}
}