diff --git a/.gitignore b/.gitignore index e17236a..e12f2f8 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ node_modules/ # Test reports TESTS-*.xml + +# Ignored, see the README.md in platform/ for details +/ios/platform/Starscream.xcframework +/ios/platform/SocketIO.xcframework diff --git a/ios/Cartfile b/ios/Cartfile index b837c3b..e3c43d6 100644 --- a/ios/Cartfile +++ b/ios/Cartfile @@ -1 +1 @@ -github "appcelerator-modules/socket.io-client-swift" "xcframework" \ No newline at end of file +github "socketio/socket.io-client-swift" \ No newline at end of file diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved index 26110ed..e14ba89 100644 --- a/ios/Cartfile.resolved +++ b/ios/Cartfile.resolved @@ -1,2 +1,2 @@ -github "appcelerator-modules/socket.io-client-swift" "ebc20ec8d82063f709ec31ec697e58a4e1e4802c" -github "daltoniam/Starscream" "3.1.1" +github "daltoniam/Starscream" "4.0.4" +github "socketio/socket.io-client-swift" "v16.0.1" diff --git a/ios/Classes/ComAppcTitaniumSocketioModuleAssets.m b/ios/Classes/ComAppcTitaniumSocketioModuleAssets.m deleted file mode 100644 index bf5181b..0000000 --- a/ios/Classes/ComAppcTitaniumSocketioModuleAssets.m +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This is a generated file. Do not edit or your changes will be lost - */ -#import "ComAppcTitaniumSocketioModuleAssets.h" - -extern NSData *filterDataInRange(NSData *thedata, NSRange range); - -@implementation ComAppcTitaniumSocketioModuleAssets - -- (NSData *)moduleAsset -{ - - return nil; -} - -- (NSData *)resolveModuleAsset:(NSString *)path -{ - - return nil; -} - -@end diff --git a/ios/Classes/SocketIO/Ack/SocketAckEmitter.swift b/ios/Classes/SocketIO/Ack/SocketAckEmitter.swift new file mode 100644 index 0000000..0a4eea7 --- /dev/null +++ b/ios/Classes/SocketIO/Ack/SocketAckEmitter.swift @@ -0,0 +1,146 @@ +// +// 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 + } + + /// :nodoc: + 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) + } + } + +} diff --git a/ios/Classes/SocketIO/Ack/SocketAckManager.swift b/ios/Classes/SocketIO/Ack/SocketAckManager.swift new file mode 100644 index 0000000..51047fb --- /dev/null +++ b/ios/Classes/SocketIO/Ack/SocketAckManager.swift @@ -0,0 +1,78 @@ +// +// 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! + + init(ack: Int) { + self.ack = ack + } + + init(ack: Int, callback: @escaping AckCallback) { + self.ack = ack + self.callback = callback + } + + func hash(into hasher: inout Hasher) { + ack.hash(into: &hasher) + } + + 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(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]) + } +} diff --git a/ios/Classes/SocketIO/Client/SocketAnyEvent.swift b/ios/Classes/SocketIO/Client/SocketAnyEvent.swift new file mode 100644 index 0000000..441e8c5 --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketAnyEvent.swift @@ -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 + } +} diff --git a/ios/Classes/SocketIO/Client/SocketEventHandler.swift b/ios/Classes/SocketIO/Client/SocketEventHandler.swift new file mode 100644 index 0000000..4f75a5a --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketEventHandler.swift @@ -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)) + } +} diff --git a/ios/Classes/SocketIO/Client/SocketIOClient.swift b/ios/Classes/SocketIO/Client/SocketIOClient.swift new file mode 100644 index 0000000..355fd21 --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketIOClient.swift @@ -0,0 +1,561 @@ +// +// 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, status.rawValue]) + } + } + + 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() + } + + /// :nodoc: + 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 and optional write completion handler. + /// + /// 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. + /// - parameter completion: Callback called on transport write completion. + open func emit(_ event: String, _ items: SocketData..., completion: (() -> ())? = nil) { + do { + try emit(event, with: items.map({ try $0.socketRepresentation() }), completion: completion) + } 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) + } + + /// 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. + /// - parameter completion: Callback called on transport write completion. + @objc + open func emit(_ event: String, with items: [Any], completion: (() -> ())? = nil) { + emit([event] + items, completion: completion) + } + + /// 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, + completion: (() -> ())? = nil + ) { + // wrap the completion handler so it always runs async via handlerQueue + let wrappedCompletion: (() -> ())? = (completion == nil) ? nil : {[weak self] in + guard let this = self else { return } + this.manager?.handleQueue.async { + completion!() + } + } + + guard status == .connected else { + wrappedCompletion?() + 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, completion: wrappedCompletion) + } + + /// 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) + } +} diff --git a/ios/Classes/SocketIO/Client/SocketIOClientConfiguration.swift b/ios/Classes/SocketIO/Client/SocketIOClientConfiguration.swift new file mode 100644 index 0000000..0ccaace --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketIOClientConfiguration.swift @@ -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.Index + + /// Iterator type. + public typealias Iterator = Array.Iterator + + /// SubSequence type. + public typealias SubSequence = Array.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) -> 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.. 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) + + /// If passed `true`, the WebSocket stream will be configured with the enableSOCKSProxy `true`. + case enableSOCKSProxy(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 minimum number of seconds to wait before reconnect attempts. + case reconnectWait(Int) + + /// The maximum number of seconds to wait before reconnect attempts. + case reconnectWaitMax(Int) + + /// The randomization factor for calculating reconnect jitter. + case randomizationFactor(Double) + + /// 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 .reconnectWaitMax: + description = "reconnectWaitMax" + case .randomizationFactor: + description = "randomizationFactor" + case .secure: + description = "secure" + case .selfSigned: + description = "selfSigned" + case .security: + description = "security" + case .sessionDelegate: + description = "sessionDelegate" + case .enableSOCKSProxy: + description = "enableSOCKSProxy" + } + + 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 .reconnectWaitMax(wait): + value = wait + case let .randomizationFactor(factor): + value = factor + case let .secure(secure): + value = secure + case let .security(security): + value = security + case let .selfSigned(signed): + value = signed + case let .sessionDelegate(delegate): + value = delegate + case let .enableSOCKSProxy(enable): + value = enable + } + + 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 + } + +} diff --git a/ios/Classes/SocketIO/Client/SocketIOClientSpec.swift b/ios/Classes/SocketIO/Client/SocketIOClientSpec.swift new file mode 100644 index 0000000..06c67e6 --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketIOClientSpec.swift @@ -0,0 +1,353 @@ +// +// 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 and optional write completion handler. + /// + /// 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. + /// - parameter completion: Callback called on transport write completion. + func emit(_ event: String, _ items: SocketData..., completion: (() -> ())?) + + /// 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. + 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. + /// + /// The payload for data is [SocketIOClientStatus, Int]. Where the second item is the raw value. Use the second one + /// if you are working in Objective-C. + /// + /// Usage: + /// + /// ```swift + /// socket.on(clientEvent: .statusChange) {data, ack in + /// // Some status changing logging + /// } + /// ``` + case statusChange + + /// Emitted when when upgrading the http connection to a websocket connection. + /// + /// Usage: + /// + /// ```swift + /// socket.on(clientEvent: .websocketUpgrade) {data, ack in + /// let headers = (data as [Any])[0] + /// // Some header logic + /// } + /// ``` + case websocketUpgrade +} diff --git a/ios/Classes/SocketIO/Client/SocketIOStatus.swift b/ios/Classes/SocketIO/Client/SocketIOStatus.swift new file mode 100644 index 0000000..4f298a2 --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketIOStatus.swift @@ -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" + } + } +} diff --git a/ios/Classes/SocketIO/Client/SocketRawView.swift b/ios/Classes/SocketIO/Client/SocketRawView.swift new file mode 100644 index 0000000..e348ade --- /dev/null +++ b/ios/Classes/SocketIO/Client/SocketRawView.swift @@ -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) + } +} diff --git a/ios/Classes/SocketIO/Engine/SocketEngine.swift b/ios/Classes/SocketIO/Engine/SocketEngine.swift new file mode 100644 index 0000000..be233d4 --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEngine.swift @@ -0,0 +1,707 @@ +// +// 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 = [Post]() + + /// `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 + + /// When `true`, the WebSocket `stream` will be configured with the enableSOCKSProxy `true`. + public private(set) var enableSOCKSProxy = 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: 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() ?? []) + } + + /// :nodoc: + 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).. (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(for: urlPollingWithSid)) + + let stream = FoundationStream() + stream.enableSOCKSProxy = enableSOCKSProxy + ws = WebSocket(request: req, stream: stream) + 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?.onHttpResponseHeaders = {[weak self] headers in + guard let this = self else { return } + + this.client?.engineDidWebsocketUpgrade(headers: headers) + } + + 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: [], completion: nil) + 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: [], completion: nil) + 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, completion: waiter.completion) + } + + 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.msg, completion: msg.completion) + } + + 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.. pongsMissedMax { + closeOutEngine(reason: "Ping timeout") + return + } + + pongsMissed += 1 + write("", withType: .ping, withData: [], completion: nil) + + engineQueue.asyncAfter(deadline: .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 + case .enableSOCKSProxy: + self.enableSOCKSProxy = 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: [], completion: nil) + // 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. + /// - parameter completion: Callback called on transport write completion. + open func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data], completion: (() -> ())? = nil) { + engineQueue.async { + guard self.connected else { + completion?() + return + } + guard !self.probing else { + self.probeWait.append((msg, type, data, completion)) + + 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, completion: completion) + } else { + DefaultSocketLogger.Logger.log("Writing ws: \(msg) has data: \(data.count != 0)", + type: SocketEngine.logType) + self.sendWebSocketMessage(msg, withType: type, withData: data, completion: completion) + } + } + } + + // 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 error = error as? WSError { + didError(reason: "\(error.message). code=\(error.code), type=\(error.type)") + } else 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") + } +} diff --git a/ios/Classes/SocketIO/Engine/SocketEngineClient.swift b/ios/Classes/SocketIO/Engine/SocketEngineClient.swift new file mode 100644 index 0000000..00d68fa --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEngineClient.swift @@ -0,0 +1,67 @@ +// +// 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) + + /// Called when when upgrading the http connection to a websocket connection. + /// + /// - parameter headers: The http headers. + func engineDidWebsocketUpgrade(headers: [String: String]) +} diff --git a/ios/Classes/SocketIO/Engine/SocketEnginePacketType.swift b/ios/Classes/SocketIO/Engine/SocketEnginePacketType.swift new file mode 100644 index 0000000..b420ff2 --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEnginePacketType.swift @@ -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 +} diff --git a/ios/Classes/SocketIO/Engine/SocketEnginePollable.swift b/ios/Classes/SocketIO/Engine/SocketEnginePollable.swift new file mode 100644 index 0000000..9e55bd9 --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEnginePollable.swift @@ -0,0 +1,244 @@ +// +// 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: [Post] { 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], completion: (() -> ())?) + + /// Call to stop polling and invalidate the URLSession. + func stopPolling() +} + +// Default polling methods +extension SocketEnginePollable { + func createRequestForPostWithPostWait() -> URLRequest { + defer { + for packet in postWait { packet.completion?() } + postWait.removeAll(keepingCapacity: true) + } + + var postStr = "" + + for packet in postWait { + postStr += "\(packet.msg.utf16.count):\(packet.msg)" + } + + 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 } + guard let data = data, let res = res as? HTTPURLResponse, res.statusCode == 200 else { + if let err = err { + DefaultSocketLogger.Logger.error(err.localizedDescription, type: "SocketEnginePolling") + } else { + DefaultSocketLogger.Logger.error("Error during long poll request", 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] _, res, err in + guard let this = self else { return } + guard let res = res as? HTTPURLResponse, res.statusCode == 200 else { + if let err = err { + DefaultSocketLogger.Logger.error(err.localizedDescription, type: "SocketEnginePolling") + } else { + DefaultSocketLogger.Logger.error("Error flushing waiting posts", 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. + /// - parameter completion: Callback called on transport write completion. + public func sendPollMessage(_ message: String, withType type: SocketEnginePacketType, withData datas: [Data], completion: (() -> ())? = nil) { + DefaultSocketLogger.Logger.log("Sending poll: \(message) as type: \(type.rawValue)", type: "SocketEnginePolling") + + postWait.append((String(type.rawValue) + message, completion)) + + 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() + } +} diff --git a/ios/Classes/SocketIO/Engine/SocketEngineSpec.swift b/ios/Classes/SocketIO/Engine/SocketEngineSpec.swift new file mode 100644 index 0000000..a7ccf33 --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEngineSpec.swift @@ -0,0 +1,186 @@ +// +// 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. + /// - parameter completion: Callback called on transport write completion. + func write(_ msg: String, withType type: SocketEnginePacketType, withData data: [Data], completion: (() -> ())?) +} + +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 { + if polling { + return .right("b4" + data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))) + } else { + return .left(Data([0x4]) + data) + } + } + + /// Send an engine message (4) + func send(_ msg: String, withData datas: [Data], completion: (() -> ())? = nil) { + write(msg, withType: .message, withData: datas, completion: completion) + } +} diff --git a/ios/Classes/SocketIO/Engine/SocketEngineWebsocket.swift b/ios/Classes/SocketIO/Engine/SocketEngineWebsocket.swift new file mode 100644 index 0000000..ea1c53f --- /dev/null +++ b/ios/Classes/SocketIO/Engine/SocketEngineWebsocket.swift @@ -0,0 +1,82 @@ +// +// 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. + /// - parameter completion: Callback called on transport write completion. + func sendWebSocketMessage(_ str: String, + withType type: SocketEnginePacketType, + withData datas: [Data], + completion: (() -> ())?) +} + +// WebSocket methods +extension SocketEngineWebsocket { + func probeWebSocket() { + if ws?.isConnected ?? false { + sendWebSocketMessage("probe", withType: .ping, withData: [], completion: nil) + } + } + + /// 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. + /// - parameter completion: Callback called on transport write completion. + public func sendWebSocketMessage(_ str: String, + withType type: SocketEnginePacketType, + withData data: [Data], + completion: (() -> ())? + ) { + DefaultSocketLogger.Logger.log("Sending ws: \(str) as type: \(type.rawValue)", type: "SocketEngineWebSocket") + + ws?.write(string: "\(type.rawValue)\(str)") + + if data.count == 0 { + completion?() + } + + for item in data { + if case let .left(bin) = createBinaryDataForSend(using: item) { + ws?.write(data: bin, completion: completion) + } + } + } +} diff --git a/ios/Classes/SocketIO/Manager/SocketManager.swift b/ios/Classes/SocketIO/Manager/SocketManager.swift new file mode 100644 index 0000000..f87fd7c --- /dev/null +++ b/ios/Classes/SocketIO/Manager/SocketManager.swift @@ -0,0 +1,577 @@ +// +// 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 minimum number of seconds to wait before attempting to reconnect. + public var reconnectWait = 10 + + /// The maximum number of seconds to wait before attempting to reconnect. + public var reconnectWaitMax = 30 + + /// The randomization factor for calculating reconnect jitter. + public var randomizationFactor = 0.5 + + /// 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 + + 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)` + /// + /// - 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() ?? []) + } + + /// :nodoc: + 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, completion: nil) + } + } + + /// 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 when upgrading the http connection to a websocket connection. + /// + /// - parameter headers: The http headers. + open func engineDidWebsocketUpgrade(headers: [String: String]) { + handleQueue.async { + self._engineDidWebsocketUpgrade(headers: headers) + } + } + private func _engineDidWebsocketUpgrade(headers: [String: String]) { + emitAll(clientEvent: .websocketUpgrade, data: [headers]) + } + + /// 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() + + let interval = reconnectInterval(attempts: currentReconnectAttempt) + DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType) + handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect) + } + + func reconnectInterval(attempts: Int) -> Double { + // apply exponential factor + let backoffFactor = pow(1.5, attempts) + let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber) + // add in a random factor smooth thundering herds + let rand = Double.random(in: 0 ..< 1) + let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber) + // add in random factor, and clamp to min and max values + let combined = interval + randomFactor + return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax)))) + } + + /// 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 .reconnectWaitMax(wait): + reconnectWaitMax = abs(wait) + case let .randomizationFactor(factor): + randomizationFactor = factor + case let .log(log): + DefaultSocketLogger.Logger.log = log + case let .logger(logger): + DefaultSocketLogger.Logger = logger + case _: + continue + } + } + + _config = config + + if socketURL.absoluteString.hasPrefix("https://") { + _config.insert(.secure(true)) + } + + _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 + } +} diff --git a/ios/Classes/SocketIO/Manager/SocketManagerSpec.swift b/ios/Classes/SocketIO/Manager/SocketManagerSpec.swift new file mode 100644 index 0000000..35d5afc --- /dev/null +++ b/ios/Classes/SocketIO/Manager/SocketManagerSpec.swift @@ -0,0 +1,145 @@ +// +// 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 minimum number of seconds to wait before attempting to reconnect. + var reconnectWait: Int { get set } + + /// The maximum number of seconds to wait before attempting to reconnect. + var reconnectWaitMax: Int { get set } + + /// The randomization factor for calculating reconnect jitter. + var randomizationFactor: Double { 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 +} diff --git a/ios/Classes/SocketIO/Parse/SocketPacket.swift b/ios/Classes/SocketIO/Parse/SocketPacket.swift new file mode 100644 index 0000000..1d818db --- /dev/null +++ b/ios/Classes/SocketIO/Parse/SocketPacket.swift @@ -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. + 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) + } +} diff --git a/ios/Classes/SocketIO/Parse/SocketParsable.swift b/ios/Classes/SocketIO/Parse/SocketParsable.swift new file mode 100644 index 0000000..9be9c60 --- /dev/null +++ b/ios/Classes/SocketIO/Parse/SocketParsable.swift @@ -0,0 +1,181 @@ +// +// 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 let int = Int(reader.read(count: 1)) { + idString += String(int) + } + + reader.advance(by: -2) + } + + 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. + 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. + 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() + } +} diff --git a/ios/Classes/SocketIO/Util/SSLSecurity.swift b/ios/Classes/SocketIO/Util/SSLSecurity.swift new file mode 100644 index 0000000..2035265 --- /dev/null +++ b/ios/Classes/SocketIO/Util/SSLSecurity.swift @@ -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) + } +} diff --git a/ios/Classes/SocketIO/Util/SocketExtensions.swift b/ios/Classes/SocketIO/Util/SocketExtensions.swift new file mode 100644 index 0000000..23170b7 --- /dev/null +++ b/ios/Classes/SocketIO/Util/SocketExtensions.swift @@ -0,0 +1,130 @@ +// +// 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 ("reconnectWaitMax", wait as Int): + return .reconnectWaitMax(wait) + case let ("randomizationFactor", factor as Double): + return .randomizationFactor(factor) + 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 + case let ("enableSOCKSProxy", enable as Bool): + return .enableSOCKSProxy(enable) + 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) + } +} diff --git a/ios/Classes/SocketIO/Util/SocketLogger.swift b/ios/Classes/SocketIO/Util/SocketLogger.swift new file mode 100644 index 0000000..ccb92be --- /dev/null +++ b/ios/Classes/SocketIO/Util/SocketLogger.swift @@ -0,0 +1,75 @@ +// +// 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) { + guard log else { return } + + abstractLog("LOG", message: message(), type: type) + } + + /// Default implementation. + func error(_ message: @autoclosure () -> String, type: String) { + guard log else { return } + + abstractLog("ERROR", message: message(), type: type) + } + + private func abstractLog(_ logType: String, message: @autoclosure () -> String, type: String) { + NSLog("\(logType) \(type): %@", message()) + } +} + +class DefaultSocketLogger : SocketLogger { + static var Logger: SocketLogger = DefaultSocketLogger() + + var log = false +} diff --git a/ios/Classes/SocketIO/Util/SocketStringReader.swift b/ios/Classes/SocketIO/Util/SocketStringReader.swift new file mode 100644 index 0000000..e9abb7e --- /dev/null +++ b/ios/Classes/SocketIO/Util/SocketStringReader.swift @@ -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.. String { + let substring = message.utf16[currentIndex...] + + guard let foundIndex = substring.firstIndex(where: { $0 == 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.. String { + return read(count: message.utf16.distance(from: currentIndex, to: message.utf16.endIndex)) + } +} diff --git a/ios/Classes/SocketIO/Util/SocketTypes.swift b/ios/Classes/SocketIO/Util/SocketTypes.swift new file mode 100644 index 0000000..7bb6517 --- /dev/null +++ b/ios/Classes/SocketIO/Util/SocketTypes.swift @@ -0,0 +1,86 @@ +// +// 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) -> () + +/// A typealias for a queued POST +public typealias Post = (msg: String, completion: (() -> ())?) + +typealias JSON = [String: Any] +typealias Probe = (msg: String, type: SocketEnginePacketType, data: [Data], completion: (() -> ())?) +typealias ProbeWaitQueue = [Probe] + +enum Either { + case left(E) + case right(V) +} diff --git a/ios/Classes/SocketIOClientProxy.h b/ios/Classes/SocketIOClientProxy.h deleted file mode 100644 index 3d1afc1..0000000 --- a/ios/Classes/SocketIOClientProxy.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// SocketIOClientProxy.h -// ti.socketio -// -// Created by Jan Vennemann on 20.05.18. -// - -#import "TiProxy.h" - -@class SocketIOClient; -@class SocketManagerProxy; - -@interface SocketIOClientProxy : TiProxy - -@property (nonatomic, strong, readonly) NSString *id; -@property (nonatomic, strong, readonly) NSNumber *connected; -@property (nonatomic, strong, readonly) NSNumber *disconnected; -@property (nonatomic, strong, readonly) SocketManagerProxy *io; - -- (instancetype)initWithContext:(id)context socket:(SocketIOClient *)socket manager:(SocketManagerProxy *)manager; - -- (void)open:(id)args; -- (void)connect:(id)args; -- (SocketIOClientProxy *)on:(id)args; -- (SocketIOClientProxy *)once:(id)args; -- (SocketIOClientProxy *)off:(id)args; -- (SocketIOClientProxy *)emit:(id)args; -- (void)close:(id)args; -- (void)disconnect:(id)args; - -- (void)fireClientEvent:(NSString *)eventName data:(NSArray *)data; - -@end diff --git a/ios/Classes/SocketIOClientProxy.m b/ios/Classes/SocketIOClientProxy.m deleted file mode 100644 index 9efc97e..0000000 --- a/ios/Classes/SocketIOClientProxy.m +++ /dev/null @@ -1,285 +0,0 @@ -// -// SocketIOClientProxy.m -// ti.socketio -// -// Created by Jan Vennemann on 20.05.18. -// - -#import "SocketIOClientProxy.h" -#import "SocketManagerProxy.h" -#import "TiUtils.h" - -#import - -@interface SocketIOClientProxy () - -@property (nonatomic, strong) SocketIOClient *socket; -@property (nonatomic, strong) NSMapTable *handlerIdentifiers; -@property (nonatomic, strong) NSMutableDictionary *eventHandlers; -@property (nonatomic, strong) NSDictionary *eventRenamingMap; - -@end - -@implementation SocketIOClientProxy - -- (instancetype)initWithContext:(id)context socket:(SocketIOClient *)socket manager:(SocketManagerProxy *)manager -{ - if (self = [self _initWithPageContext:context]) { - _io = manager; - self.socket = socket; - self.handlerIdentifiers = [NSMapTable strongToStrongObjectsMapTable]; - self.eventHandlers = [NSMutableDictionary new]; - self.eventRenamingMap = @{ - @"connect_error" : @"error", - @"reconnect_attempt" : @"reconnectAttempt", - @"reconnecting" : @"reconnectAttempt" - }; - } - - return self; -} - -#pragma mark - Public JS properties - -- (NSString *)id -{ - return self.socket.status == SocketIOStatusConnected ? self.socket.sid : nil; -} - -- (NSNumber *)connected -{ - return @(self.socket.status == SocketIOStatusConnected); -} - -- (NSNumber *)disconnected -{ - return @(self.socket.status == SocketIOStatusDisconnected); -} - -#pragma mark - Public JS methods - -- (void)open:(id)args -{ - [self connect:args]; -} - -- (void)connect:(id)args -{ - [self.socket connect]; -} - -- (SocketIOClientProxy *)on:(id)args -{ - ENSURE_TYPE(args, NSArray); - ENSURE_ARG_COUNT(args, 2); - - NSString *eventName = [TiUtils stringValue:[args objectAtIndex:0]]; - if (self.eventRenamingMap[eventName] != nil) { - eventName = self.eventRenamingMap[eventName]; - } - KrollCallback *callback = [args objectAtIndex:1]; - NSUUID *handlerId = [self.socket on:eventName - callback:^(NSArray *data, SocketAckEmitter *ack) { - // TODO: Handle ack callback - //KrollCallback *ackCallback = [KrollCallback alloc] initWithCallback:nil thisObject:nil context:[callback context]] - [callback call:data thisObject:nil]; - }]; - [self.handlerIdentifiers setObject:handlerId forKey:callback]; - [self storeEventHandler:callback forEvent:eventName]; - - return self; -} - -- (SocketIOClientProxy *)once:(id)args -{ - ENSURE_TYPE(args, NSArray); - ENSURE_ARG_COUNT(args, 2); - - NSString *eventName = [TiUtils stringValue:[args objectAtIndex:0]]; - if (self.eventRenamingMap[eventName] != nil) { - eventName = self.eventRenamingMap[eventName]; - } - KrollCallback *callback = [args objectAtIndex:1]; - NSUUID *handlerId = [self.socket once:eventName - callback:^(NSArray *data, SocketAckEmitter *ack) { - // TODO: Handle ack callback - [callback call:data thisObject:nil]; - [self removeEventHandler:callback forEvent:eventName]; - }]; - [self.handlerIdentifiers setObject:handlerId forKey:callback]; - [self storeEventHandler:callback forEvent:eventName]; - - return self; -} - -- (SocketIOClientProxy *)off:(id)args -{ - ENSURE_TYPE_OR_NIL(args, NSArray); - - if (args == nil) { - [self.socket removeAllHandlers]; - [self removeAllEventHandlers]; - } else if ([args count] == 1) { - NSString *eventName = [TiUtils stringValue:[args objectAtIndex:0]]; - if (self.eventRenamingMap[eventName] != nil) { - eventName = self.eventRenamingMap[eventName]; - } - [self.socket off:eventName]; - [self removeAllEventHandlersForEvent:eventName]; - } else if ([args count] == 2) { - NSString *eventName = [TiUtils stringValue:[args objectAtIndex:0]]; - if (self.eventRenamingMap[eventName] != nil) { - eventName = self.eventRenamingMap[eventName]; - } - KrollCallback *handler = [args objectAtIndex:1]; - NSUUID *handlerId = [self findHandlerId:handler]; - if (handlerId != nil) { - [self.socket offWithId:handlerId]; - [self removeEventHandler:handler forEvent:eventName]; - } - } - - return self; -} - -- (SocketIOClientProxy *)emit:(id)args -{ - ENSURE_TYPE(args, NSArray); - ENSURE_ARG_COUNT(args, 1); - - NSString *eventName = [TiUtils stringValue:[args objectAtIndex:0]]; - NSMutableArray *data = [NSMutableArray new]; - KrollCallback *ackCallback = nil; - - if ([args count] > 1) { - NSUInteger lastArgumentIndex = [args count] - 1; - - if ([[args objectAtIndex:lastArgumentIndex] isKindOfClass:KrollCallback.class]) { - ackCallback = [args objectAtIndex:lastArgumentIndex]; - lastArgumentIndex -= 1; - } - - // Exclude the first argument to only map the data between the second and penultimate index - NSArray *metaArguments = [args subarrayWithRange:NSMakeRange(1, lastArgumentIndex)]; - [metaArguments enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - [data addObject:obj]; - }]; - - data = [self sanitizeValue:data]; - } - - if (ackCallback != nil) { - [[self.socket emitWithAck:eventName with:data] timingOutAfter:0 - callback:^(NSArray *ackData) { - [ackCallback call:ackData thisObject:nil]; - }]; - } else { - [self.socket emit:eventName with:data]; - } - - return self; -} - -- (void)close:(id)args -{ - [self disconnect:args]; -} - -- (void)disconnect:(id)args -{ - [self.socket disconnect]; -} - -#pragma mark - Public methods - -- (void)fireClientEvent:(NSString *)eventName data:(NSArray *)data -{ - NSMutableSet *handlers = self.eventHandlers[eventName]; - for (KrollCallback *callback in handlers) { - [callback call:data thisObject:nil]; - } -} - -#pragma mark - Private methods - -- (id)sanitizeValue:(id)value -{ - if (![NSJSONSerialization isValidJSONObject:value]) { - // [TiUtils stripInvalidJSONPayload] is private, so invoke it indirectly - NSString *stringyfiedValue = [TiUtils jsonStringify:value]; - return [TiUtils jsonParse:stringyfiedValue]; - } else { - return value; - } -} - -/** -Stores an event handler for the given event name. - -For compatibility with the web client we expose the method on, once and off. -This delegates to our internal event handling using addEventListener to take -care of safely storing the handler function and protecting it against GC. - */ -- (void)storeEventHandler:(KrollCallback *)callback forEvent:(NSString *)eventName -{ - [self addEventListener:@[ eventName, callback ]]; - - NSMutableSet *handlers = self.eventHandlers[eventName]; - if (handlers == nil) { - handlers = [NSMutableSet new]; - } - [handlers addObject:callback]; -} - -- (void)removeEventHandler:(KrollCallback *)callback forEvent:(NSString *)eventName -{ - NSMutableSet *handlers = self.eventHandlers[eventName]; - if (handlers != nil) { - [handlers removeObject:callback]; - } - - [self removeEventListener:@[ eventName, callback ]]; - [self removeHandlerId:callback]; -} - -- (void)removeAllEventHandlersForEvent:(NSString *)eventName -{ - NSMutableSet *handlers = self.eventHandlers[eventName]; - if (handlers == nil) { - return; - } - for (KrollCallback *callback in handlers) { - [self removeEventHandler:callback forEvent:eventName]; - } - [self.eventHandlers removeObjectForKey:eventName]; -} - -- (void)removeAllEventHandlers -{ - for (NSString *eventName in self.eventHandlers.allKeys) { - [self removeAllEventHandlersForEvent:eventName]; - } -} - -- (NSUUID *)findHandlerId:(KrollCallback *)handler -{ - for (KrollCallback *storedCallback in self.handlerIdentifiers.keyEnumerator) { - if ([storedCallback isEqual:handler]) { - return [self.handlerIdentifiers objectForKey:storedCallback]; - } - } - - return nil; -} - -- (void)removeHandlerId:(KrollCallback *)handler -{ - for (KrollCallback *storedCallback in self.handlerIdentifiers.keyEnumerator) { - if ([storedCallback isEqual:handler]) { - [self.handlerIdentifiers removeObjectForKey:storedCallback]; - return; - } - } -} - -@end diff --git a/ios/Classes/SocketIOClientProxy.swift b/ios/Classes/SocketIOClientProxy.swift new file mode 100644 index 0000000..1dabb6a --- /dev/null +++ b/ios/Classes/SocketIOClientProxy.swift @@ -0,0 +1,235 @@ +// +// TiSocketioExampleProxy.swift +// ti.socketio +// +// Created by Hans Knöchel +// Copyright (c) 2022 TiDev. All rights reserved. +// + +import UIKit +import TitaniumKit + +@objc(SocketIOClientProxy) +class SocketIOClientProxy: TiProxy { + + @objc var io: SocketManagerProxy! + + var socket: SocketIOClient! + var handlerIdentifiers: [KrollCallback: UUID] = [:] + var eventHandlers: [String: [KrollCallback]] = [:] + var eventRenamingMap: [String: String] = [ + "connect_error" : "error", + "reconnect_attempt" : "reconnectAttempt", + "reconnecting" : "reconnectAttempt" + ] + + func _init(withPageContext context: TiEvaluator!, socket: SocketIOClient, manager: SocketManagerProxy) -> Self? { + super._init(withPageContext: context) + + self.io = manager + self.socket = socket + + return self + } + + // MARK: Public API's + + @objc(id) + func id() -> String? { + return socket.status == .connected ? socket.sid : nil + } + + @objc(connected) + func connected() -> Bool { + return socket.status == .connected + } + + @objc(disconnected) + func disconnected() -> Bool { + return socket.status == .disconnected + } + + @objc(open:) + func open(args: [Any]) { + connect(args: args) + } + + @objc(connect:) + func connect(args: [Any]?) { + NSLog("[WARN] connect()"); + socket.connect() + } + + @objc(close:) + func close(args: [Any]?) { + disconnect(args: args) + } + + @objc(disconnect:) + func disconnect(args: [Any]?) { + socket.disconnect() + } + + @objc(on:) + func on(args: [Any]) -> SocketIOClientProxy { + guard var eventName = args.first as? String, let callback = args[1] as? KrollCallback else { + fatalError("Missing required parameters") + } + + if eventRenamingMap[eventName] != nil { + eventName = eventRenamingMap[eventName]! + } + + + let handlerId = socket.on(eventName) { data, ack in + NSLog("[WARN] Socket status updated for " + eventName + ", data = " + data.description) + callback.call(data, thisObject: self) + } + + handlerIdentifiers[callback] = handlerId + NSLog("[WARN] Registering handler = " + handlerId.description + " for event = " + eventName) + storeEventHandler(callback, for: eventName) + + return self + } + + @objc(off:) + func off(args: [Any]?) -> SocketIOClientProxy { + if args == nil { + socket.removeAllHandlers() + removeAllEventHandlers() + } else if args?.count == 1 { + var eventName = args?.first as! String + if eventRenamingMap[eventName] != nil { + eventName = eventRenamingMap[eventName]! + } + socket.off(eventName) + removeAllEventHandlersForEvent(eventName) + } else if args?.count == 2 { + var eventName = args?.first as! String + if eventRenamingMap[eventName] != nil { + eventName = eventRenamingMap[eventName]! + } + let handler = args![1] as! KrollCallback + if let handlerId = findHandlerId(handler) { + socket.off(id: handlerId) + removeEventHandler(handler, for: eventName) + } + } + + return self + } + + @objc(emit:) + func emit(args: [Any]) -> SocketIOClientProxy { + guard let eventName = args.first as? String else { return self } + + var data: [Any] = [] + var ackCallback: KrollCallback? = nil + + if args.count > 1 { + var lastArgumentIndex = args.count - 1 + + if args[lastArgumentIndex] as? KrollCallback != nil { + ackCallback = args[lastArgumentIndex] as? KrollCallback + lastArgumentIndex = -1 + } + + // Exclude the first argument to only map the data between the second and penultimate index + let metaArguments = args[1...lastArgumentIndex] + for obj in metaArguments { + data.append(obj) + } + + data = sanitizeValue(data) as! [Any] + } + + if ackCallback != nil { + let _ = socket.emitWithAck(eventName, data) + } else { + socket.emit(eventName, data) + } + + return self + } + + private func findHandlerId(_ handler: KrollCallback) -> UUID? { + for (key, _) in handlerIdentifiers { + if key == handler { + return handlerIdentifiers[key] + } + } + + return nil + } + + private func removeEventHandler(_ callback: KrollCallback, for eventName: String) { + if var handlers = eventHandlers[eventName] { + handlers.remove(at: handlers.firstIndex(of: callback)!) + } + + removeEventListener([eventName, callback]) + removeHandlerId(callback) + } + + private func removeAllEventHandlersForEvent(_ eventName: String) { + let handlers = eventHandlers[eventName] + if handlers == nil { + return + } + + for callback in handlers! { + removeEventHandler(callback, for: eventName) + } + eventHandlers.removeValue(forKey: eventName) + } + + private func removeAllEventHandlers() { + for eventName in eventHandlers.keys { + removeAllEventHandlersForEvent(eventName) + } + } + + private func removeHandlerId(_ handler: KrollCallback) { + for (key, _) in handlerIdentifiers { + if key == handler { + handlerIdentifiers.removeValue(forKey: key) + return; + } + } + } + + private func sanitizeValue(_ value: Any) -> Any? { + if !JSONSerialization.isValidJSONObject(value) { + let stringyfiedValue = TiUtils.jsonStringify(value) + return TiUtils.jsonParse(stringyfiedValue) + } else { + return value + } + } + + private func fireClientEvent(_ eventName: String, data: [Any]) { + guard let handlers = eventHandlers[eventName] else { return } + + for callback in handlers { + callback.call(data, thisObject: nil) + } + } + + /** + Stores an event handler for the given event name. + + For compatibility with the web client we expose the method on, once and off. + This delegates to our internal event handling using addEventListener to take + care of safely storing the handler function and protecting it against GC. + */ + private func storeEventHandler(_ callback: KrollCallback, for eventName: String) { + addEventListener([eventName, callback]) + + var handlers = eventHandlers[eventName] + if handlers == nil { + handlers = [] + } + handlers!.append(callback) + } +} diff --git a/ios/Classes/SocketManagerProxy.h b/ios/Classes/SocketManagerProxy.h deleted file mode 100644 index 5f86131..0000000 --- a/ios/Classes/SocketManagerProxy.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// SocketManagerProxy.h -// ti.socketio -// -// Created by Jan Vennemann on 14.12.18. -// - -#import "TiProxy.h" - -@class SocketIOClientProxy; -@class SocketManager; - -@interface SocketManagerProxy : TiProxy - -@property (nonatomic, strong) SocketManager *manager; - -- (instancetype)initWithContext:(id)context url:(NSURL *)url options:(NSDictionary *)options; -- (SocketIOClientProxy *)socket:(id)args; -- (void)open:(id)args; -- (void)connect:(id)args; -- (void)close:(id)args; -- (void)disconnect:(id)args; - -@end diff --git a/ios/Classes/SocketManagerProxy.m b/ios/Classes/SocketManagerProxy.m deleted file mode 100644 index ea2e106..0000000 --- a/ios/Classes/SocketManagerProxy.m +++ /dev/null @@ -1,145 +0,0 @@ -// -// SocketManagerProxy.m -// ti.socketio -// -// Created by Jan Vennemann on 14.12.18. -// - -#import "SocketManagerProxy.h" -#import "SocketIOClientProxy.h" -#import "TiUtils.h" - -#import - -@interface SocketManagerProxy () - -@property BOOL autoConnect; -@property double timeout; - -@end - -@implementation SocketManagerProxy - -- (instancetype)initWithContext:(id)context url:(NSURL *)url options:(NSDictionary *)options; -{ - self = [self _initWithPageContext:context]; - if (self == nil) { - return nil; - } - - self.autoConnect = [TiUtils boolValue:@"autoConnect" properties:options def:YES]; - self.timeout = [TiUtils doubleValue:@"timeout" properties:options def:20000.f]; - - NSDictionary *nativeOptions = [self convertOptions:options]; - self.manager = [[SocketManager alloc] initWithSocketURL:url config:nativeOptions]; - - return self; -} - -#pragma mark - Public JS methods - -- (SocketIOClientProxy *)socket:(id)args -{ - ENSURE_ARG_COUNT(args, 1); - NSString *path = [TiUtils stringValue:[args objectAtIndex:0]]; - - SocketIOClient *socket; - if ([path isEqualToString:@"/"]) { - socket = self.manager.defaultSocket; - } else { - socket = [self.manager socketForNamespace:path]; - } - SocketIOClientProxy *socketProxy = [[SocketIOClientProxy alloc] initWithContext:self.pageContext socket:socket manager:self]; - if (self.autoConnect) { - if (fabs(self.timeout - 0.0f) < DBL_EPSILON) { - [socket connect]; - } else { - [socket connectWithTimeoutAfter:self.timeout / 1000.f - withHandler:^(void) { - [socketProxy fireClientEvent:@"connect_timeout" data:nil]; - }]; - } - } - return socketProxy; -} - -- (void)open:(id)args -{ - [self connect:args]; -} - -- (void)connect:(id)args -{ - [self.manager connect]; -} - -- (void)close:(id)args -{ - [self disconnect:args]; -} - -- (void)disconnect:(id)args -{ - [self.manager disconnect]; -} - -- (id)toString:(id)args -{ - return [NSString stringWithFormat:@"SocketManager (%p) %@", self, self.manager]; -} - -#pragma mark - Private methods - -- (NSMutableDictionary *)convertOptions:(NSDictionary *)jsOptions -{ - NSMutableDictionary *options = [NSMutableDictionary dictionaryWithDictionary:jsOptions]; - - [self options:options removeUnsupported:@"path" silent:false]; - [self options:options rename:@"reconnection" to:@"reconnects"]; - [self options:options rename:@"reconnectionAttempts" to:@"reconnectAttempts"]; - [self options:options rename:@"reconnectionDelay" to:@"reconnectWait"]; - if (options[@"reconnectWait"] != nil) { - options[@"reconnectWait"] = @([TiUtils doubleValue:@"reconnectWait" properties:options def:1000.f] / 1000); - } - [self options:options rename:@"reconnectionDelayMax" to:@"reconnectWaitMax"]; - if (options[@"reconnectWaitMax"] != nil) { - options[@"reconnectWaitMax"] = @([TiUtils doubleValue:@"reconnectWaitMax" properties:options def:5000.f] / 1000); - } - [self options:options removeUnsupported:@"timeout" silent:true]; - [self options:options removeUnsupported:@"autoConnect" silent:true]; - [self options:options removeUnsupported:@"parser" silent:false]; - - NSDictionary *queryParams = [options objectForKey:@"query"]; - if (queryParams != nil) { - [options setObject:queryParams forKey:@"connectParams"]; - [options removeObjectForKey:@"query"]; - } - - return options; -} - -- (void)options:(NSMutableDictionary *)options rename:(NSString *)fromName to:(NSString *)toName -{ - id optionValue = [options objectForKey:fromName]; - if (optionValue == nil) { - return; - } - - [options removeObjectForKey:fromName]; - [options setObject:optionValue forKey:toName]; -} - -- (void)options:(NSMutableDictionary *)options removeUnsupported:(NSString *)optionName silent:(BOOL)silent -{ - id optionValue = [options objectForKey:optionName]; - if (optionValue == nil) { - return; - } - - if (!silent) { - NSLog(@"[WARN] The option %@ is not supported on the iOS client and will be ignored.", optionName); - } - [options removeObjectForKey:optionName]; -} - -@end diff --git a/ios/Classes/SocketManagerProxy.swift b/ios/Classes/SocketManagerProxy.swift new file mode 100644 index 0000000..31dd611 --- /dev/null +++ b/ios/Classes/SocketManagerProxy.swift @@ -0,0 +1,156 @@ +// +// TiSocketioExampleProxy.swift +// ti.socketio +// +// Created by Hans Knöchel +// Copyright (c) 2022 TiDev. All rights reserved. +// + +import UIKit +import TitaniumKit + +@objc(SocketManagerProxy) +class SocketManagerProxy: TiProxy { + + var manager: SocketManager! + var autoConnect: Bool = false + var timeout: Double = 2000.0 + + func _init(withPageContext context: TiEvaluator!, and url: URL, options: [String: Any]) -> Self! { + super._init(withPageContext: context) + + autoConnect = TiUtils.boolValue("autoConnect", properties: options, def: false) + timeout = TiUtils.doubleValue("timeout", properties: options, def: 2000.0) + + let nativeOptions = convertOptions(options) + manager = SocketManager(socketURL: url, config: nativeOptions) + + return self + } + + // MARK: Public API's + + @objc(socket:) + func socket(_ args: [Any]) -> SocketIOClientProxy { + guard let path = args.first as? String else { + fatalError("Missing required parameters") + } + + let socket: SocketIOClient = { + if (path == "/") { + NSLog("[WARN] def socket"); + + return manager.defaultSocket + } else { + NSLog("[WARN] socket for ns = " + path); + return manager.socket(forNamespace: path) + } + }() + + let socketProxy = SocketIOClientProxy()._init(withPageContext: pageContext, socket: socket, manager: self)! + if autoConnect { + NSLog("[WARN] autoconenct"); + + if fabs(timeout - 0.0) < Double.ulpOfOne { + NSLog("[WARN] connect"); + + socket.connect() + } else { + NSLog("[WARN] connect after timeout = " + timeout.description); + + socket.connect(timeoutAfter: timeout / 1000.0) { + socketProxy.fireEvent("connect_timeout") + } + } + } else { + NSLog("[WARN] no autoconnect"); + + } + + return socketProxy + } + + @objc(open:) + func `open`(args: [Any]?) { + self.connect(args: args) + } + + @objc(connect:) + func connect(args: [Any]?) { + manager.connect() + } + + @objc(close:) + func close(args: [Any]?) { + self.close(args: args) + } + + @objc(disconnect:) + func disconnect(args: [Any]?) { + manager.disconnect() + } + + @objc(toString:) + override func toString(_ args: Any!) -> Any! { + return "SocketManager (\(self)) \(manager!)" + } + + // MARK: Private API's + + private func convertOptions(_ jsOptions: [String: Any]) -> [String: Any] { + var options = jsOptions + + options = self.options(removeUnsupported: options, optionName: "path", silent: false) + options = self.options(options, rename: "reconnection", toName: "reconnects") + options = self.options(options, rename: "reconnectionAttempts", toName: "reconnectAttempts") + options = self.options(options, rename: "reconnectionDelay", toName: "reconnectWait") + + if options["reconnectWait"] != nil { + options["reconnectWait"] = TiUtils.doubleValue("reconnectWait", properties: options, def: 1000.0) / 1000 + } + + options = self.options(options, rename: "reconnectionDelayMax", toName: "reconnectionWaitMax") + + if options["reconnectWaitMax"] != nil { + options["reconnectWaitMax"] = TiUtils.doubleValue("reconnectWaitMax", properties: options, def: 5000.0) / 1000 + } + + options = self.options(removeUnsupported: options, optionName: "timeout", silent: true) + options = self.options(removeUnsupported: options, optionName: "autoConnect", silent: true) + options = self.options(removeUnsupported: options, optionName: "parser", silent: false) + + let queryParams = options["query"] + if queryParams != nil { + options["connectParams"] = queryParams + options.removeValue(forKey: "query") + } + + return options + } + + private func options(_ options: [String: Any], rename fromName: String, toName: String) -> [String: Any] { + var optionsCopy = options + let optionValue = optionsCopy[fromName] + if optionValue == nil { + return optionsCopy + } + + optionsCopy.removeValue(forKey: fromName) + optionsCopy[toName] = optionValue + return optionsCopy + } + + private func options(removeUnsupported options: [String: Any], optionName: String, silent: Bool) -> [String: Any] { + var optionsCopy = options + let optionValue = optionsCopy[optionName] + if optionValue == nil { + return optionsCopy + } + + if (!silent) { + NSLog("[WARN] The option \(optionName) is not supported on the iOS client and will be ignored."); + } + optionsCopy.removeValue(forKey: optionName) + return optionsCopy + } +} diff --git a/ios/Classes/TiSocketio.h b/ios/Classes/TiSocketio.h new file mode 100644 index 0000000..eef26c1 --- /dev/null +++ b/ios/Classes/TiSocketio.h @@ -0,0 +1,17 @@ +// +// TiSocketio.h +// ti.socketio +// +// Created by Your Name +// Copyright (c) 2022 Your Company. All rights reserved. +// + +#import + +//! Project version number for TiSocketio. +FOUNDATION_EXPORT double TiSocketioVersionNumber; + +//! Project version string for TiSocketio. +FOUNDATION_EXPORT const unsigned char TiSocketioVersionString[]; + +#import "TiSocketioModuleAssets.h" diff --git a/ios/Classes/TiSocketioModule.h b/ios/Classes/TiSocketioModule.h deleted file mode 100644 index ef88b8a..0000000 --- a/ios/Classes/TiSocketioModule.h +++ /dev/null @@ -1,18 +0,0 @@ -/** - * ti.socketio - * - * Created by Jan Vennemann - * Copyright (c) 2018-Present Axway Appcelerator. All rights reserved. - */ - -#import "TiModule.h" - -@class SocketIOClientProxy; -@class SocketManagerProxy; - -@interface TiSocketioModule : TiModule - -- (SocketIOClientProxy *)connect:(id)args; -- (SocketManagerProxy *)Manager:(id)args; - -@end diff --git a/ios/Classes/TiSocketioModule.m b/ios/Classes/TiSocketioModule.m deleted file mode 100644 index 50bebe3..0000000 --- a/ios/Classes/TiSocketioModule.m +++ /dev/null @@ -1,132 +0,0 @@ -/** - * ti.socketio - * - * Created by Jan Vennemann - * Copyright (c) 2018-Present Axway Appcelerator. All rights reserved. - */ - -#import "TiSocketioModule.h" -#import "SocketIOClientProxy.h" -#import "SocketManagerProxy.h" -#import "TiBase.h" -#import "TiHost.h" -#import "TiUtils.h" - -#import - -@interface TiSocketioModule () - -@property (nonatomic, strong) NSMutableDictionary *managers; - -@end - -@implementation TiSocketioModule - -#pragma mark - Internal - -// This is generated for your module, please do not change it -- (id)moduleGUID -{ - return @"65fbdc59-432f-40d8-8ea9-02799887db3c"; -} - -// This is generated for your module, please do not change it -- (NSString *)moduleId -{ - return @"ti.socketio"; -} - -#pragma mark - Lifecycle - -- (void)startup -{ - // This method is called when the module is first loaded - // You *must* call the superclass - [super startup]; - - self.managers = [NSMutableDictionary new]; - - DebugLog(@"[DEBUG] %@ loaded", self); -} - -#pragma mark - Public APIs - -- (SocketIOClientProxy *)connect:(id)args -{ - ENSURE_TYPE(args, NSArray); - NSURL *url = [[NSURL alloc] initWithString:[TiUtils stringValue:[args objectAtIndex:0]]]; - NSMutableDictionary *options = [NSMutableDictionary new]; - if ([args count] == 2) { - ENSURE_ARG_AT_INDEX(options, args, 1, NSMutableDictionary); - } - - return [self lookupClientForUrl:url options:options]; -} - -- (SocketManagerProxy *)Manager:(id)args -{ - ENSURE_TYPE(args, NSArray); - ENSURE_ARG_COUNT(args, 1) - NSString *urlString = nil; - ENSURE_ARG_AT_INDEX(urlString, args, 0, NSString) - NSURL *url = [[NSURL alloc] initWithString:urlString]; - NSDictionary *options = [NSDictionary new]; - if ([args count] == 2) { - ENSURE_ARG_AT_INDEX(options, args, 1, NSDictionary); - } - return [[SocketManagerProxy alloc] initWithContext:self.pageContext url:url options:options]; -} - -#pragma mark - Private methods - -- (SocketIOClientProxy *)lookupClientForUrl:(NSURL *)url options:(NSMutableDictionary *)options -{ - NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; - - // make sure we always have a scheme and port - if (urlComponents.scheme == nil) { - urlComponents.scheme = @"https"; - } - if (urlComponents.port == nil) { - if ([urlComponents.scheme isEqualToString:@"http"] || [urlComponents.scheme isEqualToString:@"ws"]) { - urlComponents.port = @(80); - } else if ([urlComponents.scheme isEqualToString:@"https"] || [urlComponents.scheme isEqualToString:@"wss"]) { - urlComponents.port = @(443); - } - } - - NSString *path = urlComponents.path != nil && urlComponents.path.length > 0 ? urlComponents.path : @"/"; - urlComponents.path = nil; - - // assign parameters from query string - if (urlComponents.queryItems != nil && options[@"connectParams"] == nil) { - NSMutableDictionary *connectParams = [NSMutableDictionary new]; - for (NSURLQueryItem *queryItem in urlComponents.queryItems) { - connectParams[queryItem.name] = queryItem.value; - } - options[@"connectParams"] = connectParams; - } - urlComponents.query = nil; - - NSString *cacheIdentifier = urlComponents.string; - - BOOL forceNew = [TiUtils boolValue:@"forceNew" properties:options def:NO]; - [options removeObjectForKey:@"forceNew"]; - BOOL sameNamespace = self.managers[cacheIdentifier].manager.nsps[path] != nil; - BOOL newConnection = sameNamespace || forceNew; - - SocketManagerProxy *manager; - if (newConnection) { - manager = [[SocketManagerProxy alloc] initWithContext:self.pageContext url:urlComponents.URL options:options]; - } else { - if (self.managers[cacheIdentifier] == nil) { - manager = [[SocketManagerProxy alloc] initWithContext:self.pageContext url:urlComponents.URL options:options]; - self.managers[cacheIdentifier] = manager; - } - manager = self.managers[cacheIdentifier]; - } - - return [manager socket:@[ path, options ]]; -} - -@end diff --git a/ios/Classes/TiSocketioModule.swift b/ios/Classes/TiSocketioModule.swift new file mode 100644 index 0000000..7e691bc --- /dev/null +++ b/ios/Classes/TiSocketioModule.swift @@ -0,0 +1,107 @@ +// +// TiSocketioModule.swift +// ti.socketio +// +// Created by Hans Knöchel +// Copyright (c) 2022 TiDev. All rights reserved. +// + +import UIKit +import TitaniumKit + +@objc(TiSocketioModule) +class TiSocketioModule: TiModule { + + var managers: [String: SocketManagerProxy] = [:] + + func moduleGUID() -> String { + return "65fbdc59-432f-40d8-8ea9-02799887db3c" + } + + override func moduleId() -> String! { + return "ti.socketio" + } + + @objc(connect:) + func connect(args: [Any]) -> SocketIOClientProxy { + guard let urlString = args.first as? String, + let url = URL(string: urlString) else { + fatalError("Missing required parameters") + } + + var options: [String: Any] = [:] + + if args.count == 2 , let customOptions = args[1] as? [String: Any] { + options = customOptions + } + + return lookupClient(for: url, and: options) + } + + @objc(Manager:) + func Manager(args: [Any]) -> SocketManagerProxy { + guard let urlString = args.first as? String, let url = URL(string: urlString) else { + fatalError("Missing required parameters") + } + + var options: [String: Any] = [:] + if args.count == 2 { + options = args[1] as! [String: Any] + } + + return SocketManagerProxy()._init(withPageContext: pageContext, and: url, options: options) + } + + private func lookupClient(for url: URL, and options: [String: Any]) -> SocketIOClientProxy { + guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + fatalError("Cannot construct URL components!") + } + + var _options = options + + if urlComponents.scheme == nil { + urlComponents.scheme = "https" + } + if urlComponents.port == nil { + if urlComponents.scheme == "http" || urlComponents.scheme == "ws" { + urlComponents.port = 80 + } else if urlComponents.scheme == "https" || urlComponents.scheme == "wss" { + urlComponents.port = 443 + } + } + + let path = !urlComponents.path.isEmpty ? urlComponents.path : "/" + if urlComponents.queryItems != nil && options["connectParams"] == nil { + var connectParams: [String: Any] = [:] + for queryItem in urlComponents.queryItems ?? [] { + connectParams[queryItem.name] = queryItem.value + } + } + urlComponents.query = nil + + let cacheIdentifier: String = urlComponents.string ?? "" + + let forceNew = TiUtils.boolValue("forceNew", properties: options, def: false) + _options.removeValue(forKey: "forceNew") + let sameNamespace = self.managers[cacheIdentifier]?.manager.nsps[path] != nil + let newConnection = sameNamespace || forceNew + + var manager: SocketManagerProxy! + if newConnection { + manager = SocketManagerProxy()._init(withPageContext: pageContext, + and: urlComponents.url!, + options: _options) + } else { + if self.managers[cacheIdentifier] == nil { + manager = SocketManagerProxy()._init(withPageContext: pageContext, and: urlComponents.url!, options: options) + managers[cacheIdentifier] = manager + } + manager = self.managers[cacheIdentifier] + } + + NSLog("[WARN] path = " + path); + NSLog("[WARN] options = " + options.description); + + return manager.socket([path, options]) + } +} diff --git a/ios/Classes/TiSocketioModuleAssets.m b/ios/Classes/TiSocketioModuleAssets.m index b7e8bb5..05643d8 100644 --- a/ios/Classes/TiSocketioModuleAssets.m +++ b/ios/Classes/TiSocketioModuleAssets.m @@ -3,18 +3,20 @@ */ #import "TiSocketioModuleAssets.h" -extern NSData *filterDataInRange(NSData *thedata, NSRange range); +extern NSData* filterDataInRange(NSData* thedata, NSRange range); @implementation TiSocketioModuleAssets - (NSData *)moduleAsset { + return nil; } - (NSData *)resolveModuleAsset:(NSString *)path { + return nil; } diff --git a/ios/Info.plist b/ios/Info.plist new file mode 100644 index 0000000..fbe1e6b --- /dev/null +++ b/ios/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/ios/README.md b/ios/README.md index 21bd0fa..97ff1e6 100644 --- a/ios/README.md +++ b/ios/README.md @@ -4,7 +4,7 @@ This is the iOS part of the ti.socketio module. ## Development guide -This module uses [Carthage](https://github.com/Carthage/Carthage) to manage native dependencies. You might notice that no frameworks are checked in to the `platform` folder of this repository. Before you can build the project run `carthage bootstrap` in this directory to checkout and build the reuired [SocketIO.framework](https://github.com/socketio/socket.io-client-swift) and its dependencies. Now copy all built frameworks from the `Carthage/Build/iOS` folder to `platform`. +This module uses [Carthage](https://github.com/Carthage/Carthage) to manage native dependencies. You might notice that no frameworks are checked in to the `platform` folder of this repository. Before you can build the project run `carthage bootstrap` in this directory to checkout and build the required [SocketIO.framework](https://github.com/socketio/socket.io-client-swift) and its dependencies. Now copy all built frameworks from the `Carthage/Build/iOS` folder to `platform`. You are responsible to manually integrate any dependency changes into the module. After you modified the Cartfile run the `carthage update` command. After that make sure to copy the built frameworks to the platform folder again. **Do not check-in the frameworks from the platform folder** diff --git a/ios/TiSocketio_Prefix.pch b/ios/TiSocketio_Prefix.pch index 186d246..2fb3c7f 100644 --- a/ios/TiSocketio_Prefix.pch +++ b/ios/TiSocketio_Prefix.pch @@ -1,5 +1,5 @@ #ifdef __OBJC__ #import - #import #endif + diff --git a/ios/hooks/carthage.js b/ios/hooks/carthage.js deleted file mode 100644 index 972abc9..0000000 --- a/ios/hooks/carthage.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Carthage build hook - * - * Copyright (c) 2019-present by Axway Appcelerator - * All Rights Reserved. - */ - -'use strict'; - -const spawn = require('child_process').spawn; -const path = require('path'); - -exports.id = 'ti.socketio.carthage'; -exports.cliVersion = '>=3.2'; -exports.init = init; - -/** - * Main entry point for our plugin which looks for the platform specific - * plugin to invoke - */ -function init (logger, config, cli, appc) { - cli.on('build.module.pre.compile', { - pre: function (builder, done) { - logger.info('Running carthage...'); - - const p = spawn('carthage', [ 'bootstrap', '--platform', 'ios', '--cache-builds', '--use-xcframeworks' ], { cwd: builder.projectDir }); - p.stderr.on('data', data => logger.error(data.toString().trim())); - p.stdout.on('data', data => logger.trace(data.toString().trim())); - p.on('close', function (code) { - if (code !== 0) { - return done(new Error(`Failed to run carthage properly, exited with code: ${code}`)); - } - - const fs = require('fs-extra'); - const subdirs = fs.readdirSync(path.join(builder.projectDir, 'Carthage/Build')); - const frameworkDirs = subdirs.filter(dir => dir.endsWith('.xcframework')); - for (const frameworkDir of frameworkDirs) { - fs.copySync(path.join(builder.projectDir, 'Carthage/Build', frameworkDir), path.join(builder.projectDir, 'platform', frameworkDir)); - } - done(); - }); - } - }); -} diff --git a/ios/hooks/ti.swiftsupport.js b/ios/hooks/ti.swiftsupport.js index e344e1d..9994003 100644 --- a/ios/hooks/ti.swiftsupport.js +++ b/ios/hooks/ti.swiftsupport.js @@ -1,24 +1,28 @@ /** * Ti.SwiftSupport - * Copyright (c) 2017-present by Axway Appcelerator + * Copyright (c) 2018-present by Axway Appcelerator. * All Rights Reserved. */ 'use strict'; -exports.id = 'ti.socketio.swiftsupport'; +exports.id = 'ti.swiftsupport'; exports.cliVersion = '>=3.2'; exports.init = init; /** * Main entry point for our plugin which looks for the platform specific - * plugin to invoke + * plugin to invoke. + * + * @param {Object} logger The logger instance. + * @param {Object} config The hook config. + * @param {Object} cli The Titanium CLI instance. + * @param {Object} appc The Appcelerator CLI instance. */ +// eslint-disable-next-line no-unused-vars function init(logger, config, cli, appc) { cli.on('build.ios.xcodeproject', { - pre: function(data) { - logger.info('Enabling Swift support ...'); - + pre: function (data) { var xobjs = data.args[0].hash.project.objects; Object.keys(xobjs.PBXNativeTarget).forEach(function (targetUuid) { diff --git a/ios/manifest b/ios/manifest index 997de42..7ad9060 100644 --- a/ios/manifest +++ b/ios/manifest @@ -2,9 +2,9 @@ # this is your module manifest and used by Titanium # during compilation, packaging, distribution, etc. # -version: 3.0.0 +version: 4.0.0 apiversion: 2 -architectures: armv7 arm64 i386 x86_64 +architectures: arm64 x86_64 description: Socket.io client for Titanium author: Axway Appcelerator license: Apache-2.0 diff --git a/ios/ti.socketio.xcodeproj/project.pbxproj b/ios/ti.socketio.xcodeproj/project.pbxproj index b7e027c..3956f54 100644 --- a/ios/ti.socketio.xcodeproj/project.pbxproj +++ b/ios/ti.socketio.xcodeproj/project.pbxproj @@ -3,343 +3,386 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { -/* Begin PBXAggregateTarget section */ - 24416B8111C4CA220047AFDD /* Build & Test */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 24416B8A11C4CA520047AFDD /* Build configuration list for PBXAggregateTarget "Build & Test" */; - buildPhases = ( - 24416B8011C4CA220047AFDD /* ShellScript */, - ); - dependencies = ( - 24416B8511C4CA280047AFDD /* PBXTargetDependency */, - ); - name = "Build & Test"; - productName = "Build & test"; - }; -/* End PBXAggregateTarget section */ - /* Begin PBXBuildFile section */ - 24DD6CF91134B3F500162E58 /* TiSocketioModule.h in Headers */ = {isa = PBXBuildFile; fileRef = 24DD6CF71134B3F500162E58 /* TiSocketioModule.h */; }; - 24DD6CFA1134B3F500162E58 /* TiSocketioModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 24DD6CF81134B3F500162E58 /* TiSocketioModule.m */; }; - 24DE9E1111C5FE74003F90F6 /* TiSocketioModuleAssets.h in Headers */ = {isa = PBXBuildFile; fileRef = 24DE9E0F11C5FE74003F90F6 /* TiSocketioModuleAssets.h */; }; - 24DE9E1211C5FE74003F90F6 /* TiSocketioModuleAssets.m in Sources */ = {isa = PBXBuildFile; fileRef = 24DE9E1011C5FE74003F90F6 /* TiSocketioModuleAssets.m */; }; - 4A2196D422529C8C00DD6DEA /* Starscream.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AB2678320B1AAE000E4E3DD /* Starscream.xcframework */; }; - 4A2196D522529C9500DD6DEA /* SocketIO.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AB2678220B1AAE000E4E3DD /* SocketIO.xcframework */; }; - 4AA6A17D21C3E22200AE770E /* SocketManagerProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AA6A17B21C3E22200AE770E /* SocketManagerProxy.h */; }; - 4AA6A17E21C3E22200AE770E /* SocketManagerProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AA6A17C21C3E22200AE770E /* SocketManagerProxy.m */; }; - 4AB2678820B1B13500E4E3DD /* SocketIOClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AB2678620B1B13500E4E3DD /* SocketIOClientProxy.h */; }; - 4AB2678920B1B13500E4E3DD /* SocketIOClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AB2678720B1B13500E4E3DD /* SocketIOClientProxy.m */; }; - AA747D9F0F9514B9006C5449 /* TiSocketio_Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = AA747D9E0F9514B9006C5449 /* TiSocketio_Prefix.pch */; }; - AACBBE4A0F95108600F1A2B1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AACBBE490F95108600F1A2B1 /* Foundation.framework */; }; + 3A013128282C378F003C6E8A /* Starscream.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A013126282C378F003C6E8A /* Starscream.xcframework */; }; + 3A01312B282C37EF003C6E8A /* SocketManagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A01312A282C37EF003C6E8A /* SocketManagerProxy.swift */; }; + 3AB7F6A029C6643D00064C4E /* SocketStringReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68229C6643D00064C4E /* SocketStringReader.swift */; }; + 3AB7F6A129C6643D00064C4E /* SocketLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68329C6643D00064C4E /* SocketLogger.swift */; }; + 3AB7F6A229C6643D00064C4E /* SocketExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68429C6643D00064C4E /* SocketExtensions.swift */; }; + 3AB7F6A329C6643D00064C4E /* SSLSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68529C6643D00064C4E /* SSLSecurity.swift */; }; + 3AB7F6A429C6643D00064C4E /* SocketTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68629C6643D00064C4E /* SocketTypes.swift */; }; + 3AB7F6A529C6643D00064C4E /* SocketParsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68829C6643D00064C4E /* SocketParsable.swift */; }; + 3AB7F6A629C6643D00064C4E /* SocketPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68929C6643D00064C4E /* SocketPacket.swift */; }; + 3AB7F6A729C6643D00064C4E /* SocketManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68B29C6643D00064C4E /* SocketManagerSpec.swift */; }; + 3AB7F6A829C6643D00064C4E /* SocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68C29C6643D00064C4E /* SocketManager.swift */; }; + 3AB7F6A929C6643D00064C4E /* SocketEngineClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68E29C6643D00064C4E /* SocketEngineClient.swift */; }; + 3AB7F6AA29C6643D00064C4E /* SocketEngineWebsocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F68F29C6643D00064C4E /* SocketEngineWebsocket.swift */; }; + 3AB7F6AB29C6643D00064C4E /* SocketEnginePollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69029C6643D00064C4E /* SocketEnginePollable.swift */; }; + 3AB7F6AC29C6643D00064C4E /* SocketEngineSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69129C6643D00064C4E /* SocketEngineSpec.swift */; }; + 3AB7F6AD29C6643D00064C4E /* SocketEnginePacketType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69229C6643D00064C4E /* SocketEnginePacketType.swift */; }; + 3AB7F6AE29C6643D00064C4E /* SocketEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69329C6643D00064C4E /* SocketEngine.swift */; }; + 3AB7F6AF29C6643D00064C4E /* SocketIOClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69529C6643D00064C4E /* SocketIOClientConfiguration.swift */; }; + 3AB7F6B029C6643D00064C4E /* SocketAnyEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69629C6643D00064C4E /* SocketAnyEvent.swift */; }; + 3AB7F6B129C6643D00064C4E /* SocketIOClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69729C6643D00064C4E /* SocketIOClient.swift */; }; + 3AB7F6B229C6643D00064C4E /* SocketIOClientOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69829C6643D00064C4E /* SocketIOClientOption.swift */; }; + 3AB7F6B329C6643D00064C4E /* SocketEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69929C6643D00064C4E /* SocketEventHandler.swift */; }; + 3AB7F6B429C6643D00064C4E /* SocketRawView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69A29C6643D00064C4E /* SocketRawView.swift */; }; + 3AB7F6B529C6643D00064C4E /* SocketIOClientSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69B29C6643D00064C4E /* SocketIOClientSpec.swift */; }; + 3AB7F6B629C6643D00064C4E /* SocketIOStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69C29C6643D00064C4E /* SocketIOStatus.swift */; }; + 3AB7F6B729C6643D00064C4E /* SocketAckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69E29C6643D00064C4E /* SocketAckManager.swift */; }; + 3AB7F6B829C6643D00064C4E /* SocketAckEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB7F69F29C6643D00064C4E /* SocketAckEmitter.swift */; }; + B1E00E65241D179500C384C1 /* TitaniumKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1E00E64241D179500C384C1 /* TitaniumKit.xcframework */; }; + DB24E03C20766AC90033B2B1 /* SocketIOClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB24E03B20766AC90033B2B1 /* SocketIOClientProxy.swift */; }; + DB34CDE0207B998A005F8E8C /* TiSocketioModuleAssets.m in Sources */ = {isa = PBXBuildFile; fileRef = DB34CDDE207B998A005F8E8C /* TiSocketioModuleAssets.m */; }; + DB34CDE1207B998A005F8E8C /* TiSocketioModuleAssets.h in Headers */ = {isa = PBXBuildFile; fileRef = DB34CDDF207B998A005F8E8C /* TiSocketioModuleAssets.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DB34CDE6207B9EBD005F8E8C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB34CDE5207B9EBD005F8E8C /* Foundation.framework */; }; + DB52E2401E9CCF8D00AAAEE0 /* TiSocketio_Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = DB52E23F1E9CCF8D00AAAEE0 /* TiSocketio_Prefix.pch */; }; + DB52E2431E9CD0F800AAAEE0 /* TiSocketioModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB52E2421E9CD0F800AAAEE0 /* TiSocketioModule.swift */; }; + DB75E5161E9CD59000809B2D /* TiSocketio.h in Headers */ = {isa = PBXBuildFile; fileRef = DB75E5151E9CD58100809B2D /* TiSocketio.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 24416B8411C4CA280047AFDD /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D2AAC07D0554694100DB518D; - remoteInfo = "ti-socketio"; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ - 24DD6CF71134B3F500162E58 /* TiSocketioModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TiSocketioModule.h; path = Classes/TiSocketioModule.h; sourceTree = ""; }; - 24DD6CF81134B3F500162E58 /* TiSocketioModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TiSocketioModule.m; path = Classes/TiSocketioModule.m; sourceTree = ""; }; - 24DD6D1B1134B66800162E58 /* titanium.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = titanium.xcconfig; sourceTree = ""; }; - 24DE9E0F11C5FE74003F90F6 /* TiSocketioModuleAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TiSocketioModuleAssets.h; path = Classes/TiSocketioModuleAssets.h; sourceTree = ""; }; - 24DE9E1011C5FE74003F90F6 /* TiSocketioModuleAssets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TiSocketioModuleAssets.m; path = Classes/TiSocketioModuleAssets.m; sourceTree = ""; }; - 4AA6A17B21C3E22200AE770E /* SocketManagerProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SocketManagerProxy.h; path = Classes/SocketManagerProxy.h; sourceTree = ""; }; - 4AA6A17C21C3E22200AE770E /* SocketManagerProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SocketManagerProxy.m; path = Classes/SocketManagerProxy.m; sourceTree = ""; }; - 4AB2678220B1AAE000E4E3DD /* SocketIO.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SocketIO.xcframework; path = platform/SocketIO.xcframework; sourceTree = ""; }; - 4AB2678320B1AAE000E4E3DD /* Starscream.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Starscream.xcframework; path = platform/Starscream.xcframework; sourceTree = ""; }; - 4AB2678620B1B13500E4E3DD /* SocketIOClientProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SocketIOClientProxy.h; path = Classes/SocketIOClientProxy.h; sourceTree = ""; }; - 4AB2678720B1B13500E4E3DD /* SocketIOClientProxy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SocketIOClientProxy.m; path = Classes/SocketIOClientProxy.m; sourceTree = ""; }; - AA747D9E0F9514B9006C5449 /* TiSocketio_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiSocketio_Prefix.pch; sourceTree = SOURCE_ROOT; }; - AACBBE490F95108600F1A2B1 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - D2AAC07E0554694100DB518D /* libtisocketio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libtisocketio.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A013126282C378F003C6E8A /* Starscream.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Starscream.xcframework; path = platform/Starscream.xcframework; sourceTree = ""; }; + 3A01312A282C37EF003C6E8A /* SocketManagerProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SocketManagerProxy.swift; path = Classes/SocketManagerProxy.swift; sourceTree = ""; }; + 3AB7F68229C6643D00064C4E /* SocketStringReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketStringReader.swift; sourceTree = ""; }; + 3AB7F68329C6643D00064C4E /* SocketLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketLogger.swift; sourceTree = ""; }; + 3AB7F68429C6643D00064C4E /* SocketExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketExtensions.swift; sourceTree = ""; }; + 3AB7F68529C6643D00064C4E /* SSLSecurity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSLSecurity.swift; sourceTree = ""; }; + 3AB7F68629C6643D00064C4E /* SocketTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketTypes.swift; sourceTree = ""; }; + 3AB7F68829C6643D00064C4E /* SocketParsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketParsable.swift; sourceTree = ""; }; + 3AB7F68929C6643D00064C4E /* SocketPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketPacket.swift; sourceTree = ""; }; + 3AB7F68B29C6643D00064C4E /* SocketManagerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManagerSpec.swift; sourceTree = ""; }; + 3AB7F68C29C6643D00064C4E /* SocketManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketManager.swift; sourceTree = ""; }; + 3AB7F68E29C6643D00064C4E /* SocketEngineClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineClient.swift; sourceTree = ""; }; + 3AB7F68F29C6643D00064C4E /* SocketEngineWebsocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineWebsocket.swift; sourceTree = ""; }; + 3AB7F69029C6643D00064C4E /* SocketEnginePollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEnginePollable.swift; sourceTree = ""; }; + 3AB7F69129C6643D00064C4E /* SocketEngineSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngineSpec.swift; sourceTree = ""; }; + 3AB7F69229C6643D00064C4E /* SocketEnginePacketType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEnginePacketType.swift; sourceTree = ""; }; + 3AB7F69329C6643D00064C4E /* SocketEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEngine.swift; sourceTree = ""; }; + 3AB7F69529C6643D00064C4E /* SocketIOClientConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientConfiguration.swift; sourceTree = ""; }; + 3AB7F69629C6643D00064C4E /* SocketAnyEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAnyEvent.swift; sourceTree = ""; }; + 3AB7F69729C6643D00064C4E /* SocketIOClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClient.swift; sourceTree = ""; }; + 3AB7F69829C6643D00064C4E /* SocketIOClientOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientOption.swift; sourceTree = ""; }; + 3AB7F69929C6643D00064C4E /* SocketEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketEventHandler.swift; sourceTree = ""; }; + 3AB7F69A29C6643D00064C4E /* SocketRawView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketRawView.swift; sourceTree = ""; }; + 3AB7F69B29C6643D00064C4E /* SocketIOClientSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOClientSpec.swift; sourceTree = ""; }; + 3AB7F69C29C6643D00064C4E /* SocketIOStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketIOStatus.swift; sourceTree = ""; }; + 3AB7F69E29C6643D00064C4E /* SocketAckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAckManager.swift; sourceTree = ""; }; + 3AB7F69F29C6643D00064C4E /* SocketAckEmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketAckEmitter.swift; sourceTree = ""; }; + B1E00E64241D179500C384C1 /* TitaniumKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TitaniumKit.xcframework; path = "$(TITANIUM_SDK)/iphone/Frameworks/TitaniumKit.xcframework"; sourceTree = ""; }; + DB24E03B20766AC90033B2B1 /* SocketIOClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SocketIOClientProxy.swift; path = Classes/SocketIOClientProxy.swift; sourceTree = ""; }; + DB34CDDE207B998A005F8E8C /* TiSocketioModuleAssets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TiSocketioModuleAssets.m; path = Classes/TiSocketioModuleAssets.m; sourceTree = SOURCE_ROOT; }; + DB34CDDF207B998A005F8E8C /* TiSocketioModuleAssets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TiSocketioModuleAssets.h; path = Classes/TiSocketioModuleAssets.h; sourceTree = SOURCE_ROOT; }; + DB34CDE5207B9EBD005F8E8C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + DB52E22A1E9CCD7000AAAEE0 /* TiSocketio.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TiSocketio.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB52E22E1E9CCD7000AAAEE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + DB52E23F1E9CCF8D00AAAEE0 /* TiSocketio_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TiSocketio_Prefix.pch; sourceTree = SOURCE_ROOT; }; + DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = titanium.xcconfig; sourceTree = ""; }; + DB52E2421E9CD0F800AAAEE0 /* TiSocketioModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TiSocketioModule.swift; path = Classes/TiSocketioModule.swift; sourceTree = ""; }; + DB75E5151E9CD58100809B2D /* TiSocketio.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TiSocketio.h; path = Classes/TiSocketio.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - D2AAC07C0554694100DB518D /* Frameworks */ = { + DB52E2261E9CCD7000AAAEE0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4A2196D522529C9500DD6DEA /* SocketIO.framework in Frameworks */, - 4A2196D422529C8C00DD6DEA /* Starscream.xcframework in Frameworks */, - AACBBE4A0F95108600F1A2B1 /* Foundation.xcframework in Frameworks */, + DB34CDE6207B9EBD005F8E8C /* Foundation.framework in Frameworks */, + 3A013128282C378F003C6E8A /* Starscream.xcframework in Frameworks */, + B1E00E65241D179500C384C1 /* TitaniumKit.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 034768DFFF38A50411DB9C8B /* Products */ = { + 3AB7F68029C6643D00064C4E /* SocketIO */ = { isa = PBXGroup; children = ( - D2AAC07E0554694100DB518D /* libtisocketio.a */, + 3AB7F68129C6643D00064C4E /* Util */, + 3AB7F68729C6643D00064C4E /* Parse */, + 3AB7F68A29C6643D00064C4E /* Manager */, + 3AB7F68D29C6643D00064C4E /* Engine */, + 3AB7F69429C6643D00064C4E /* Client */, + 3AB7F69D29C6643D00064C4E /* Ack */, ); - name = Products; + name = SocketIO; + path = Classes/SocketIO; + sourceTree = ""; + }; + 3AB7F68129C6643D00064C4E /* Util */ = { + isa = PBXGroup; + children = ( + 3AB7F68229C6643D00064C4E /* SocketStringReader.swift */, + 3AB7F68329C6643D00064C4E /* SocketLogger.swift */, + 3AB7F68429C6643D00064C4E /* SocketExtensions.swift */, + 3AB7F68529C6643D00064C4E /* SSLSecurity.swift */, + 3AB7F68629C6643D00064C4E /* SocketTypes.swift */, + ); + path = Util; + sourceTree = ""; + }; + 3AB7F68729C6643D00064C4E /* Parse */ = { + isa = PBXGroup; + children = ( + 3AB7F68829C6643D00064C4E /* SocketParsable.swift */, + 3AB7F68929C6643D00064C4E /* SocketPacket.swift */, + ); + path = Parse; + sourceTree = ""; + }; + 3AB7F68A29C6643D00064C4E /* Manager */ = { + isa = PBXGroup; + children = ( + 3AB7F68B29C6643D00064C4E /* SocketManagerSpec.swift */, + 3AB7F68C29C6643D00064C4E /* SocketManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + 3AB7F68D29C6643D00064C4E /* Engine */ = { + isa = PBXGroup; + children = ( + 3AB7F68E29C6643D00064C4E /* SocketEngineClient.swift */, + 3AB7F68F29C6643D00064C4E /* SocketEngineWebsocket.swift */, + 3AB7F69029C6643D00064C4E /* SocketEnginePollable.swift */, + 3AB7F69129C6643D00064C4E /* SocketEngineSpec.swift */, + 3AB7F69229C6643D00064C4E /* SocketEnginePacketType.swift */, + 3AB7F69329C6643D00064C4E /* SocketEngine.swift */, + ); + path = Engine; + sourceTree = ""; + }; + 3AB7F69429C6643D00064C4E /* Client */ = { + isa = PBXGroup; + children = ( + 3AB7F69529C6643D00064C4E /* SocketIOClientConfiguration.swift */, + 3AB7F69629C6643D00064C4E /* SocketAnyEvent.swift */, + 3AB7F69729C6643D00064C4E /* SocketIOClient.swift */, + 3AB7F69829C6643D00064C4E /* SocketIOClientOption.swift */, + 3AB7F69929C6643D00064C4E /* SocketEventHandler.swift */, + 3AB7F69A29C6643D00064C4E /* SocketRawView.swift */, + 3AB7F69B29C6643D00064C4E /* SocketIOClientSpec.swift */, + 3AB7F69C29C6643D00064C4E /* SocketIOStatus.swift */, + ); + path = Client; + sourceTree = ""; + }; + 3AB7F69D29C6643D00064C4E /* Ack */ = { + isa = PBXGroup; + children = ( + 3AB7F69E29C6643D00064C4E /* SocketAckManager.swift */, + 3AB7F69F29C6643D00064C4E /* SocketAckEmitter.swift */, + ); + path = Ack; sourceTree = ""; }; - 0867D691FE84028FC02AAC07 /* ti-socketio */ = { + DB258CA41F0964DE000D0D8D /* Misc */ = { isa = PBXGroup; children = ( - 08FB77AEFE84172EC02AAC07 /* Classes */, - 32C88DFF0371C24200C91783 /* Other Sources */, - 0867D69AFE84028FC02AAC07 /* Frameworks */, - 034768DFFF38A50411DB9C8B /* Products */, + DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */, + DB34CDDF207B998A005F8E8C /* TiSocketioModuleAssets.h */, + DB34CDDE207B998A005F8E8C /* TiSocketioModuleAssets.m */, + DB75E5151E9CD58100809B2D /* TiSocketio.h */, + DB52E23F1E9CCF8D00AAAEE0 /* TiSocketio_Prefix.pch */, + DB52E22E1E9CCD7000AAAEE0 /* Info.plist */, ); - name = "ti-socketio"; + name = Misc; sourceTree = ""; }; - 0867D69AFE84028FC02AAC07 /* Frameworks */ = { + DB258CA51F0964F6000D0D8D /* Sources */ = { isa = PBXGroup; children = ( - 4AB2678220B1AAE000E4E3DD /* SocketIO.framework */, - 4AB2678320B1AAE000E4E3DD /* Starscream.xcframework */, - AACBBE490F95108600F1A2B1 /* Foundation.xcframework */, + 3AB7F68029C6643D00064C4E /* SocketIO */, + DB52E2421E9CD0F800AAAEE0 /* TiSocketioModule.swift */, + DB24E03B20766AC90033B2B1 /* SocketIOClientProxy.swift */, + 3A01312A282C37EF003C6E8A /* SocketManagerProxy.swift */, + ); + name = Sources; + sourceTree = ""; + }; + DB34CDE2207B9D6A005F8E8C /* Frameworks */ = { + isa = PBXGroup; + children = ( + B1E00E64241D179500C384C1 /* TitaniumKit.xcframework */, + DB34CDE5207B9EBD005F8E8C /* Foundation.framework */, + 3A013126282C378F003C6E8A /* Starscream.xcframework */, ); name = Frameworks; sourceTree = ""; }; - 08FB77AEFE84172EC02AAC07 /* Classes */ = { + DB52E2201E9CCD7000AAAEE0 = { + isa = PBXGroup; + children = ( + DB52E22C1E9CCD7000AAAEE0 /* TiSocketio */, + DB52E22B1E9CCD7000AAAEE0 /* Products */, + DB34CDE2207B9D6A005F8E8C /* Frameworks */, + ); + sourceTree = ""; + }; + DB52E22B1E9CCD7000AAAEE0 /* Products */ = { isa = PBXGroup; children = ( - 4AB2678620B1B13500E4E3DD /* SocketIOClientProxy.h */, - 4AB2678720B1B13500E4E3DD /* SocketIOClientProxy.m */, - 4AA6A17B21C3E22200AE770E /* SocketManagerProxy.h */, - 4AA6A17C21C3E22200AE770E /* SocketManagerProxy.m */, - 24DD6CF71134B3F500162E58 /* TiSocketioModule.h */, - 24DD6CF81134B3F500162E58 /* TiSocketioModule.m */, - 24DE9E0F11C5FE74003F90F6 /* TiSocketioModuleAssets.h */, - 24DE9E1011C5FE74003F90F6 /* TiSocketioModuleAssets.m */, + DB52E22A1E9CCD7000AAAEE0 /* TiSocketio.framework */, ); - name = Classes; + name = Products; sourceTree = ""; }; - 32C88DFF0371C24200C91783 /* Other Sources */ = { + DB52E22C1E9CCD7000AAAEE0 /* TiSocketio */ = { isa = PBXGroup; children = ( - AA747D9E0F9514B9006C5449 /* TiSocketio_Prefix.pch */, - 24DD6D1B1134B66800162E58 /* titanium.xcconfig */, + DB258CA51F0964F6000D0D8D /* Sources */, + DB258CA41F0964DE000D0D8D /* Misc */, ); - name = "Other Sources"; + name = TiSocketio; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - D2AAC07A0554694100DB518D /* Headers */ = { + DB52E2271E9CCD7000AAAEE0 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - AA747D9F0F9514B9006C5449 /* TiSocketio_Prefix.pch in Headers */, - 4AB2678820B1B13500E4E3DD /* SocketIOClientProxy.h in Headers */, - 4AA6A17D21C3E22200AE770E /* SocketManagerProxy.h in Headers */, - 24DD6CF91134B3F500162E58 /* TiSocketioModule.h in Headers */, - 24DE9E1111C5FE74003F90F6 /* TiSocketioModuleAssets.h in Headers */, + DB75E5161E9CD59000809B2D /* TiSocketio.h in Headers */, + DB34CDE1207B998A005F8E8C /* TiSocketioModuleAssets.h in Headers */, + DB52E2401E9CCF8D00AAAEE0 /* TiSocketio_Prefix.pch in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - D2AAC07D0554694100DB518D /* ti.socketio */ = { + DB52E2291E9CCD7000AAAEE0 /* TiSocketio */ = { isa = PBXNativeTarget; - buildConfigurationList = 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "ti.socketio" */; + buildConfigurationList = DB52E2321E9CCD7000AAAEE0 /* Build configuration list for PBXNativeTarget "TiSocketio" */; buildPhases = ( - D2AAC07A0554694100DB518D /* Headers */, - D2AAC07B0554694100DB518D /* Sources */, - D2AAC07C0554694100DB518D /* Frameworks */, + DB52E2251E9CCD7000AAAEE0 /* Sources */, + DB52E2261E9CCD7000AAAEE0 /* Frameworks */, + DB52E2271E9CCD7000AAAEE0 /* Headers */, + DB52E2281E9CCD7000AAAEE0 /* Resources */, ); buildRules = ( ); dependencies = ( + DBB66220208481A400EAD6D9 /* PBXTargetDependency */, ); - name = ti.socketio; - productName = "ti-socketio"; - productReference = D2AAC07E0554694100DB518D /* libtisocketio.a */; - productType = "com.apple.product-type.library.static"; + name = TiSocketio; + productName = TiSocketio; + productReference = DB52E22A1E9CCD7000AAAEE0 /* TiSocketio.framework */; + productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 0867D690FE84028FC02AAC07 /* Project object */ = { + DB52E2211E9CCD7000AAAEE0 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1330; + TargetAttributes = { + DB52E2291E9CCD7000AAAEE0 = { + CreatedOnToolsVersion = 8.3; + LastSwiftMigration = 0940; + ProvisioningStyle = Automatic; + }; + }; }; - buildConfigurationList = 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "ti.socketio" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = English; - hasScannedForEncodings = 1; + buildConfigurationList = DB52E2241E9CCD7000AAAEE0 /* Build configuration list for PBXProject "ti.socketio" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; knownRegions = ( - English, - Japanese, - French, - German, + en, + Base, ); - mainGroup = 0867D691FE84028FC02AAC07 /* ti-socketio */; - productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + mainGroup = DB52E2201E9CCD7000AAAEE0; + productRefGroup = DB52E22B1E9CCD7000AAAEE0 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - D2AAC07D0554694100DB518D /* ti.socketio */, - 24416B8111C4CA220047AFDD /* Build & Test */, + DB52E2291E9CCD7000AAAEE0 /* TiSocketio */, ); }; /* End PBXProject section */ -/* Begin PBXShellScriptBuildPhase section */ - 24416B8011C4CA220047AFDD /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; +/* Begin PBXResourcesBuildPhase section */ + DB52E2281E9CCD7000AAAEE0 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# shell script goes here\n\nappc run --project-dir \"${PROJECT_DIR}\"\nexit $?\n"; }; -/* End PBXShellScriptBuildPhase section */ +/* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - D2AAC07B0554694100DB518D /* Sources */ = { + DB52E2251E9CCD7000AAAEE0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 24DD6CFA1134B3F500162E58 /* TiSocketioModule.m in Sources */, - 4AA6A17E21C3E22200AE770E /* SocketManagerProxy.m in Sources */, - 4AB2678920B1B13500E4E3DD /* SocketIOClientProxy.m in Sources */, - 24DE9E1211C5FE74003F90F6 /* TiSocketioModuleAssets.m in Sources */, + 3AB7F6B529C6643D00064C4E /* SocketIOClientSpec.swift in Sources */, + 3AB7F6A529C6643D00064C4E /* SocketParsable.swift in Sources */, + 3AB7F6A729C6643D00064C4E /* SocketManagerSpec.swift in Sources */, + 3AB7F6AE29C6643D00064C4E /* SocketEngine.swift in Sources */, + 3AB7F6A229C6643D00064C4E /* SocketExtensions.swift in Sources */, + 3AB7F6A429C6643D00064C4E /* SocketTypes.swift in Sources */, + 3AB7F6B129C6643D00064C4E /* SocketIOClient.swift in Sources */, + DB24E03C20766AC90033B2B1 /* SocketIOClientProxy.swift in Sources */, + 3AB7F6A029C6643D00064C4E /* SocketStringReader.swift in Sources */, + 3AB7F6B629C6643D00064C4E /* SocketIOStatus.swift in Sources */, + 3AB7F6A829C6643D00064C4E /* SocketManager.swift in Sources */, + 3AB7F6B829C6643D00064C4E /* SocketAckEmitter.swift in Sources */, + 3AB7F6A929C6643D00064C4E /* SocketEngineClient.swift in Sources */, + 3AB7F6A629C6643D00064C4E /* SocketPacket.swift in Sources */, + 3AB7F6AA29C6643D00064C4E /* SocketEngineWebsocket.swift in Sources */, + 3AB7F6B329C6643D00064C4E /* SocketEventHandler.swift in Sources */, + 3AB7F6A129C6643D00064C4E /* SocketLogger.swift in Sources */, + 3A01312B282C37EF003C6E8A /* SocketManagerProxy.swift in Sources */, + DB34CDE0207B998A005F8E8C /* TiSocketioModuleAssets.m in Sources */, + 3AB7F6B429C6643D00064C4E /* SocketRawView.swift in Sources */, + 3AB7F6B229C6643D00064C4E /* SocketIOClientOption.swift in Sources */, + 3AB7F6B729C6643D00064C4E /* SocketAckManager.swift in Sources */, + 3AB7F6A329C6643D00064C4E /* SSLSecurity.swift in Sources */, + 3AB7F6AC29C6643D00064C4E /* SocketEngineSpec.swift in Sources */, + 3AB7F6AF29C6643D00064C4E /* SocketIOClientConfiguration.swift in Sources */, + 3AB7F6B029C6643D00064C4E /* SocketAnyEvent.swift in Sources */, + DB52E2431E9CD0F800AAAEE0 /* TiSocketioModule.swift in Sources */, + 3AB7F6AD29C6643D00064C4E /* SocketEnginePacketType.swift in Sources */, + 3AB7F6AB29C6643D00064C4E /* SocketEnginePollable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 24416B8511C4CA280047AFDD /* PBXTargetDependency */ = { + DBB66220208481A400EAD6D9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = D2AAC07D0554694100DB518D /* ti.socketio */; - targetProxy = 24416B8411C4CA280047AFDD /* PBXContainerItemProxy */; + name = TitaniumKit; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 1DEB921F08733DC00010E9CD /* Debug */ = { + DB52E2301E9CCD7000AAAEE0 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; + baseConfigurationReference = DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ENABLE_MODULE_DEBUGGING = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEFINES_MODULE = YES; - DSTROOT = /tmp/TiSocketio.dst; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/platform", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - GCC_C_LANGUAGE_STANDARD = c99; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; - GCC_PREPROCESSOR_DEFINITIONS = "TI_VERSION=$(TI_VERSION)"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_VERSION = ""; - GCC_WARN_ABOUT_RETURN_TYPE = NO; - GCC_WARN_MISSING_PARENTHESES = NO; - GCC_WARN_SHADOW = NO; - GCC_WARN_STRICT_SELECTOR_MATCH = NO; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_PARAMETER = NO; - GCC_WARN_UNUSED_VALUE = NO; - GCC_WARN_UNUSED_VARIABLE = NO; - INSTALL_PATH = /usr/local/lib; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LIBRARY_SEARCH_PATHS = ""; - OTHER_CFLAGS = ( - "-DDEBUG", - "-DTI_POST_1_2", - ); - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = tisocketio; - PROVISIONING_PROFILE = ""; - "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; - RUN_CLANG_STATIC_ANALYZER = NO; - SDKROOT = iphoneos; - USER_HEADER_SEARCH_PATHS = ""; - WARNING_CFLAGS = "-Wno-arc-performSelector-leaks"; - }; - name = Debug; - }; - 1DEB922008733DC00010E9CD /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; - buildSettings = { - CLANG_ENABLE_MODULE_DEBUGGING = YES; - CLANG_ENABLE_OBJC_ARC = YES; - COPY_PHASE_STRIP = NO; - DEFINES_MODULE = YES; - DEPLOYMENT_POSTPROCESSING = YES; - DSTROOT = /tmp/TiSocketio.dst; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/platform", - "$(PROJECT_DIR)/Carthage/Build/iOS", - ); - GCC_C_LANGUAGE_STANDARD = c99; - GCC_MODEL_TUNING = G5; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; - GCC_PREPROCESSOR_DEFINITIONS = ( - "TI_VERSION=$(TI_VERSION)", - NDEBUG, - ); - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_VERSION = ""; - GCC_WARN_ABOUT_RETURN_TYPE = NO; - GCC_WARN_MISSING_PARENTHESES = NO; - GCC_WARN_SHADOW = NO; - GCC_WARN_STRICT_SELECTOR_MATCH = NO; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_PARAMETER = NO; - GCC_WARN_UNUSED_VALUE = NO; - GCC_WARN_UNUSED_VARIABLE = NO; - INSTALL_PATH = /usr/local/lib; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LIBRARY_SEARCH_PATHS = ""; - OTHER_CFLAGS = "-DTI_POST_1_2"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = tisocketio; - RUN_CLANG_STATIC_ANALYZER = NO; - SDKROOT = iphoneos; - USER_HEADER_SEARCH_PATHS = ""; - WARNING_CFLAGS = "-Wno-arc-performSelector-leaks"; - }; - name = Release; - }; - 1DEB922308733DC00010E9CD /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; - buildSettings = { - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LIBRARY = "compiler-default"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; @@ -347,63 +390,66 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = NO; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - DSTROOT = /tmp/TiSocketio.dst; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = c99; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; - GCC_PREPROCESSOR_DEFINITIONS = "TI_VERSION=$(TI_VERSION)"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_VERSION = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = NO; - GCC_WARN_MISSING_PARENTHESES = NO; - GCC_WARN_SHADOW = NO; - GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_PARAMETER = NO; - GCC_WARN_UNUSED_VALUE = NO; - GCC_WARN_UNUSED_VARIABLE = NO; - INSTALL_PATH = /usr/local/lib; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = ( - "-DDEBUG", - "-DTI_POST_1_2", - ); - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = tisocketio; - PROVISIONING_PROFILE = ""; - "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; - RUN_CLANG_STATIC_ANALYZER = NO; SDKROOT = iphoneos; - USER_HEADER_SEARCH_PATHS = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; }; name = Debug; }; - 1DEB922408733DC00010E9CD /* Release */ = { + DB52E2311E9CCD7000AAAEE0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; + baseConfigurationReference = DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LIBRARY = "compiler-default"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; @@ -411,98 +457,159 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = NO; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - DSTROOT = /tmp/TiSocketio.dst; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = c99; - GCC_MODEL_TUNING = G5; + GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; - GCC_PREPROCESSOR_DEFINITIONS = "TI_VERSION=$(TI_VERSION)"; - GCC_TREAT_WARNINGS_AS_ERRORS = NO; - GCC_VERSION = ""; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = NO; - GCC_WARN_MISSING_PARENTHESES = NO; - GCC_WARN_SHADOW = NO; - GCC_WARN_STRICT_SELECTOR_MATCH = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_PARAMETER = NO; - GCC_WARN_UNUSED_VALUE = NO; - GCC_WARN_UNUSED_VARIABLE = NO; - INSTALL_PATH = /usr/local/lib; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - OTHER_CFLAGS = "-DTI_POST_1_2"; - OTHER_LDFLAGS = "-ObjC"; - PRODUCT_NAME = tisocketio; - RUN_CLANG_STATIC_ANALYZER = NO; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - USER_HEADER_SEARCH_PATHS = ""; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = "$(ARCHS_STANDARD)"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; }; name = Release; }; - 24416B8211C4CA220047AFDD /* Debug */ = { + DB52E2331E9CCD7000AAAEE0 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; + baseConfigurationReference = DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */; buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - COPY_PHASE_STRIP = NO; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - PRODUCT_NAME = "Build & test"; + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = "TI_VERSION=$(TI_VERSION)"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ""; + OTHER_CFLAGS = ( + "-DDEBUG", + "-DTI_POST_1_2", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.appcelerator.ti.socketio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Titanium"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VALID_ARCHS = "$(ARCHS_STANDARD)"; }; name = Debug; }; - 24416B8311C4CA220047AFDD /* Release */ = { + DB52E2341E9CCD7000AAAEE0 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 24DD6D1B1134B66800162E58 /* titanium.xcconfig */; + baseConfigurationReference = DB52E2411E9CD09900AAAEE0 /* titanium.xcconfig */; buildSettings = { - CLANG_ENABLE_OBJC_WEAK = YES; - COPY_PHASE_STRIP = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - PRODUCT_NAME = "Build & test"; - ZERO_LINK = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = TiSocketio_Prefix.pch; + GCC_PREPROCESSOR_DEFINITIONS = "TI_VERSION=$(TI_VERSION)"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ""; + OTHER_CFLAGS = "-DTI_POST_1_2"; + OTHER_LDFLAGS = ( + "-ObjC", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.appcelerator.ti.socketio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Titanium"; + SWIFT_VERSION = 5.0; + VALID_ARCHS = "$(ARCHS_STANDARD)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 1DEB921E08733DC00010E9CD /* Build configuration list for PBXNativeTarget "ti.socketio" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 1DEB921F08733DC00010E9CD /* Debug */, - 1DEB922008733DC00010E9CD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 1DEB922208733DC00010E9CD /* Build configuration list for PBXProject "ti.socketio" */ = { + DB52E2241E9CCD7000AAAEE0 /* Build configuration list for PBXProject "ti.socketio" */ = { isa = XCConfigurationList; buildConfigurations = ( - 1DEB922308733DC00010E9CD /* Debug */, - 1DEB922408733DC00010E9CD /* Release */, + DB52E2301E9CCD7000AAAEE0 /* Debug */, + DB52E2311E9CCD7000AAAEE0 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 24416B8A11C4CA520047AFDD /* Build configuration list for PBXAggregateTarget "Build & Test" */ = { + DB52E2321E9CCD7000AAAEE0 /* Build configuration list for PBXNativeTarget "TiSocketio" */ = { isa = XCConfigurationList; buildConfigurations = ( - 24416B8211C4CA220047AFDD /* Debug */, - 24416B8311C4CA220047AFDD /* Release */, + DB52E2331E9CCD7000AAAEE0 /* Debug */, + DB52E2341E9CCD7000AAAEE0 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = 0867D690FE84028FC02AAC07 /* Project object */; + rootObject = DB52E2211E9CCD7000AAAEE0 /* Project object */; } diff --git a/ios/timodule.xml b/ios/timodule.xml index ae31597..85bc571 100644 --- a/ios/timodule.xml +++ b/ios/timodule.xml @@ -1,9 +1,5 @@ - diff --git a/ios/titanium.xcconfig b/ios/titanium.xcconfig index 27ff8c2..a78f83c 100644 --- a/ios/titanium.xcconfig +++ b/ios/titanium.xcconfig @@ -4,7 +4,7 @@ // OF YOUR TITANIUM SDK YOU'RE BUILDING FOR // // -TITANIUM_SDK_VERSION = 9.3.2.GA +TITANIUM_SDK_VERSION = 12.0.0.GA // // THESE SHOULD BE OK GENERALLY AS-IS @@ -12,4 +12,3 @@ TITANIUM_SDK_VERSION = 9.3.2.GA TITANIUM_SDK = /Users/$(USER)/Library/Application Support/Titanium/mobilesdk/osx/$(TITANIUM_SDK_VERSION) HEADER_SEARCH_PATHS = $(inherited) "$(TITANIUM_SDK)/iphone/include" FRAMEWORK_SEARCH_PATHS = $(inherited) "$(TITANIUM_SDK)/iphone/Frameworks/**" -