Repository: davidstump/SwiftPhoenixClient Branch: master Commit: 25a4d8a3fa75 Files: 116 Total size: 1.2 MB Directory structure: gitextract_igy9h_6n/ ├── .gitignore ├── .slather.yml ├── .sourcery.yml ├── .travis.yml ├── CHANGELOG.md ├── Cartfile.private ├── Cartfile.resolved ├── Examples/ │ └── Basic/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ ├── basic/ │ │ └── BasicChatViewController.swift │ └── chatroom/ │ └── ChatRoomViewController.swift ├── Gemfile ├── LICENSE ├── Package.swift ├── README.md ├── RELEASING.md ├── Sources/ │ ├── Supporting Files/ │ │ ├── Info.plist │ │ └── SwiftPhoenixClient.h │ └── SwiftPhoenixClient/ │ ├── Channel.swift │ ├── Defaults.swift │ ├── Delegated.swift │ ├── HeartbeatTimer.swift │ ├── Message.swift │ ├── PhoenixTransport.swift │ ├── Presence.swift │ ├── Push.swift │ ├── Socket.swift │ ├── SynchronizedArray.swift │ └── TimeoutTimer.swift ├── SwiftPhoenixClient.podspec ├── SwiftPhoenixClient.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata/ │ └── xcschemes/ │ ├── Basic.xcscheme │ ├── RxSwiftPhoenixClient.xcscheme │ ├── StarscreamSwiftPhoenixClient.xcscheme │ ├── SwiftPhoenixClient.xcscheme │ └── SwiftPhoenixClientTests.xcscheme ├── Tests/ │ ├── Fakes/ │ │ ├── FakeTimerQueue.swift │ │ ├── FakeTimerQueueSpec.swift │ │ └── SocketSpy.swift │ ├── Helpers/ │ │ └── TestHelpers.swift │ ├── Info.plist │ ├── Mocks/ │ │ ├── MockableClass.generated.swift │ │ └── MockableProtocol.generated.swift │ └── SwiftPhoenixClientTests/ │ ├── ChannelSpec.swift │ ├── DefaultSerializerSpec.swift │ ├── HeartbeatTimerSpec.swift │ ├── MessageSpec.swift │ ├── PresenceSpec.swift │ ├── SocketSpec.swift │ ├── TimeoutTimerSpec.swift │ └── URLSessionTransportSpec.swift ├── docs/ │ ├── Classes/ │ │ ├── Channel.html │ │ ├── Defaults.html │ │ ├── Message.html │ │ ├── Presence/ │ │ │ ├── Events.html │ │ │ └── Options.html │ │ ├── Presence.html │ │ ├── Push.html │ │ └── Socket.html │ ├── Classes.html │ ├── Enums/ │ │ └── ChannelState.html │ ├── Enums.html │ ├── Global Variables.html │ ├── Protocols/ │ │ └── Serializer.html │ ├── Protocols.html │ ├── Structs/ │ │ ├── ChannelEvent.html │ │ └── Delegated.html │ ├── Structs.html │ ├── Typealiases.html │ ├── css/ │ │ ├── highlight.css │ │ └── jazzy.css │ ├── docsets/ │ │ ├── SwiftPhoenixClient.docset/ │ │ │ └── Contents/ │ │ │ ├── Info.plist │ │ │ └── Resources/ │ │ │ ├── Documents/ │ │ │ │ ├── Classes/ │ │ │ │ │ ├── Channel.html │ │ │ │ │ ├── Defaults.html │ │ │ │ │ ├── Message.html │ │ │ │ │ ├── Presence/ │ │ │ │ │ │ ├── Events.html │ │ │ │ │ │ └── Options.html │ │ │ │ │ ├── Presence.html │ │ │ │ │ ├── Push.html │ │ │ │ │ └── Socket.html │ │ │ │ ├── Classes.html │ │ │ │ ├── Enums/ │ │ │ │ │ └── ChannelState.html │ │ │ │ ├── Enums.html │ │ │ │ ├── Global Variables.html │ │ │ │ ├── Protocols/ │ │ │ │ │ └── Serializer.html │ │ │ │ ├── Protocols.html │ │ │ │ ├── Structs/ │ │ │ │ │ ├── ChannelEvent.html │ │ │ │ │ └── Delegated.html │ │ │ │ ├── Structs.html │ │ │ │ ├── Typealiases.html │ │ │ │ ├── css/ │ │ │ │ │ ├── highlight.css │ │ │ │ │ └── jazzy.css │ │ │ │ ├── index.html │ │ │ │ ├── js/ │ │ │ │ │ └── jazzy.js │ │ │ │ ├── search.json │ │ │ │ └── undocumented.json │ │ │ └── docSet.dsidx │ │ └── SwiftPhoenixClient.tgz │ ├── index.html │ ├── js/ │ │ └── jazzy.js │ ├── search.json │ └── undocumented.json ├── fastlane/ │ ├── Appfile │ ├── Fastfile │ └── README.md └── sourcery/ ├── MockableClass.stencil ├── MockableProtocol.stencil └── MockableWebSocketClient.stencil ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # OS X .DS_Store # Xcode build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout profile *.moved-aside DerivedData *.hmap *.xccheckout # AppCode .idea/ Carthage Demo/Pods .ruby-version .ruby-gemset # Swift Package Manager .build .swiftpm Packages Package.pins ================================================ FILE: .slather.yml ================================================ # .slather.yml # CodeCov coverage_service: cobertura_xml xcodeproj: SwiftPhoenixClient.xcodeproj scheme: SwiftPhoenixClient workspace: SwiftPhoenixClient.xcworkspace source_directory: SwiftPhoenixClient output_directory: fastlane/test_output ignore: - ../* - Pods/* ================================================ FILE: .sourcery.yml ================================================ sources: - Sources/ templates: - sourcery/ output: Tests/Mocks/ ================================================ FILE: .travis.yml ================================================ language: swift os: osx osx_image: xcode11.4 cache: bundler: true directories: - Carthage before_install: - brew update - brew outdated carthage || brew upgrade carthage - carthage bootstrap --use-xcframeworks --platform iOS --no-use-binaries --cache-builds script: - bundle exec fastlane test # after_success: # - bash <(curl -s https://codecov.io/bash) ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) This product uses [Semantic Versioning](https://semver.org/). ### 5.3.5 - Fix `objc_loadWeakRetained` on iOS 18 in the `Transport` layer ### 5.3.4 - Added various weak self checks ### 5.3.3 - Added weak self references in heartbeat timer - Added guards against weak self in PhoenixTransport ### 5.3.3 - Additional thread related crashes ### 5.3.2 - Fixed various thread-related crashes ### 5.3.1 - Added `socket.headers` which will be added to the `URLRequest` when opening a WebSocket connection - Using thread-safe array for Socket callback bindings, fixing a crash when creating a channel - Breaking a retain cycle in socket ### 5.3.0 - Fix retain cycles in `URLSessionTransport` and using default operation queue - Adding an optional `leeway` to the `HeartbeatTimer` - Added additional `open` methods in `URLSessionTransport` for further customization - Using a thread-safe array for Channel bindings ## 5.2.2 - Changed `URLSessionTransport` to `open` to provide for custom behavior, such as SSL Pinning ## 5.2.1 - Added `connectionState` to `Socket` which exposes the Socket's ready state ## 5.2.0 - [#226](https://github.com/davidstump/SwiftPhoenixClient/pull/226) Adds `URLResponse` as an optional value in `socket.onError` callbacks to allow for checking status codes from the server when the Socket connection errors out. See Examples in PR for more details ## 5.1.0 - Improves reconnection logic around a heartbeat timeout ## 5.0.0 - Removes RxSwift dependency - Removes Starscream dependency - Creating new repos to host these extensions ## 4.0.0 - Updates RxSwift version to 6.x ## 3.0.0 This ia a **BREAKING** release. The following has changed to properly matched the phoenix.js library - `message.payload.response` is now automatically unwrapped and returned as `message.payload` for `phx_reply` events. - The client now, be default, uses the JSON V2 Serializer which was added in phoenix 1.3. If you are still running 1.2 or earlier, then you will need to continue using SwiftPhoenixClient 2.1.0, or provide your own custom `vsn`, `encoder` and `decoder` to the `Socket` class. ## 2.1.1 - Fixed HeartbeatTimer to add thread safety and fix crash reported in #188 ## 2.1.0 - Updated Presence.Options init method to be public - Updated URLSessionWebsocketTask init method to accept a custom configuration ## 2.0.0 - Restructured project - Added support for URLSession's Websocket Task - Split Starscream and RxSwift into optional modules ## [1.3.0] - Fixed Cartfile declaration of Starscream - Added `HeartbeatTimer` class which allows running Timers to run on their own thread - Made `Socket` init public to allow customization of the transport methhod ## [1.2.1] - Pinned back Starscream version to fix Carthage build issue ## [1.2.0](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.2...1.2.0) - [#153](https://github.com/davidstump/SwiftPhoenixClient/pull/153): Added ability to pass a closure when initializing a `Socket` to dynamically change `params` when reconnecting - Fixed Package.swift and updated it to use latest Starscream ## [1.1.2](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.1...1.1.2) - [#151](https://github.com/davidstump/SwiftPhoenixClient/pull/151): Made isJoined, isJoining, etc methods on Channel public ## [1.1.1](https://github.com/davidstump/SwiftPhoenixClient/compare/1.1.0...1.1.1) - [#141](https://github.com/davidstump/SwiftPhoenixClient/pull/141): tvOS support - [#145](https://github.com/davidstump/SwiftPhoenixClient/pull/145): Refactored Socket reconnect strategy - [#146](https://github.com/davidstump/SwiftPhoenixClient/pull/146): Refactored Channel rejoin strategy ## [1.1.0] - Swift 5 ## [1.0.1] - Fixed issue with Carthage installs ## [1.0.0] - Rewrite of large parts of the Socket and Channel classes - Optional API for automatic retain cycle handling - Presence support ## [0.9.3] ## Added - [#119](https://github.com/davidstump/SwiftPhoenixClient/pull/119): A working implementation of Presence ## Changed - [#120](https://github.com/davidstump/SwiftPhoenixClient/pull/120): Xcode 10 and Swift 4.2 ## [0.9.2] ## Fixed - [#111](https://github.com/davidstump/SwiftPhoenixClient/pull/111): Strong memory cycles between Socket, Channel and Timers - [#112](https://github.com/davidstump/SwiftPhoenixClient/pull/112): Leak when Socket disconnects and properly call `onClose()` - [#114](https://github.com/davidstump/SwiftPhoenixClient/pull/114): Carthage failing on builds and app store uploads ## Changed - [#116](https://github.com/davidstump/SwiftPhoenixClient/pull/116): A Channel's `topic` is now exposed as `public` ## [0.9.1] ### Added - Added security configuration to the underlying WebSocket. ## [0.9.0] Continue to improve the API and behavior of the library to behave similar to the JS library. This release introduces some breaking changes in the API that will require updates to your code. See the [usage guide] for help. ### Updated - Swift 4.1 ### Changed - All callbacks now receive a `Message` object. The `Payload` can be accessed using `message.payload` ### Added - `channel.join()` can now take optional params to override the ones set while creating the Channel - Timeouts when sending messages - Rejoin timer which can be configured to attempt to rejoin given a function. Defaults to 1s, 2s, 5s, 10s and then retries every 10s - Socket and Channel `on` callbacks are able to hold more than just a single callback Thanks to @murphb52 and @ALucasVanDongen for helping with some of the development and testing of this release! ## [0.8.1] ### Fixed - Initial params are not sent through when opening a channel ## [0.8.0] ### Updated - Starscream to 3.0.4 - Swift 4 - Mirror [Phoenix.js](https://hexdocs.pm/phoenix/js/) more closely [Unreleased]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.3.0...HEAD [1.3.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.2.1...1.3.0 [1.2.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/1.2.0...1.2.1 [0.9.3]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.2...0.9.3 [0.9.2]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.1...0.9.2 [0.9.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.9.0...0.9.1 [0.9.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.8.1...0.9.0 [0.8.1]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.8.0...0.8.1 [0.8.0]: https://github.com/davidstump/SwiftPhoenixClient/compare/0.6.0...0.8.0 [migration guide]: https://github.com/davidstump/SwiftPhoenixClient/wiki/Usage-Guide ================================================ FILE: Cartfile.private ================================================ github "Quick/Quick" ~> 4.0.0 github "Quick/Nimble" ~> 9.0.0 ================================================ FILE: Cartfile.resolved ================================================ github "Quick/Nimble" "v9.2.1" github "Quick/Quick" "v4.0.0" ================================================ FILE: Examples/Basic/AppDelegate.swift ================================================ // // AppDelegate.swift // Basic // // Created by Daniel Rees on 10/23/20. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } } ================================================ FILE: Examples/Basic/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/Basic/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/Basic/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Examples/Basic/Base.lproj/Main.storyboard ================================================ Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. ================================================ FILE: Examples/Basic/Info.plist ================================================ NSAppTransportSecurity NSAllowsArbitraryLoads CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Examples/Basic/SceneDelegate.swift ================================================ // // SceneDelegate.swift // Basic // // Created by Daniel Rees on 10/23/20. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? } ================================================ FILE: Examples/Basic/basic/BasicChatViewController.swift ================================================ // // BasicChatViewController.swift // Basic // // Created by Daniel Rees on 10/23/20. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import UIKit import SwiftPhoenixClient /* Testing with Basic Chat This class is designed to provide as a sandbox to test various client features in a "real" environment where network can be dropped, disconnected, servers can quit, etc. For a more advanced example, see the ChatRoomViewController This example is intended to connect to a local chat server. Setup 1. Select which Transpart is being tested. Steps 1. Connect the Socket 2. Verify System pings come through 3. Send a message and verify it is returned by the server 4. From a web client, send a message and verify it is received by the app. 5. Disconnect and Connect the Socket again 6. Kill the server, verifying that the retry starts 7. Start the server again, verifying that the client reconnects 8. After the client reconnects, verify pings and messages work as before 9. Disconnect the client and kill the server again 10. While the server is disconnected, connect the client 11. Start the server and verify that the client connects once the server is available */ let endpoint = "http://localhost:4000/socket/websocket" class BasicChatViewController: UIViewController { // MARK: - Child Views @IBOutlet weak var connectButton: UIButton! @IBOutlet weak var messageField: UITextField! @IBOutlet weak var chatWindow: UITextView! // MARK: - Variables let username: String = "Basic" var topic: String = "rooms:lobby" // Test the URLSessionTransport let socket = Socket(endpoint) var lobbyChannel: Channel! // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() self.chatWindow.text = "" // To automatically manage retain cycles, use `delegate*(to:)` methods. // If you would prefer to handle them yourself, youcan use the same // methods without the `delegate` functions, just be sure you avoid // memory leakse with `[weak self]` socket.delegateOnOpen(to: self) { (self) in self.addText("Socket Opened") DispatchQueue.main.async { self.connectButton.setTitle("Disconnect", for: .normal) } } socket.delegateOnClose(to: self) { (self) in self.addText("Socket Closed") DispatchQueue.main.async { self.connectButton.setTitle("Connect", for: .normal) } } socket.delegateOnError(to: self) { (self, arg1) in let (error, response) = arg1 if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode > 400 { self.addText("Socket Errored: \(statusCode)") self.socket.disconnect() } else { self.addText("Socket Errored: " + error.localizedDescription) } } socket.logger = { msg in print("LOG:", msg) } } // MARK: - IBActions @IBAction func onConnectButtonPressed(_ sender: Any) { if socket.isConnected { disconnectAndLeave() } else { connectAndJoin() } } @IBAction func sendMessage(_ sender: UIButton) { let payload = ["user": username, "body": messageField.text!] self.lobbyChannel .push("new:msg", payload: payload) .receive("ok") { (message) in print("success", message) } .receive("error") { (errorMessage) in print("error: ", errorMessage) } messageField.text = "" } private func disconnectAndLeave() { // Be sure the leave the channel or call socket.remove(lobbyChannel) lobbyChannel.leave() socket.disconnect { self.addText("Socket Disconnected") } } private func connectAndJoin() { let channel = socket.channel(topic, params: ["status":"joining"]) channel.delegateOn("join", to: self) { (self, _) in self.addText("You joined the room.") } channel.delegateOn("new:msg", to: self) { (self, message) in let payload = message.payload guard let username = payload["user"], let body = payload["body"] else { return } let newMessage = "[\(username)] \(body)" self.addText(newMessage) } channel.delegateOn("user:entered", to: self) { (self, message) in self.addText("[anonymous entered]") } self.lobbyChannel = channel self.lobbyChannel .join() .delegateReceive("ok", to: self) { (self, _) in self.addText("Joined Channel") }.delegateReceive("error", to: self) { (self, message) in self.addText("Failed to join channel: \(message.payload)") } self.socket.connect() } private func addText(_ text: String) { DispatchQueue.main.async { let updatedText = self.chatWindow.text.appending(text).appending("\n") self.chatWindow.text = updatedText let bottom = NSMakeRange(updatedText.count - 1, 1) self.chatWindow.scrollRangeToVisible(bottom) } } } ================================================ FILE: Examples/Basic/chatroom/ChatRoomViewController.swift ================================================ // // ChatRoomViewController.swift // Basic // // Created by Daniel Rees on 12/22/20. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import UIKit import SwiftPhoenixClient struct Shout { let name: String let message: String } /* ChatRoom provides a "real" example of using SwiftPhoenixClient, including how to use the Rx extensions for it. It also utilizes logic to disconnect/reconnect the socket when the app enters and exits the foreground. NOTE: iOS can, at will, kill your connection if the app enters the background without notiftying your process that it has been killed. Thus resulting in a disconnected socket when the app comes back to the foreground. The best way around this is to listen to UIApplication.didBecomeActiveNotification events and manually check if the socket is still connected and attempt to reconnect and rejoin any channels. In this example, the channel is left and socket is disconnected when the app enters the background and then a new channel is created and joined and the socket is connected when the app enters the foreground. This example utilizes the PhxChat example at https://github.com/dwyl/phoenix-chat-example */ class ChatRoomViewController: UIViewController { // MARK: - Child Views @IBOutlet weak var messageInput: UITextField! @IBOutlet weak var tableView: UITableView! // MARK: - Attributes private let username: String = "ChatRoom" // private let socket = Socket("http://localhost:4000/socket/websocket") private let socket = Socket("https://phoenix-chat.fly.dev/socket/websocket") private let topic: String = "room:lobby" private var lobbyChannel: Channel? private var shouts: [Shout] = [] // Notifcation Subscriptions private var didbecomeActiveObservervation: NSObjectProtocol? private var willResignActiveObservervation: NSObjectProtocol? // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() self.tableView.dataSource = self // When app enters foreground, be sure that the socket is connected self.observeDidBecomeActive() // Connect to the chat for the first time self.connectToChat() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) // When the Controller is removed from the view hierarchy, then stop // observing app lifecycle and disconnect from the chat self.removeAppActiveObservation() self.disconnectFromChat() } // MARK: - IB Actions @IBAction func onExitButtonPressed(_ sender: Any) { self.navigationController?.popViewController(animated: true) } @IBAction func onSendButtonPressed(_ sender: Any) { // Create and send the payload let payload = ["name": username, "message": messageInput.text!] self.lobbyChannel?.push("shout", payload: payload) // Clear the text intput self.messageInput.text = "" } //---------------------------------------------------------------------- // MARK: - Background/Foreground reconnect strategy //---------------------------------------------------------------------- private func observeDidBecomeActive() { //Make sure there's no other observations self.removeAppActiveObservation() self.didbecomeActiveObservervation = NotificationCenter.default .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in self?.connectToChat() } // When the app resigns being active, the leave any existing channels // and disconnect from the websocket. self.willResignActiveObservervation = NotificationCenter.default .addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in self?.disconnectFromChat() } } private func removeAppActiveObservation() { if let observer = self.didbecomeActiveObservervation { NotificationCenter.default.removeObserver(observer) self.didbecomeActiveObservervation = nil } if let observer = self.willResignActiveObservervation { NotificationCenter.default.removeObserver(observer) self.willResignActiveObservervation = nil } } private func connectToChat() { // Setup the socket to receive open/close events socket.delegateOnOpen(to: self) { (self) in print("CHAT ROOM: Socket Opened") } socket.delegateOnClose(to: self) { (self) in print("CHAT ROOM: Socket Closed") } socket.delegateOnError(to: self) { (self, error) in let (error, response) = error if let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode > 400 { print("CHAT ROOM: Socket Errored. \(statusCode)") self.socket.disconnect() } else { print("CHAT ROOM: Socket Errored. \(error)") } } socket.logger = { msg in print("LOG:", msg) } // Setup the Channel to receive and send messages let channel = socket.channel(topic, params: ["status": "joining"]) channel.delegateOn("shout", to: self) { (self, message) in let payload = message.payload guard let name = payload["name"] as? String, let message = payload["message"] as? String else { return } let shout = Shout(name: name, message: message) self.shouts.append(shout) DispatchQueue.main.async { let indexPath = IndexPath(row: self.shouts.count - 1, section: 0) self.tableView.reloadData() //reloadRows(at: [indexPath], with: .automatic) self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) } } // Now connect the socket and join the channel self.lobbyChannel = channel self.lobbyChannel? .join() .delegateReceive("ok", to: self, callback: { (self, _) in print("CHANNEL: rooms:lobby joined") }) .delegateReceive("error", to: self, callback: { (self, message) in print("CHANNEL: rooms:lobby failed to join. \(message.payload)") }) self.socket.connect() } private func disconnectFromChat() { if let channel = self.lobbyChannel { channel.leave() self.socket.remove(channel) } self.socket.disconnect() self.shouts = [] } } extension ChatRoomViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.shouts.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "shout_cell") let shout = self.shouts[indexPath.row] cell.textLabel?.text = shout.message cell.detailTextLabel?.text = shout.name return cell } } ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gem 'fastlane' gem 'slather' ================================================ FILE: LICENSE ================================================ Copyright (c) 2015 David Stump 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. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SwiftPhoenixClient", platforms: [ .macOS(.v10_12), .iOS(.v10), .tvOS(.v10), .watchOS(.v3) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library(name: "SwiftPhoenixClient", targets: ["SwiftPhoenixClient"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "SwiftPhoenixClient", dependencies: [] ), .testTarget( name: "SwiftPhoenixClientTests", dependencies: ["SwiftPhoenixClient"]), ] ) ================================================ FILE: README.md ================================================ # Swift Phoenix Client [![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg?style=flat)](https://swift.org/) [![Version](https://img.shields.io/cocoapods/v/SwiftPhoenixClient.svg?style=flat)](http://cocoapods.org/pods/SwiftPhoenixClient) [![License](https://img.shields.io/cocoapods/l/SwiftPhoenixClient.svg?style=flat)](http://cocoapods.org/pods/SwiftPhoenixClient) [![Platform](https://img.shields.io/cocoapods/p/SwiftPhoenixClient.svg?style=flat)](http://cocoapods.org/pods/SwiftPhoenixClient) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Open Source Helpers](https://www.codetriage.com/davidstump/swiftphoenixclient/badges/users.svg)](https://www.codetriage.com/davidstump/swiftphoenixclient) ## About SwiftPhoenixClient is a Swift port of phoenix.js, allowing your swift projects to connect to a Phoenix Websocket backend. We try out best to keep the library up to date with phoenix.js but if there is something that is missing, please create an issue or, even better, a PR to address the change. ## Sample Projects You can view the example of how to use SwiftPhoenixClient in the Example/ dir. There are two primary classes, `BasicViewController` and `ChatRoomViewController`. The `BasicViewController` is designed to test against a [local chat server](https://github.com/chrismccord/phoenix_chat_example) where as `ChatRoomViewController` is a more "complete" example which targets dwyl's [phoenix-chat-example](https://github.com/dwyl/phoenix-chat-example) Heroku app. ### SwiftPhoenixClient The core module which provides the Phoenix Channels and Presence logic. It also uses URLSession's default WebSocket implementation which has a minimum iOS target of 13.0. ## Installation ### CocoaPods You can install SwiftPhoenix Client via CocoaPods by adding the following to your Podfile. Keep in mind that in order to use Swift Phoenix Client, the minimum iOS target must be '9.0' ```RUBY pod "SwiftPhoenixClient", '~> 5.3' ``` and running `pod install`. From there you will need to add `import SwiftPhoenixClient` in any class you want it to be used. ### Carthage If you use Carthage to manage your dependencies, simply add SwiftPhoenixClient to your `Cartfile`: ``` github "davidstump/SwiftPhoenixClient" ~> 5.3 ``` Then run `carthage update`. If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). ### SwiftPackageManager _Note: Instructions below are for using **SwiftPM** without the Xcode UI. It's the easiest to go to your Project Settings -> Swift Packages and add SwiftPhoenixClient from there._ To integrate using Apple's Swift package manager, without Xcode integration, add the following as a dependency to your `Package.swift`: ```swift .package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMajor(from: "5.2.2")) ``` and then specify `"SwiftPhoenixClient"` as a dependency of the Target in which you wish to use SwiftPhoenixClient. ## Usage Using the Swift Phoenix Client is extremely easy (and familiar if you have used the phoenix.js client). See the [Usage Guide](https://github.com/davidstump/SwiftPhoenixClient/wiki/Usage-Guide) for details instructions. You can also check out the [documentation](http://davidstump.github.io/SwiftPhoenixClient/) ## Example Check out the [ViewController](https://github.com/davidstump/SwiftPhoenixClient/blob/master/Examples/Basic/chatroom/ChatRoomViewController.swift) in this repo for a brief example of a simple iOS chat application using the [Phoenix Chat Example](https://github.com/dwyl/phoenix-chat-example) Also check out both the Swift and Elixir channels on IRC. ## Development Check out the wiki page for [getting started](https://github.com/davidstump/SwiftPhoenixClient/wiki/Contributing) ## Thanks Many many thanks to [Daniel Rees](https://github.com/dsrees) for his many contributions and continued maintenance of this project! ## License SwiftPhoenixClient is available under the MIT license. See the LICENSE file for more info. ================================================ FILE: RELEASING.md ================================================ Release Process =============== 1. Ensure `version` in `SwiftPhoenixClient.podsec` is set to the version you want to release. 2. Run a trial pod release `pod lib lint` 3. Update `CHANGELOG.md` with the version about to be released along with notes. 4. Commit: `git commit -am "Prepare version X.Y.X"` 5. Tag: `git tag -a X.Y.Z -m "Version X.Y.Z"` 6. Push: `git push && git push --tags` 7. Release to Cocoapods `pod trunk push SwiftPhoenixClient.podspec` 8. Add the new release with notes (https://github.com/davidstump/SwiftPhoenixClient/releases). ================================================ FILE: Sources/Supporting Files/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright Copyright © 2021 SwiftPhoenixClient. All rights reserved. ================================================ FILE: Sources/Supporting Files/SwiftPhoenixClient.h ================================================ // // SwiftPhoenixClient.h // SwiftPhoenixClient // // Created by Daniel Rees on 10/21/20. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // #import //! Project version number for SwiftPhoenixClient. FOUNDATION_EXPORT double SwiftPhoenixClientVersionNumber; //! Project version string for SwiftPhoenixClient. FOUNDATION_EXPORT const unsigned char SwiftPhoenixClientVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Sources/SwiftPhoenixClient/Channel.swift ================================================ // Copyright (c) 2021 David Stump // // 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 Swift import Foundation /// Container class of bindings to the channel struct Binding { // The event that the Binding is bound to let event: String // The reference number of the Binding let ref: Int // The callback to be triggered let callback: Delegated } /// /// Represents a Channel which is bound to a topic /// /// A Channel can bind to multiple events on a given topic and /// be informed when those events occur within a topic. /// /// ### Example: /// /// let channel = socket.channel("room:123", params: ["token": "Room Token"]) /// channel.on("new_msg") { payload in print("Got message", payload") } /// channel.push("new_msg, payload: ["body": "This is a message"]) /// .receive("ok") { payload in print("Sent message", payload) } /// .receive("error") { payload in print("Send failed", payload) } /// .receive("timeout") { payload in print("Networking issue...", payload) } /// /// channel.join() /// .receive("ok") { payload in print("Channel Joined", payload) } /// .receive("error") { payload in print("Failed ot join", payload) } /// .receive("timeout") { payload in print("Networking issue...", payload) } /// import Foundation public class Channel { /// The topic of the Channel. e.g. "rooms:friends" public let topic: String /// The params sent when joining the channel public var params: Payload { didSet { self.joinPush.payload = params } } /// The Socket that the channel belongs to weak var socket: Socket? /// Current state of the Channel var state: ChannelState /// Collection of event bindings let syncBindingsDel: SynchronizedArray /// Tracks event binding ref counters var bindingRef: Int /// Timout when attempting to join a Channel var timeout: TimeInterval /// Set to true once the channel calls .join() var joinedOnce: Bool /// Push to send when the channel calls .join() var joinPush: Push! /// Buffer of Pushes that will be sent once the Channel's socket connects var pushBuffer: [Push] /// Timer to attempt to rejoin var rejoinTimer: TimeoutTimer /// Refs of stateChange hooks var stateChangeRefs: [String] /// Initialize a Channel /// /// - parameter topic: Topic of the Channel /// - parameter params: Optional. Parameters to send when joining. /// - parameter socket: Socket that the channel is a part of init(topic: String, params: [String: Any] = [:], socket: Socket) { self.state = ChannelState.closed self.topic = topic self.params = params self.socket = socket self.syncBindingsDel = SynchronizedArray() self.bindingRef = 0 self.timeout = socket.timeout self.joinedOnce = false self.pushBuffer = [] self.stateChangeRefs = [] self.rejoinTimer = TimeoutTimer() // Setup Timer delgation self.rejoinTimer.callback .delegate(to: self) { (self) in if self.socket?.isConnected == true { self.rejoin() } } self.rejoinTimer.timerCalculation .delegate(to: self) { (self, tries) -> TimeInterval in return self.socket?.rejoinAfter(tries) ?? 5.0 } // Respond to socket events let onErrorRef = self.socket?.delegateOnError(to: self, callback: { (self, _) in self.rejoinTimer.reset() }) if let ref = onErrorRef { self.stateChangeRefs.append(ref) } let onOpenRef = self.socket?.delegateOnOpen(to: self, callback: { (self) in self.rejoinTimer.reset() if (self.isErrored) { self.rejoin() } }) if let ref = onOpenRef { self.stateChangeRefs.append(ref) } // Setup Push Event to be sent when joining self.joinPush = Push(channel: self, event: ChannelEvent.join, payload: self.params, timeout: self.timeout) /// Handle when a response is received after join() self.joinPush.delegateReceive("ok", to: self) { (self, _) in // Mark the Channel as joined self.state = ChannelState.joined // Reset the timer, preventing it from attempting to join again self.rejoinTimer.reset() // Send and buffered messages and clear the buffer self.pushBuffer.forEach( { $0.send() }) self.pushBuffer = [] } // Perform if Channel errors while attempting to joi self.joinPush.delegateReceive("error", to: self) { (self, _) in self.state = .errored if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() } } // Handle when the join push times out when sending after join() self.joinPush.delegateReceive("timeout", to: self) { (self, _) in // log that the channel timed out self.socket?.logItems("channel", "timeout \(self.topic) \(self.joinRef ?? "") after \(self.timeout)s") // Send a Push to the server to leave the channel let leavePush = Push(channel: self, event: ChannelEvent.leave, timeout: self.timeout) leavePush.send() // Mark the Channel as in an error and attempt to rejoin if socket is connected self.state = ChannelState.errored self.joinPush.reset() if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() } } /// Perfom when the Channel has been closed self.delegateOnClose(to: self) { (self, _) in // Reset any timer that may be on-going self.rejoinTimer.reset() // Log that the channel was left self.socket?.logItems("channel", "close topic: \(self.topic) joinRef: \(self.joinRef ?? "nil")") // Mark the channel as closed and remove it from the socket self.state = ChannelState.closed self.socket?.remove(self) } /// Perfom when the Channel errors self.delegateOnError(to: self) { (self, message) in // Log that the channel received an error self.socket?.logItems("channel", "error topic: \(self.topic) joinRef: \(self.joinRef ?? "nil") mesage: \(message)") // If error was received while joining, then reset the Push if (self.isJoining) { // Make sure that the "phx_join" isn't buffered to send once the socket // reconnects. The channel will send a new join event when the socket connects. if let safeJoinRef = self.joinRef { self.socket?.removeFromSendBuffer(ref: safeJoinRef) } // Reset the push to be used again later self.joinPush.reset() } // Mark the channel as errored and attempt to rejoin if socket is currently connected self.state = ChannelState.errored if (self.socket?.isConnected == true) { self.rejoinTimer.scheduleTimeout() } } // Perform when the join reply is received self.delegateOn(ChannelEvent.reply, to: self) { (self, message) in // Trigger bindings self.trigger(event: self.replyEventName(message.ref), payload: message.rawPayload, ref: message.ref, joinRef: message.joinRef) } } deinit { rejoinTimer.reset() } /// Overridable message hook. Receives all events for specialized message /// handling before dispatching to the channel callbacks. /// /// - parameter msg: The Message received by the client from the server /// - return: Must return the message, modified or unmodified public var onMessage: (_ message: Message) -> Message = { (message) in return message } /// Joins the channel /// /// - parameter timeout: Optional. Defaults to Channel's timeout /// - return: Push event @discardableResult public func join(timeout: TimeInterval? = nil) -> Push { guard !joinedOnce else { fatalError("tried to join multiple times. 'join' " + "can only be called a single time per channel instance") } // Join the Channel if let safeTimeout = timeout { self.timeout = safeTimeout } self.joinedOnce = true self.rejoin() return joinPush } /// Hook into when the Channel is closed. Does not handle retain cycles. /// Use `delegateOnClose(to:)` for automatic handling of retain cycles. /// /// Example: /// /// let channel = socket.channel("topic") /// channel.onClose() { [weak self] message in /// self?.print("Channel \(message.topic) has closed" /// } /// /// - parameter callback: Called when the Channel closes /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func onClose(_ callback: @escaping ((Message) -> Void)) -> Int { return self.on(ChannelEvent.close, callback: callback) } /// Hook into when the Channel is closed. Automatically handles retain /// cycles. Use `onClose()` to handle yourself. /// /// Example: /// /// let channel = socket.channel("topic") /// channel.delegateOnClose(to: self) { (self, message) in /// self.print("Channel \(message.topic) has closed" /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Channel closes /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func delegateOnClose(to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { return self.delegateOn(ChannelEvent.close, to: owner, callback: callback) } /// Hook into when the Channel receives an Error. Does not handle retain /// cycles. Use `delegateOnError(to:)` for automatic handling of retain /// cycles. /// /// Example: /// /// let channel = socket.channel("topic") /// channel.onError() { [weak self] (message) in /// self?.print("Channel \(message.topic) has errored" /// } /// /// - parameter callback: Called when the Channel closes /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func onError(_ callback: @escaping ((_ message: Message) -> Void)) -> Int { return self.on(ChannelEvent.error, callback: callback) } /// Hook into when the Channel receives an Error. Automatically handles /// retain cycles. Use `onError()` to handle yourself. /// /// Example: /// /// let channel = socket.channel("topic") /// channel.delegateOnError(to: self) { (self, message) in /// self.print("Channel \(message.topic) has closed" /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Channel closes /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func delegateOnError(to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { return self.delegateOn(ChannelEvent.error, to: owner, callback: callback) } /// Subscribes on channel events. Does not handle retain cycles. Use /// `delegateOn(_:, to:)` for automatic handling of retain cycles. /// /// Subscription returns a ref counter, which can be used later to /// unsubscribe the exact event listener /// /// Example: /// /// let channel = socket.channel("topic") /// let ref1 = channel.on("event") { [weak self] (message) in /// self?.print("do stuff") /// } /// let ref2 = channel.on("event") { [weak self] (message) in /// self?.print("do other stuff") /// } /// channel.off("event", ref1) /// /// Since unsubscription of ref1, "do stuff" won't print, but "do other /// stuff" will keep on printing on the "event" /// /// - parameter event: Event to receive /// - parameter callback: Called with the event's message /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func on(_ event: String, callback: @escaping ((Message) -> Void)) -> Int { var delegated = Delegated() delegated.manuallyDelegate(with: callback) return self.on(event, delegated: delegated) } /// Subscribes on channel events. Automatically handles retain cycles. Use /// `on()` to handle yourself. /// /// Subscription returns a ref counter, which can be used later to /// unsubscribe the exact event listener /// /// Example: /// /// let channel = socket.channel("topic") /// let ref1 = channel.delegateOn("event", to: self) { (self, message) in /// self?.print("do stuff") /// } /// let ref2 = channel.delegateOn("event", to: self) { (self, message) in /// self?.print("do other stuff") /// } /// channel.off("event", ref1) /// /// Since unsubscription of ref1, "do stuff" won't print, but "do other /// stuff" will keep on printing on the "event" /// /// - parameter event: Event to receive /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called with the event's message /// - return: Ref counter of the subscription. See `func off()` @discardableResult public func delegateOn(_ event: String, to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { var delegated = Delegated() delegated.delegate(to: owner, with: callback) return self.on(event, delegated: delegated) } /// Shared method between `on` and `manualOn` @discardableResult private func on(_ event: String, delegated: Delegated) -> Int { let ref = bindingRef self.bindingRef = ref + 1 self.syncBindingsDel.append(Binding(event: event, ref: ref, callback: delegated)) return ref } /// Unsubscribes from a channel event. If a `ref` is given, only the exact /// listener will be removed. Else all listeners for the `event` will be /// removed. /// /// Example: /// /// let channel = socket.channel("topic") /// let ref1 = channel.on("event") { _ in print("ref1 event" } /// let ref2 = channel.on("event") { _ in print("ref2 event" } /// let ref3 = channel.on("other_event") { _ in print("ref3 other" } /// let ref4 = channel.on("other_event") { _ in print("ref4 other" } /// channel.off("event", ref1) /// channel.off("other_event") /// /// After this, only "ref2 event" will be printed if the channel receives /// "event" and nothing is printed if the channel receives "other_event". /// /// - parameter event: Event to unsubscribe from /// - paramter ref: Ref counter returned when subscribing. Can be omitted public func off(_ event: String, ref: Int? = nil) { self.syncBindingsDel.removeAll { (bind) -> Bool in bind.event == event && (ref == nil || ref == bind.ref) } } /// Push a payload to the Channel /// /// Example: /// /// channel /// .push("event", payload: ["message": "hello") /// .receive("ok") { _ in { print("message sent") } /// /// - parameter event: Event to push /// - parameter payload: Payload to push /// - parameter timeout: Optional timeout @discardableResult public func push(_ event: String, payload: Payload, timeout: TimeInterval = Defaults.timeoutInterval) -> Push { guard joinedOnce else { fatalError("Tried to push \(event) to \(self.topic) before joining. Use channel.join() before pushing events") } let pushEvent = Push(channel: self, event: event, payload: payload, timeout: timeout) if canPush { pushEvent.send() } else { pushEvent.startTimeout() pushBuffer.append(pushEvent) } return pushEvent } /// Leaves the channel /// /// Unsubscribes from server events, and instructs channel to terminate on /// server /// /// Triggers onClose() hooks /// /// To receive leave acknowledgements, use the a `receive` /// hook to bind to the server ack, ie: /// /// Example: //// /// channel.leave().receive("ok") { _ in { print("left") } /// /// - parameter timeout: Optional timeout /// - return: Push that can add receive hooks @discardableResult public func leave(timeout: TimeInterval = Defaults.timeoutInterval) -> Push { // If attempting a rejoin during a leave, then reset, cancelling the rejoin self.rejoinTimer.reset() // Now set the state to leaving self.state = .leaving /// Delegated callback for a successful or a failed channel leave var onCloseDelegate = Delegated() onCloseDelegate.delegate(to: self) { (self, message) in self.socket?.logItems("channel", "leave \(self.topic)") // Triggers onClose() hooks self.trigger(event: ChannelEvent.close, payload: ["reason": "leave"]) } // Push event to send to the server let leavePush = Push(channel: self, event: ChannelEvent.leave, timeout: timeout) // Perform the same behavior if successfully left the channel // or if sending the event timed out leavePush .receive("ok", delegated: onCloseDelegate) .receive("timeout", delegated: onCloseDelegate) leavePush.send() // If the Channel cannot send push events, trigger a success locally if !canPush { leavePush.trigger("ok", payload: [:]) } // Return the push so it can be bound to return leavePush } /// Overridable message hook. Receives all events for specialized message /// handling before dispatching to the channel callbacks. /// /// - parameter event: The event the message was for /// - parameter payload: The payload for the message /// - parameter ref: The reference of the message /// - return: Must return the payload, modified or unmodified public func onMessage(callback: @escaping (Message) -> Message) { self.onMessage = callback } //---------------------------------------------------------------------- // MARK: - Internal //---------------------------------------------------------------------- /// Checks if an event received by the Socket belongs to this Channel func isMember(_ message: Message) -> Bool { // Return false if the message's topic does not match the Channel's topic guard message.topic == self.topic else { return false } guard let safeJoinRef = message.joinRef, safeJoinRef != self.joinRef, ChannelEvent.isLifecyleEvent(message.event) else { return true } self.socket?.logItems("channel", "dropping outdated message", message.topic, message.event, message.rawPayload, safeJoinRef) return false } /// Sends the payload to join the Channel func sendJoin(_ timeout: TimeInterval) { self.state = ChannelState.joining self.joinPush.resend(timeout) } /// Rejoins the channel func rejoin(_ timeout: TimeInterval? = nil) { // Do not attempt to rejoin if the channel is in the process of leaving guard !self.isLeaving else { return } // Leave potentially duplicate channels self.socket?.leaveOpenTopic(topic: self.topic) // Send the joinPush self.sendJoin(timeout ?? self.timeout) } /// Triggers an event to the correct event bindings created by /// `channel.on("event")`. /// /// - parameter message: Message to pass to the event bindings func trigger(_ message: Message) { let handledMessage = self.onMessage(message) self.syncBindingsDel.forEach { binding in if binding.event == message.event { binding.callback.call(handledMessage) } } } /// Triggers an event to the correct event bindings created by //// `channel.on("event")`. /// /// - parameter event: Event to trigger /// - parameter payload: Payload of the event /// - parameter ref: Ref of the event. Defaults to empty /// - parameter joinRef: Ref of the join event. Defaults to nil func trigger(event: String, payload: Payload = [:], ref: String = "", joinRef: String? = nil) { let message = Message(ref: ref, topic: self.topic, event: event, payload: payload, joinRef: joinRef ?? self.joinRef) self.trigger(message) } /// - parameter ref: The ref of the event push /// - return: The event name of the reply func replyEventName(_ ref: String) -> String { return "chan_reply_\(ref)" } /// The Ref send during the join message. var joinRef: String? { return self.joinPush.ref } /// - return: True if the Channel can push messages, meaning the socket /// is connected and the channel is joined var canPush: Bool { return self.socket?.isConnected == true && self.isJoined } } //---------------------------------------------------------------------- // MARK: - Public API //---------------------------------------------------------------------- extension Channel { /// - return: True if the Channel has been closed public var isClosed: Bool { return state == .closed } /// - return: True if the Channel experienced an error public var isErrored: Bool { return state == .errored } /// - return: True if the channel has joined public var isJoined: Bool { return state == .joined } /// - return: True if the channel has requested to join public var isJoining: Bool { return state == .joining } /// - return: True if the channel has requested to leave public var isLeaving: Bool { return state == .leaving } } ================================================ FILE: Sources/SwiftPhoenixClient/Defaults.swift ================================================ // Copyright (c) 2021 David Stump // // 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 collection of default values and behaviors used across the Client public class Defaults { /// Default timeout when sending messages public static let timeoutInterval: TimeInterval = 10.0 /// Default interval to send heartbeats on public static let heartbeatInterval: TimeInterval = 30.0 /// Default maximum amount of time which the system may delay heartbeat events in order to minimize power usage public static let heartbeatLeeway: DispatchTimeInterval = .milliseconds(10) /// Default reconnect algorithm for the socket public static let reconnectSteppedBackOff: (Int) -> TimeInterval = { tries in return tries > 9 ? 5.0 : [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.5, 1.0, 2.0][tries - 1] } /** Default rejoin algorithm for individual channels */ public static let rejoinSteppedBackOff: (Int) -> TimeInterval = { tries in return tries > 3 ? 10 : [1, 2, 5][tries - 1] } public static let vsn = "2.0.0" /// Default encode function, utilizing JSONSerialization.data public static let encode: (Any) -> Data = { json in return try! JSONSerialization .data(withJSONObject: json, options: JSONSerialization.WritingOptions()) } /// Default decode function, utilizing JSONSerialization.jsonObject public static let decode: (Data) -> Any? = { data in guard let json = try? JSONSerialization .jsonObject(with: data, options: JSONSerialization.ReadingOptions()) else { return nil } return json } public static let heartbeatQueue: DispatchQueue = DispatchQueue(label: "com.phoenix.socket.heartbeat") } /// Represents the multiple states that a Channel can be in /// throughout it's lifecycle. public enum ChannelState: String { case closed = "closed" case errored = "errored" case joined = "joined" case joining = "joining" case leaving = "leaving" } /// Represents the different events that can be sent through /// a channel regarding a Channel's lifecycle. public struct ChannelEvent { public static let heartbeat = "heartbeat" public static let join = "phx_join" public static let leave = "phx_leave" public static let reply = "phx_reply" public static let error = "phx_error" public static let close = "phx_close" static func isLifecyleEvent(_ event: String) -> Bool { switch event { case join, leave, reply, error, close: return true default: return false } } } ================================================ FILE: Sources/SwiftPhoenixClient/Delegated.swift ================================================ // Copyright (c) 2021 David Stump // // 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. /// Provides a memory-safe way of passing callbacks around while not creating /// retain cycles. This file was copied from https://github.com/dreymonde/Delegated /// instead of added as a dependency to reduce the number of packages that /// ship with SwiftPhoenixClient public struct Delegated { private(set) var callback: ((Input) -> Output?)? public init() { } public mutating func delegate(to target: Target, with callback: @escaping (Target, Input) -> Output) { self.callback = { [weak target] input in guard let target = target else { return nil } return callback(target, input) } } public func call(_ input: Input) -> Output? { return self.callback?(input) } public var isDelegateSet: Bool { return callback != nil } } extension Delegated { public mutating func stronglyDelegate(to target: Target, with callback: @escaping (Target, Input) -> Output) { self.callback = { input in return callback(target, input) } } public mutating func manuallyDelegate(with callback: @escaping (Input) -> Output) { self.callback = callback } public mutating func removeDelegate() { self.callback = nil } } extension Delegated where Input == Void { public mutating func delegate(to target: Target, with callback: @escaping (Target) -> Output) { self.delegate(to: target, with: { target, voidInput in callback(target) }) } public mutating func stronglyDelegate(to target: Target, with callback: @escaping (Target) -> Output) { self.stronglyDelegate(to: target, with: { target, voidInput in callback(target) }) } } extension Delegated where Input == Void { public func call() -> Output? { return self.call(()) } } extension Delegated where Output == Void { public func call(_ input: Input) { self.callback?(input) } } extension Delegated where Input == Void, Output == Void { public func call() { self.call(()) } } ================================================ FILE: Sources/SwiftPhoenixClient/HeartbeatTimer.swift ================================================ // Copyright (c) 2021 David Stump // // 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 /** Heartbeat Timer class which manages the lifecycle of the underlying timer which triggers when a heartbeat should be fired. This heartbeat runs on it's own Queue so that it does not interfere with the main queue but guarantees thread safety. */ class HeartbeatTimer { //---------------------------------------------------------------------- // MARK: - Dependencies //---------------------------------------------------------------------- // The interval to wait before firing the Timer let timeInterval: TimeInterval /// The maximum amount of time which the system may delay the delivery of the timer events let leeway: DispatchTimeInterval // The DispatchQueue to schedule the timers on let queue: DispatchQueue // UUID which specifies the Timer instance. Verifies that timers are different let uuid: String = UUID().uuidString //---------------------------------------------------------------------- // MARK: - Properties //---------------------------------------------------------------------- // The underlying, cancelable, resettable, timer. private var temporaryTimer: DispatchSourceTimer? = nil // The event handler that is called by the timer when it fires. private var temporaryEventHandler: (() -> Void)? = nil /** Create a new HeartbeatTimer - Parameters: - timeInterval: Interval to fire the timer. Repeats - queue: Queue to schedule the timer on - leeway: The maximum amount of time which the system may delay the delivery of the timer events */ init(timeInterval: TimeInterval, queue: DispatchQueue = Defaults.heartbeatQueue, leeway: DispatchTimeInterval = Defaults.heartbeatLeeway) { self.timeInterval = timeInterval self.queue = queue self.leeway = leeway } /** Create a new HeartbeatTimer - Parameter timeInterval: Interval to fire the timer. Repeats */ convenience init(timeInterval: TimeInterval) { self.init(timeInterval: timeInterval, queue: Defaults.heartbeatQueue) } func start(eventHandler: @escaping () -> Void) { queue.sync { [weak self] in guard let self = self else { return } // Create a new DispatchSourceTimer, passing the event handler let timer = DispatchSource.makeTimerSource(flags: [], queue: queue) timer.setEventHandler(handler: eventHandler) // Schedule the timer to first fire in `timeInterval` and then // repeat every `timeInterval` timer.schedule(deadline: DispatchTime.now() + self.timeInterval, repeating: self.timeInterval, leeway: self.leeway) // Start the timer timer.resume() self.temporaryEventHandler = eventHandler self.temporaryTimer = timer } } func stop() { // Must be queued synchronously to prevent threading issues. queue.sync { [weak self] in guard let self = self else { return } // DispatchSourceTimer will automatically cancel when released temporaryTimer = nil temporaryEventHandler = nil } } /** True if the Timer exists and has not been cancelled. False otherwise */ var isValid: Bool { guard let timer = self.temporaryTimer else { return false } return !timer.isCancelled } /** Calls the Timer's event handler immediately. This method is primarily used in tests (not ideal) */ func fire() { guard isValid else { return } self.temporaryEventHandler?() } } extension HeartbeatTimer: Equatable { static func == (lhs: HeartbeatTimer, rhs: HeartbeatTimer) -> Bool { return lhs.uuid == rhs.uuid } } ================================================ FILE: Sources/SwiftPhoenixClient/Message.swift ================================================ // Copyright (c) 2021 David Stump // // 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 /// Data that is received from the Server. public class Message { /// Reference number. Empty if missing public let ref: String /// Join Reference number internal let joinRef: String? /// Message topic public let topic: String /// Message event public let event: String /// The raw payload from the Message, including a nested response from /// phx_reply events. It is recommended to use `payload` instead. internal let rawPayload: Payload /// Message payload public var payload: Payload { guard let response = rawPayload["response"] as? Payload else { return rawPayload } return response } /// Convenience accessor. Equivalent to getting the status as such: /// ```swift /// message.payload["status"] /// ``` public var status: String? { return rawPayload["status"] as? String } init(ref: String = "", topic: String = "", event: String = "", payload: Payload = [:], joinRef: String? = nil) { self.ref = ref self.topic = topic self.event = event self.rawPayload = payload self.joinRef = joinRef } init?(json: [Any?]) { guard json.count > 4 else { return nil } self.joinRef = json[0] as? String self.ref = json[1] as? String ?? "" if let topic = json[2] as? String, let event = json[3] as? String, let payload = json[4] as? Payload { self.topic = topic self.event = event self.rawPayload = payload } else { return nil } } } ================================================ FILE: Sources/SwiftPhoenixClient/PhoenixTransport.swift ================================================ // Copyright (c) 2021 David Stump // // 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 //---------------------------------------------------------------------- // MARK: - Transport Protocol //---------------------------------------------------------------------- /** Defines a `Socket`'s Transport layer. */ // sourcery: AutoMockable public protocol PhoenixTransport { /// The current `ReadyState` of the `Transport` layer var readyState: PhoenixTransportReadyState { get } /// Delegate for the `Transport` layer var delegate: PhoenixTransportDelegate? { get set } /** Connect to the server - Parameters: - headers: Headers to include in the URLRequests when opening the Websocket connection. Can be empty [:] */ func connect(with headers: [String: Any]) /** Disconnect from the server. - Parameters: - code: Status code as defined by Section 7.4 of RFC 6455. - reason: Reason why the connection is closing. Optional. */ func disconnect(code: Int, reason: String?) /** Sends a message to the server. - Parameter data: Data to send. */ func send(data: Data) } //---------------------------------------------------------------------- // MARK: - Transport Delegate Protocol //---------------------------------------------------------------------- /** Delegate to receive notifications of events that occur in the `Transport` layer */ public protocol PhoenixTransportDelegate { /** Notified when the `Transport` opens. - Parameter response: Response from the server indicating that the WebSocket handshake was successful and the connection has been upgraded to webSockets */ func onOpen(response: URLResponse?) /** Notified when the `Transport` receives an error. - Parameter error: Client-side error from the underlying `Transport` implementation - Parameter response: Response from the server, if any, that occurred with the Error */ func onError(error: Error, response: URLResponse?) /** Notified when the `Transport` receives a message from the server. - Parameter message: Message received from the server */ func onMessage(message: String) /** Notified when the `Transport` closes. - Parameter code: Code that was sent when the `Transport` closed - Parameter reason: A concise human-readable prose explanation for the closure */ func onClose(code: Int, reason: String?) } //---------------------------------------------------------------------- // MARK: - Transport Ready State Enum //---------------------------------------------------------------------- /** Available `ReadyState`s of a `Transport` layer. */ public enum PhoenixTransportReadyState { /// The `Transport` is opening a connection to the server. case connecting /// The `Transport` is connected to the server. case open /// The `Transport` is closing the connection to the server. case closing /// The `Transport` has disconnected from the server. case closed } //---------------------------------------------------------------------- // MARK: - Default Websocket Transport Implementation //---------------------------------------------------------------------- /** A `Transport` implementation that relies on URLSession's native WebSocket implementation. This implementation ships default with SwiftPhoenixClient however SwiftPhoenixClient supports earlier OS versions using one of the submodule `Transport` implementations. Or you can create your own implementation using your own WebSocket library or implementation. */ @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketDelegate { /// The URL to connect to internal let url: URL /// The URLSession configuration internal let configuration: URLSessionConfiguration /// The underling URLSession. Assigned during `connect()` private var session: URLSession? = nil /// The ongoing task. Assigned during `connect()` private var task: URLSessionWebSocketTask? = nil /// Holds the current receive task private var receiveMessageTask: Task? { didSet { oldValue?.cancel() } } /** Initializes a `Transport` layer built using URLSession's WebSocket Example: ```swift let url = URL("wss://example.com/socket") let transport: Transport = URLSessionTransport(url: url) ``` Using a custom `URLSessionConfiguration` ```swift let url = URL("wss://example.com/socket") let configuration = URLSessionConfiguration.default let transport: Transport = URLSessionTransport(url: url, configuration: configuration) ``` - parameter url: URL to connect to - parameter configuration: Provide your own URLSessionConfiguration. Uses `.default` if none provided */ public init(url: URL, configuration: URLSessionConfiguration = .default) { // URLSession requires that the endpoint be "wss" instead of "https". let endpoint = url.absoluteString let wsEndpoint = endpoint .replacingOccurrences(of: "http://", with: "ws://") .replacingOccurrences(of: "https://", with: "wss://") // Force unwrapping should be safe here since a valid URL came in and we just // replaced the protocol. self.url = URL(string: wsEndpoint)! self.configuration = configuration super.init() } deinit { self.delegate = nil receiveMessageTask?.cancel() } // MARK: - Transport public var readyState: PhoenixTransportReadyState = .closed public var delegate: PhoenixTransportDelegate? = nil public func connect(with headers: [String : Any]) { // Set the transport state as connecting self.readyState = .connecting // Create the session and websocket task self.session = URLSession(configuration: self.configuration, delegate: self, delegateQueue: nil) var request = URLRequest(url: url) headers.forEach { (key: String, value: Any) in guard let value = value as? String else { return } request.addValue(value, forHTTPHeaderField: key) } self.task = self.session?.webSocketTask(with: request) // Start the task self.task?.resume() } open func disconnect(code: Int, reason: String?) { /* TODO: 1. Provide a "strict" mode that fails if an invalid close code is given 2. If strict mode is disabled, default to CloseCode.invalid 3. Provide default .normalClosure function */ guard let closeCode = URLSessionWebSocketTask.CloseCode.init(rawValue: code) else { fatalError("Could not create a CloseCode with invalid code: [\(code)].") } self.readyState = .closing self.task?.cancel(with: closeCode, reason: reason?.data(using: .utf8)) self.session?.finishTasksAndInvalidate() receiveMessageTask?.cancel() } open func send(data: Data) { self.task?.send(.string(String(data: data, encoding: .utf8)!)) { (error) in // TODO: What is the behavior when an error occurs? } } // MARK: - URLSessionWebSocketDelegate open func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { // The Websocket is connected. Set Transport state to open and inform delegate self.readyState = .open self.delegate?.onOpen(response: webSocketTask.response) // Start receiving messages self.receive() } open func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { // A close frame was received from the server. self.readyState = .closed self.delegate?.onClose(code: closeCode.rawValue, reason: reason.flatMap { String(data: $0, encoding: .utf8) }) } open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { // The task has terminated. Inform the delegate that the transport has closed abnormally // if this was caused by an error. guard let err = error else { return } self.abnormalErrorReceived(err, response: task.response) } // MARK: - Private private func receive() { receiveMessageTask = Task { [weak self] in guard let self else { return } do { let message = try await task?.receive() switch message { case .data: print("Data received. This method is unsupported by the Client") case .string(let text): delegate?.onMessage(message: text) default: fatalError("Nil message received.") } // Since `.receive()` is only good for a single message, it must // be called again after a message is received in order to // received the next message. receive() } catch { print("Error when receiving \(error)") abnormalErrorReceived(error, response: nil) } } } private func abnormalErrorReceived(_ error: Error, response: URLResponse?) { // Set the state of the Transport to closed self.readyState = .closed // Inform the Transport's delegate that an error occurred. self.delegate?.onError(error: error, response: response) // An abnormal error is results in an abnormal closure, such as internet getting dropped // so inform the delegate that the Transport has closed abnormally. This will kick off // the reconnect logic. self.delegate?.onClose(code: Socket.CloseCode.abnormal.rawValue, reason: error.localizedDescription) } } ================================================ FILE: Sources/SwiftPhoenixClient/Presence.swift ================================================ // Copyright (c) 2021 David Stump // // 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 /// The Presence object provides features for syncing presence information from /// the server with the client and handling presences joining and leaving. /// /// ## Syncing state from the server /// /// To sync presence state from the server, first instantiate an object and pass /// your channel in to track lifecycle events: /// /// let channel = socket.channel("some:topic") /// let presence = Presence(channel) /// /// If you have custom syncing state events, you can configure the `Presence` /// object to use those instead. /// /// let options = Options(events: [.state: "my_state", .diff: "my_diff"]) /// let presence = Presence(channel, opts: options) /// /// Next, use the presence.onSync callback to react to state changes from the /// server. For example, to render the list of users every time the list /// changes, you could write: /// /// presence.onSync { renderUsers(presence.list()) } /// /// ## Listing Presences /// /// presence.list is used to return a list of presence information based on the /// local state of metadata. By default, all presence metadata is returned, but /// a listBy function can be supplied to allow the client to select which /// metadata to use for a given presence. For example, you may have a user /// online from different devices with a metadata status of "online", but they /// have set themselves to "away" on another device. In this case, the app may /// choose to use the "away" status for what appears on the UI. The example /// below defines a listBy function which prioritizes the first metadata which /// was registered for each user. This could be the first tab they opened, or /// the first device they came online from: /// /// let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in /// let first = pres["metas"]!.first! /// first["count"] = pres["metas"]!.count /// first["id"] = id /// return first /// } /// let onlineUsers = presence.list(by: listBy) /// /// (NOTE: The underlying behavior is a `map` on the `presence.state`. You are /// mapping the `state` dictionary into whatever datastructure suites your needs) /// /// ## Handling individual presence join and leave events /// /// The presence.onJoin and presence.onLeave callbacks can be used to react to /// individual presences joining and leaving the app. For example: /// /// let presence = Presence(channel) /// presence.onJoin { [weak self] (key, current, newPres) in /// if let cur = current { /// print("user additional presence", cur) /// } else { /// print("user entered for the first time", newPres) /// } /// } /// /// presence.onLeave { [weak self] (key, current, leftPres) in /// if current["metas"]?.isEmpty == true { /// print("user has left from all devices", leftPres) /// } else { /// print("user left from a device", current) /// } /// } /// /// presence.onSync { renderUsers(presence.list()) } public final class Presence { //---------------------------------------------------------------------- // MARK: - Enums and Structs //---------------------------------------------------------------------- /// Custom options that can be provided when creating Presence /// /// ### Example: /// /// let options = Options(events: [.state: "my_state", .diff: "my_diff"]) /// let presence = Presence(channel, opts: options) public struct Options { let events: [Events: String] /// Default set of Options used when creating Presence. Uses the /// phoenix events "presence_state" and "presence_diff" static public let defaults = Options(events: [.state: "presence_state", .diff: "presence_diff"]) public init(events: [Events: String]) { self.events = events } } /// Presense Events public enum Events: String { case state = "state" case diff = "diff" } //---------------------------------------------------------------------- // MARK: - Typaliases //---------------------------------------------------------------------- /// Meta details of a Presence. Just a dictionary of properties public typealias Meta = [String: Any] /// A mapping of a String to an array of Metas. e.g. {"metas": [{id: 1}]} public typealias Map = [String: [Meta]] /// A mapping of a Presence state to a mapping of Metas public typealias State = [String: Map] // Diff has keys "joins" and "leaves", pointing to a Presence.State each // containing the users that joined and left. public typealias Diff = [String: State] /// Closure signature of OnJoin callbacks public typealias OnJoin = (_ key: String, _ current: Map?, _ new: Map) -> Void /// Closure signature for OnLeave callbacks public typealias OnLeave = (_ key: String, _ current: Map, _ left: Map) -> Void //// Closure signature for OnSync callbacks public typealias OnSync = () -> Void /// Collection of callbacks with default values struct Caller { var onJoin: OnJoin = {_,_,_ in } var onLeave: OnLeave = {_,_,_ in } var onSync: OnSync = {} } //---------------------------------------------------------------------- // MARK: - Properties //---------------------------------------------------------------------- /// The channel the Presence belongs to weak var channel: Channel? /// Caller to callback hooks var caller: Caller /// The state of the Presence private(set) public var state: State /// Pending `join` and `leave` diffs that need to be synced private(set) public var pendingDiffs: [Diff] /// The channel's joinRef, set when state events occur private(set) public var joinRef: String? public var isPendingSyncState: Bool { guard let safeJoinRef = self.joinRef else { return true } return safeJoinRef != self.channel?.joinRef } /// Callback to be informed of joins public var onJoin: OnJoin { get { return caller.onJoin } set { caller.onJoin = newValue } } /// Set the OnJoin callback public func onJoin(_ callback: @escaping OnJoin) { self.onJoin = callback } /// Callback to be informed of leaves public var onLeave: OnLeave { get { return caller.onLeave } set { caller.onLeave = newValue } } /// Set the OnLeave callback public func onLeave(_ callback: @escaping OnLeave) { self.onLeave = callback } /// Callback to be informed of synces public var onSync: OnSync { get { return caller.onSync } set { caller.onSync = newValue } } /// Set the OnSync callback public func onSync(_ callback: @escaping OnSync) { self.onSync = callback } public init(channel: Channel, opts: Options = Options.defaults) { self.state = [:] self.pendingDiffs = [] self.channel = channel self.joinRef = nil self.caller = Caller() guard // Do not subscribe to events if they were not provided let stateEvent = opts.events[.state], let diffEvent = opts.events[.diff] else { return } self.channel?.delegateOn(stateEvent, to: self) { (self, message) in guard let newState = message.rawPayload as? State else { return } self.joinRef = self.channel?.joinRef self.state = Presence.syncState(self.state, newState: newState, onJoin: self.caller.onJoin, onLeave: self.caller.onLeave) self.pendingDiffs.forEach({ (diff) in self.state = Presence.syncDiff(self.state, diff: diff, onJoin: self.caller.onJoin, onLeave: self.caller.onLeave) }) self.pendingDiffs = [] self.caller.onSync() } self.channel?.delegateOn(diffEvent, to: self) { (self, message) in guard let diff = message.rawPayload as? Diff else { return } if self.isPendingSyncState { self.pendingDiffs.append(diff) } else { self.state = Presence.syncDiff(self.state, diff: diff, onJoin: self.caller.onJoin, onLeave: self.caller.onLeave) self.caller.onSync() } } } /// Returns the array of presences, with deault selected metadata. public func list() -> [Map] { return self.list(by: { _, pres in pres }) } /// Returns the array of presences, with selected metadata public func list(by transformer: (String, Map) -> T) -> [T] { return Presence.listBy(self.state, transformer: transformer) } /// Filter the Presence state with a given function public func filter(by filter: ((String, Map) -> Bool)?) -> State { return Presence.filter(self.state, by: filter) } //---------------------------------------------------------------------- // MARK: - Static //---------------------------------------------------------------------- // Used to sync the list of presences on the server // with the client's state. An optional `onJoin` and `onLeave` callback can // be provided to react to changes in the client's local presences across // disconnects and reconnects with the server. // // - returns: Presence.State @discardableResult public static func syncState(_ currentState: State, newState: State, onJoin: OnJoin = {_,_,_ in }, onLeave: OnLeave = {_,_,_ in }) -> State { let state = currentState var leaves: Presence.State = [:] var joins: Presence.State = [:] state.forEach { (key, presence) in if newState[key] == nil { leaves[key] = presence } } newState.forEach { (key, newPresence) in if let currentPresence = state[key] { let newRefs = newPresence["metas"]!.map({ $0["phx_ref"] as! String }) let curRefs = currentPresence["metas"]!.map({ $0["phx_ref"] as! String }) let joinedMetas = newPresence["metas"]!.filter({ (meta: Meta) -> Bool in !curRefs.contains { $0 == meta["phx_ref"] as! String } }) let leftMetas = currentPresence["metas"]!.filter({ (meta: Meta) -> Bool in !newRefs.contains { $0 == meta["phx_ref"] as! String } }) if joinedMetas.count > 0 { joins[key] = newPresence joins[key]!["metas"] = joinedMetas } if leftMetas.count > 0 { leaves[key] = currentPresence leaves[key]!["metas"] = leftMetas } } else { joins[key] = newPresence } } return Presence.syncDiff(state, diff: ["joins": joins, "leaves": leaves], onJoin: onJoin, onLeave: onLeave) } // Used to sync a diff of presence join and leave // events from the server, as they happen. Like `syncState`, `syncDiff` // accepts optional `onJoin` and `onLeave` callbacks to react to a user // joining or leaving from a device. // // - returns: Presence.State @discardableResult public static func syncDiff(_ currentState: State, diff: Diff, onJoin: OnJoin = {_,_,_ in }, onLeave: OnLeave = {_,_,_ in }) -> State { var state = currentState diff["joins"]?.forEach { (key, newPresence) in let currentPresence = state[key] state[key] = newPresence if let curPresence = currentPresence { let joinedRefs = state[key]!["metas"]!.map({ $0["phx_ref"] as! String }) let curMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in !joinedRefs.contains { $0 == meta["phx_ref"] as! String } } state[key]!["metas"]!.insert(contentsOf: curMetas, at: 0) } onJoin(key, currentPresence, newPresence) } diff["leaves"]?.forEach({ (key, leftPresence) in guard var curPresence = state[key] else { return } let refsToRemove = leftPresence["metas"]!.map { $0["phx_ref"] as! String } let keepMetas = curPresence["metas"]!.filter { (meta: Meta) -> Bool in !refsToRemove.contains { $0 == meta["phx_ref"] as! String } } curPresence["metas"] = keepMetas onLeave(key, curPresence, leftPresence) if keepMetas.count > 0 { state[key]!["metas"] = keepMetas } else { state.removeValue(forKey: key) } }) return state } public static func filter(_ presences: State, by filter: ((String, Map) -> Bool)?) -> State { let safeFilter = filter ?? { key, pres in true } return presences.filter(safeFilter) } public static func listBy(_ presences: State, transformer: (String, Map) -> T) -> [T] { return presences.map(transformer) } } ================================================ FILE: Sources/SwiftPhoenixClient/Push.swift ================================================ // Copyright (c) 2021 David Stump // // 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 /// Represnts pushing data to a `Channel` through the `Socket` public class Push { /// The channel sending the Push public weak var channel: Channel? /// The event, for example `phx_join` public let event: String /// The payload, for example ["user_id": "abc123"] public var payload: Payload /// The push timeout. Default is 10.0 seconds public var timeout: TimeInterval /// The server's response to the Push var receivedMessage: Message? /// Timer which triggers a timeout event var timeoutTimer: TimerQueue /// WorkItem to be performed when the timeout timer fires var timeoutWorkItem: DispatchWorkItem? /// Hooks into a Push. Where .receive("ok", callback(Payload)) are stored var receiveHooks: [String: [Delegated]] /// True if the Push has been sent var sent: Bool /// The reference ID of the Push var ref: String? /// The event that is associated with the reference ID of the Push var refEvent: String? /// Initializes a Push /// /// - parameter channel: The Channel /// - parameter event: The event, for example ChannelEvent.join /// - parameter payload: Optional. The Payload to send, e.g. ["user_id": "abc123"] /// - parameter timeout: Optional. The push timeout. Default is 10.0s init(channel: Channel, event: String, payload: Payload = [:], timeout: TimeInterval = Defaults.timeoutInterval) { self.channel = channel self.event = event self.payload = payload self.timeout = timeout self.receivedMessage = nil self.timeoutTimer = TimerQueue.main self.receiveHooks = [:] self.sent = false self.ref = nil } /// Resets and sends the Push /// - parameter timeout: Optional. The push timeout. Default is 10.0s public func resend(_ timeout: TimeInterval = Defaults.timeoutInterval) { self.timeout = timeout self.reset() self.send() } /// Sends the Push. If it has already timed out, then the call will /// be ignored and return early. Use `resend` in this case. public func send() { guard !hasReceived(status: "timeout") else { return } self.startTimeout() self.sent = true self.channel?.socket?.push( topic: channel?.topic ?? "", event: self.event, payload: self.payload, ref: self.ref, joinRef: channel?.joinRef ) } /// Receive a specific event when sending an Outbound message. Subscribing /// to status events with this method does not guarantees no retain cycles. /// You should pass `weak self` in the capture list of the callback. You /// can call `.delegateReceive(status:, to:, callback:) and the library will /// handle it for you. /// /// Example: /// /// channel /// .send(event:"custom", payload: ["body": "example"]) /// .receive("error") { [weak self] payload in /// print("Error: ", payload) /// } /// /// - parameter status: Status to receive /// - parameter callback: Callback to fire when the status is recevied @discardableResult public func receive(_ status: String, callback: @escaping ((Message) -> ())) -> Push { var delegated = Delegated() delegated.manuallyDelegate(with: callback) return self.receive(status, delegated: delegated) } /// Receive a specific event when sending an Outbound message. Automatically /// prevents retain cycles. See `manualReceive(status:, callback:)` if you /// want to handle this yourself. /// /// Example: /// /// channel /// .send(event:"custom", payload: ["body": "example"]) /// .delegateReceive("error", to: self) { payload in /// print("Error: ", payload) /// } /// /// - parameter status: Status to receive /// - parameter owner: The class that is calling .receive. Usually `self` /// - parameter callback: Callback to fire when the status is recevied @discardableResult public func delegateReceive(_ status: String, to owner: Target, callback: @escaping ((Target, Message) -> ())) -> Push { var delegated = Delegated() delegated.delegate(to: owner, with: callback) return self.receive(status, delegated: delegated) } /// Shared behavior between `receive` calls @discardableResult internal func receive(_ status: String, delegated: Delegated) -> Push { // If the message has already been received, pass it to the callback immediately if hasReceived(status: status), let receivedMessage = self.receivedMessage { delegated.call(receivedMessage) } if receiveHooks[status] == nil { /// Create a new array of hooks if no previous hook is associated with status receiveHooks[status] = [delegated] } else { /// A previous hook for this status already exists. Just append the new hook receiveHooks[status]?.append(delegated) } return self } /// Resets the Push as it was after it was first tnitialized. internal func reset() { self.cancelRefEvent() self.ref = nil self.refEvent = nil self.receivedMessage = nil self.sent = false } /// Finds the receiveHook which needs to be informed of a status response /// /// - parameter status: Status which was received, e.g. "ok", "error", "timeout" /// - parameter response: Response that was received private func matchReceive(_ status: String, message: Message) { receiveHooks[status]?.forEach( { $0.call(message) } ) } /// Reverses the result on channel.on(ChannelEvent, callback) that spawned the Push private func cancelRefEvent() { guard let refEvent = self.refEvent else { return } self.channel?.off(refEvent) } /// Cancel any ongoing Timeout Timer internal func cancelTimeout() { self.timeoutWorkItem?.cancel() self.timeoutWorkItem = nil } /// Starts the Timer which will trigger a timeout after a specific _timeout_ /// time, in milliseconds, is reached. internal func startTimeout() { // Cancel any existing timeout before starting a new one if let safeWorkItem = timeoutWorkItem, !safeWorkItem.isCancelled { self.cancelTimeout() } guard let channel = channel, let socket = channel.socket else { return } let ref = socket.makeRef() let refEvent = channel.replyEventName(ref) self.ref = ref self.refEvent = refEvent /// If a response is received before the Timer triggers, cancel timer /// and match the recevied event to it's corresponding hook channel.delegateOn(refEvent, to: self) { (self, message) in self.cancelRefEvent() self.cancelTimeout() self.receivedMessage = message /// Check if there is event a status available guard let status = message.status else { return } self.matchReceive(status, message: message) } /// Setup and start the Timeout timer. let workItem = DispatchWorkItem { self.trigger("timeout", payload: [:]) } self.timeoutWorkItem = workItem self.timeoutTimer.queue(timeInterval: timeout, execute: workItem) } /// Checks if a status has already been received by the Push. /// /// - parameter status: Status to check /// - return: True if given status has been received by the Push. internal func hasReceived(status: String) -> Bool { return self.receivedMessage?.status == status } /// Triggers an event to be sent though the Channel internal func trigger(_ status: String, payload: Payload) { /// If there is no ref event, then there is nothing to trigger on the channel guard let refEvent = self.refEvent else { return } var mutPayload = payload mutPayload["status"] = status self.channel?.trigger(event: refEvent, payload: mutPayload) } } ================================================ FILE: Sources/SwiftPhoenixClient/Socket.swift ================================================ // Copyright (c) 2021 David Stump // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation public enum SocketError: Error { case abnormalClosureError } /// Alias for a JSON dictionary [String: Any] public typealias Payload = [String: Any] /// Alias for a function returning an optional JSON dictionary (`Payload?`) public typealias PayloadClosure = () -> Payload? /// Struct that gathers callbacks assigned to the Socket struct StateChangeCallbacks { let open: SynchronizedArray<(ref: String, callback: Delegated)> = .init() let close: SynchronizedArray<(ref: String, callback: Delegated<(Int, String?), Void>)> = .init() let error: SynchronizedArray<(ref: String, callback: Delegated<(Error, URLResponse?), Void>)> = .init() let message: SynchronizedArray<(ref: String, callback: Delegated)> = .init() } /// ## Socket Connection /// A single connection is established to the server and /// channels are multiplexed over the connection. /// Connect to the server using the `Socket` class: /// /// ```swift /// let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] }) /// socket.connect() /// ``` /// /// The `Socket` constructor takes the mount point of the socket, /// the authentication params, as well as options that can be found in /// the Socket docs, such as configuring the heartbeat. public class Socket: PhoenixTransportDelegate { //---------------------------------------------------------------------- // MARK: - Public Attributes //---------------------------------------------------------------------- /// The string WebSocket endpoint (ie `"ws://example.com/socket"`, /// `"wss://example.com"`, etc.) That was passed to the Socket during /// initialization. The URL endpoint will be modified by the Socket to /// include `"/websocket"` if missing. public let endPoint: String /// The fully qualified socket URL public private(set) var endPointUrl: URL /// Resolves to return the `paramsClosure` result at the time of calling. /// If the `Socket` was created with static params, then those will be /// returned every time. public var params: Payload? { return self.paramsClosure?() } /// The optional params closure used to get params when connecting. Must /// be set when initializing the Socket. public let paramsClosure: PayloadClosure? /// The WebSocket transport. Default behavior is to provide a /// URLSessionWebsocketTask. See README for alternatives. private let transport: ((URL) -> PhoenixTransport) /// Phoenix serializer version, defaults to "2.0.0" public let vsn: String /// Override to provide custom encoding of data before writing to the socket public var encode: (Any) -> Data = Defaults.encode /// Override to provide custom decoding of data read from the socket public var decode: (Data) -> Any? = Defaults.decode /// Timeout to use when opening connections public var timeout: TimeInterval = Defaults.timeoutInterval /// Custom headers to be added to the socket connection request public var headers: [String : Any] = [:] /// Interval between sending a heartbeat public var heartbeatInterval: TimeInterval = Defaults.heartbeatInterval /// The maximum amount of time which the system may delay heartbeats in order to optimize power usage public var heartbeatLeeway: DispatchTimeInterval = Defaults.heartbeatLeeway /// Interval between socket reconnect attempts, in seconds public var reconnectAfter: (Int) -> TimeInterval = Defaults.reconnectSteppedBackOff /// Interval between channel rejoin attempts, in seconds public var rejoinAfter: (Int) -> TimeInterval = Defaults.rejoinSteppedBackOff /// The optional function to receive logs public var logger: ((String) -> Void)? /// Disables heartbeats from being sent. Default is false. public var skipHeartbeat: Bool = false /// Enable/Disable SSL certificate validation. Default is false. This /// must be set before calling `socket.connect()` in order to be applied public var disableSSLCertValidation: Bool = false #if os(Linux) #else /// Configure custom SSL validation logic, eg. SSL pinning. This /// must be set before calling `socket.connect()` in order to apply. // public var security: SSLTrustValidator? /// Configure the encryption used by your client by setting the /// allowed cipher suites supported by your server. This must be /// set before calling `socket.connect()` in order to apply. public var enabledSSLCipherSuites: [SSLCipherSuite]? #endif //---------------------------------------------------------------------- // MARK: - Private Attributes //---------------------------------------------------------------------- /// Callbacks for socket state changes let stateChangeCallbacks: StateChangeCallbacks = StateChangeCallbacks() /// Collection on channels created for the Socket public var channels: [Channel] { _channels.copy() } private var _channels = SynchronizedArray() /// Buffers messages that need to be sent once the socket has connected. It is an array /// of tuples, with the ref of the message to send and the callback that will send the message. let sendBuffer = SynchronizedArray<(ref: String?, callback: () throws -> ())>() /// Ref counter for messages var ref: UInt64 = UInt64.min // 0 (max: 18,446,744,073,709,551,615) /// Timer that triggers sending new Heartbeat messages var heartbeatTimer: HeartbeatTimer? /// Ref counter for the last heartbeat that was sent var pendingHeartbeatRef: String? /// Timer to use when attempting to reconnect var reconnectTimer: TimeoutTimer /// Close status var closeStatus: CloseStatus = .unknown /// The connection to the server var connection: PhoenixTransport? = nil //---------------------------------------------------------------------- // MARK: - Initialization //---------------------------------------------------------------------- @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public convenience init(_ endPoint: String, params: Payload? = nil, vsn: String = Defaults.vsn) { self.init(endPoint: endPoint, transport: { url in return URLSessionTransport(url: url) }, paramsClosure: { params }, vsn: vsn) } @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public convenience init(_ endPoint: String, paramsClosure: PayloadClosure?, vsn: String = Defaults.vsn) { self.init(endPoint: endPoint, transport: { url in return URLSessionTransport(url: url) }, paramsClosure: paramsClosure, vsn: vsn) } public init(endPoint: String, transport: @escaping ((URL) -> PhoenixTransport), paramsClosure: PayloadClosure? = nil, vsn: String = Defaults.vsn) { self.transport = transport self.paramsClosure = paramsClosure self.endPoint = endPoint self.vsn = vsn self.endPointUrl = Socket.buildEndpointUrl(endpoint: endPoint, paramsClosure: paramsClosure, vsn: vsn) self.reconnectTimer = TimeoutTimer() self.reconnectTimer.callback.delegate(to: self) { (self) in self.logItems("Socket attempting to reconnect") self.teardown(reason: "reconnection") { self.connect() } } self.reconnectTimer.timerCalculation .delegate(to: self) { (self, tries) -> TimeInterval in let interval = self.reconnectAfter(tries) self.logItems("Socket reconnecting in \(interval)s") return interval } } deinit { reconnectTimer.reset() } //---------------------------------------------------------------------- // MARK: - Public //---------------------------------------------------------------------- /// - return: The socket protocol, wss or ws public var websocketProtocol: String { switch endPointUrl.scheme { case "https": return "wss" case "http": return "ws" default: return endPointUrl.scheme ?? "" } } /// - return: True if the socket is connected public var isConnected: Bool { return self.connectionState == .open } public var isConnecting: Bool { return self.connectionState == .connecting } /// - return: The state of the connect. [.connecting, .open, .closing, .closed] public var connectionState: PhoenixTransportReadyState { return self.connection?.readyState ?? .closed } /// Connects the Socket. The params passed to the Socket on initialization /// will be sent through the connection. If the Socket is already connected, /// then this call will be ignored. public func connect() { // Do not attempt to reconnect if the socket is currently connected or in the process of connecting guard !isConnected && !isConnecting else { return } // Reset the close status when attempting to connect self.closeStatus = .unknown // We need to build this right before attempting to connect as the // parameters could be built upon demand and change over time self.endPointUrl = Socket.buildEndpointUrl(endpoint: self.endPoint, paramsClosure: self.paramsClosure, vsn: vsn) self.connection = self.transport(self.endPointUrl) self.connection?.delegate = self // self.connection?.disableSSLCertValidation = disableSSLCertValidation // // #if os(Linux) // #else // self.connection?.security = security // self.connection?.enabledSSLCipherSuites = enabledSSLCipherSuites // #endif self.connection?.connect(with: self.headers) } /// Disconnects the socket /// /// - parameter code: Optional. Closing status code /// - parameter callback: Optional. Called when disconnected public func disconnect(code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil) { // The socket was closed cleanly by the User self.closeStatus = CloseStatus(closeCode: code.rawValue) // Reset any reconnects and teardown the socket connection self.reconnectTimer.reset() self.teardown(code: code, reason: reason, callback: callback) } internal func teardown(code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil) { self.connection?.delegate = nil self.connection?.disconnect(code: code.rawValue, reason: reason) self.connection = nil // The socket connection has been torndown, heartbeats are not needed self.heartbeatTimer?.stop() // Since the connection's delegate was nil'd out, inform all state // callbacks that the connection has closed self.stateChangeCallbacks.close.forEach({ $0.callback.call((code.rawValue, reason)) }) callback?() } //---------------------------------------------------------------------- // MARK: - Register Socket State Callbacks //---------------------------------------------------------------------- /// Registers callbacks for connection open events. Does not handle retain /// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles. /// /// Example: /// /// socket.onOpen() { [weak self] in /// self?.print("Socket Connection Open") /// } /// /// - parameter callback: Called when the Socket is opened @discardableResult public func onOpen(callback: @escaping () -> Void) -> String { return self.onOpen { _ in callback() } } /// Registers callbacks for connection open events. Does not handle retain /// cycles. Use `delegateOnOpen(to:)` for automatic handling of retain cycles. /// /// Example: /// /// socket.onOpen() { [weak self] response in /// self?.print("Socket Connection Open") /// } /// /// - parameter callback: Called when the Socket is opened @discardableResult public func onOpen(callback: @escaping (URLResponse?) -> Void) -> String { var delegated = Delegated() delegated.manuallyDelegate(with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.open) } /// Registers callbacks for connection open events. Automatically handles /// retain cycles. Use `onOpen()` to handle yourself. /// /// Example: /// /// socket.delegateOnOpen(to: self) { self in /// self.print("Socket Connection Open") /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket is opened @discardableResult public func delegateOnOpen(to owner: T, callback: @escaping ((T) -> Void)) -> String { return self.delegateOnOpen(to: owner) { owner, _ in callback(owner) } } /// Registers callbacks for connection open events. Automatically handles /// retain cycles. Use `onOpen()` to handle yourself. /// /// Example: /// /// socket.delegateOnOpen(to: self) { self, response in /// self.print("Socket Connection Open") /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket is opened @discardableResult public func delegateOnOpen(to owner: T, callback: @escaping ((T, URLResponse?) -> Void)) -> String { var delegated = Delegated() delegated.delegate(to: owner, with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.open) } /// Registers callbacks for connection close events. Does not handle retain /// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles. /// /// Example: /// /// socket.onClose() { [weak self] in /// self?.print("Socket Connection Close") /// } /// /// - parameter callback: Called when the Socket is closed @discardableResult public func onClose(callback: @escaping () -> Void) -> String { return self.onClose { _, _ in callback() } } /// Registers callbacks for connection close events. Does not handle retain /// cycles. Use `delegateOnClose(_:)` for automatic handling of retain cycles. /// /// Example: /// /// socket.onClose() { [weak self] code, reason in /// self?.print("Socket Connection Close") /// } /// /// - parameter callback: Called when the Socket is closed @discardableResult public func onClose(callback: @escaping (Int, String?) -> Void) -> String { var delegated = Delegated<(Int, String?), Void>() delegated.manuallyDelegate(with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.close) } /// Registers callbacks for connection close events. Automatically handles /// retain cycles. Use `onClose()` to handle yourself. /// /// Example: /// /// socket.delegateOnClose(self) { self in /// self.print("Socket Connection Close") /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket is closed @discardableResult public func delegateOnClose(to owner: T, callback: @escaping ((T) -> Void)) -> String { return self.delegateOnClose(to: owner) { owner, _ in callback(owner) } } /// Registers callbacks for connection close events. Automatically handles /// retain cycles. Use `onClose()` to handle yourself. /// /// Example: /// /// socket.delegateOnClose(self) { self, code, reason in /// self.print("Socket Connection Close") /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket is closed @discardableResult public func delegateOnClose(to owner: T, callback: @escaping ((T, (Int, String?)) -> Void)) -> String { var delegated = Delegated<(Int, String?), Void>() delegated.delegate(to: owner, with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.close) } /// Registers callbacks for connection error events. Does not handle retain /// cycles. Use `delegateOnError(to:)` for automatic handling of retain cycles. /// /// Example: /// /// socket.onError() { [weak self] (error) in /// self?.print("Socket Connection Error", error) /// } /// /// - parameter callback: Called when the Socket errors @discardableResult public func onError(callback: @escaping ((Error, URLResponse?)) -> Void) -> String { var delegated = Delegated<(Error, URLResponse?), Void>() delegated.manuallyDelegate(with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.error) } /// Registers callbacks for connection error events. Automatically handles /// retain cycles. Use `manualOnError()` to handle yourself. /// /// Example: /// /// socket.delegateOnError(to: self) { (self, error) in /// self.print("Socket Connection Error", error) /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket errors @discardableResult public func delegateOnError(to owner: T, callback: @escaping ((T, (Error, URLResponse?)) -> Void)) -> String { var delegated = Delegated<(Error, URLResponse?), Void>() delegated.delegate(to: owner, with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.error) } /// Registers callbacks for connection message events. Does not handle /// retain cycles. Use `delegateOnMessage(_to:)` for automatic handling of /// retain cycles. /// /// Example: /// /// socket.onMessage() { [weak self] (message) in /// self?.print("Socket Connection Message", message) /// } /// /// - parameter callback: Called when the Socket receives a message event @discardableResult public func onMessage(callback: @escaping (Message) -> Void) -> String { var delegated = Delegated() delegated.manuallyDelegate(with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.message) } /// Registers callbacks for connection message events. Automatically handles /// retain cycles. Use `onMessage()` to handle yourself. /// /// Example: /// /// socket.delegateOnMessage(self) { (self, message) in /// self.print("Socket Connection Message", message) /// } /// /// - parameter owner: Class registering the callback. Usually `self` /// - parameter callback: Called when the Socket receives a message event @discardableResult public func delegateOnMessage(to owner: T, callback: @escaping ((T, Message) -> Void)) -> String { var delegated = Delegated() delegated.delegate(to: owner, with: callback) return self.append(callback: delegated, to: self.stateChangeCallbacks.message) } private func append(callback: T, to array: SynchronizedArray<(ref: String, callback: T)>) -> String { let ref = makeRef() array.append((ref, callback)) return ref } /// Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should /// call this method when you are finished when the Socket in order to release /// any references held by the socket. public func releaseCallbacks() { self.stateChangeCallbacks.open.removeAll() self.stateChangeCallbacks.close.removeAll() self.stateChangeCallbacks.error.removeAll() self.stateChangeCallbacks.message.removeAll() } //---------------------------------------------------------------------- // MARK: - Channel Initialization //---------------------------------------------------------------------- /// Initialize a new Channel /// /// Example: /// /// let channel = socket.channel("rooms", params: ["user_id": "abc123"]) /// /// - parameter topic: Topic of the channel /// - parameter params: Optional. Parameters for the channel /// - return: A new channel public func channel(_ topic: String, params: [String: Any] = [:]) -> Channel { let channel = Channel(topic: topic, params: params, socket: self) _channels.append(channel) return channel } /// Removes the Channel from the socket. This does not cause the channel to /// inform the server that it is leaving. You should call channel.leave() /// prior to removing the Channel. /// /// Example: /// /// channel.leave() /// socket.remove(channel) /// /// - parameter channel: Channel to remove public func remove(_ channel: Channel) { self.off(channel.stateChangeRefs) _channels.removeAll(where: { $0.joinRef == channel.joinRef }) } /// Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations. /// /// /// - Parameter refs: List of refs returned by calls to `onOpen`, `onClose`, etc public func off(_ refs: [String]) { self.stateChangeCallbacks.open.removeAll { refs.contains($0.ref) } self.stateChangeCallbacks.close.removeAll { refs.contains($0.ref) } self.stateChangeCallbacks.error.removeAll { refs.contains($0.ref) } self.stateChangeCallbacks.message.removeAll { refs.contains($0.ref) } } //---------------------------------------------------------------------- // MARK: - Sending Data //---------------------------------------------------------------------- /// Sends data through the Socket. This method is internal. Instead, you /// should call `push(_:, payload:, timeout:)` on the Channel you are /// sending an event to. /// /// - parameter topic: /// - parameter event: /// - parameter payload: /// - parameter ref: Optional. Defaults to nil /// - parameter joinRef: Optional. Defaults to nil internal func push(topic: String, event: String, payload: Payload, ref: String? = nil, joinRef: String? = nil) { let callback: (() throws -> ()) = { [weak self] in guard let self else { return } let body: [Any?] = [joinRef, ref, topic, event, payload] let data = self.encode(body) self.logItems("push", "Sending \(String(data: data, encoding: String.Encoding.utf8) ?? "")" ) self.connection?.send(data: data) } /// If the socket is connected, then execute the callback immediately. if isConnected { try? callback() } else { /// If the socket is not connected, add the push to a buffer which will /// be sent immediately upon connection. self.sendBuffer.append((ref: ref, callback: callback)) } } /// - return: the next message ref, accounting for overflows public func makeRef() -> String { self.ref = (ref == UInt64.max) ? 0 : self.ref + 1 return String(ref) } /// Logs the message. Override Socket.logger for specialized logging. noops by default /// /// - parameter items: List of items to be logged. Behaves just like debugPrint() func logItems(_ items: Any...) { let msg = items.map( { return String(describing: $0) } ).joined(separator: ", ") self.logger?("SwiftPhoenixClient: \(msg)") } //---------------------------------------------------------------------- // MARK: - Connection Events //---------------------------------------------------------------------- /// Called when the underlying Websocket connects to it's host internal func onConnectionOpen(response: URLResponse?) { self.logItems("transport", "Connected to \(endPoint)") // Reset the close status now that the socket has been connected self.closeStatus = .unknown // Send any messages that were waiting for a connection self.flushSendBuffer() // Reset how the socket tried to reconnect self.reconnectTimer.reset() // Restart the heartbeat timer self.resetHeartbeat() // Inform all onOpen callbacks that the Socket has opened self.stateChangeCallbacks.open.forEach({ $0.callback.call((response)) }) } internal func onConnectionClosed(code: Int, reason: String?) { self.logItems("transport", "close") // Send an error to all channels self.triggerChannelError() // Prevent the heartbeat from triggering if the self.heartbeatTimer?.stop() // Only attempt to reconnect if the socket did not close normally, // or if it was closed abnormally but on client side (e.g. due to heartbeat timeout) if (self.closeStatus.shouldReconnect) { self.reconnectTimer.scheduleTimeout() } self.stateChangeCallbacks.close.forEach({ $0.callback.call((code, reason)) }) } internal func onConnectionError(_ error: Error, response: URLResponse?) { self.logItems("transport", error, response ?? "") // Send an error to all channels self.triggerChannelError() // Inform any state callbacks of the error self.stateChangeCallbacks.error.forEach({ $0.callback.call((error, response)) }) } internal func onConnectionMessage(_ rawMessage: String) { self.logItems("receive ", rawMessage) guard let data = rawMessage.data(using: String.Encoding.utf8), let json = decode(data) as? [Any?], let message = Message(json: json) else { self.logItems("receive: Unable to parse JSON: \(rawMessage)") return } // Clear heartbeat ref, preventing a heartbeat timeout disconnect if message.ref == pendingHeartbeatRef { pendingHeartbeatRef = nil } if message.event == "phx_close" { print("Close Event Received") } // Dispatch the message to all channels that belong to the topic _channels.forEach { channel in if channel.isMember(message) { channel.trigger(message) } } // Inform all onMessage callbacks of the message self.stateChangeCallbacks.message.forEach({ $0.callback.call(message) }) } /// Triggers an error event to all of the connected Channels internal func triggerChannelError() { _channels.forEach { channel in // Only trigger a channel error if it is in an "opened" state if !(channel.isErrored || channel.isLeaving || channel.isClosed) { channel.trigger(event: ChannelEvent.error) } } } /// Send all messages that were buffered before the socket opened internal func flushSendBuffer() { guard isConnected else { return } self.sendBuffer.forEach( { try? $0.callback() } ) self.sendBuffer.removeAll() } /// Removes an item from the sendBuffer with the matching ref internal func removeFromSendBuffer(ref: String) { self.sendBuffer.removeAll { $0.ref == ref } } /// Builds a fully qualified socket `URL` from `endPoint` and `params`. internal static func buildEndpointUrl(endpoint: String, paramsClosure params: PayloadClosure?, vsn: String) -> URL { guard let url = URL(string: endpoint), var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { fatalError("Malformed URL: \(endpoint)") } // Ensure that the URL ends with "/websocket if !urlComponents.path.contains("/websocket") { // Do not duplicate '/' in the path if urlComponents.path.last != "/" { urlComponents.path.append("/") } // append 'websocket' to the path urlComponents.path.append("websocket") } urlComponents.queryItems = [URLQueryItem(name: "vsn", value: vsn)] // If there are parameters, append them to the URL if let params = params?() { urlComponents.queryItems?.append(contentsOf: params.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) }) } guard let qualifiedUrl = urlComponents.url else { fatalError("Malformed URL while adding parameters") } return qualifiedUrl } // Leaves any channel that is open that has a duplicate topic internal func leaveOpenTopic(topic: String) { guard let dupe = _channels.first(where: { $0.topic == topic && ($0.isJoined || $0.isJoining) }) else { return } self.logItems("transport", "leaving duplicate topic: [\(topic)]" ) dupe.leave() } //---------------------------------------------------------------------- // MARK: - Heartbeat //---------------------------------------------------------------------- internal func resetHeartbeat() { // Clear anything related to the heartbeat self.pendingHeartbeatRef = nil self.heartbeatTimer?.stop() // Do not start up the heartbeat timer if skipHeartbeat is true guard !skipHeartbeat else { return } self.heartbeatTimer = HeartbeatTimer(timeInterval: heartbeatInterval, leeway: heartbeatLeeway) self.heartbeatTimer?.start(eventHandler: { [weak self] in self?.sendHeartbeat() }) } /// Sends a heartbeat payload to the phoenix servers @objc func sendHeartbeat() { // Do not send if the connection is closed guard isConnected else { return } // If there is a pending heartbeat ref, then the last heartbeat was // never acknowledged by the server. Close the connection and attempt // to reconnect. if let _ = self.pendingHeartbeatRef { self.pendingHeartbeatRef = nil self.logItems("transport", "heartbeat timeout. Attempting to re-establish connection") // Close the socket manually, flagging the closure as abnormal. Do not use // `teardown` or `disconnect` as they will nil out the websocket delegate. self.abnormalClose("heartbeat timeout") return } // The last heartbeat was acknowledged by the server. Send another one self.pendingHeartbeatRef = self.makeRef() self.push(topic: "phoenix", event: ChannelEvent.heartbeat, payload: [:], ref: self.pendingHeartbeatRef) } internal func abnormalClose(_ reason: String) { self.closeStatus = .abnormal /* We use NORMAL here since the client is the one determining to close the connection. However, we set to close status to abnormal so that the client knows that it should attempt to reconnect. If the server subsequently acknowledges with code 1000 (normal close), the socket will keep the `.abnormal` close status and trigger a reconnection. */ self.connection?.disconnect(code: CloseCode.normal.rawValue, reason: reason) } //---------------------------------------------------------------------- // MARK: - TransportDelegate //---------------------------------------------------------------------- public func onOpen(response: URLResponse?) { self.onConnectionOpen(response: response) } public func onError(error: Error, response: URLResponse?) { self.onConnectionError(error, response: response) } public func onMessage(message: String) { self.onConnectionMessage(message) } public func onClose(code: Int, reason: String? = nil) { self.closeStatus.update(transportCloseCode: code) self.onConnectionClosed(code: code, reason: reason) } } //---------------------------------------------------------------------- // MARK: - Close Codes //---------------------------------------------------------------------- extension Socket { public enum CloseCode : Int { case abnormal = 999 case normal = 1000 case goingAway = 1001 } } //---------------------------------------------------------------------- // MARK: - Close Status //---------------------------------------------------------------------- extension Socket { /// Indicates the different closure states a socket can be in. enum CloseStatus { /// Undetermined closure state case unknown /// A clean closure requested either by the client or the server case clean /// An abnormal closure requested by the client case abnormal /// Temporarily close the socket, pausing reconnect attempts. Useful on mobile /// clients when disconnecting a because the app resigned active but should /// reconnect when app enters active state. case temporary init(closeCode: Int) { switch closeCode { case CloseCode.abnormal.rawValue: self = .abnormal case CloseCode.goingAway.rawValue: self = .temporary default: self = .clean } } mutating func update(transportCloseCode: Int) { switch self { case .unknown, .clean, .temporary: // Allow transport layer to override these statuses. self = .init(closeCode: transportCloseCode) case .abnormal: // Do not allow transport layer to override the abnormal close status. // The socket itself should reset it on the next connection attempt. // See `Socket.abnormalClose(_:)` for more information. break } } var shouldReconnect: Bool { switch self { case .unknown, .abnormal: return true case .clean, .temporary: return false } } } } ================================================ FILE: Sources/SwiftPhoenixClient/SynchronizedArray.swift ================================================ // // SynchronizedArray.swift // SwiftPhoenixClient // // Created by Daniel Rees on 4/12/23. // Copyright © 2023 SwiftPhoenixClient. All rights reserved. // import Foundation /// A thread-safe array. public class SynchronizedArray { fileprivate let queue = DispatchQueue(label: "spc_sync_array", attributes: .concurrent) fileprivate var array: [Element] public init(_ array: [Element] = []) { self.array = array } public func copy() -> [Element] { queue.sync { self.array } } func append( _ newElement: Element) { queue.async(flags: .barrier) { self.array.append(newElement) } } func first(where predicate: (Element) -> Bool) -> Element? { queue.sync { self.array.first(where: predicate) } } func forEach(_ body: (Element) -> Void) { queue.sync { self.array }.forEach(body) } func removeAll() { queue.async(flags: .barrier) { self.array.removeAll() } } func removeAll(where shouldBeRemoved: @escaping (Element) -> Bool) { queue.async(flags: .barrier) { self.array.removeAll(where: shouldBeRemoved) } } } ================================================ FILE: Sources/SwiftPhoenixClient/TimeoutTimer.swift ================================================ // Copyright (c) 2021 David Stump // // 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. /// Creates a timer that can perform calculated reties by setting /// `timerCalculation` , such as exponential backoff. /// /// ### Example /// /// let reconnectTimer = TimeoutTimer() /// /// // Receive a callbcak when the timer is fired /// reconnectTimer.callback.delegate(to: self) { (_) in /// print("timer was fired") /// } /// /// // Provide timer interval calculation /// reconnectTimer.timerCalculation.delegate(to: self) { (_, tries) -> TimeInterval in /// return tries > 2 ? 1000 : [1000, 5000, 10000][tries - 1] /// } /// /// reconnectTimer.scheduleTimeout() // fires after 1000ms /// reconnectTimer.scheduleTimeout() // fires after 5000ms /// reconnectTimer.reset() /// reconnectTimer.scheduleTimeout() // fires after 1000ms import Foundation // sourcery: AutoMockable class TimeoutTimer { /// Callback to be informed when the underlying Timer fires var callback = Delegated<(), Void>() /// Provides TimeInterval to use when scheduling the timer var timerCalculation = Delegated() /// The work to be done when the queue fires var workItem: DispatchWorkItem? = nil /// The number of times the underlyingTimer hass been set off. var tries: Int = 0 /// The Queue to execute on. In testing, this is overridden var queue: TimerQueue = TimerQueue.main /// Resets the Timer, clearing the number of tries and stops /// any scheduled timeout. func reset() { self.tries = 0 self.clearTimer() } /// Schedules a timeout callback to fire after a calculated timeout duration. func scheduleTimeout() { // Clear any ongoing timer, not resetting the number of tries self.clearTimer() // Get the next calculated interval, in milliseconds. Do not // start the timer if the interval is returned as nil. guard let timeInterval = self.timerCalculation.call(self.tries + 1) else { return } let workItem = DispatchWorkItem { self.tries += 1 self.callback.call() } self.workItem = workItem self.queue.queue(timeInterval: timeInterval, execute: workItem) } /// Invalidates any ongoing Timer. Will not clear how many tries have been made private func clearTimer() { self.workItem?.cancel() self.workItem = nil } } /// Wrapper class around a DispatchQueue. Allows for providing a fake clock /// during tests. class TimerQueue { // Can be overriden in tests static var main = TimerQueue() func queue(timeInterval: TimeInterval, execute: DispatchWorkItem) { // TimeInterval is always in seconds. Multiply it by 1000 to convert // to milliseconds and round to the nearest millisecond. let dispatchInterval = Int(round(timeInterval * 1000)) let dispatchTime = DispatchTime.now() + .milliseconds(dispatchInterval) DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: execute) } } ================================================ FILE: SwiftPhoenixClient.podspec ================================================ # # Be sure to run `pod lib lint SwiftPhoenixClient.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = "SwiftPhoenixClient" s.version = "5.3.5" s.summary = "Connect your Phoenix and iOS applications through WebSockets!" s.swift_version = "5.0" s.description = <<-EOS SwiftPhoenixClient is a Swift port of phoenix.js, abstracting away the details of the Phoenix Channels library and providing a near identical experience to connect to your Phoenix WebSockets on iOS. RxSwift extensions exist as well when subscribing to channel events. A default Transport layer is implmenented for iOS 13 or later. If targeting an earlier iOS version, please see the StarscreamSwiftPhoenixClient extention. EOS s.homepage = "https://github.com/davidstump/SwiftPhoenixClient" s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "David Stump" => "david@davidstump.net" } s.source = { :git => "https://github.com/davidstump/SwiftPhoenixClient.git", :tag => s.version.to_s } s.ios.deployment_target = '11.0' s.osx.deployment_target = '10.13' s.tvos.deployment_target = '11.0' s.watchos.deployment_target = '4.0' s.swift_version = '5.0' s.source_files = "Sources/SwiftPhoenixClient/" end ================================================ FILE: SwiftPhoenixClient.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 125A9EF82543C84700292017 /* SwiftPhoenixClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */; }; 125A9F082543C8D000292017 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 125A9EFF2543C8D000292017 /* Assets.xcassets */; }; 125A9F092543C8D000292017 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 125A9F002543C8D000292017 /* LaunchScreen.storyboard */; }; 125A9F0A2543C8D000292017 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 125A9F022543C8D000292017 /* Main.storyboard */; }; 125A9F0B2543C8D000292017 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F042543C8D000292017 /* AppDelegate.swift */; }; 125A9F0D2543C8D000292017 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F062543C8D000292017 /* SceneDelegate.swift */; }; 125A9F0F2543CDDE00292017 /* BasicChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 125A9F0E2543CDDE00292017 /* BasicChatViewController.swift */; }; 1281439C25410D52008615A7 /* SwiftPhoenixClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */; }; 128143B225410DF3008615A7 /* SwiftPhoenixClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 128143B025410DF3008615A7 /* SwiftPhoenixClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12922E202543B10B0034B257 /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12922E1F2543B10B0034B257 /* Socket.swift */; }; 12922E222543B37B0034B257 /* PhoenixTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12922E212543B37B0034B257 /* PhoenixTransport.swift */; }; 12991DFB254C3EB800BB8650 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFA254C3EB800BB8650 /* Defaults.swift */; }; 12991DFD254C3ED300BB8650 /* Delegated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFC254C3ED300BB8650 /* Delegated.swift */; }; 12991DFF254C3EF900BB8650 /* HeartbeatTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */; }; 12991E01254C3F1300BB8650 /* TimeoutTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E00254C3F1300BB8650 /* TimeoutTimer.swift */; }; 12991E03254C3F2300BB8650 /* Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E02254C3F2300BB8650 /* Push.swift */; }; 12991E05254C3F3300BB8650 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E04254C3F3300BB8650 /* Channel.swift */; }; 12991E07254C3F4B00BB8650 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E06254C3F4B00BB8650 /* Message.swift */; }; 12991E09254C3F5B00BB8650 /* Presence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E08254C3F5B00BB8650 /* Presence.swift */; }; 12991E0F254C454C00BB8650 /* SocketSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E0E254C454C00BB8650 /* SocketSpy.swift */; }; 12991E11254C456F00BB8650 /* FakeTimerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E10254C456F00BB8650 /* FakeTimerQueue.swift */; }; 12991E13254C458100BB8650 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E12254C458100BB8650 /* TestHelpers.swift */; }; 12991E15254C45FC00BB8650 /* FakeTimerQueueSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */; }; 12EF620425524B6800A6EE9B /* SocketSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF61FF25524B6800A6EE9B /* SocketSpec.swift */; }; 12EF620525524B6800A6EE9B /* ChannelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620025524B6800A6EE9B /* ChannelSpec.swift */; }; 12EF620625524B6800A6EE9B /* PresenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620125524B6800A6EE9B /* PresenceSpec.swift */; }; 12EF620725524B6800A6EE9B /* TimeoutTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */; }; 12EF620825524B6800A6EE9B /* DefaultSerializerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */; }; 12EF620E25524EEF00A6EE9B /* MockableClass.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620C25524EEF00A6EE9B /* MockableClass.generated.swift */; }; 12EF620F25524EEF00A6EE9B /* MockableProtocol.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12EF620D25524EEF00A6EE9B /* MockableProtocol.generated.swift */; }; 635669C4261631DC0068B665 /* URLSessionTransportSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */; }; 63ACBE9426D53DF500171582 /* HeartbeatTimerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */; }; 63B526DE2656D53700289719 /* Nimble.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63B526DC2656D53700289719 /* Nimble.xcframework */; }; 63B526DF2656D53700289719 /* Quick.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63B526DD2656D53700289719 /* Quick.xcframework */; }; 63DE6CCA272A2ECB00E2A728 /* MessageSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */; }; 63F0F58D2592E44800C904FB /* ChatRoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0F58C2592E44800C904FB /* ChatRoomViewController.swift */; }; 63F3765329E7296F00A5AB6E /* SynchronizedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 125A9EFA2543C84700292017 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1281438925410D52008615A7 /* Project object */; proxyType = 1; remoteGlobalIDString = 1281439125410D52008615A7; remoteInfo = SwiftPhoenixClient; }; 1281439D25410D52008615A7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1281438925410D52008615A7 /* Project object */; proxyType = 1; remoteGlobalIDString = 1281439125410D52008615A7; remoteInfo = SwiftPhoenixClient; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 63B526B42656D0DE00289719 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 125A9EE42543C82800292017 /* Basic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Basic.app; sourceTree = BUILT_PRODUCTS_DIR; }; 125A9EFF2543C8D000292017 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 125A9F012543C8D000292017 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 125A9F032543C8D000292017 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 125A9F042543C8D000292017 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 125A9F052543C8D000292017 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 125A9F062543C8D000292017 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 125A9F0E2543CDDE00292017 /* BasicChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicChatViewController.swift; sourceTree = ""; }; 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftPhoenixClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1281439B25410D52008615A7 /* SwiftPhoenixClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftPhoenixClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 128143B025410DF3008615A7 /* SwiftPhoenixClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftPhoenixClient.h; sourceTree = ""; }; 128143B125410DF3008615A7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 12922E1F2543B10B0034B257 /* Socket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socket.swift; sourceTree = ""; }; 12922E212543B37B0034B257 /* PhoenixTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoenixTransport.swift; sourceTree = ""; }; 12991DFA254C3EB800BB8650 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 12991DFC254C3ED300BB8650 /* Delegated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegated.swift; sourceTree = ""; }; 12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatTimer.swift; sourceTree = ""; }; 12991E00254C3F1300BB8650 /* TimeoutTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeoutTimer.swift; sourceTree = ""; }; 12991E02254C3F2300BB8650 /* Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Push.swift; sourceTree = ""; }; 12991E04254C3F3300BB8650 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; 12991E06254C3F4B00BB8650 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 12991E08254C3F5B00BB8650 /* Presence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = ""; }; 12991E0E254C454C00BB8650 /* SocketSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSpy.swift; sourceTree = ""; }; 12991E10254C456F00BB8650 /* FakeTimerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTimerQueue.swift; sourceTree = ""; }; 12991E12254C458100BB8650 /* TestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = ""; }; 12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTimerQueueSpec.swift; sourceTree = ""; }; 12EF61FF25524B6800A6EE9B /* SocketSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketSpec.swift; sourceTree = ""; }; 12EF620025524B6800A6EE9B /* ChannelSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelSpec.swift; sourceTree = ""; }; 12EF620125524B6800A6EE9B /* PresenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceSpec.swift; sourceTree = ""; }; 12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeoutTimerSpec.swift; sourceTree = ""; }; 12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultSerializerSpec.swift; sourceTree = ""; }; 12EF620C25524EEF00A6EE9B /* MockableClass.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockableClass.generated.swift; sourceTree = ""; }; 12EF620D25524EEF00A6EE9B /* MockableProtocol.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockableProtocol.generated.swift; sourceTree = ""; }; 635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransportSpec.swift; sourceTree = ""; }; 63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatTimerSpec.swift; sourceTree = ""; }; 63B526842656CF9600289719 /* Starscream.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Starscream.xcframework; path = Carthage/Build/Starscream.xcframework; sourceTree = ""; }; 63B5268D2656CFFE00289719 /* RxSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RxSwift.xcframework; path = Carthage/Build/RxSwift.xcframework; sourceTree = ""; }; 63B526DC2656D53700289719 /* Nimble.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Nimble.xcframework; path = Carthage/Build/Nimble.xcframework; sourceTree = ""; }; 63B526DD2656D53700289719 /* Quick.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Quick.xcframework; path = Carthage/Build/Quick.xcframework; sourceTree = ""; }; 63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSpec.swift; sourceTree = ""; }; 63F0F58C2592E44800C904FB /* ChatRoomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRoomViewController.swift; sourceTree = ""; }; 63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedArray.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 125A9EE12543C82800292017 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 125A9EF82543C84700292017 /* SwiftPhoenixClient.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 1281438F25410D52008615A7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 1281439825410D52008615A7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 63B526DE2656D53700289719 /* Nimble.xcframework in Frameworks */, 63B526DF2656D53700289719 /* Quick.xcframework in Frameworks */, 1281439C25410D52008615A7 /* SwiftPhoenixClient.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 125A9EFD2543C8D000292017 /* Basic */ = { isa = PBXGroup; children = ( 63F0F5922592E59800C904FB /* chatroom */, 63F0F5912592E58800C904FB /* basic */, 125A9EFF2543C8D000292017 /* Assets.xcassets */, 125A9F002543C8D000292017 /* LaunchScreen.storyboard */, 125A9F022543C8D000292017 /* Main.storyboard */, 125A9F042543C8D000292017 /* AppDelegate.swift */, 125A9F052543C8D000292017 /* Info.plist */, 125A9F062543C8D000292017 /* SceneDelegate.swift */, ); path = Basic; sourceTree = ""; }; 1281438825410D52008615A7 = { isa = PBXGroup; children = ( 128143AC25410DF3008615A7 /* Sources */, 128143B425410E01008615A7 /* Tests */, 128143BB25410E13008615A7 /* Examples */, 128143D82541142F008615A7 /* Frameworks */, 1281439325410D52008615A7 /* Products */, ); sourceTree = ""; }; 1281439325410D52008615A7 /* Products */ = { isa = PBXGroup; children = ( 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */, 1281439B25410D52008615A7 /* SwiftPhoenixClientTests.xctest */, 125A9EE42543C82800292017 /* Basic.app */, ); name = Products; sourceTree = ""; }; 128143AC25410DF3008615A7 /* Sources */ = { isa = PBXGroup; children = ( 128143AE25410DF3008615A7 /* SwiftPhoenixClient */, 128143AF25410DF3008615A7 /* Supporting Files */, ); path = Sources; sourceTree = ""; }; 128143AE25410DF3008615A7 /* SwiftPhoenixClient */ = { isa = PBXGroup; children = ( 12991E04254C3F3300BB8650 /* Channel.swift */, 12991DFA254C3EB800BB8650 /* Defaults.swift */, 12991DFC254C3ED300BB8650 /* Delegated.swift */, 12991DFE254C3EF900BB8650 /* HeartbeatTimer.swift */, 12991E06254C3F4B00BB8650 /* Message.swift */, 12991E08254C3F5B00BB8650 /* Presence.swift */, 12991E02254C3F2300BB8650 /* Push.swift */, 12922E1F2543B10B0034B257 /* Socket.swift */, 12991E00254C3F1300BB8650 /* TimeoutTimer.swift */, 12922E212543B37B0034B257 /* PhoenixTransport.swift */, 63F3765229E7296F00A5AB6E /* SynchronizedArray.swift */, ); path = SwiftPhoenixClient; sourceTree = ""; }; 128143AF25410DF3008615A7 /* Supporting Files */ = { isa = PBXGroup; children = ( 128143B025410DF3008615A7 /* SwiftPhoenixClient.h */, 128143B125410DF3008615A7 /* Info.plist */, ); path = "Supporting Files"; sourceTree = ""; }; 128143B425410E01008615A7 /* Tests */ = { isa = PBXGroup; children = ( 12991E0C254C450A00BB8650 /* Fakes */, 12991E0D254C451700BB8650 /* Helpers */, 12EF620925524DBF00A6EE9B /* Mocks */, 12EF61FE25524B6800A6EE9B /* SwiftPhoenixClientTests */, ); path = Tests; sourceTree = ""; }; 128143BB25410E13008615A7 /* Examples */ = { isa = PBXGroup; children = ( 125A9EFD2543C8D000292017 /* Basic */, ); path = Examples; sourceTree = ""; }; 128143D82541142F008615A7 /* Frameworks */ = { isa = PBXGroup; children = ( 63B526DC2656D53700289719 /* Nimble.xcframework */, 63B526DD2656D53700289719 /* Quick.xcframework */, 63B5268D2656CFFE00289719 /* RxSwift.xcframework */, 63B526842656CF9600289719 /* Starscream.xcframework */, ); name = Frameworks; sourceTree = ""; }; 12991E0C254C450A00BB8650 /* Fakes */ = { isa = PBXGroup; children = ( 12991E0E254C454C00BB8650 /* SocketSpy.swift */, 12991E10254C456F00BB8650 /* FakeTimerQueue.swift */, 12991E14254C45FC00BB8650 /* FakeTimerQueueSpec.swift */, ); path = Fakes; sourceTree = ""; }; 12991E0D254C451700BB8650 /* Helpers */ = { isa = PBXGroup; children = ( 12991E12254C458100BB8650 /* TestHelpers.swift */, ); path = Helpers; sourceTree = ""; }; 12EF61FE25524B6800A6EE9B /* SwiftPhoenixClientTests */ = { isa = PBXGroup; children = ( 12EF61FF25524B6800A6EE9B /* SocketSpec.swift */, 12EF620025524B6800A6EE9B /* ChannelSpec.swift */, 12EF620125524B6800A6EE9B /* PresenceSpec.swift */, 12EF620225524B6800A6EE9B /* TimeoutTimerSpec.swift */, 12EF620325524B6800A6EE9B /* DefaultSerializerSpec.swift */, 635669C3261631DC0068B665 /* URLSessionTransportSpec.swift */, 63ACBE9326D53DF500171582 /* HeartbeatTimerSpec.swift */, 63DE6CC9272A2ECB00E2A728 /* MessageSpec.swift */, ); path = SwiftPhoenixClientTests; sourceTree = ""; }; 12EF620925524DBF00A6EE9B /* Mocks */ = { isa = PBXGroup; children = ( 12EF620C25524EEF00A6EE9B /* MockableClass.generated.swift */, 12EF620D25524EEF00A6EE9B /* MockableProtocol.generated.swift */, ); path = Mocks; sourceTree = ""; }; 63F0F5912592E58800C904FB /* basic */ = { isa = PBXGroup; children = ( 125A9F0E2543CDDE00292017 /* BasicChatViewController.swift */, ); path = basic; sourceTree = ""; }; 63F0F5922592E59800C904FB /* chatroom */ = { isa = PBXGroup; children = ( 63F0F58C2592E44800C904FB /* ChatRoomViewController.swift */, ); path = chatroom; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 1281438D25410D52008615A7 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 128143B225410DF3008615A7 /* SwiftPhoenixClient.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 125A9EE32543C82800292017 /* Basic */ = { isa = PBXNativeTarget; buildConfigurationList = 125A9EF52543C82900292017 /* Build configuration list for PBXNativeTarget "Basic" */; buildPhases = ( 125A9EE02543C82800292017 /* Sources */, 125A9EE12543C82800292017 /* Frameworks */, 125A9EE22543C82800292017 /* Resources */, 63B526B42656D0DE00289719 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( 125A9EFB2543C84700292017 /* PBXTargetDependency */, ); name = Basic; productName = Basic; productReference = 125A9EE42543C82800292017 /* Basic.app */; productType = "com.apple.product-type.application"; }; 1281439125410D52008615A7 /* SwiftPhoenixClient */ = { isa = PBXNativeTarget; buildConfigurationList = 128143A625410D52008615A7 /* Build configuration list for PBXNativeTarget "SwiftPhoenixClient" */; buildPhases = ( 1281438D25410D52008615A7 /* Headers */, 1281438E25410D52008615A7 /* Sources */, 1281438F25410D52008615A7 /* Frameworks */, 1281439025410D52008615A7 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SwiftPhoenixClient; productName = SwiftPhoenixClient; productReference = 1281439225410D52008615A7 /* SwiftPhoenixClient.framework */; productType = "com.apple.product-type.framework"; }; 1281439A25410D52008615A7 /* SwiftPhoenixClientTests */ = { isa = PBXNativeTarget; buildConfigurationList = 128143A925410D52008615A7 /* Build configuration list for PBXNativeTarget "SwiftPhoenixClientTests" */; buildPhases = ( 1281439725410D52008615A7 /* Sources */, 1281439825410D52008615A7 /* Frameworks */, 1281439925410D52008615A7 /* Resources */, ); buildRules = ( ); dependencies = ( 1281439E25410D52008615A7 /* PBXTargetDependency */, ); name = SwiftPhoenixClientTests; productName = SwiftPhoenixClientTests; productReference = 1281439B25410D52008615A7 /* SwiftPhoenixClientTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 1281438925410D52008615A7 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1170; LastUpgradeCheck = 1420; ORGANIZATIONNAME = SwiftPhoenixClient; TargetAttributes = { 125A9EE32543C82800292017 = { CreatedOnToolsVersion = 11.7; LastSwiftMigration = 1230; }; 1281439125410D52008615A7 = { CreatedOnToolsVersion = 11.7; }; 1281439A25410D52008615A7 = { CreatedOnToolsVersion = 11.7; }; }; }; buildConfigurationList = 1281438C25410D52008615A7 /* Build configuration list for PBXProject "SwiftPhoenixClient" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 1281438825410D52008615A7; productRefGroup = 1281439325410D52008615A7 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 1281439125410D52008615A7 /* SwiftPhoenixClient */, 1281439A25410D52008615A7 /* SwiftPhoenixClientTests */, 125A9EE32543C82800292017 /* Basic */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 125A9EE22543C82800292017 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 125A9F082543C8D000292017 /* Assets.xcassets in Resources */, 125A9F0A2543C8D000292017 /* Main.storyboard in Resources */, 125A9F092543C8D000292017 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 1281439025410D52008615A7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 1281439925410D52008615A7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 125A9EE02543C82800292017 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 125A9F0B2543C8D000292017 /* AppDelegate.swift in Sources */, 125A9F0F2543CDDE00292017 /* BasicChatViewController.swift in Sources */, 63F0F58D2592E44800C904FB /* ChatRoomViewController.swift in Sources */, 125A9F0D2543C8D000292017 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 1281438E25410D52008615A7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 12991DFB254C3EB800BB8650 /* Defaults.swift in Sources */, 12991E05254C3F3300BB8650 /* Channel.swift in Sources */, 63F3765329E7296F00A5AB6E /* SynchronizedArray.swift in Sources */, 12991E09254C3F5B00BB8650 /* Presence.swift in Sources */, 12991E01254C3F1300BB8650 /* TimeoutTimer.swift in Sources */, 12991E07254C3F4B00BB8650 /* Message.swift in Sources */, 12991DFD254C3ED300BB8650 /* Delegated.swift in Sources */, 12991DFF254C3EF900BB8650 /* HeartbeatTimer.swift in Sources */, 12991E03254C3F2300BB8650 /* Push.swift in Sources */, 12922E202543B10B0034B257 /* Socket.swift in Sources */, 12922E222543B37B0034B257 /* PhoenixTransport.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 1281439725410D52008615A7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 12EF620825524B6800A6EE9B /* DefaultSerializerSpec.swift in Sources */, 63ACBE9426D53DF500171582 /* HeartbeatTimerSpec.swift in Sources */, 12991E11254C456F00BB8650 /* FakeTimerQueue.swift in Sources */, 12991E15254C45FC00BB8650 /* FakeTimerQueueSpec.swift in Sources */, 635669C4261631DC0068B665 /* URLSessionTransportSpec.swift in Sources */, 12EF620725524B6800A6EE9B /* TimeoutTimerSpec.swift in Sources */, 63DE6CCA272A2ECB00E2A728 /* MessageSpec.swift in Sources */, 12EF620F25524EEF00A6EE9B /* MockableProtocol.generated.swift in Sources */, 12991E0F254C454C00BB8650 /* SocketSpy.swift in Sources */, 12991E13254C458100BB8650 /* TestHelpers.swift in Sources */, 12EF620525524B6800A6EE9B /* ChannelSpec.swift in Sources */, 12EF620E25524EEF00A6EE9B /* MockableClass.generated.swift in Sources */, 12EF620625524B6800A6EE9B /* PresenceSpec.swift in Sources */, 12EF620425524B6800A6EE9B /* SocketSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 125A9EFB2543C84700292017 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 1281439125410D52008615A7 /* SwiftPhoenixClient */; targetProxy = 125A9EFA2543C84700292017 /* PBXContainerItemProxy */; }; 1281439E25410D52008615A7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 1281439125410D52008615A7 /* SwiftPhoenixClient */; targetProxy = 1281439D25410D52008615A7 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 125A9F002543C8D000292017 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 125A9F012543C8D000292017 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; 125A9F022543C8D000292017 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 125A9F032543C8D000292017 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 125A9EF62543C82900292017 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = Examples/Basic/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClient.Basic; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 125A9EF72543C82900292017 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = Examples/Basic/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClient.Basic; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 128143A425410D52008615A7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = 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; CLANG_WARN_INT_CONVERSION = YES; 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 = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/Carthage/Build/"; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 128143A525410D52008615A7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = 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; CLANG_WARN_INT_CONVERSION = YES; 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 = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/Carthage/Build/"; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 128143A725410D52008615A7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClient; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 11.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; 128143A825410D52008615A7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClient; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 11.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; 128143AA25410D52008615A7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; "FRAMEWORK_SEARCH_PATHS[sdk=appletvos*]" = ( "$(SRCROOT)/Carthage/Build/tvOS/", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( "$(SRCROOT)/Carthage/Build/Nimble.xcframework/ios-arm64_armv7", "$(SRCROOT)/Carthage/Build/Quick.xcframework/ios-arm64_armv7", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( "$(SRCROOT)/Carthage/Build/Nimble.xcframework/ios-arm64_i386_x86_64-simulator", "$(SRCROOT)/Carthage/Build/Quick.xcframework/ios-arm64_i386_x86_64-simulator", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=macosx*]" = ( "$(SRCROOT)/Carthage/Build/Mac/", "$(inherited)", ); INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting Files/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClientTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; 128143AB25410D52008615A7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; "FRAMEWORK_SEARCH_PATHS[sdk=appletv*]" = ( "$(SRCROOT)/Carthage/Build/tvOS/", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=iphone*]" = ( "$(SRCROOT)/Carthage/Build/iOS/", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=macosx*]" = ( "$(SRCROOT)/Carthage/Build/Mac/", "$(inherited)", ); "FRAMEWORK_SEARCH_PATHS[sdk=watch*]" = ( "$(SRCROOT)/Carthage/Build/watchOS/", "$(inherited)", ); INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting Files/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.github.SwiftPhoenixClientTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 125A9EF52543C82900292017 /* Build configuration list for PBXNativeTarget "Basic" */ = { isa = XCConfigurationList; buildConfigurations = ( 125A9EF62543C82900292017 /* Debug */, 125A9EF72543C82900292017 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1281438C25410D52008615A7 /* Build configuration list for PBXProject "SwiftPhoenixClient" */ = { isa = XCConfigurationList; buildConfigurations = ( 128143A425410D52008615A7 /* Debug */, 128143A525410D52008615A7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 128143A625410D52008615A7 /* Build configuration list for PBXNativeTarget "SwiftPhoenixClient" */ = { isa = XCConfigurationList; buildConfigurations = ( 128143A725410D52008615A7 /* Debug */, 128143A825410D52008615A7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 128143A925410D52008615A7 /* Build configuration list for PBXNativeTarget "SwiftPhoenixClientTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 128143AA25410D52008615A7 /* Debug */, 128143AB25410D52008615A7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 1281438925410D52008615A7 /* Project object */; } ================================================ FILE: SwiftPhoenixClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: SwiftPhoenixClient.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/Basic.xcscheme ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/RxSwiftPhoenixClient.xcscheme ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/StarscreamSwiftPhoenixClient.xcscheme ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/SwiftPhoenixClient.xcscheme ================================================ ================================================ FILE: SwiftPhoenixClient.xcodeproj/xcshareddata/xcschemes/SwiftPhoenixClientTests.xcscheme ================================================ ================================================ FILE: Tests/Fakes/FakeTimerQueue.swift ================================================ // Copyright (c) 2021 David Stump // // 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 Foundation @testable import SwiftPhoenixClient /** Provides a fake TimerQueue that allows tests to manipulate the clock without actually waiting on a real Timer. */ class FakeTimerQueue: TimerQueue { var tickTime: TimeInterval = 0.0 var workItems: [(deadline: TimeInterval, workItem: DispatchWorkItem)] = [] func reset() { self.tickTime = 0.0 self.workItems = [] } func tick(_ timeInterval: TimeInterval) { // calculate what time to advance to let advanceTo = self.tickTime + timeInterval // Filter all work items that are due to be fired and have not been // cancelled. Return early if there are no items to fire var pastDue = workItems .filter({ $0.deadline <= advanceTo && !$0.workItem.isCancelled }) // Keep looping until there are no more work items that are passed the // advance to time while !pastDue.isEmpty { // Perform all work items that are due pastDue.forEach({ self.tickTime = $0.deadline $0.workItem.perform() }) // Remove all work items that are past due or canceled workItems.removeAll(where: { $0.deadline <= self.tickTime || $0.workItem.isCancelled }) pastDue = workItems .filter({ $0.deadline <= advanceTo && !$0.workItem.isCancelled }) } // Now that all work has been performed, advance the clock self.tickTime = advanceTo } override func queue(timeInterval: TimeInterval, execute: DispatchWorkItem) { let deadline = tickTime + timeInterval self.workItems.append((deadline, execute)) } // Helper for writing tests func queue(timeInterval: TimeInterval, execute work: @escaping () -> Void) { self.queue(timeInterval: timeInterval, execute: DispatchWorkItem(block: work)) } } ================================================ FILE: Tests/Fakes/FakeTimerQueueSpec.swift ================================================ // Copyright (c) 2021 David Stump // // 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 Quick import Nimble @testable import SwiftPhoenixClient /// Tests the FakeTimerQueue that is used in all other tests to verify that /// the fake timer is behaving as expected in all tests to prevent false /// negatives or positives when writing tests class FakeTimerQueueSpec: QuickSpec { override func spec() { var queue: FakeTimerQueue! beforeEach { queue = FakeTimerQueue() } afterEach { queue.reset() } describe("reset") { it("resets the queue", closure: { var task100msCalled = false var task200msCalled = false var task300msCalled = false queue.queue(timeInterval: 0.1, execute: { task100msCalled = true }) queue.queue(timeInterval: 0.2, execute: { task200msCalled = true }) queue.queue(timeInterval: 0.3, execute: { task300msCalled = true }) queue.tick(0.250) expect(queue.tickTime).to(equal(0.250)) expect(queue.workItems).to(haveCount(1)) expect(task100msCalled).to(beTrue()) expect(task200msCalled).to(beTrue()) expect(task300msCalled).to(beFalse()) queue.reset() expect(queue.tickTime).to(equal(0)) expect(queue.workItems).to(beEmpty()) }) } describe("triggers") { it("triggers work that is passed due", closure: { var task100msCalled = false var task200msCalled = false var task300msCalled = false queue.queue(timeInterval: 0.1, execute: { task100msCalled = true }) queue.queue(timeInterval: 0.2, execute: { task200msCalled = true }) queue.queue(timeInterval: 0.3, execute: { task300msCalled = true }) queue.tick(0.100) expect(queue.tickTime).to(equal(0.100)) expect(task100msCalled).to(beTrue()) queue.tick(0.100) expect(queue.tickTime).to(equal(0.200)) expect(task200msCalled).to(beTrue()) queue.tick(0.050) expect(queue.tickTime).to(equal(0.250)) expect(task300msCalled).to(beFalse()) }) it("triggers all work that is passed due", closure: { var task100msCalled = false var task200msCalled = false var task300msCalled = false queue.queue(timeInterval: 0.1, execute: { task100msCalled = true }) queue.queue(timeInterval: 0.2, execute: { task200msCalled = true }) queue.queue(timeInterval: 0.3, execute: { task300msCalled = true }) queue.tick(0.250) expect(queue.tickTime).to(equal(0.250)) expect(queue.workItems).to(haveCount(1)) expect(task100msCalled).to(beTrue()) expect(task200msCalled).to(beTrue()) expect(task300msCalled).to(beFalse()) }) it("triggers work that is scheduled for a time that is after tick", closure: { var task100msCalled = false var task200msCalled = false var task300msCalled = false queue.queue(timeInterval: 0.1, execute: { task100msCalled = true queue.queue(timeInterval: 0.1, execute: { task200msCalled = true }) }) queue.queue(timeInterval: 0.3, execute: { task300msCalled = true }) queue.tick(0.250) expect(queue.tickTime).to(equal(0.250)) expect(task100msCalled).to(beTrue()) expect(task200msCalled).to(beTrue()) expect(task300msCalled).to(beFalse()) }) it("does not triggers nested work that is scheduled outside of the tick", closure: { var task100msCalled = false var task200msCalled = false var task300msCalled = false queue.queue(timeInterval: 0.1, execute: { task100msCalled = true queue.queue(timeInterval: 0.1, execute: { task200msCalled = true queue.queue(timeInterval: 0.1, execute: { task300msCalled = true }) }) }) queue.tick(0.250) expect(queue.tickTime).to(equal(0.250)) expect(task100msCalled).to(beTrue()) expect(task200msCalled).to(beTrue()) expect(task300msCalled).to(beFalse()) }) } } } ================================================ FILE: Tests/Fakes/SocketSpy.swift ================================================ // Copyright (c) 2021 David Stump // // 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. @testable import SwiftPhoenixClient class SocketSpy: Socket { private(set) var pushCalled: Bool? private(set) var pushCallCount: Int = 0 private(set) var pushArgs: [Int: (topic: String, event: String, payload: Payload, ref: String?, joinRef: String?)] = [:] override func push(topic: String, event: String, payload: Payload, ref: String? = nil, joinRef: String? = nil) { self.pushCalled = true self.pushCallCount += 1 self.pushArgs[pushCallCount] = (topic: topic, event: event, payload: payload, ref: ref, joinRef: joinRef) super.push(topic: topic, event: event, payload: payload, ref: ref, joinRef: joinRef) } } ================================================ FILE: Tests/Helpers/TestHelpers.swift ================================================ // Copyright (c) 2021 David Stump // // 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 @testable import SwiftPhoenixClient enum TestError: Error { case stub } func toWebSocketText(data: [Any?]) -> String { let encoded = Defaults.encode(data) return String(decoding: encoded, as: UTF8.self) } /// Transforms two Dictionaries into NSDictionaries so they can be conpared func transform(_ lhs: [AnyHashable: Any], and rhs: [AnyHashable: Any]) -> (lhs: NSDictionary, rhs: NSDictionary) { return (NSDictionary(dictionary: lhs), NSDictionary(dictionary: rhs)) } extension Channel { /// Utility method to easily filter the bindings for a channel by their event func getBindings(_ event: String) -> [Binding]? { return self.syncBindingsDel.filter({ $0.event == event }) } } ================================================ FILE: Tests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Tests/Mocks/MockableClass.generated.swift ================================================ // Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // swiftlint:disable line_length // swiftlint:disable variable_name @testable import SwiftPhoenixClient class ChannelMock: Channel { var socketSetCount: Int = 0 var socketDidGetSet: Bool { return socketSetCount > 0 } override var socket: Socket? { didSet { socketSetCount += 1 } } override var state: ChannelState { get { return underlyingState } set(value) { underlyingState = value } } var underlyingState: (ChannelState)! override var syncBindingsDel: SynchronizedArray { get { return underlyingSyncBindingsDel } set(value) { underlyingSyncBindingsDel = value } } var underlyingSyncBindingsDel: (SynchronizedArray)! override var bindingRef: Int { get { return underlyingBindingRef } set(value) { underlyingBindingRef = value } } var underlyingBindingRef: (Int)! override var timeout: TimeInterval { get { return underlyingTimeout } set(value) { underlyingTimeout = value } } var underlyingTimeout: (TimeInterval)! override var joinedOnce: Bool { get { return underlyingJoinedOnce } set(value) { underlyingJoinedOnce = value } } var underlyingJoinedOnce: (Bool)! var joinPushSetCount: Int = 0 var joinPushDidGetSet: Bool { return joinPushSetCount > 0 } override var joinPush: Push! { didSet { joinPushSetCount += 1 } } override var rejoinTimer: TimeoutTimer { get { return underlyingRejoinTimer } set(value) { underlyingRejoinTimer = value } } var underlyingRejoinTimer: (TimeoutTimer)! override var onMessage: (_ message: Message) -> Message { get { return underlyingOnMessage } set(value) { underlyingOnMessage = value } } var underlyingOnMessage: ((_ message: Message) -> Message)! //MARK: - init var initTopicParamsSocketReceivedArguments: (topic: String, params: [String: Any], socket: Socket)? var initTopicParamsSocketClosure: ((String, [String: Any], Socket) -> Void)? //MARK: - deinit var deinitCallsCount = 0 var deinitCalled: Bool { return deinitCallsCount > 0 } var deinitClosure: (() -> Void)? //MARK: - join var joinTimeoutCallsCount = 0 var joinTimeoutCalled: Bool { return joinTimeoutCallsCount > 0 } var joinTimeoutReceivedTimeout: TimeInterval? var joinTimeoutReturnValue: Push! var joinTimeoutClosure: ((TimeInterval?) -> Push)? override func join(timeout: TimeInterval? = nil) -> Push { joinTimeoutCallsCount += 1 joinTimeoutReceivedTimeout = timeout return joinTimeoutClosure.map({ $0(timeout) }) ?? joinTimeoutReturnValue } //MARK: - onClose var onCloseCallsCount = 0 var onCloseCalled: Bool { return onCloseCallsCount > 0 } var onCloseReceivedCallback: ((Message) -> Void)? var onCloseReturnValue: Int! var onCloseClosure: ((@escaping ((Message) -> Void)) -> Int)? override func onClose(_ callback: @escaping ((Message) -> Void)) -> Int { onCloseCallsCount += 1 onCloseReceivedCallback = callback return onCloseClosure.map({ $0(callback) }) ?? onCloseReturnValue } //MARK: - delegateOnClose var delegateOnCloseToCallbackCallsCount = 0 var delegateOnCloseToCallbackCalled: Bool { return delegateOnCloseToCallbackCallsCount > 0 } var delegateOnCloseToCallbackReturnValue: Int! override func delegateOnClose(to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { delegateOnCloseToCallbackCallsCount += 1 return delegateOnCloseToCallbackReturnValue } //MARK: - onError var onErrorCallsCount = 0 var onErrorCalled: Bool { return onErrorCallsCount > 0 } var onErrorReceivedCallback: ((_ message: Message) -> Void)? var onErrorReturnValue: Int! var onErrorClosure: ((@escaping ((_ message: Message) -> Void)) -> Int)? override func onError(_ callback: @escaping ((_ message: Message) -> Void)) -> Int { onErrorCallsCount += 1 onErrorReceivedCallback = callback return onErrorClosure.map({ $0(callback) }) ?? onErrorReturnValue } //MARK: - delegateOnError var delegateOnErrorToCallbackCallsCount = 0 var delegateOnErrorToCallbackCalled: Bool { return delegateOnErrorToCallbackCallsCount > 0 } var delegateOnErrorToCallbackReturnValue: Int! override func delegateOnError(to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { delegateOnErrorToCallbackCallsCount += 1 return delegateOnErrorToCallbackReturnValue } //MARK: - on var onCallbackCallsCount = 0 var onCallbackCalled: Bool { return onCallbackCallsCount > 0 } var onCallbackReceivedArguments: (event: String, callback: (Message) -> Void)? var onCallbackReturnValue: Int! var onCallbackClosure: ((String, @escaping ((Message) -> Void)) -> Int)? override func on(_ event: String, callback: @escaping ((Message) -> Void)) -> Int { onCallbackCallsCount += 1 onCallbackReceivedArguments = (event: event, callback: callback) return onCallbackClosure.map({ $0(event, callback) }) ?? onCallbackReturnValue } //MARK: - delegateOn var delegateOnToCallbackCallsCount = 0 var delegateOnToCallbackCalled: Bool { return delegateOnToCallbackCallsCount > 0 } var delegateOnToCallbackReturnValue: Int! override func delegateOn(_ event: String, to owner: Target, callback: @escaping ((Target, Message) -> Void)) -> Int { delegateOnToCallbackCallsCount += 1 return delegateOnToCallbackReturnValue } //MARK: - off var offRefCallsCount = 0 var offRefCalled: Bool { return offRefCallsCount > 0 } var offRefReceivedArguments: (event: String, ref: Int?)? var offRefClosure: ((String, Int?) -> Void)? override func off(_ event: String, ref: Int? = nil) { offRefCallsCount += 1 offRefReceivedArguments = (event: event, ref: ref) offRefClosure?(event, ref) } //MARK: - push var pushPayloadTimeoutCallsCount = 0 var pushPayloadTimeoutCalled: Bool { return pushPayloadTimeoutCallsCount > 0 } var pushPayloadTimeoutReceivedArguments: (event: String, payload: Payload, timeout: TimeInterval)? var pushPayloadTimeoutReturnValue: Push! var pushPayloadTimeoutClosure: ((String, Payload, TimeInterval) -> Push)? override func push(_ event: String, payload: Payload, timeout: TimeInterval = Defaults.timeoutInterval) -> Push { pushPayloadTimeoutCallsCount += 1 pushPayloadTimeoutReceivedArguments = (event: event, payload: payload, timeout: timeout) return pushPayloadTimeoutClosure.map({ $0(event, payload, timeout) }) ?? pushPayloadTimeoutReturnValue } //MARK: - leave var leaveTimeoutCallsCount = 0 var leaveTimeoutCalled: Bool { return leaveTimeoutCallsCount > 0 } var leaveTimeoutReceivedTimeout: TimeInterval? var leaveTimeoutReturnValue: Push! var leaveTimeoutClosure: ((TimeInterval) -> Push)? override func leave(timeout: TimeInterval = Defaults.timeoutInterval) -> Push { leaveTimeoutCallsCount += 1 leaveTimeoutReceivedTimeout = timeout return leaveTimeoutClosure.map({ $0(timeout) }) ?? leaveTimeoutReturnValue } //MARK: - onMessage var onMessageCallbackCallsCount = 0 var onMessageCallbackCalled: Bool { return onMessageCallbackCallsCount > 0 } var onMessageCallbackReceivedCallback: ((Message) -> Message)? var onMessageCallbackClosure: ((@escaping (Message) -> Message) -> Void)? override func onMessage(callback: @escaping (Message) -> Message) { onMessageCallbackCallsCount += 1 onMessageCallbackReceivedCallback = callback onMessageCallbackClosure?(callback) } //MARK: - isMember var isMemberCallsCount = 0 var isMemberCalled: Bool { return isMemberCallsCount > 0 } var isMemberReceivedMessage: Message? var isMemberReturnValue: Bool! var isMemberClosure: ((Message) -> Bool)? override func isMember(_ message: Message) -> Bool { isMemberCallsCount += 1 isMemberReceivedMessage = message return isMemberClosure.map({ $0(message) }) ?? isMemberReturnValue } //MARK: - sendJoin var sendJoinCallsCount = 0 var sendJoinCalled: Bool { return sendJoinCallsCount > 0 } var sendJoinReceivedTimeout: TimeInterval? var sendJoinClosure: ((TimeInterval) -> Void)? override func sendJoin(_ timeout: TimeInterval) { sendJoinCallsCount += 1 sendJoinReceivedTimeout = timeout sendJoinClosure?(timeout) } //MARK: - rejoin var rejoinCallsCount = 0 var rejoinCalled: Bool { return rejoinCallsCount > 0 } var rejoinReceivedTimeout: TimeInterval? var rejoinClosure: ((TimeInterval?) -> Void)? override func rejoin(_ timeout: TimeInterval? = nil) { rejoinCallsCount += 1 rejoinReceivedTimeout = timeout rejoinClosure?(timeout) } //MARK: - trigger var triggerCallsCount = 0 var triggerCalled: Bool { return triggerCallsCount > 0 } var triggerReceivedMessage: Message? var triggerClosure: ((Message) -> Void)? override func trigger(_ message: Message) { triggerCallsCount += 1 triggerReceivedMessage = message triggerClosure?(message) } //MARK: - trigger var triggerEventPayloadRefJoinRefCallsCount = 0 var triggerEventPayloadRefJoinRefCalled: Bool { return triggerEventPayloadRefJoinRefCallsCount > 0 } var triggerEventPayloadRefJoinRefReceivedArguments: (event: String, payload: Payload, ref: String, joinRef: String?)? var triggerEventPayloadRefJoinRefClosure: ((String, Payload, String, String?) -> Void)? override func trigger(event: String, payload: Payload = [:], ref: String = "", joinRef: String? = nil) { triggerEventPayloadRefJoinRefCallsCount += 1 triggerEventPayloadRefJoinRefReceivedArguments = (event: event, payload: payload, ref: ref, joinRef: joinRef) triggerEventPayloadRefJoinRefClosure?(event, payload, ref, joinRef) } //MARK: - replyEventName var replyEventNameCallsCount = 0 var replyEventNameCalled: Bool { return replyEventNameCallsCount > 0 } var replyEventNameReceivedRef: String? var replyEventNameReturnValue: String! var replyEventNameClosure: ((String) -> String)? override func replyEventName(_ ref: String) -> String { replyEventNameCallsCount += 1 replyEventNameReceivedRef = ref return replyEventNameClosure.map({ $0(ref) }) ?? replyEventNameReturnValue } } class PushMock: Push { var channelSetCount: Int = 0 var channelDidGetSet: Bool { return channelSetCount > 0 } override var channel: Channel? { didSet { channelSetCount += 1 } } override var timeout: TimeInterval { get { return underlyingTimeout } set(value) { underlyingTimeout = value } } var underlyingTimeout: (TimeInterval)! var receivedMessageSetCount: Int = 0 var receivedMessageDidGetSet: Bool { return receivedMessageSetCount > 0 } override var receivedMessage: Message? { didSet { receivedMessageSetCount += 1 } } override var timeoutTimer: TimerQueue { get { return underlyingTimeoutTimer } set(value) { underlyingTimeoutTimer = value } } var underlyingTimeoutTimer: (TimerQueue)! var timeoutWorkItemSetCount: Int = 0 var timeoutWorkItemDidGetSet: Bool { return timeoutWorkItemSetCount > 0 } override var timeoutWorkItem: DispatchWorkItem? { didSet { timeoutWorkItemSetCount += 1 } } override var sent: Bool { get { return underlyingSent } set(value) { underlyingSent = value } } var underlyingSent: (Bool)! var refSetCount: Int = 0 var refDidGetSet: Bool { return refSetCount > 0 } override var ref: String? { didSet { refSetCount += 1 } } var refEventSetCount: Int = 0 var refEventDidGetSet: Bool { return refEventSetCount > 0 } override var refEvent: String? { didSet { refEventSetCount += 1 } } //MARK: - init var initChannelEventPayloadTimeoutReceivedArguments: (channel: Channel, event: String, payload: Payload, timeout: TimeInterval)? var initChannelEventPayloadTimeoutClosure: ((Channel, String, Payload, TimeInterval) -> Void)? //MARK: - resend var resendCallsCount = 0 var resendCalled: Bool { return resendCallsCount > 0 } var resendReceivedTimeout: TimeInterval? var resendClosure: ((TimeInterval) -> Void)? override func resend(_ timeout: TimeInterval = Defaults.timeoutInterval) { resendCallsCount += 1 resendReceivedTimeout = timeout resendClosure?(timeout) } //MARK: - send var sendCallsCount = 0 var sendCalled: Bool { return sendCallsCount > 0 } var sendClosure: (() -> Void)? override func send() { sendCallsCount += 1 sendClosure?() } //MARK: - receive var receiveCallbackCallsCount = 0 var receiveCallbackCalled: Bool { return receiveCallbackCallsCount > 0 } var receiveCallbackReceivedArguments: (status: String, callback: (Message) -> ())? var receiveCallbackReturnValue: Push! var receiveCallbackClosure: ((String, @escaping ((Message) -> ())) -> Push)? override func receive(_ status: String, callback: @escaping ((Message) -> ())) -> Push { receiveCallbackCallsCount += 1 receiveCallbackReceivedArguments = (status: status, callback: callback) return receiveCallbackClosure.map({ $0(status, callback) }) ?? receiveCallbackReturnValue } //MARK: - delegateReceive var delegateReceiveToCallbackCallsCount = 0 var delegateReceiveToCallbackCalled: Bool { return delegateReceiveToCallbackCallsCount > 0 } var delegateReceiveToCallbackReturnValue: Push! override func delegateReceive(_ status: String, to owner: Target, callback: @escaping ((Target, Message) -> ())) -> Push { delegateReceiveToCallbackCallsCount += 1 return delegateReceiveToCallbackReturnValue } //MARK: - receive var receiveDelegatedCallsCount = 0 var receiveDelegatedCalled: Bool { return receiveDelegatedCallsCount > 0 } var receiveDelegatedReceivedArguments: (status: String, delegated: Delegated)? var receiveDelegatedReturnValue: Push! var receiveDelegatedClosure: ((String, Delegated) -> Push)? override func receive(_ status: String, delegated: Delegated) -> Push { receiveDelegatedCallsCount += 1 receiveDelegatedReceivedArguments = (status: status, delegated: delegated) return receiveDelegatedClosure.map({ $0(status, delegated) }) ?? receiveDelegatedReturnValue } //MARK: - reset var resetCallsCount = 0 var resetCalled: Bool { return resetCallsCount > 0 } var resetClosure: (() -> Void)? override func reset() { resetCallsCount += 1 resetClosure?() } //MARK: - cancelTimeout var cancelTimeoutCallsCount = 0 var cancelTimeoutCalled: Bool { return cancelTimeoutCallsCount > 0 } var cancelTimeoutClosure: (() -> Void)? override func cancelTimeout() { cancelTimeoutCallsCount += 1 cancelTimeoutClosure?() } //MARK: - startTimeout var startTimeoutCallsCount = 0 var startTimeoutCalled: Bool { return startTimeoutCallsCount > 0 } var startTimeoutClosure: (() -> Void)? override func startTimeout() { startTimeoutCallsCount += 1 startTimeoutClosure?() } //MARK: - hasReceived var hasReceivedStatusCallsCount = 0 var hasReceivedStatusCalled: Bool { return hasReceivedStatusCallsCount > 0 } var hasReceivedStatusReceivedStatus: String? var hasReceivedStatusReturnValue: Bool! var hasReceivedStatusClosure: ((String) -> Bool)? override func hasReceived(status: String) -> Bool { hasReceivedStatusCallsCount += 1 hasReceivedStatusReceivedStatus = status return hasReceivedStatusClosure.map({ $0(status) }) ?? hasReceivedStatusReturnValue } //MARK: - trigger var triggerPayloadCallsCount = 0 var triggerPayloadCalled: Bool { return triggerPayloadCallsCount > 0 } var triggerPayloadReceivedArguments: (status: String, payload: Payload)? var triggerPayloadClosure: ((String, Payload) -> Void)? override func trigger(_ status: String, payload: Payload) { triggerPayloadCallsCount += 1 triggerPayloadReceivedArguments = (status: status, payload: payload) triggerPayloadClosure?(status, payload) } } class SocketMock: Socket { override var endPointUrl: URL { get { return underlyingEndPointUrl } set(value) { underlyingEndPointUrl = value } } var underlyingEndPointUrl: (URL)! override var encode: (Any) -> Data { get { return underlyingEncode } set(value) { underlyingEncode = value } } var underlyingEncode: ((Any) -> Data)! override var decode: (Data) -> Any? { get { return underlyingDecode } set(value) { underlyingDecode = value } } var underlyingDecode: ((Data) -> Any?)! override var timeout: TimeInterval { get { return underlyingTimeout } set(value) { underlyingTimeout = value } } var underlyingTimeout: (TimeInterval)! override var heartbeatInterval: TimeInterval { get { return underlyingHeartbeatInterval } set(value) { underlyingHeartbeatInterval = value } } var underlyingHeartbeatInterval: (TimeInterval)! override var heartbeatLeeway: DispatchTimeInterval { get { return underlyingHeartbeatLeeway } set(value) { underlyingHeartbeatLeeway = value } } var underlyingHeartbeatLeeway: (DispatchTimeInterval)! override var reconnectAfter: (Int) -> TimeInterval { get { return underlyingReconnectAfter } set(value) { underlyingReconnectAfter = value } } var underlyingReconnectAfter: ((Int) -> TimeInterval)! override var rejoinAfter: (Int) -> TimeInterval { get { return underlyingRejoinAfter } set(value) { underlyingRejoinAfter = value } } var underlyingRejoinAfter: ((Int) -> TimeInterval)! var loggerSetCount: Int = 0 var loggerDidGetSet: Bool { return loggerSetCount > 0 } override var logger: ((String) -> Void)? { didSet { loggerSetCount += 1 } } override var skipHeartbeat: Bool { get { return underlyingSkipHeartbeat } set(value) { underlyingSkipHeartbeat = value } } var underlyingSkipHeartbeat: (Bool)! override var disableSSLCertValidation: Bool { get { return underlyingDisableSSLCertValidation } set(value) { underlyingDisableSSLCertValidation = value } } var underlyingDisableSSLCertValidation: (Bool)! var enabledSSLCipherSuitesSetCount: Int = 0 var enabledSSLCipherSuitesDidGetSet: Bool { return enabledSSLCipherSuitesSetCount > 0 } override var enabledSSLCipherSuites: [SSLCipherSuite]? { didSet { enabledSSLCipherSuitesSetCount += 1 } } override var stateChangeCallbacks: StateChangeCallbacks { get { return underlyingStateChangeCallbacks } set(value) { underlyingStateChangeCallbacks = value } } var underlyingStateChangeCallbacks: (StateChangeCallbacks)! override var ref: UInt64 { get { return underlyingRef } set(value) { underlyingRef = value } } var underlyingRef: (UInt64)! var heartbeatTimerSetCount: Int = 0 var heartbeatTimerDidGetSet: Bool { return heartbeatTimerSetCount > 0 } override var heartbeatTimer: HeartbeatTimer? { didSet { heartbeatTimerSetCount += 1 } } var pendingHeartbeatRefSetCount: Int = 0 var pendingHeartbeatRefDidGetSet: Bool { return pendingHeartbeatRefSetCount > 0 } override var pendingHeartbeatRef: String? { didSet { pendingHeartbeatRefSetCount += 1 } } override var reconnectTimer: TimeoutTimer { get { return underlyingReconnectTimer } set(value) { underlyingReconnectTimer = value } } var underlyingReconnectTimer: (TimeoutTimer)! override var closeStatus: CloseStatus { get { return underlyingCloseStatus } set(value) { underlyingCloseStatus = value } } var underlyingCloseStatus: (CloseStatus)! var connectionSetCount: Int = 0 var connectionDidGetSet: Bool { return connectionSetCount > 0 } override var connection: PhoenixTransport? { didSet { connectionSetCount += 1 } } //MARK: - init var initParamsVsnReceivedArguments: (endPoint: String, params: Payload?, vsn: String)? var initParamsVsnClosure: ((String, Payload?, String) -> Void)? //MARK: - init var initParamsClosureVsnReceivedArguments: (endPoint: String, paramsClosure: PayloadClosure?, vsn: String)? var initParamsClosureVsnClosure: ((String, PayloadClosure?, String) -> Void)? //MARK: - init var initEndPointTransportParamsClosureVsnReceivedArguments: (endPoint: String, transport: (URL) -> PhoenixTransport, paramsClosure: PayloadClosure?, vsn: String)? var initEndPointTransportParamsClosureVsnClosure: ((String, @escaping (URL) -> PhoenixTransport, PayloadClosure?, String) -> Void)? //MARK: - deinit var deinitCallsCount = 0 var deinitCalled: Bool { return deinitCallsCount > 0 } var deinitClosure: (() -> Void)? //MARK: - connect var connectCallsCount = 0 var connectCalled: Bool { return connectCallsCount > 0 } var connectClosure: (() -> Void)? override func connect() { connectCallsCount += 1 connectClosure?() } //MARK: - disconnect var disconnectCodeReasonCallbackCallsCount = 0 var disconnectCodeReasonCallbackCalled: Bool { return disconnectCodeReasonCallbackCallsCount > 0 } var disconnectCodeReasonCallbackReceivedArguments: (code: CloseCode, reason: String?, callback: (() -> Void)?)? var disconnectCodeReasonCallbackClosure: ((CloseCode, String?, (() -> Void)?) -> Void)? override func disconnect(code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil) { disconnectCodeReasonCallbackCallsCount += 1 disconnectCodeReasonCallbackReceivedArguments = (code: code, reason: reason, callback: callback) disconnectCodeReasonCallbackClosure?(code, reason, callback) } //MARK: - teardown var teardownCodeReasonCallbackCallsCount = 0 var teardownCodeReasonCallbackCalled: Bool { return teardownCodeReasonCallbackCallsCount > 0 } var teardownCodeReasonCallbackReceivedArguments: (code: CloseCode, reason: String?, callback: (() -> Void)?)? var teardownCodeReasonCallbackClosure: ((CloseCode, String?, (() -> Void)?) -> Void)? override func teardown(code: CloseCode = CloseCode.normal, reason: String? = nil, callback: (() -> Void)? = nil) { teardownCodeReasonCallbackCallsCount += 1 teardownCodeReasonCallbackReceivedArguments = (code: code, reason: reason, callback: callback) teardownCodeReasonCallbackClosure?(code, reason, callback) } //MARK: - onOpen var onOpenCallbackCallsCount = 0 var onOpenCallbackCalled: Bool { return onOpenCallbackCallsCount > 0 } var onOpenCallbackReceivedCallback: (() -> Void)? var onOpenCallbackReturnValue: String! var onOpenCallbackClosure: ((@escaping () -> Void) -> String)? override func onOpen(callback: @escaping () -> Void) -> String { onOpenCallbackCallsCount += 1 onOpenCallbackReceivedCallback = callback return onOpenCallbackClosure.map({ $0(callback) }) ?? onOpenCallbackReturnValue } //MARK: - delegateOnOpen var delegateOnOpenToCallbackCallsCount = 0 var delegateOnOpenToCallbackCalled: Bool { return delegateOnOpenToCallbackCallsCount > 0 } var delegateOnOpenToCallbackReturnValue: String! override func delegateOnOpen(to owner: T, callback: @escaping ((T) -> Void)) -> String { delegateOnOpenToCallbackCallsCount += 1 return delegateOnOpenToCallbackReturnValue } //MARK: - onClose var onCloseCallbackCallsCount = 0 var onCloseCallbackCalled: Bool { return onCloseCallbackCallsCount > 0 } var onCloseCallbackReceivedCallback: (() -> Void)? var onCloseCallbackReturnValue: String! var onCloseCallbackClosure: ((@escaping () -> Void) -> String)? override func onClose(callback: @escaping () -> Void) -> String { onCloseCallbackCallsCount += 1 onCloseCallbackReceivedCallback = callback return onCloseCallbackClosure.map({ $0(callback) }) ?? onCloseCallbackReturnValue } //MARK: - onClose var onCloseCallbackWithCodeCallsCount = 0 var onCloseCallbackWithCodeCalled: Bool { return onCloseCallbackWithCodeCallsCount > 0 } var onCloseCallbackWithCodeReceivedCallback: ((Int, String?) -> Void)? var onCloseCallbackWithCodeReturnValue: String! var onCloseCallbackWithCodeClosure: ((@escaping (Int, String?) -> Void) -> String)? override func onClose(callback: @escaping (Int, String?) -> Void) -> String { onCloseCallbackWithCodeCallsCount += 1 onCloseCallbackWithCodeReceivedCallback = callback return onCloseCallbackWithCodeClosure.map({ $0(callback) }) ?? onCloseCallbackWithCodeReturnValue } //MARK: - delegateOnClose var delegateOnCloseToCallbackCallsCount = 0 var delegateOnCloseToCallbackCalled: Bool { return delegateOnCloseToCallbackCallsCount > 0 } var delegateOnCloseToCallbackReturnValue: String! override func delegateOnClose(to owner: T, callback: @escaping (T) -> Void) -> String { delegateOnCloseToCallbackCallsCount += 1 return delegateOnCloseToCallbackReturnValue } //MARK: - delegateOnClose var delegateOnCloseToCallbackWithCodeCallsCount = 0 var delegateOnCloseToCallbackWithCodeCalled: Bool { return delegateOnCloseToCallbackWithCodeCallsCount > 0 } var delegateOnCloseToCallbackWithCodeReturnValue: String! override func delegateOnClose(to owner: T, callback: @escaping (T, (Int, String?)) -> Void) -> String { delegateOnCloseToCallbackWithCodeCallsCount += 1 return delegateOnCloseToCallbackWithCodeReturnValue } //MARK: - onError var onErrorCallbackCallsCount = 0 var onErrorCallbackCalled: Bool { return onErrorCallbackCallsCount > 0 } var onErrorCallbackReceivedCallback: (((Error, URLResponse?)) -> Void)? var onErrorCallbackReturnValue: String! var onErrorCallbackClosure: ((@escaping ((Error, URLResponse?)) -> Void) -> String)? override func onError(callback: @escaping ((Error, URLResponse?)) -> Void) -> String { onErrorCallbackCallsCount += 1 onErrorCallbackReceivedCallback = callback return onErrorCallbackClosure.map({ $0(callback) }) ?? onErrorCallbackReturnValue } //MARK: - delegateOnError var delegateOnErrorToCallbackCallsCount = 0 var delegateOnErrorToCallbackCalled: Bool { return delegateOnErrorToCallbackCallsCount > 0 } var delegateOnErrorToCallbackReturnValue: String! override func delegateOnError(to owner: T, callback: @escaping ((T, (Error, URLResponse?)) -> Void)) -> String { delegateOnErrorToCallbackCallsCount += 1 return delegateOnErrorToCallbackReturnValue } //MARK: - onMessage var onMessageCallbackCallsCount = 0 var onMessageCallbackCalled: Bool { return onMessageCallbackCallsCount > 0 } var onMessageCallbackReceivedCallback: ((Message) -> Void)? var onMessageCallbackReturnValue: String! var onMessageCallbackClosure: ((@escaping (Message) -> Void) -> String)? override func onMessage(callback: @escaping (Message) -> Void) -> String { onMessageCallbackCallsCount += 1 onMessageCallbackReceivedCallback = callback return onMessageCallbackClosure.map({ $0(callback) }) ?? onMessageCallbackReturnValue } //MARK: - delegateOnMessage var delegateOnMessageToCallbackCallsCount = 0 var delegateOnMessageToCallbackCalled: Bool { return delegateOnMessageToCallbackCallsCount > 0 } var delegateOnMessageToCallbackReturnValue: String! override func delegateOnMessage(to owner: T, callback: @escaping ((T, Message) -> Void)) -> String { delegateOnMessageToCallbackCallsCount += 1 return delegateOnMessageToCallbackReturnValue } //MARK: - releaseCallbacks var releaseCallbacksCallsCount = 0 var releaseCallbacksCalled: Bool { return releaseCallbacksCallsCount > 0 } var releaseCallbacksClosure: (() -> Void)? override func releaseCallbacks() { releaseCallbacksCallsCount += 1 releaseCallbacksClosure?() } //MARK: - channel var channelParamsCallsCount = 0 var channelParamsCalled: Bool { return channelParamsCallsCount > 0 } var channelParamsReceivedArguments: (topic: String, params: [String: Any])? var channelParamsReturnValue: Channel! var channelParamsClosure: ((String, [String: Any]) -> Channel)? override func channel(_ topic: String, params: [String: Any] = [:]) -> Channel { channelParamsCallsCount += 1 channelParamsReceivedArguments = (topic: topic, params: params) return channelParamsClosure.map({ $0(topic, params) }) ?? channelParamsReturnValue } //MARK: - remove var removeCallsCount = 0 var removeCalled: Bool { return removeCallsCount > 0 } var removeReceivedChannel: Channel? var removeClosure: ((Channel) -> Void)? override func remove(_ channel: Channel) { removeCallsCount += 1 removeReceivedChannel = channel removeClosure?(channel) } //MARK: - off var offCallsCount = 0 var offCalled: Bool { return offCallsCount > 0 } var offReceivedRefs: [String]? var offClosure: (([String]) -> Void)? override func off(_ refs: [String]) { offCallsCount += 1 offReceivedRefs = refs offClosure?(refs) } //MARK: - push var pushTopicEventPayloadRefJoinRefCallsCount = 0 var pushTopicEventPayloadRefJoinRefCalled: Bool { return pushTopicEventPayloadRefJoinRefCallsCount > 0 } var pushTopicEventPayloadRefJoinRefReceivedArguments: (topic: String, event: String, payload: Payload, ref: String?, joinRef: String?)? var pushTopicEventPayloadRefJoinRefClosure: ((String, String, Payload, String?, String?) -> Void)? override func push(topic: String, event: String, payload: Payload, ref: String? = nil, joinRef: String? = nil) { pushTopicEventPayloadRefJoinRefCallsCount += 1 pushTopicEventPayloadRefJoinRefReceivedArguments = (topic: topic, event: event, payload: payload, ref: ref, joinRef: joinRef) pushTopicEventPayloadRefJoinRefClosure?(topic, event, payload, ref, joinRef) } //MARK: - makeRef var makeRefCallsCount = 0 var makeRefCalled: Bool { return makeRefCallsCount > 0 } var makeRefReturnValue: String! var makeRefClosure: (() -> String)? override func makeRef() -> String { makeRefCallsCount += 1 return makeRefClosure.map({ $0() }) ?? makeRefReturnValue } //MARK: - logItems var logItemsCallsCount = 0 var logItemsCalled: Bool { return logItemsCallsCount > 0 } var logItemsReceivedItems: Any? var logItemsClosure: ((Any) -> Void)? override func logItems(_ items: Any...) { logItemsCallsCount += 1 logItemsReceivedItems = items logItemsClosure?(items) } //MARK: - onConnectionOpen var onConnectionOpenCallsCount = 0 var onConnectionOpenCalled: Bool { return onConnectionOpenCallsCount > 0 } var onConnectionOpenClosure: (() -> Void)? override func onConnectionOpen() { onConnectionOpenCallsCount += 1 onConnectionOpenClosure?() } //MARK: - onConnectionClosed var onConnectionClosedCodeReasonCallsCount = 0 var onConnectionClosedCodeReasonCalled: Bool { return onConnectionClosedCodeReasonCallsCount > 0 } var onConnectionClosedCodeReasonReceivedArguments: (code: Int, reason: String?)? var onConnectionClosedCodeReasonClosure: ((Int, String?) -> Void)? override func onConnectionClosed(code: Int, reason: String?) { onConnectionClosedCodeReasonCallsCount += 1 onConnectionClosedCodeReasonReceivedArguments = (code: code, reason: reason) onConnectionClosedCodeReasonClosure?(code, reason) } //MARK: - onConnectionError var onConnectionErrorResponseCallsCount = 0 var onConnectionErrorResponseCalled: Bool { return onConnectionErrorResponseCallsCount > 0 } var onConnectionErrorResponseReceivedArguments: (error: Error, response: URLResponse?)? var onConnectionErrorResponseClosure: ((Error, URLResponse?) -> Void)? override func onConnectionError(_ error: Error, response: URLResponse?) { onConnectionErrorResponseCallsCount += 1 onConnectionErrorResponseReceivedArguments = (error: error, response: response) onConnectionErrorResponseClosure?(error, response) } //MARK: - onConnectionMessage var onConnectionMessageCallsCount = 0 var onConnectionMessageCalled: Bool { return onConnectionMessageCallsCount > 0 } var onConnectionMessageReceivedRawMessage: String? var onConnectionMessageClosure: ((String) -> Void)? override func onConnectionMessage(_ rawMessage: String) { onConnectionMessageCallsCount += 1 onConnectionMessageReceivedRawMessage = rawMessage onConnectionMessageClosure?(rawMessage) } //MARK: - triggerChannelError var triggerChannelErrorCallsCount = 0 var triggerChannelErrorCalled: Bool { return triggerChannelErrorCallsCount > 0 } var triggerChannelErrorClosure: (() -> Void)? override func triggerChannelError() { triggerChannelErrorCallsCount += 1 triggerChannelErrorClosure?() } //MARK: - flushSendBuffer var flushSendBufferCallsCount = 0 var flushSendBufferCalled: Bool { return flushSendBufferCallsCount > 0 } var flushSendBufferClosure: (() -> Void)? override func flushSendBuffer() { flushSendBufferCallsCount += 1 flushSendBufferClosure?() } //MARK: - removeFromSendBuffer var removeFromSendBufferRefCallsCount = 0 var removeFromSendBufferRefCalled: Bool { return removeFromSendBufferRefCallsCount > 0 } var removeFromSendBufferRefReceivedRef: String? var removeFromSendBufferRefClosure: ((String) -> Void)? override func removeFromSendBuffer(ref: String) { removeFromSendBufferRefCallsCount += 1 removeFromSendBufferRefReceivedRef = ref removeFromSendBufferRefClosure?(ref) } //MARK: - leaveOpenTopic var leaveOpenTopicTopicCallsCount = 0 var leaveOpenTopicTopicCalled: Bool { return leaveOpenTopicTopicCallsCount > 0 } var leaveOpenTopicTopicReceivedTopic: String? var leaveOpenTopicTopicClosure: ((String) -> Void)? override func leaveOpenTopic(topic: String) { leaveOpenTopicTopicCallsCount += 1 leaveOpenTopicTopicReceivedTopic = topic leaveOpenTopicTopicClosure?(topic) } //MARK: - resetHeartbeat var resetHeartbeatCallsCount = 0 var resetHeartbeatCalled: Bool { return resetHeartbeatCallsCount > 0 } var resetHeartbeatClosure: (() -> Void)? override func resetHeartbeat() { resetHeartbeatCallsCount += 1 resetHeartbeatClosure?() } //MARK: - sendHeartbeat var sendHeartbeatCallsCount = 0 var sendHeartbeatCalled: Bool { return sendHeartbeatCallsCount > 0 } var sendHeartbeatClosure: (() -> Void)? override func sendHeartbeat() { sendHeartbeatCallsCount += 1 sendHeartbeatClosure?() } //MARK: - abnormalClose var abnormalCloseCallsCount = 0 var abnormalCloseCalled: Bool { return abnormalCloseCallsCount > 0 } var abnormalCloseReceivedReason: String? var abnormalCloseClosure: ((String) -> Void)? override func abnormalClose(_ reason: String) { abnormalCloseCallsCount += 1 abnormalCloseReceivedReason = reason abnormalCloseClosure?(reason) } //MARK: - onOpen var onOpenCallsCount = 0 var onOpenCalled: Bool { return onOpenCallsCount > 0 } var onOpenClosure: (() -> Void)? override func onOpen() { onOpenCallsCount += 1 onOpenClosure?() } //MARK: - onError var onErrorErrorResponseCallsCount = 0 var onErrorErrorResponseCalled: Bool { return onErrorErrorResponseCallsCount > 0 } var onErrorErrorResponseReceivedArguments: (error: Error, response: URLResponse?)? var onErrorErrorResponseClosure: ((Error, URLResponse?) -> Void)? override func onError(error: Error, response: URLResponse?) { onErrorErrorResponseCallsCount += 1 onErrorErrorResponseReceivedArguments = (error: error, response: response) onErrorErrorResponseClosure?(error, response) } //MARK: - onMessage var onMessageMessageCallsCount = 0 var onMessageMessageCalled: Bool { return onMessageMessageCallsCount > 0 } var onMessageMessageReceivedMessage: String? var onMessageMessageClosure: ((String) -> Void)? override func onMessage(message: String) { onMessageMessageCallsCount += 1 onMessageMessageReceivedMessage = message onMessageMessageClosure?(message) } //MARK: - onClose var onCloseCodeReasonCallsCount = 0 var onCloseCodeReasonCalled: Bool { return onCloseCodeReasonCallsCount > 0 } var onCloseCodeReasonReceivedArguments: (code: Int, reason: String?)? var onCloseCodeReasonClosure: ((Int, String?) -> Void)? override func onClose(code: Int, reason: String? = nil) { onCloseCodeReasonCallsCount += 1 onCloseCodeReasonReceivedArguments = (code: code, reason: reason) onCloseCodeReasonClosure?(code, reason) } } class TimeoutTimerMock: TimeoutTimer { override var callback: Delegated<(), Void> { get { return underlyingCallback } set(value) { underlyingCallback = value } } var underlyingCallback: (Delegated<(), Void>)! override var timerCalculation: Delegated { get { return underlyingTimerCalculation } set(value) { underlyingTimerCalculation = value } } var underlyingTimerCalculation: (Delegated)! var workItemSetCount: Int = 0 var workItemDidGetSet: Bool { return workItemSetCount > 0 } override var workItem: DispatchWorkItem? { didSet { workItemSetCount += 1 } } override var tries: Int { get { return underlyingTries } set(value) { underlyingTries = value } } var underlyingTries: (Int)! override var queue: TimerQueue { get { return underlyingQueue } set(value) { underlyingQueue = value } } var underlyingQueue: (TimerQueue)! //MARK: - reset var resetCallsCount = 0 var resetCalled: Bool { return resetCallsCount > 0 } var resetClosure: (() -> Void)? override func reset() { resetCallsCount += 1 resetClosure?() } //MARK: - scheduleTimeout var scheduleTimeoutCallsCount = 0 var scheduleTimeoutCalled: Bool { return scheduleTimeoutCallsCount > 0 } var scheduleTimeoutClosure: (() -> Void)? override func scheduleTimeout() { scheduleTimeoutCallsCount += 1 scheduleTimeoutClosure?() } } ================================================ FILE: Tests/Mocks/MockableProtocol.generated.swift ================================================ // Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // swiftlint:disable line_length // swiftlint:disable variable_name import Foundation #if os(iOS) || os(tvOS) || os(watchOS) import UIKit #elseif os(OSX) import AppKit #endif @testable import SwiftPhoenixClient class PhoenixTransportMock: PhoenixTransport { var readyState: PhoenixTransportReadyState { get { return underlyingReadyState } set(value) { underlyingReadyState = value } } var underlyingReadyState: PhoenixTransportReadyState! var delegate: PhoenixTransportDelegate? //MARK: - connect var connectCallsCount = 0 var connectCalled: Bool { return connectCallsCount > 0 } var connectClosure: (() -> Void)? func connect() { connectCallsCount += 1 connectClosure?() } //MARK: - disconnect var disconnectCodeReasonCallsCount = 0 var disconnectCodeReasonCalled: Bool { return disconnectCodeReasonCallsCount > 0 } var disconnectCodeReasonReceivedArguments: (code: Int, reason: String?)? var disconnectCodeReasonReceivedInvocations: [(code: Int, reason: String?)] = [] var disconnectCodeReasonClosure: ((Int, String?) -> Void)? func disconnect(code: Int, reason: String?) { disconnectCodeReasonCallsCount += 1 disconnectCodeReasonReceivedArguments = (code: code, reason: reason) disconnectCodeReasonReceivedInvocations.append((code: code, reason: reason)) disconnectCodeReasonClosure?(code, reason) } //MARK: - send var sendDataCallsCount = 0 var sendDataCalled: Bool { return sendDataCallsCount > 0 } var sendDataReceivedData: Data? var sendDataReceivedInvocations: [Data] = [] var sendDataClosure: ((Data) -> Void)? func send(data: Data) { sendDataCallsCount += 1 sendDataReceivedData = data sendDataReceivedInvocations.append(data) sendDataClosure?(data) } } ================================================ FILE: Tests/SwiftPhoenixClientTests/ChannelSpec.swift ================================================ // // ChannelSpec.swift // SwiftPhoenixClient // // Created by Daniel Rees on 5/18/18. // import Quick import Nimble @testable import SwiftPhoenixClient class ChannelSpec: QuickSpec { override func spec() { // Mocks var mockClient: PhoenixTransportMock! var mockSocket: SocketMock! // Constants let kDefaultRef = "1" let kDefaultTimeout: TimeInterval = 10.0 // Clock var fakeClock: FakeTimerQueue! // UUT var channel: Channel! /// Utility method to easily filter the bindings for a channel by their event func getBindings(_ event: String) -> [Binding]? { return channel.syncBindingsDel.filter({ $0.event == event }) } beforeEach { // Any TimeoutTimer that is created will receive the fake clock // when scheduling work items fakeClock = FakeTimerQueue() TimerQueue.main = fakeClock mockClient = PhoenixTransportMock() mockSocket = SocketMock(endPoint: "/socket", transport: { _ in mockClient }) mockSocket.connection = mockClient mockSocket.timeout = kDefaultTimeout mockSocket.makeRefReturnValue = kDefaultRef mockSocket.reconnectAfter = { tries -> TimeInterval in return tries > 3 ? 10 : [1, 2, 5, 10][tries - 1] } mockSocket.rejoinAfter = Defaults.rejoinSteppedBackOff channel = Channel(topic: "topic", params: ["one": "two"], socket: mockSocket) mockSocket.channelParamsReturnValue = channel } afterEach { fakeClock.reset() } describe("constructor") { it("sets defaults", closure: { channel = Channel(topic: "topic", params: ["one": "two"], socket: mockSocket) expect(channel.state).to(equal(ChannelState.closed)) expect(channel.topic).to(equal("topic")) expect(channel.params["one"] as? String).to(equal("two")) expect(channel.socket === mockSocket).to(beTrue()) expect(channel.timeout).to(equal(10)) expect(channel.joinedOnce).to(beFalse()) expect(channel.joinPush).toNot(beNil()) expect(channel.pushBuffer).to(beEmpty()) }) it("sets up joinPush with literal params", closure: { channel = Channel(topic: "topic", params: ["one": "two"], socket: mockSocket) let joinPush = channel.joinPush expect(joinPush?.channel === channel).to(beTrue()) expect(joinPush?.payload["one"] as? String).to(equal("two")) expect(joinPush?.event).to(equal("phx_join")) expect(joinPush?.timeout).to(equal(10)) }) it("should not introduce any retain cycles", closure: { weak var weakChannel = Channel(topic: "topic", params: ["one": 2], socket: mockSocket) expect(weakChannel).to(beNil()) }) } describe("onMessage") { it("returns message by default", closure: { let message = channel.onMessage(Message(ref: "original")) expect(message.ref).to(equal("original")) }) it("can be overridden", closure: { channel.onMessage = { message in return Message(ref: "modified") } let message = channel.onMessage(Message(ref: "original")) expect(message.ref).to(equal("modified")) }) } describe("updating join params") { it("can update join params", closure: { let params: Payload = ["value": 1] let change: Payload = ["value": 2] channel = Channel(topic: "topic", params: params, socket: mockSocket) let joinPush = channel.joinPush expect(joinPush?.channel === channel).to(beTrue()) expect(joinPush?.payload["value"] as? Int).to(equal(1)) expect(joinPush?.event).to(equal(ChannelEvent.join)) expect(joinPush?.timeout).to(equal(10)) channel.params = change expect(joinPush?.channel === channel).to(beTrue()) expect(joinPush?.payload["value"] as? Int).to(equal(2)) expect(channel?.params["value"] as? Int).to(equal(2)) expect(joinPush?.event).to(equal(ChannelEvent.join)) expect(joinPush?.timeout).to(equal(10)) }) } describe("join") { it("sets state to joining", closure: { channel.join() expect(channel.state.rawValue).to(equal("joining")) }) it("sets joinedOnce to true", closure: { expect(channel.joinedOnce).to(beFalse()) channel.join() expect(channel.joinedOnce).to(beTrue()) }) it("throws if attempting to join multiple times", closure: { channel.join() // Method is not marked to throw expect { channel.join() }.to(throwAssertion()) }) it("triggers socket push with channel params", closure: { channel.join() expect(mockSocket.pushTopicEventPayloadRefJoinRefCalled).to(beTrue()) let args = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args?.topic).to(equal("topic")) expect(args?.event).to(equal("phx_join")) expect(args?.payload["one"] as? String).to(equal("two")) expect(args?.ref).to(equal(kDefaultRef)) expect(args?.joinRef).to(equal(channel.joinRef)) }) it("can set timeout on joinPush", closure: { let newTimeout: TimeInterval = 2.0 let joinPush = channel.joinPush expect(joinPush?.timeout).to(equal(kDefaultTimeout)) let _ = channel.join(timeout: newTimeout) expect(joinPush?.timeout).to(equal(newTimeout)) }) it("leaves existing duplicate topic on new join") { let transport: ((URL) -> PhoenixTransport) = { _ in return mockClient } let spySocket = SocketSpy(endPoint: "/socket", transport: transport) let channel = spySocket.channel("topic", params: ["one": "two"]) mockClient.readyState = .open spySocket.onConnectionOpen() channel.join() .receive("ok") { (message) in let newChannel = spySocket.channel("topic") expect(channel.isJoined).to(beTrue()) newChannel.join() expect(channel.isJoined).to(beFalse()) } channel.joinPush.trigger("ok", payload: [:]) } } describe("timeout behavior") { var spySocket: SocketSpy! var joinPush: Push! var timeout: TimeInterval! func receiveSocketOpen() { mockClient.readyState = .open spySocket.onConnectionOpen() } beforeEach { mockClient.readyState = .closed let transport: ((URL) -> PhoenixTransport) = { _ in return mockClient } spySocket = SocketSpy(endPoint: "/socket", transport: transport) channel = Channel(topic: "topic", params: ["one": "two"], socket: spySocket) joinPush = channel.joinPush timeout = joinPush.timeout } it("succeeds before timeout", closure: { spySocket.connect() receiveSocketOpen() channel.join() expect(spySocket.pushCalled).to(beTrue()) expect(channel.timeout).to(equal(10.0)) fakeClock.tick(0.100) joinPush.trigger("ok", payload: [:]) expect(channel.state).to(equal(.joined)) fakeClock.tick(timeout) expect(spySocket.pushCallCount).to(equal(1)) }) it("retries with backoff after timeout", closure: { spySocket.connect() receiveSocketOpen() var timeoutCallCount = 0 channel.join().receive("timeout", callback: { (_) in timeoutCallCount += 1 }) expect(spySocket.pushCallCount).to(equal(1)) expect(spySocket.pushArgs[1]?.event).to(equal("phx_join")) expect(timeoutCallCount).to(equal(0)) fakeClock.tick(timeout) // leave pushed to server expect(spySocket.pushCallCount).to(equal(2)) expect(spySocket.pushArgs[2]?.event).to(equal("phx_leave")) expect(timeoutCallCount).to(equal(1)) fakeClock.tick(timeout + 1) // rejoin expect(spySocket.pushCallCount).to(equal(4)) expect(spySocket.pushArgs[3]?.event).to(equal("phx_join")) expect(spySocket.pushArgs[4]?.event).to(equal("phx_leave")) expect(timeoutCallCount).to(equal(2)) fakeClock.tick(10) joinPush.trigger("ok", payload: [:]) expect(spySocket.pushCallCount).to(equal(5)) expect(spySocket.pushArgs[5]?.event).to(equal("phx_join")) expect(channel.state).to(equal(.joined)) }) it("with socket and join delay", closure: { channel.join() expect(spySocket.pushCallCount).to(equal(1)) // Open the socket after a delay fakeClock.tick(9.0) expect(spySocket.pushCallCount).to(equal(1)) // join request returns between timeouts fakeClock.tick(1.0) spySocket.connect() expect(channel.state).to(equal(.errored)) receiveSocketOpen() joinPush.trigger("ok", payload: [:]) fakeClock.tick(1.0) expect(channel.state).to(equal(.joined)) expect(spySocket.pushCallCount).to(equal(3)) }) it("with socket delay only", closure: { channel.join() expect(channel.state).to(equal(.joining)) // connect socket after a delay fakeClock.tick(6.0) spySocket.connect() // open Socket after delay fakeClock.tick(5.0) receiveSocketOpen() joinPush.trigger("ok", payload: [:]) joinPush.trigger("ok", payload: [:]) expect(channel.state).to(equal(.joined)) }) } describe("joinPush") { var spySocket: SocketSpy! var joinPush: Push! beforeEach { mockClient.readyState = .open spySocket = SocketSpy(endPoint: "/socket", transport: { _ in return mockClient }) spySocket.connect() channel = Channel(topic: "topic", params: ["one": "two"], socket: spySocket) joinPush = channel.joinPush channel.join() } func receivesOk() { fakeClock.tick(joinPush.timeout / 2) joinPush.trigger("ok", payload: ["a": "b"]) } func receivesTimeout() { fakeClock.tick(joinPush.timeout * 2) } func receiveError() { fakeClock.tick(joinPush.timeout / 2) joinPush.trigger("error", payload: ["a": "b"]) } describe("receives 'ok'", { it("sets channel state to joined", closure: { expect(channel.state).toNot(equal(.joined)) receivesOk() expect(channel.state).to(equal(.joined)) }) it("triggers receive(ok) callback after ok response", closure: { var callbackCallCount: Int = 0 joinPush.receive("ok", callback: {_ in callbackCallCount += 1}) receivesOk() expect(callbackCallCount).to(equal(1)) }) it("triggers receive('ok') callback if ok response already received", closure: { receivesOk() var callbackCallCount: Int = 0 joinPush.receive("ok", callback: {_ in callbackCallCount += 1}) expect(callbackCallCount).to(equal(1)) }) it("does not trigger other receive callbacks after ok response", closure: { var callbackCallCount: Int = 0 joinPush .receive("error", callback: {_ in callbackCallCount += 1}) .receive("timeout", callback: {_ in callbackCallCount += 1}) receivesOk() receivesTimeout() expect(callbackCallCount).to(equal(0)) }) it("clears timeoutTimer workItem", closure: { expect(joinPush.timeoutWorkItem).toNot(beNil()) receivesOk() expect(joinPush.timeoutWorkItem).to(beNil()) }) it("sets receivedMessage", closure: { expect(joinPush.receivedMessage).to(beNil()) receivesOk() expect(joinPush.receivedMessage).toNot(beNil()) expect(joinPush.receivedMessage?.status).to(equal("ok")) expect(joinPush.receivedMessage?.payload["a"] as? String).to(equal("b")) }) it("removes channel binding", closure: { var bindings = getBindings("chan_reply_3") expect(bindings).to(haveCount(1)) receivesOk() bindings = getBindings("chan_reply_3") expect(bindings).to(haveCount(0)) }) it("sets channel state to joined", closure: { receivesOk() expect(channel.state).to(equal(.joined)) }) it("resets channel rejoinTimer", closure: { let mockRejoinTimer = TimeoutTimerMock() channel.rejoinTimer = mockRejoinTimer receivesOk() expect(mockRejoinTimer.resetCallsCount).to(equal(1)) }) it("sends and empties channel's buffered pushEvents", closure: { let mockPush = PushMock(channel: channel, event: "new:msg") channel.pushBuffer.append(mockPush) receivesOk() expect(mockPush.sendCalled).to(beTrue()) expect(channel.pushBuffer).to(haveCount(0)) }) }) describe("receives 'timeout'", { it("sets channel state to errored", closure: { var timeoutReceived = false joinPush.receive("timeout", callback: { (_) in timeoutReceived = true expect(channel.state).to(equal(.errored)) }) receivesTimeout() expect(timeoutReceived).to(beTrue()) }) it("triggers receive('timeout') callback after ok response", closure: { var receiveTimeoutCallCount = 0 joinPush.receive("timeout", callback: { (_) in receiveTimeoutCallCount += 1 }) receivesTimeout() expect(receiveTimeoutCallCount).to(equal(1)) }) it("does not trigger other receive callbacks after timeout response", closure: { var receiveOkCallCount = 0 var receiveErrorCallCount = 0 var timeoutReceived = false joinPush .receive("ok") {_ in receiveOkCallCount += 1 } .receive("error") {_ in receiveErrorCallCount += 1 } .receive("timeout", callback: { (_) in expect(receiveOkCallCount).to(equal(0)) expect(receiveErrorCallCount).to(equal(0)) timeoutReceived = true }) receivesTimeout() receivesOk() expect(timeoutReceived).to(beTrue()) }) it("schedules rejoinTimer timeout", closure: { let mockRejoinTimer = TimeoutTimerMock() channel.rejoinTimer = mockRejoinTimer receivesTimeout() expect(mockRejoinTimer.scheduleTimeoutCalled).to(beTrue()) }) }) describe("receives `error`", { it("triggers receive('error') callback after error response", closure: { expect(channel.state).to(equal(.joining)) var errorCallsCount = 0 joinPush.receive("error") { (_) in errorCallsCount += 1 } receiveError() joinPush.trigger("error", payload: [:]) expect(errorCallsCount).to(equal(1)) }) it("triggers receive('error') callback if error response already received", closure: { receiveError() var errorCallsCount = 0 joinPush.receive("error") { (_) in errorCallsCount += 1 } expect(errorCallsCount).to(equal(1)) }) it("does not trigger other receive callbacks after ok response", closure: { var receiveOkCallCount = 0 var receiveTimeoutCallCount = 0 var receiveErrorCallCount = 0 joinPush .receive("ok") {_ in receiveOkCallCount += 1 } .receive("error", callback: { (_) in receiveErrorCallCount += 1 channel.leave() }) .receive("timeout") {_ in receiveTimeoutCallCount += 1 } receiveError() receivesTimeout() expect(receiveErrorCallCount).to(equal(1)) expect(receiveOkCallCount).to(equal(0)) expect(receiveTimeoutCallCount).to(equal(0)) }) it("clears timeoutTimer workItem", closure: { expect(joinPush.timeoutWorkItem).toNot(beNil()) receiveError() expect(joinPush.timeoutWorkItem).to(beNil()) }) it("sets receivedMessage", closure: { expect(joinPush.receivedMessage).to(beNil()) receiveError() expect(joinPush.receivedMessage).toNot(beNil()) expect(joinPush.receivedMessage?.status).to(equal("error")) expect(joinPush.receivedMessage?.payload["a"] as? String).to(equal("b")) }) it("removes channel binding", closure: { var bindings = getBindings("chan_reply_3") expect(bindings).to(haveCount(1)) receiveError() bindings = getBindings("chan_reply_3") expect(bindings).to(haveCount(0)) }) it("does not sets channel state to joined", closure: { receiveError() expect(channel.state).toNot(equal(.joined)) }) it("does not trigger channel's buffered pushEvents", closure: { let mockPush = PushMock(channel: channel, event: "new:msg") channel.pushBuffer.append(mockPush) receiveError() expect(mockPush.sendCalled).to(beFalse()) expect(channel.pushBuffer).to(haveCount(1)) }) }) } describe("onError") { var spySocket: SocketSpy! var joinPush: Push! beforeEach { mockClient.readyState = .open spySocket = SocketSpy(endPoint: "/socket", transport: { _ in return mockClient }) spySocket.connect() channel = Channel(topic: "topic", params: ["one": "two"], socket: spySocket) joinPush = channel.joinPush channel.join() joinPush.trigger("ok", payload: [:]) } it("does not trigger redundant errors during backoff", closure: { // Spy the channel's Join Push let mockPush = PushMock(channel: channel, event: "event") channel.joinPush = mockPush expect(mockPush.resendCalled).to(beFalse()) channel.trigger(event: ChannelEvent.error) fakeClock.tick(1.0) expect(mockPush.resendCalled).to(beTrue()) expect(mockPush.resendCallsCount).to(equal(1)) channel.trigger(event: "error") fakeClock.tick(1.0) expect(mockPush.resendCallsCount).to(equal(1)) }) describe("while joining") { var mockPush: PushMock! beforeEach { channel = Channel(topic: "topic", params: ["one": "two"], socket: mockSocket) // Spy the channel's Join Push mockPush = PushMock(channel: channel, event: "event") mockPush.ref = "10" channel.joinPush = mockPush channel.state = .joining } it("removes the joinPush message from send buffer") { channel.trigger(event: ChannelEvent.error) expect(mockSocket.removeFromSendBufferRefCalled).to(beTrue()) expect(mockSocket.removeFromSendBufferRefReceivedRef).to(equal("10")) } it("resets the joinPush") { channel.trigger(event: ChannelEvent.error) expect(mockPush.resetCalled).to(beTrue()) } } it("sets channel state to .errored", closure: { expect(channel.state).toNot(equal(.errored)) channel.trigger(event: ChannelEvent.error) expect(channel.state).to(equal(.errored)) }) it("tries to rejoin with backoff", closure: { let mockRejoinTimer = TimeoutTimerMock() channel.rejoinTimer = mockRejoinTimer channel.trigger(event: ChannelEvent.error) expect(mockRejoinTimer.scheduleTimeoutCalled).to(beTrue()) }) it("does not rejoin if channel leaving", closure: { channel.state = .leaving let mockPush = PushMock(channel: channel, event: "event") channel.joinPush = mockPush spySocket.onConnectionError(TestError.stub, response: nil) fakeClock.tick(1.0) expect(mockPush.sendCallsCount).to(equal(0)) fakeClock.tick(2.0) expect(mockPush.sendCallsCount).to(equal(0)) expect(channel.state).to(equal(.leaving)) }) it("does nothing if channel is closed", closure: { channel.state = .closed let mockPush = PushMock(channel: channel, event: "event") channel.joinPush = mockPush spySocket.onConnectionError(TestError.stub, response: nil) fakeClock.tick(1.0) expect(mockPush.sendCallsCount).to(equal(0)) fakeClock.tick(2.0) expect(mockPush.sendCallsCount).to(equal(0)) expect(channel.state).to(equal(.closed)) }) it("triggers additional callbacks", closure: { var onErrorCallCount = 0 channel.onError({ (_) in onErrorCallCount += 1 }) joinPush.trigger("ok", payload: [:]) expect(channel.state).to(equal(.joined)) expect(onErrorCallCount).to(equal(0)) channel.trigger(event: ChannelEvent.error) expect(onErrorCallCount).to(equal(1)) }) } describe("onClose") { beforeEach { mockClient.readyState = .open channel.join() } it("sets state to closed", closure: { expect(channel.state).toNot(equal(.closed)) channel.trigger(event: ChannelEvent.close) expect(channel.state).to(equal(.closed)) }) it("does not rejoin", closure: { let mockJoinPush = PushMock(channel: channel, event: "phx_join") channel.joinPush = mockJoinPush channel.trigger(event: ChannelEvent.close) fakeClock.tick(1.0) expect(mockJoinPush.sendCalled).to(beFalse()) fakeClock.tick(2.0) expect(mockJoinPush.sendCalled).to(beFalse()) }) it("resets the rejoin timer", closure: { let mockRejoinTimer = TimeoutTimerMock() channel.rejoinTimer = mockRejoinTimer channel.trigger(event: ChannelEvent.close) expect(mockRejoinTimer.resetCalled).to(beTrue()) }) it("removes self from socket", closure: { channel.trigger(event: ChannelEvent.close) expect(mockSocket.removeCalled).to(beTrue()) let removedChannel = mockSocket.removeReceivedChannel expect(removedChannel === channel).to(beTrue()) }) it("triggers additional callbacks", closure: { var onCloseCallCount = 0 channel.onClose({ (_) in onCloseCallCount += 1 }) channel.trigger(event: ChannelEvent.close) expect(onCloseCallCount).to(equal(1)) }) } describe("canPush") { it("returns true when socket connected and channel joined", closure: { channel.state = .joined mockClient.readyState = .open expect(channel.canPush).to(beTrue()) }) it("otherwise returns false", closure: { channel.state = .joined mockClient.readyState = .closed expect(channel.canPush).to(beFalse()) channel.state = .joining mockClient.readyState = .open expect(channel.canPush).to(beFalse()) channel.state = .joining mockClient.readyState = .closed expect(channel.canPush).to(beFalse()) }) } describe("on") { beforeEach { mockSocket.makeRefClosure = nil mockSocket.makeRefReturnValue = kDefaultRef } it("sets up callback for event", closure: { var onCallCount = 0 channel.trigger(event: "event", ref: kDefaultRef) expect(onCallCount).to(equal(0)) channel.on("event", callback: { (_) in onCallCount += 1 }) channel.trigger(event: "event", ref: kDefaultRef) expect(onCallCount).to(equal(1)) }) it("other event callbacks are ignored", closure: { var onCallCount = 0 let ignoredOnCallCount = 0 channel.trigger(event: "event", ref: kDefaultRef) expect(ignoredOnCallCount).to(equal(0)) channel.on("event", callback: { (_) in onCallCount += 1 }) channel.trigger(event: "event", ref: kDefaultRef) expect(ignoredOnCallCount).to(equal(0)) }) it("generates unique refs for callbacks ", closure: { let ref1 = channel.on("event1", callback: { _ in }) let ref2 = channel.on("event2", callback: { _ in }) expect(ref1).toNot(equal(ref2)) expect(ref1 + 1).to(equal(ref2)) }) } describe("off") { beforeEach { mockSocket.makeRefClosure = nil mockSocket.makeRefReturnValue = kDefaultRef } it("removes all callbacks for event", closure: { var callCount1 = 0 var callCount2 = 0 var callCount3 = 0 channel.on("event", callback: { _ in callCount1 += 1}) channel.on("event", callback: { _ in callCount2 += 1}) channel.on("other", callback: { _ in callCount3 += 1}) channel.off("event") channel.trigger(event: "event", ref: kDefaultRef) channel.trigger(event: "other", ref: kDefaultRef) expect(callCount1).to(equal(0)) expect(callCount2).to(equal(0)) expect(callCount3).to(equal(1)) }) it("removes callback by ref", closure: { var callCount1 = 0 var callCount2 = 0 let ref1 = channel.on("event", callback: { _ in callCount1 += 1}) let _ = channel.on("event", callback: { _ in callCount2 += 1}) channel.off("event", ref: ref1) channel.trigger(event: "event", ref: kDefaultRef) expect(callCount1).to(equal(0)) expect(callCount2).to(equal(1)) }) } describe("push") { beforeEach { mockSocket.makeRefClosure = nil mockSocket.makeRefReturnValue = kDefaultRef mockClient.readyState = .open } it("sends push event when successfully joined", closure: { channel.join().trigger("ok", payload: [:]) channel.push("event", payload: ["foo": "bar"]) expect(mockSocket.pushTopicEventPayloadRefJoinRefCalled).to(beTrue()) let args = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args?.topic).to(equal("topic")) expect(args?.event).to(equal("event")) expect(args?.payload["foo"] as? String).to(equal("bar")) expect(args?.joinRef).to(equal(channel.joinRef)) expect(args?.ref).to(equal(kDefaultRef)) }) it("enqueues push event to be sent once join has succeeded", closure: { let joinPush = channel.join() channel.push("event", payload: ["foo": "bar"]) let args = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args?.payload["foo"]).to(beNil()) fakeClock.tick(channel.timeout / 2) joinPush.trigger("ok", payload: [:]) expect(mockSocket.pushTopicEventPayloadRefJoinRefCalled).to(beTrue()) let args2 = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args2?.payload["foo"] as? String).to(equal("bar")) }) it("does not push if channel join times out", closure: { let joinPush = channel.join() channel.push("event", payload: ["foo": "bar"]) let args = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args?.payload["foo"]).to(beNil()) fakeClock.tick(channel.timeout * 2) joinPush.trigger("ok", payload: [:]) expect(mockSocket.pushTopicEventPayloadRefJoinRefCalled).to(beTrue()) let args2 = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args2?.payload["foo"]).to(beNil()) }) it("uses channel timeout by default", closure: { channel.join().trigger("ok", payload: [:]) var timeoutCallsCount = 0 channel .push("event", payload: ["foo": "bar"]) .receive("timeout", callback: { (_) in timeoutCallsCount += 1 }) fakeClock.tick(channel.timeout / 2) expect(timeoutCallsCount).to(equal(0)) fakeClock.tick(channel.timeout) expect(timeoutCallsCount).to(equal(1)) }) it("accepts timeout arg", closure: { channel.join().trigger("ok", payload: [:]) var timeoutCallsCount = 0 channel .push("event", payload: ["foo": "bar"], timeout: channel.timeout * 2) .receive("timeout", callback: { (_) in timeoutCallsCount += 1 }) fakeClock.tick(channel.timeout) expect(timeoutCallsCount).to(equal(0)) fakeClock.tick(channel.timeout * 2) expect(timeoutCallsCount).to(equal(1)) }) it("does not time out after receiving 'ok'", closure: { channel.join().trigger("ok", payload: [:]) var timeoutCallsCount = 0 let push = channel.push("event", payload: ["foo": "bar"]) push.receive("timeout", callback: { (_) in timeoutCallsCount += 1 }) fakeClock.tick(channel.timeout / 2) expect(timeoutCallsCount).to(equal(0)) push.trigger("ok", payload: [:]) fakeClock.tick(channel.timeout) expect(timeoutCallsCount).to(equal(0)) }) it("throws if channel has not been joined", closure: { expect { channel.push("event", payload: [:]) }.to(throwAssertion()) }) } describe("leave") { beforeEach { mockClient.readyState = .open channel.join().trigger("ok", payload: [:]) } it("unsubscribes from server events", closure: { mockSocket.makeRefClosure = nil mockSocket.makeRefReturnValue = kDefaultRef let joinRef = channel.joinRef channel.leave() expect(mockSocket.pushTopicEventPayloadRefJoinRefCalled).to(beTrue()) let args = mockSocket.pushTopicEventPayloadRefJoinRefReceivedArguments expect(args?.topic).to(equal("topic")) expect(args?.event).to(equal("phx_leave")) expect(args?.payload).to(beEmpty()) expect(args?.joinRef).to(equal(joinRef)) expect(args?.ref).to(equal(kDefaultRef)) }) it("closes channel on 'ok' from server", closure: { let socket = Socket(endPoint: "/socket", transport: { _ in return mockClient }) let channel = socket.channel("topic", params: ["one": "two"]) channel.join().trigger("ok", payload: [:]) let anotherChannel = socket.channel("another", params: ["three": "four"]) expect(socket.channels).to(haveCount(2)) channel.leave().trigger("ok", payload: [:]) expect(socket.channels).to(haveCount(1)) expect(socket.channels.first === anotherChannel).to(beTrue()) }) } describe("isMemeber") { it("returns false if the message topic does not match the channel") { let message = Message(topic: "other") expect(channel.isMember(message)).to(beFalse()) } it("returns true if topics match but the message doesn't have a join ref") { let message = Message(topic: "topic", event: ChannelEvent.close, joinRef: nil) expect(channel.isMember(message)).to(beTrue()) } it("returns true if topics and join refs match") { channel.joinPush.ref = "2" let message = Message(topic: "topic", event: ChannelEvent.close, joinRef: "2") expect(channel.isMember(message)).to(beTrue()) } it("returns true if topics and join refs match but event is not lifecycle") { channel.joinPush.ref = "2" let message = Message(topic: "topic", event: "event", joinRef: "2") expect(channel.isMember(message)).to(beTrue()) } it("returns false topics match and is a lifecycle event but join refs do not match ") { channel.joinPush.ref = "2" let message = Message(topic: "topic", event: ChannelEvent.close, joinRef: "1") expect(channel.isMember(message)).to(beFalse()) } } describe("isClosed") { it("returns true if state is .closed", closure: { channel.state = .joined expect(channel.isClosed).to(beFalse()) channel.state = .closed expect(channel.isClosed).to(beTrue()) }) } describe("isErrored") { it("returns true if state is .errored", closure: { channel.state = .joined expect(channel.isErrored).to(beFalse()) channel.state = .errored expect(channel.isErrored).to(beTrue()) }) } describe("isJoined") { it("returns true if state is .joined", closure: { channel.state = .leaving expect(channel.isJoined).to(beFalse()) channel.state = .joined expect(channel.isJoined).to(beTrue()) }) } describe("isJoining") { it("returns true if state is .joining", closure: { channel.state = .joined expect(channel.isJoining).to(beFalse()) channel.state = .joining expect(channel.isJoining).to(beTrue()) }) } describe("isLeaving") { it("returns true if state is .leaving", closure: { channel.state = .joined expect(channel.isLeaving).to(beFalse()) channel.state = .leaving expect(channel.isLeaving).to(beTrue()) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/DefaultSerializerSpec.swift ================================================ // // DefaultSerializerSpec.swift // SwiftPhoenixClient // // Created by Daniel Rees on 1/17/19. // import Quick import Nimble @testable import SwiftPhoenixClient class DefaultSerializerSpec: QuickSpec { override func spec() { describe("encode and decode message") { it("converts dictionary to Data and back to Message", closure: { let body: [Any] = ["join_ref", "ref", "topic", "event", ["user_id": "abc123"]] let data = Defaults.encode(body) expect(String(data: data, encoding: .utf8)).to(equal("[\"join_ref\",\"ref\",\"topic\",\"event\",{\"user_id\":\"abc123\"}]")) let json = Defaults.decode(data) as? [Any] let message = Message(json: json!) expect(message?.ref).to(equal("ref")) expect(message?.joinRef).to(equal("join_ref")) expect(message?.topic).to(equal("topic")) expect(message?.event).to(equal("event")) expect(message?.payload["user_id"] as? String).to(equal("abc123")) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/HeartbeatTimerSpec.swift ================================================ // // HeartbeatTimerSpec.swift // SwiftPhoenixClientTests // // Created by Daniel Rees on 8/24/21. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import Quick import Nimble @testable import SwiftPhoenixClient class HeartbeatTimerSpec: QuickSpec { override func spec() { let queue = DispatchQueue(label: "heartbeat.timer.spec") var timer: HeartbeatTimer! beforeEach { timer = HeartbeatTimer(timeInterval: 10, queue: queue) } describe("isValid") { it("returns false if is not started") { expect(timer.isValid).to(beFalse()) } it("returns true if the timer has started") { timer.start { /* no-op */ } expect(timer.isValid).to(beTrue()) } it("returns false if timer has been stopped") { timer.start { /* no-op */ } timer.stop() expect(timer.isValid).to(beFalse()) } } describe("fire") { it("calls the event handler") { var timerCalled = 0 timer.start { timerCalled += 1 } expect(timerCalled).to(equal(0)) timer.fire() expect(timerCalled).to(equal(1)) } it("does not call event handler if stopped") { var timerCalled = 0 timer.start { timerCalled += 1 } expect(timerCalled).to(equal(0)) timer.stop() timer.fire() expect(timerCalled).to(equal(0)) } } describe("equatable") { it("equates different timers correctly", closure: { let timerA = HeartbeatTimer(timeInterval: 10, queue: queue) let timerB = HeartbeatTimer(timeInterval: 10, queue: queue) expect(timerA).to(equal(timerA)) expect(timerA).toNot(equal(timerB)) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/MessageSpec.swift ================================================ // // MessageSpec.swift // SwiftPhoenixClientTests // // Created by Daniel Rees on 10/27/21. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import Quick import Nimble @testable import SwiftPhoenixClient class MessageSpec: QuickSpec { override func spec() { describe("json parsing") { it("parses a normal message") { let json: [Any] = ["2", "6", "my-topic", "update", ["user": "James S.", "message": "This is a test"]] let message = Message(json: json) expect(message?.ref).to(equal("6")) expect(message?.joinRef).to(equal("2")) expect(message?.topic).to(equal("my-topic")) expect(message?.event).to(equal("update")) expect(message?.payload["user"] as? String).to(equal("James S.")) expect(message?.payload["message"] as? String).to(equal("This is a test")) expect(message?.status).to(beNil()) } it("parses a reply") { let json: [Any] = ["2", "6", "my-topic", "phx_reply", ["response": ["user": "James S.", "message": "This is a test"], "status": "ok"]] let message = Message(json: json) expect(message?.ref).to(equal("6")) expect(message?.joinRef).to(equal("2")) expect(message?.topic).to(equal("my-topic")) expect(message?.event).to(equal("phx_reply")) expect(message?.payload["user"] as? String).to(equal("James S.")) expect(message?.payload["message"] as? String).to(equal("This is a test")) expect(message?.status).to(equal("ok")) } } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/PresenceSpec.swift ================================================ // // PresenceSpec.swift // SwiftPhoenixClient // // Created by Simon Bergström on 2018-10-03. // import Quick import Nimble @testable import SwiftPhoenixClient class PresenceSpec: QuickSpec { override func spec() { /// Fixtures let fixJoins: Presence.State = ["u1": ["metas": [["id":1, "phx_ref": "1.2"]]]] let fixLeaves: Presence.State = ["u2": ["metas": [["id":2, "phx_ref": "2"]]]] let fixState: Presence.State = [ "u1": ["metas": [["id":1, "phx_ref": "1"]]], "u2": ["metas": [["id":2, "phx_ref": "2"]]], "u3": ["metas": [["id":3, "phx_ref": "3"]]] ] let listByFirst: (_ key: String, _ presence: Presence.Map) -> Presence.Meta = { key, pres in return pres["metas"]!.first! } /// Mocks var mockSocket: SocketMock! var channel: Channel! /// UUT var presence: Presence! beforeEach { let mockTransport = PhoenixTransportMock() mockSocket = SocketMock(endPoint: "/socket", transport: { _ in mockTransport }) mockSocket.timeout = 10.0 mockSocket.makeRefReturnValue = "1" mockSocket.reconnectAfter = { _ in return 1 } channel = Channel(topic: "topic", params: [:], socket: mockSocket) channel.joinPush.ref = "1" presence = Presence(channel: channel) } describe("init") { it("sets defaults", closure: { expect(presence.state).to(beEmpty()) expect(presence.pendingDiffs).to(beEmpty()) expect(presence.channel === channel).to(beTrue()) expect(presence.joinRef).to(beNil()) }) it("binds to channel with default options", closure: { expect(presence.channel?.getBindings("presence_state")).to(haveCount(1)) expect(presence.channel?.getBindings("presence_diff")).to(haveCount(1)) }) it("binds to channel with custom options ", closure: { let channel = Channel(topic: "topic", socket: mockSocket) let customOptions = Presence.Options(events: [.state: "custom_state", .diff: "custom_diff"]) let p = Presence(channel: channel, opts: customOptions) expect(p.channel?.getBindings("presence_state")).to(beEmpty()) expect(p.channel?.getBindings("presence_diff")).to(beEmpty()) expect(p.channel?.getBindings("custom_state")).to(haveCount(1)) expect(p.channel?.getBindings("custom_diff")).to(haveCount(1)) }) it("syncs state and diffs", closure: { let user1: Presence.Map = ["metas": [["id": 1, "phx_ref": "1"]]] let user2: Presence.Map = ["metas": [["id": 2, "phx_ref": "2"]]] let newState: Presence.State = ["u1": user1, "u2": user2] channel.trigger(event: "presence_state", payload: newState, ref: "1") let s = presence.list(by: listByFirst) expect(s).to(haveCount(2)) // can't check values because maps are lazy // expect(s[0]["id"] as? Int).to(equal(1)) // expect(s[0]["phx_ref"] as? String).to(equal("1")) // // expect(s[1]["id"] as? Int).to(equal(2)) // expect(s[1]["phx_ref"] as? String).to(equal("2")) channel.trigger(event: "presence_diff", payload: ["joins": [:], "leaves": ["u1": user1]], ref: "2") let l = presence.list(by: listByFirst) expect(l).to(haveCount(1)) expect(l[0]["id"] as? Int).to(equal(2)) expect(l[0]["phx_ref"] as? String).to(equal("2")) }) it("applies pending diff if state is not yet synced", closure: { var onJoins: [(id: String, current: Presence.Map?, new: Presence.Map)] = [] var onLeaves: [(id: String, current: Presence.Map, left: Presence.Map)] = [] presence.onJoin({ (key, current, new) in onJoins.append((key, current, new)) }) presence.onLeave({ (key, current, left) in onLeaves.append((key, current, left)) }) let user1 = ["metas": [["id": 1, "phx_ref": "1"]]] let user2 = ["metas": [["id": 2, "phx_ref": "2"]]] let user3 = ["metas": [["id": 3, "phx_ref": "3"]]] let newState = ["u1": user1, "u2": user2] let leaves = ["u2": user2] let payload1 = ["joins": [:], "leaves": leaves] channel.trigger(event: "presence_diff", payload: payload1, ref: "") // there is no state expect(presence.list(by: listByFirst)).to(beEmpty()) // pending diffs 1 expect(presence.pendingDiffs).to(haveCount(1)) expect(presence.pendingDiffs[0]["joins"]).to(beEmpty()) let t1 = transform(presence.pendingDiffs[0]["leaves"]!, and: leaves) expect(t1.lhs).to(equal(t1.rhs)) channel.trigger(event: "presence_state", payload: newState, ref: "") expect(onLeaves).to(haveCount(1)) expect(onLeaves[0].id).to(equal("u2")) expect(onLeaves[0].current["metas"]).to(beEmpty()) expect(onLeaves[0].left["metas"]?[0]["id"] as? Int).to(equal(2)) let s = presence.list(by: listByFirst) expect(s).to(haveCount(1)) expect(s[0]["id"] as? Int).to(equal(1)) expect(s[0]["phx_ref"] as? String).to(equal("1")) expect(presence.pendingDiffs).to(beEmpty()) expect(onJoins).to(haveCount(2)) // can't check values because maps are lazy // expect(onJoins[0].id).to(equal("u1")) // expect(onJoins[0].current).to(beNil()) // expect(onJoins[0].new["metas"]?[0]["id"] as? Int).to(equal(1)) // // expect(onJoins[1].id).to(equal("u2")) // expect(onJoins[1].current).to(beNil()) // expect(onJoins[1].new["metas"]?[0]["id"] as? Int).to(equal(2)) // disconnect then reconnect expect(presence.isPendingSyncState).to(beFalse()) channel.joinPush.ref = "2" expect(presence.isPendingSyncState).to(beTrue()) channel.trigger(event: "presence_diff", payload: ["joins": [:], "leaves": ["u1": user1]], ref: "") let d = presence.list(by: listByFirst) expect(d).to(haveCount(1)) expect(d[0]["id"] as? Int).to(equal(1)) expect(d[0]["phx_ref"] as? String).to(equal("1")) channel.trigger(event: "presence_state", payload: ["u1": user1, "u3": user3], ref: "") let s2 = presence.list(by: listByFirst) expect(s2).to(haveCount(1)) expect(s2[0]["id"] as? Int).to(equal(3)) expect(s2[0]["phx_ref"] as? String).to(equal("3")) }) it("allows custom states", closure: { let channel = Channel(topic: "topic", socket: mockSocket) channel.joinPush.ref = "1" let customOptions = Presence.Options(events: [.state: "the_state", .diff: "the_diff"]) let presence = Presence(channel: channel, opts: customOptions) let user1: Presence.Map = ["metas": [["id": 1, "phx_ref": "1"]]] channel.trigger(event: "the_state", payload: ["user1": user1], ref: "") let s = presence.list(by: listByFirst) expect(s).to(haveCount(1)) expect(s[0]["id"] as? Int).to(equal(1)) expect(s[0]["phx_ref"] as? String).to(equal("1")) channel.trigger(event: "the_diff", payload: ["joins": [:], "leaves": ["user1": user1]], ref: "2") expect(presence.list(by: listByFirst)).to(beEmpty()) }) } describe("syncState") { it("syncs empty state", closure: { let newState: Presence.State = ["u1": ["metas": [["id":1, "phx_ref": "1"]]]] var state: Presence.State = [:] let stateBefore = state Presence.syncState(state, newState: newState) let t1 = transform(state, and: stateBefore) expect(t1.lhs).to(equal(t1.rhs)) state = Presence.syncState(state, newState: newState) let t2 = transform(state, and: newState) expect(t2.lhs).to(equal(t2.rhs)) }) it("onJoins new presences and onLeave's left presences", closure: { let newState = fixState var state = ["u4": ["metas": [["id":4, "phx_ref": "4"]]]] var joined: Presence.Diff = [:] var left: Presence.Diff = [:] let onJoin: Presence.OnJoin = { key, current, newPres in var state: Presence.State = ["newPres": newPres] if let c = current { state["current"] = c } // Diff = [String: Presence.State] joined[key] = state } let onLeave: Presence.OnLeave = { key, current, leftPres in // Diff = [String: Presence.State] left[key] = ["current": current, "leftPres": leftPres] } let stateBefore = state Presence.syncState(state, newState: newState, onJoin: onJoin, onLeave: onLeave) let t1 = transform(state, and: stateBefore) expect(t1.lhs).to(equal(t1.rhs)) state = Presence.syncState(state, newState: newState, onJoin: onJoin, onLeave: onLeave) let t2 = transform(state, and: newState) expect(t2.lhs).to(equal(t2.rhs)) // assert equality in joined let joinedExpectation: Presence.Diff = [ "u1": ["newPres": ["metas": [["id":1, "phx_ref": "1"]] ]], "u2": ["newPres": ["metas": [["id":2, "phx_ref": "2"]] ]], "u3": ["newPres": ["metas": [["id":3, "phx_ref": "3"]] ]] ] let tJoin = transform(joined, and: joinedExpectation) expect(tJoin.lhs).to(equal(tJoin.rhs)) // assert equality in left let leftExpectation: Presence.Diff = ["u4": [ "current": ["metas": [] ], "leftPres": ["metas": [["id":4, "phx_ref": "4"]] ] ] ] let tLeft = transform(left, and: leftExpectation) expect(tLeft.lhs).to(equal(tLeft.rhs)) }) it("onJoins only newly added metas", closure: { var state = ["u3": ["metas": [["id":3, "phx_ref": "3"]]]] let newState = ["u3": [ "metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.new"]] ]] var joined: Presence.Diff = [:] var left: Presence.Diff = [:] let onJoin: Presence.OnJoin = { key, current, newPres in var state: Presence.State = ["newPres": newPres] if let c = current { state["current"] = c } // Diff = [String: Presence.State] joined[key] = state } let onLeave: Presence.OnLeave = { key, current, leftPres in // Diff = [String: Presence.State] left[key] = ["current": current, "leftPres": leftPres] } state = Presence.syncState(state, newState: newState, onJoin: onJoin, onLeave: onLeave) let t2 = transform(state, and: newState) expect(t2.lhs).to(equal(t2.rhs)) // assert equality in joined let joinedExpectation: Presence.Diff = [ "u3": ["current": ["metas": [["id":3, "phx_ref": "3"]] ], "newPres": ["metas": [["id":3, "phx_ref": "3.new"]] ] ] ] let tJoin = transform(joined, and: joinedExpectation) expect(tJoin.lhs).to(equal(tJoin.rhs)) // assert equality in left expect(left).to(beEmpty()) }) it("onLeaves only newly removed metas", closure: { let newState = ["u3": ["metas": [["id":3, "phx_ref": "3"]]]] var state = ["u3": [ "metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.left"]] ]] var joined: Presence.Diff = [:] var left: Presence.Diff = [:] let onJoin: Presence.OnJoin = { key, current, newPres in var state: Presence.State = ["newPres": newPres] if let c = current { state["current"] = c } // Diff = [String: Presence.State] joined[key] = state } let onLeave: Presence.OnLeave = { key, current, leftPres in // Diff = [String: Presence.State] left[key] = ["current": current, "leftPres": leftPres] } state = Presence.syncState(state, newState: newState, onJoin: onJoin, onLeave: onLeave) let t2 = transform(state, and: newState) expect(t2.lhs).to(equal(t2.rhs)) // assert equality in joined let leftExpectation: Presence.Diff = [ "u3": ["current": ["metas": [["id":3, "phx_ref": "3"]] ], "leftPres": ["metas": [["id":3, "phx_ref": "3.left"]] ] ] ] let tLeft = transform(left, and: leftExpectation) expect(tLeft.lhs).to(equal(tLeft.rhs)) // assert equality in left expect(joined).to(beEmpty()) }) it("syncs both joined and left metas", closure: { let newState = ["u3": [ "metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.new"]] ]] var state = ["u3": [ "metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.left"]] ]] var joined: Presence.Diff = [:] var left: Presence.Diff = [:] let onJoin: Presence.OnJoin = { key, current, newPres in var state: Presence.State = ["newPres": newPres] if let c = current { state["current"] = c } // Diff = [String: Presence.State] joined[key] = state } let onLeave: Presence.OnLeave = { key, current, leftPres in // Diff = [String: Presence.State] left[key] = ["current": current, "leftPres": leftPres] } state = Presence.syncState(state, newState: newState, onJoin: onJoin, onLeave: onLeave) let t2 = transform(state, and: newState) expect(t2.lhs).to(equal(t2.rhs)) // assert equality in joined let joinedExpectation: Presence.Diff = [ "u3": ["current": ["metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.left"]] ], "newPres": ["metas": [["id":3, "phx_ref": "3.new"]] ] ] ] let tJoin = transform(joined, and: joinedExpectation) expect(tJoin.lhs).to(equal(tJoin.rhs)) // assert equality in left let leftExpectation: Presence.Diff = [ "u3": ["current": ["metas": [["id":3, "phx_ref": "3"], ["id":3, "phx_ref": "3.new"]] ], "leftPres": ["metas": [["id":3, "phx_ref": "3.left"]] ] ] ] let tLeft = transform(left, and: leftExpectation) expect(tLeft.lhs).to(equal(tLeft.rhs)) }) } describe("syncDiff") { it("syncs empty state", closure: { let joins: Presence.State = ["u1": ["metas": [["id":1, "phx_ref": "1"]]]] var state: Presence.State = [:] Presence.syncDiff(state, diff: ["joins": joins, "leaves": [:]]) expect(state).to(beEmpty()) state = Presence.syncDiff(state, diff: ["joins": joins, "leaves": [:]]) let t1 = transform(state, and: joins) expect(t1.lhs).to(equal(t1.rhs)) }) it("removes presence when meta is empty and adds additional meta", closure: { var state = fixState let diff: Presence.Diff = ["joins": fixJoins, "leaves": fixLeaves] state = Presence.syncDiff(state, diff: diff) let expectation: Presence.State = [ "u1": ["metas": [ ["id":1, "phx_ref": "1"], ["id":1, "phx_ref": "1.2"]] ], "u3": ["metas": [["id":3, "phx_ref": "3"]] ] ] let t1 = transform(state, and: expectation) expect(t1.lhs).to(equal(t1.rhs)) }) it("removes meta while leaving key if other metas exist", closure: { var state: Presence.State = [ "u1": ["metas": [ ["id":1, "phx_ref": "1"], ["id":1, "phx_ref": "1.2"]] ]] let leaves: Presence.State = ["u1": ["metas": [["id":1, "phx_ref": "1"]]]] let diff: Presence.Diff = ["joins": [:], "leaves": leaves] state = Presence.syncDiff(state, diff: diff) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/SocketSpec.swift ================================================ // // SocketSpec.swift // SwiftPhoenixClient // // Created by Daniel Rees on 2/10/18. // import Quick import Nimble @testable import SwiftPhoenixClient @available(iOS 13, *) class SocketSpec: QuickSpec { let encode = Defaults.encode let decode = Defaults.decode override func spec() { describe("constructor") { it("sets defaults", closure: { let socket = Socket("wss://localhost:4000/socket") expect(socket.channels).to(haveCount(0)) expect(socket.sendBuffer).to(haveCount(0)) expect(socket.ref).to(equal(0)) expect(socket.endPoint).to(equal("wss://localhost:4000/socket")) expect(socket.stateChangeCallbacks.open).to(beEmpty()) expect(socket.stateChangeCallbacks.close).to(beEmpty()) expect(socket.stateChangeCallbacks.error).to(beEmpty()) expect(socket.stateChangeCallbacks.message).to(beEmpty()) expect(socket.timeout).to(equal(Defaults.timeoutInterval)) expect(socket.heartbeatInterval).to(equal(Defaults.heartbeatInterval)) expect(socket.logger).to(beNil()) expect(socket.reconnectAfter(1)).to(equal(0.010)) // 10ms expect(socket.reconnectAfter(2)).to(equal(0.050)) // 50ms expect(socket.reconnectAfter(3)).to(equal(0.100)) // 100ms expect(socket.reconnectAfter(4)).to(equal(0.150)) // 150ms expect(socket.reconnectAfter(5)).to(equal(0.200)) // 200ms expect(socket.reconnectAfter(6)).to(equal(0.250)) // 250ms expect(socket.reconnectAfter(7)).to(equal(0.500)) // 500ms expect(socket.reconnectAfter(8)).to(equal(1.000)) // 1_000ms (1s) expect(socket.reconnectAfter(9)).to(equal(2.000)) // 2_000ms (2s) expect(socket.reconnectAfter(10)).to(equal(5.00)) // 5_000ms (5s) expect(socket.reconnectAfter(11)).to(equal(5.00)) // 5_000ms (5s) }) it("overrides some defaults", closure: { let socket = Socket("wss://localhost:4000/socket", paramsClosure: { ["one": 2] }) socket.timeout = 40000 socket.heartbeatInterval = 60000 socket.logger = { _ in } socket.reconnectAfter = { _ in return 10 } expect(socket.timeout).to(equal(40000)) expect(socket.heartbeatInterval).to(equal(60000)) expect(socket.logger).toNot(beNil()) expect(socket.reconnectAfter(1)).to(equal(10)) expect(socket.reconnectAfter(2)).to(equal(10)) }) // it("sets queue for underlying transport", closure: { // let socket = Socket(endPoint: "wss://localhost:4000/socket", transport: { (url) -> WebSocketClient in // let webSocket = WebSocket(url: url) // webSocket.callbackQueue = DispatchQueue(label: "test_queue") // return webSocket // }) // socket.timeout = 40000 // socket.heartbeatInterval = 60000 // socket.logger = { _ in } // socket.reconnectAfter = { _ in return 10 } // socket.connect() // expect(socket.connection).to(beAKindOf(Transport.self)) // expect((socket.connection as! WebSocket).callbackQueue.label).to(equal("test_queue")) // }) it("should construct a valid URL", closure: { // test vsn expect(Socket("http://localhost:4000/socket/websocket", paramsClosure: { ["token": "abc123"] }, vsn: "1.0.0") .endPointUrl .absoluteString) .to(equal("http://localhost:4000/socket/websocket?vsn=1.0.0&token=abc123")) // test params expect(Socket("http://localhost:4000/socket/websocket", paramsClosure: { ["token": "abc123"] }) .endPointUrl .absoluteString) .to(equal("http://localhost:4000/socket/websocket?vsn=2.0.0&token=abc123")) expect(Socket("ws://localhost:4000/socket/websocket", paramsClosure: { ["token": "abc123", "user_id": 1] }) .endPointUrl .absoluteString) .to(satisfyAnyOf( // absoluteString does not seem to return a string with the params in a deterministic order equal("ws://localhost:4000/socket/websocket?vsn=2.0.0&token=abc123&user_id=1"), equal("ws://localhost:4000/socket/websocket?vsn=2.0.0&user_id=1&token=abc123") ) ) // test params with spaces expect(Socket("ws://localhost:4000/socket/websocket", paramsClosure: { ["token": "abc 123", "user_id": 1] }) .endPointUrl .absoluteString) .to(satisfyAnyOf( // absoluteString does not seem to return a string with the params in a deterministic order equal("ws://localhost:4000/socket/websocket?vsn=2.0.0&token=abc%20123&user_id=1"), equal("ws://localhost:4000/socket/websocket?vsn=2.0.0&user_id=1&token=abc%20123") ) ) }) it("should not introduce any retain cycles", closure: { // Must remain as a weak var in order to deallocate the socket. This tests that the // reconnect timer does not old on to the Socket causing a memory leak. weak var socket = Socket("http://localhost:4000/socket/websocket") expect(socket).to(beNil()) }) } describe("params") { it("changes dynamically with a closure") { var authToken = "abc123" let socket = Socket("ws://localhost:4000/socket/websocket", paramsClosure: { ["token": authToken] }) expect(socket.params?["token"] as? String).to(equal("abc123")) authToken = "xyz987" expect(socket.params?["token"] as? String).to(equal("xyz987")) } } describe("websocketProtocol") { it("returns wss when protocol is https", closure: { let socket = Socket("https://example.com/") expect(socket.websocketProtocol).to(equal("wss")) }) it("returns wss when protocol is wss", closure: { let socket = Socket("wss://example.com/") expect(socket.websocketProtocol).to(equal("wss")) }) it("returns ws when protocol is http", closure: { let socket = Socket("http://example.com/") expect(socket.websocketProtocol).to(equal("ws")) }) it("returns ws when protocol is ws", closure: { let socket = Socket("ws://example.com/") expect(socket.websocketProtocol).to(equal("ws")) }) it("returns empty if there is no scheme", closure: { let socket = Socket("example.com/") expect(socket.websocketProtocol).to(beEmpty()) }) } // describe("endPointUrl") { // it("does nothing with the url", closure: { // let socket = Socket("http://example.com/websocket") // expect(socket.endPointUrl.absoluteString).to(equal("http://example.com/websocket")) // }) // // it("appends /websocket correctly", closure: { // let socketA = Socket("wss://example.org/chat/") // expect(socketA.endPointUrl.absoluteString).to(equal("wss://example.org/chat/websocket")) // // let socketB = Socket("ws://example.org/chat") // expect(socketB.endPointUrl.absoluteString).to(equal("ws://example.org/chat/websocket")) // }) // } describe("connect with Websocket") { // Mocks var mockWebSocket: PhoenixTransportMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) socket.skipHeartbeat = true } it("establishes websocket connection with endpoint", closure: { socket.connect() expect(socket.connection).toNot(beNil()) }) it("set callbacks for connection", closure: { var open = 0 socket.onOpen { open += 1 } var close = 0 socket.onClose { close += 1 } var lastError: (Error, URLResponse?)? socket.onError(callback: { (error) in lastError = error }) var lastMessage: Message? socket.onMessage(callback: { (message) in lastMessage = message }) mockWebSocket.readyState = .closed socket.connect() mockWebSocket.delegate?.onOpen() expect(open).to(equal(1)) mockWebSocket.delegate?.onClose(code: 1000, reason: nil) expect(close).to(equal(1)) mockWebSocket.delegate?.onError(error: TestError.stub, response: nil) expect(lastError).toNot(beNil()) let data: [Any] = ["2", "6", "topic", "event", ["response": ["go": true], "status": "ok"]] let text = toWebSocketText(data: data) mockWebSocket.delegate?.onMessage(message: text) expect(lastMessage?.payload["go"] as? Bool).to(beTrue()) }) it("removes callbacks", closure: { var open = 0 socket.onOpen { open += 1 } var close = 0 socket.onClose { close += 1 } var lastError: (Error, URLResponse?)? socket.onError(callback: { (error) in lastError = error }) var lastMessage: Message? socket.onMessage(callback: { (message) in lastMessage = message }) mockWebSocket.readyState = .closed socket.releaseCallbacks() socket.connect() mockWebSocket.delegate?.onOpen() expect(open).to(equal(0)) mockWebSocket.delegate?.onClose(code: 1000, reason: nil) expect(close).to(equal(0)) mockWebSocket.delegate?.onError(error: TestError.stub, response: nil) expect(lastError).to(beNil()) let data: [Any] = ["2", "6", "topic", "event", ["response": ["go": true], "status": "ok"]] let text = toWebSocketText(data: data) mockWebSocket.delegate?.onMessage(message: text) expect(lastMessage).to(beNil()) }) it("does not connect if already connected", closure: { mockWebSocket.readyState = .open socket.connect() socket.connect() expect(mockWebSocket.connectCallsCount).to(equal(1)) }) } describe("disconnect") { // Mocks var mockWebSocket: PhoenixTransportMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) } it("removes existing connection", closure: { socket.connect() socket.disconnect() expect(socket.connection).to(beNil()) expect(mockWebSocket.disconnectCodeReasonReceivedArguments?.code) .to(equal(Socket.CloseCode.normal.rawValue)) }) it("flags the socket as closed cleanly", closure: { expect(socket.closeStatus).to(equal(.unknown)) socket.disconnect() expect(socket.closeStatus).to(equal(.clean)) }) it("calls callback", closure: { var callCount = 0 socket.connect() socket.disconnect(code: .goingAway) { callCount += 1 } expect(mockWebSocket.disconnectCodeReasonCalled).to(beTrue()) expect(mockWebSocket.disconnectCodeReasonReceivedArguments?.reason).to(beNil()) expect(mockWebSocket.disconnectCodeReasonReceivedArguments?.code) .to(equal(Socket.CloseCode.goingAway.rawValue)) expect(callCount).to(equal(1)) }) it("calls onClose for all state callbacks", closure: { var callCount = 0 socket.onClose { callCount += 1 } socket.disconnect() expect(callCount).to(equal(1)) }) it("invalidates and invalidates the heartbeat timer", closure: { var timerCalled = 0 let queue = DispatchQueue(label: "test.heartbeat") let timer = HeartbeatTimer(timeInterval: 10, queue: queue) timer.start { timerCalled += 1 } socket.heartbeatTimer = timer socket.disconnect() expect(socket.heartbeatTimer?.isValid).to(beFalse()) timer.fire() expect(timerCalled).to(equal(0)) }) it("does nothing if not connected", closure: { socket.disconnect() expect(mockWebSocket.disconnectCodeReasonCalled).to(beFalse()) }) } describe("channel") { var socket: Socket! beforeEach { socket = Socket("/socket") } it("returns channel with given topic and params", closure: { let channel = socket.channel("topic", params: ["one": "two"]) socket.ref = 1006 // No deep equal, so hack it expect(channel.socket?.ref).to(equal(socket.ref)) expect(channel.topic).to(equal("topic")) expect(channel.params["one"] as? String).to(equal("two")) }) it("adds channel to sockets channel list", closure: { expect(socket.channels).to(beEmpty()) let channel = socket.channel("topic", params: ["one": "two"]) expect(socket.channels).to(haveCount(1)) expect(socket.channels[0].topic).to(equal(channel.topic)) }) } describe("remove") { var socket: Socket! beforeEach { socket = Socket("/socket") } it("removes given channel from channels", closure: { let channel1 = socket.channel("topic-1") let channel2 = socket.channel("topic-2") channel1.joinPush.ref = "1" channel2.joinPush.ref = "2" socket.remove(channel1) expect(socket.channels).to(haveCount(1)) expect(socket.channels[0].topic).to(equal(channel2.topic)) }) } describe("push") { // Mocks var mockWebSocket: PhoenixTransportMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) } it("sends data to connection when connected", closure: { mockWebSocket.readyState = .open socket.connect() socket.push(topic: "topic", event: "event", payload: ["one": "two"], ref: "6", joinRef: "2") expect(mockWebSocket.sendDataCalled).to(beTrue()) let json = self.decode(mockWebSocket.sendDataReceivedData!) as! [Any] expect(json[0] as? String).to(equal("2")) expect(json[1] as? String).to(equal("6")) expect(json[2] as? String).to(equal("topic")) expect(json[3] as? String).to(equal("event")) expect(json[4] as? [String: String]).to(equal(["one": "two"])) expect(String(data: mockWebSocket.sendDataReceivedData!, encoding: .utf8)) .to(equal("[\"2\",\"6\",\"topic\",\"event\",{\"one\":\"two\"}]")) }) it("excludes ref information if not passed", closure: { mockWebSocket.readyState = .open socket.connect() socket.push(topic: "topic", event: "event", payload: ["one": "two"]) let json = self.decode(mockWebSocket.sendDataReceivedData!) as! [Any?] expect(json[0] as? String).to(beNil()) expect(json[1] as? String).to(beNil()) expect(json[2] as? String).to(equal("topic")) expect(json[3] as? String).to(equal("event")) expect(json[4] as? [String: String]).to(equal(["one": "two"])) expect(String(data: mockWebSocket.sendDataReceivedData!, encoding: .utf8)) .to(equal("[null,null,\"topic\",\"event\",{\"one\":\"two\"}]")) }) it("buffers data when not connected", closure: { mockWebSocket.readyState = .closed socket.connect() expect(socket.sendBuffer).to(beEmpty()) socket.push(topic: "topic1", event: "event1", payload: ["one": "two"]) expect(mockWebSocket.sendDataCalled).to(beFalse()) expect(socket.sendBuffer).to(haveCount(1)) socket.push(topic: "topic2", event: "event2", payload: ["one": "two"]) expect(mockWebSocket.sendDataCalled).to(beFalse()) expect(socket.sendBuffer).to(haveCount(2)) socket.sendBuffer.forEach( { try? $0.callback() } ) expect(mockWebSocket.sendDataCalled).to(beTrue()) expect(mockWebSocket.sendDataCallsCount).to(equal(2)) }) } describe("makeRef") { var socket: Socket! beforeEach { socket = Socket("/socket") } it("returns next message ref", closure: { expect(socket.ref).to(equal(0)) expect(socket.makeRef()).to(equal("1")) expect(socket.ref).to(equal(1)) expect(socket.makeRef()).to(equal("2")) expect(socket.ref).to(equal(2)) }) it("resets to 0 if it hits max int", closure: { socket.ref = UInt64.max expect(socket.makeRef()).to(equal("0")) expect(socket.ref).to(equal(0)) }) } describe("sendHeartbeat v1") { // Mocks var mockWebSocket: PhoenixTransportMock! // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket("/socket") socket.connection = mockWebSocket } it("closes socket when heartbeat is not ack'd within heartbeat window", closure: { mockWebSocket.readyState = .open socket.sendHeartbeat() expect(mockWebSocket.disconnectCodeReasonCalled).to(beFalse()) expect(socket.pendingHeartbeatRef).toNot(beNil()) socket.sendHeartbeat() expect(mockWebSocket.disconnectCodeReasonCalled).to(beTrue()) expect(socket.pendingHeartbeatRef).to(beNil()) }) it("pushes heartbeat data when connected", closure: { mockWebSocket.readyState = .open socket.sendHeartbeat() expect(socket.pendingHeartbeatRef).to(equal(String(socket.ref))) expect(mockWebSocket.sendDataCalled).to(beTrue()) let json = self.decode(mockWebSocket.sendDataReceivedData!) as? [Any?] expect(json?[0] as? String).to(beNil()) expect(json?[1] as? String).to(equal(socket.pendingHeartbeatRef)) expect(json?[2] as? String).to(equal("phoenix")) expect(json?[3] as? String).to(equal("heartbeat")) expect(json?[4] as? [String: String]).to(beEmpty()) expect(String(data: mockWebSocket.sendDataReceivedData!, encoding: .utf8)) .to(equal("[null,\"1\",\"phoenix\",\"heartbeat\",{}]")) }) it("does nothing when not connected", closure: { mockWebSocket.readyState = .closed socket.sendHeartbeat() expect(mockWebSocket.disconnectCodeReasonCalled).to(beFalse()) expect(mockWebSocket.sendDataCalled).to(beFalse()) }) } describe("flushSendBuffer") { // Mocks var mockWebSocket: PhoenixTransportMock! // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket("/socket") socket.connection = mockWebSocket } it("calls callbacks in buffer when connected", closure: { var oneCalled = 0 socket.sendBuffer.append(("0", { oneCalled += 1 })) var twoCalled = 0 socket.sendBuffer.append(("1", { twoCalled += 1 })) let threeCalled = 0 mockWebSocket.readyState = .open socket.flushSendBuffer() expect(oneCalled).to(equal(1)) expect(twoCalled).to(equal(1)) expect(threeCalled).to(equal(0)) }) it("empties send buffer", closure: { socket.sendBuffer.append((nil, { })) mockWebSocket.readyState = .open socket.flushSendBuffer() expect(socket.sendBuffer).to(beEmpty()) }) } describe("removeFromSendBuffer") { // Mocks var mockWebSocket: PhoenixTransportMock! // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket("/socket") socket.connection = mockWebSocket } it("removes a callback with a matching ref", closure: { var oneCalled = 0 socket.sendBuffer.append(("0", { oneCalled += 1 })) var twoCalled = 0 socket.sendBuffer.append(("1", { twoCalled += 1 })) let threeCalled = 0 mockWebSocket.readyState = .open socket.removeFromSendBuffer(ref: "0") socket.flushSendBuffer() expect(oneCalled).to(equal(0)) expect(twoCalled).to(equal(1)) expect(threeCalled).to(equal(0)) }) } describe("onConnectionOpen") { // Mocks var mockWebSocket: PhoenixTransportMock! var mockTimeoutTimer: TimeoutTimerMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() mockTimeoutTimer = TimeoutTimerMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) socket.reconnectAfter = { _ in return 10 } socket.reconnectTimer = mockTimeoutTimer socket.skipHeartbeat = true mockWebSocket.readyState = .open socket.connect() } it("flushes the send buffer", closure: { var oneCalled = 0 socket.sendBuffer.append(("0", { oneCalled += 1 })) socket.onConnectionOpen() expect(oneCalled).to(equal(1)) expect(socket.sendBuffer).to(beEmpty()) }) it("resets reconnectTimer", closure: { socket.onConnectionOpen() expect(mockTimeoutTimer.resetCalled).to(beTrue()) }) it("triggers onOpen callbacks", closure: { var oneCalled = 0 socket.onOpen { oneCalled += 1 } var twoCalled = 0 socket.onOpen { twoCalled += 1 } var threeCalled = 0 socket.onClose { threeCalled += 1 } socket.onConnectionOpen() expect(oneCalled).to(equal(1)) expect(twoCalled).to(equal(1)) expect(threeCalled).to(equal(0)) }) } describe("resetHeartbeat") { // Mocks var mockWebSocket: PhoenixTransportMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) } it("clears any pending heartbeat", closure: { socket.pendingHeartbeatRef = "1" socket.resetHeartbeat() expect(socket.pendingHeartbeatRef).to(beNil()) }) it("does not schedule heartbeat if skipHeartbeat == true", closure: { socket.skipHeartbeat = true socket.resetHeartbeat() expect(socket.heartbeatTimer).to(beNil()) }) it("creates a timer and sends a heartbeat", closure: { mockWebSocket.readyState = .open socket.connect() socket.heartbeatInterval = 1 expect(socket.heartbeatTimer).to(beNil()) socket.resetHeartbeat() expect(socket.heartbeatTimer).toNot(beNil()) expect(socket.heartbeatTimer?.timeInterval).to(equal(1)) // Fire the timer socket.heartbeatTimer?.fire() expect(mockWebSocket.sendDataCalled).to(beTrue()) let json = self.decode(mockWebSocket.sendDataReceivedData!) as? [Any?] expect(json?[0] as? String).to(beNil()) expect(json?[1] as? String).to(equal(String(socket.ref))) expect(json?[2] as? String).to(equal("phoenix")) expect(json?[3] as? String).to(equal(ChannelEvent.heartbeat)) expect(json?[4] as? [String: Any]).to(beEmpty()) }) it("should invalidate an old timer and create a new one", closure: { mockWebSocket.readyState = .open let queue = DispatchQueue(label: "test.heartbeat") let timer = HeartbeatTimer(timeInterval: 1000, queue: queue) var timerCalled = 0 timer.start { timerCalled += 1 } socket.heartbeatTimer = timer expect(timer.isValid).to(beTrue()) socket.resetHeartbeat() expect(timer.isValid).to(beFalse()) expect(socket.heartbeatTimer).toNot(equal(timer)) }) } describe("onConnectionClosed") { // Mocks var mockWebSocket: PhoenixTransportMock! var mockTimeoutTimer: TimeoutTimerMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() mockTimeoutTimer = TimeoutTimerMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) // socket.reconnectAfter = { _ in return 10 } socket.reconnectTimer = mockTimeoutTimer // socket.skipHeartbeat = true } it("schedules reconnectTimer timeout if normal close", closure: { socket.onConnectionClosed(code: Socket.CloseCode.normal.rawValue, reason: nil) expect(mockTimeoutTimer.scheduleTimeoutCalled).to(beTrue()) }) it("does not schedule reconnectTimer timeout if normal close after explicit disconnect", closure: { socket.disconnect() expect(mockTimeoutTimer.scheduleTimeoutCalled).to(beFalse()) }) it("schedules reconnectTimer timeout if not normal close", closure: { socket.onConnectionClosed(code: 1001, reason: nil) expect(mockTimeoutTimer.scheduleTimeoutCalled).to(beTrue()) }) it("schedules reconnectTimer timeout if connection cannot be made after a previous clean disconnect", closure: { socket.disconnect() socket.connect() socket.onConnectionClosed(code: 1001, reason: nil) expect(mockTimeoutTimer.scheduleTimeoutCalled).to(beTrue()) }) it("triggers channel error if joining", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join() expect(channel.state).to(equal(.joining)) socket.onConnectionClosed(code: 1001, reason: nil) expect(errorCalled).to(beTrue()) }) it("triggers channel error if joined", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join().trigger("ok", payload: [:]) expect(channel.state).to(equal(.joined)) socket.onConnectionClosed(code: 1001, reason: nil) expect(errorCalled).to(beTrue()) }) it("does not trigger channel error after leave", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join().trigger("ok", payload: [:]) channel.leave() expect(channel.state).to(equal(.closed)) socket.onConnectionClosed(code: 1001, reason: nil) expect(errorCalled).to(beFalse()) }) it("triggers onClose callbacks", closure: { var oneCalled = 0 socket.onClose { oneCalled += 1 } var twoCalled = 0 socket.onClose { twoCalled += 1 } var threeCalled = 0 socket.onOpen { threeCalled += 1 } socket.onConnectionClosed(code: 1000, reason: nil) expect(oneCalled).to(equal(1)) expect(twoCalled).to(equal(1)) expect(threeCalled).to(equal(0)) }) } describe("onConnectionError") { // Mocks var mockWebSocket: PhoenixTransportMock! var mockTimeoutTimer: TimeoutTimerMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() mockTimeoutTimer = TimeoutTimerMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) socket.reconnectAfter = { _ in return 10 } socket.reconnectTimer = mockTimeoutTimer socket.skipHeartbeat = true mockWebSocket.readyState = .open socket.connect() } it("triggers onClose callbacks", closure: { var lastError: (Error, URLResponse?)? socket.onError(callback: { (error) in lastError = error }) socket.onConnectionError(TestError.stub, response: nil) expect(lastError).toNot(beNil()) }) it("triggers channel error if joining", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join() expect(channel.state).to(equal(.joining)) socket.onConnectionError(TestError.stub, response: nil) expect(errorCalled).to(beTrue()) }) it("triggers channel error if joined", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join().trigger("ok", payload: [:]) expect(channel.state).to(equal(.joined)) socket.onConnectionError(TestError.stub, response: nil) expect(errorCalled).to(beTrue()) }) it("does not trigger channel error after leave", closure: { let channel = socket.channel("topic") var errorCalled = false channel.on(ChannelEvent.error, callback: { _ in errorCalled = true }) channel.join().trigger("ok", payload: [:]) channel.leave() expect(channel.state).to(equal(.closed)) socket.onConnectionError(TestError.stub, response: nil) expect(errorCalled).to(beFalse()) }) } describe("onConnectionMessage") { // Mocks var mockWebSocket: PhoenixTransportMock! var mockTimeoutTimer: TimeoutTimerMock! let mockWebSocketTransport: ((URL) -> PhoenixTransportMock) = { _ in return mockWebSocket } // UUT var socket: Socket! beforeEach { mockWebSocket = PhoenixTransportMock() mockTimeoutTimer = TimeoutTimerMock() socket = Socket(endPoint: "/socket", transport: mockWebSocketTransport) socket.reconnectAfter = { _ in return 10 } socket.reconnectTimer = mockTimeoutTimer socket.skipHeartbeat = true mockWebSocket.readyState = .open socket.connect() } it("parses raw message and triggers channel event", closure: { let targetChannel = socket.channel("topic") let otherChannel = socket.channel("off-topic") var targetMessage: Message? targetChannel.on("event", callback: { (msg) in targetMessage = msg }) var otherMessage: Message? otherChannel.on("event", callback: { (msg) in otherMessage = msg }) let data: [Any?] = [nil, nil, "topic", "event", ["status": "ok", "response": ["one": "two"]]] let rawMessage = toWebSocketText(data: data) socket.onConnectionMessage(rawMessage) expect(targetMessage?.topic).to(equal("topic")) expect(targetMessage?.event).to(equal("event")) expect(targetMessage?.payload["one"] as? String).to(equal("two")) expect(otherMessage).to(beNil()) }) it("triggers onMessage callbacks", closure: { var message: Message? socket.onMessage(callback: { (msg) in message = msg }) let data: [Any?] = [nil, nil, "topic", "event", ["status": "ok", "response": ["one": "two"]]] let rawMessage = toWebSocketText(data: data) socket.onConnectionMessage(rawMessage) expect(message?.topic).to(equal("topic")) expect(message?.event).to(equal("event")) expect(message?.payload["one"] as? String).to(equal("two")) }) it("clears pending heartbeat", closure: { socket.pendingHeartbeatRef = "5" let rawMessage = "[null,\"5\",\"phoenix\",\"phx_reply\",{\"response\":{},\"status\":\"ok\"}]" socket.onConnectionMessage(rawMessage) expect(socket.pendingHeartbeatRef).to(beNil()) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/TimeoutTimerSpec.swift ================================================ // // TimeoutTimerSpec.swift // SwiftPhoenixClientTests // // Created by Daniel Rees on 2/10/19. // import Quick import Nimble @testable import SwiftPhoenixClient class TimeoutTimerSpec: QuickSpec { public func secondsBetweenDates(_ first: Date, _ second: Date) -> Double { var diff = first.timeIntervalSince1970 - second.timeIntervalSince1970 diff = fabs(diff) return diff } override func spec() { let fakeClock = FakeTimerQueue() var timer: TimeoutTimer! beforeEach { fakeClock.reset() timer = TimeoutTimer() timer.queue = fakeClock } describe("retain cycles") { it("does not hold any retain cycles", closure: { // var weakTimer: TimeoutTimer? = TimeoutTimer() // // var weakTimerCalled: Bool = false // timer.callback.delegate(to: weakTimer!) { (_) in // weakTimerCalled = true // } // // timer.timerCalculation.delegate(to: weakTimer!) { _,_ in 1.0 } // // // timer.scheduleTimeout() // fakeClock.tick(600) // weakTimer = nil // // fakeClock.tick(600) // expect(timer.tries).to(equal(1)) // expect(weakTimerCalled).to(beFalse()) }) } describe("scheduleTimeout") { it("schedules timeouts, resets the timer, and schedules another timeout", closure: { var callbackTimes: [Date] = [] timer.callback.delegate(to: self) { (_) in callbackTimes.append(Date()) } timer.timerCalculation.delegate(to: self) { (_, tries) -> TimeInterval in return tries > 2 ? 10.0 : [1.0, 2.0, 5.0][tries - 1] } timer.scheduleTimeout() fakeClock.tick(1100) expect(timer.tries).to(equal(1)) timer.scheduleTimeout() fakeClock.tick(2100) expect(timer.tries).to(equal(2)) timer.reset() timer.scheduleTimeout() fakeClock.tick(1100) expect(timer.tries).to(equal(1)) }) it("does not start timer if no interval is provided", closure: { timer.scheduleTimeout() expect(timer.workItem).to(beNil()) }) } } } ================================================ FILE: Tests/SwiftPhoenixClientTests/URLSessionTransportSpec.swift ================================================ // // URLSessionTransportSpec.swift // SwiftPhoenixClientTests // // Created by Daniel Rees on 4/1/21. // Copyright © 2021 SwiftPhoenixClient. All rights reserved. // import Quick import Nimble @testable import SwiftPhoenixClient class URLSessionTransportSpec: QuickSpec { override func spec() { describe("init") { it("replaces http with ws protocols") { if #available(iOS 13, *) { expect( URLSessionTransport(url: URL(string:"http://localhost:4000/socket/websocket")!) .url.absoluteString ).to(equal("ws://localhost:4000/socket/websocket")) expect( URLSessionTransport(url: URL(string:"https://localhost:4000/socket/websocket")!) .url.absoluteString ).to(equal("wss://localhost:4000/socket/websocket")) expect( URLSessionTransport(url: URL(string:"ws://localhost:4000/socket/websocket")!) .url.absoluteString ).to(equal("ws://localhost:4000/socket/websocket")) expect( URLSessionTransport(url: URL(string:"wss://localhost:4000/socket/websocket")!) .url.absoluteString ).to(equal("wss://localhost:4000/socket/websocket")) } else { // Fallback on earlier versions expect("wrong iOS version").to(equal("You must run this test on an iOS 13 device")) } } it("accepts an override for the configuration") { if #available(iOS 13, *) { let configuration = URLSessionConfiguration.default expect( URLSessionTransport(url: URL(string:"wss://localhost:4000")!, configuration: configuration) .configuration ).to(equal(configuration)) } else { // Fallback on earlier versions expect("wrong iOS version").to(equal("You must run this test on an iOS 13 device")) } } } } } ================================================ FILE: docs/Classes/Channel.html ================================================ Channel Class Reference

SwiftPhoenixClient Docs (72% documented)

Channel

public class Channel

Undocumented

  • The topic of the Channel. e.g. “rooms:friends”

    Declaration

    Swift

    public let topic: String
  • The params sent when joining the channel

    Declaration

    Swift

    public var params: Payload { get set }
  • Overridable message hook. Receives all events for specialized message handling before dispatching to the channel callbacks.

    • return: Must return the message, modified or unmodified

    Declaration

    Swift

    public var onMessage: (Message) -> Message

    Parameters

    msg

    The Message received by the client from the server

  • Joins the channel

    • return: Push event

    Declaration

    Swift

    @discardableResult
    public func join(timeout: TimeInterval? = nil) -> Push

    Parameters

    timeout

    Optional. Defaults to Channel’s timeout

  • Hook into when the Channel is closed. Does not handle retain cycles. Use delegateOnClose(to:) for automatic handling of retain cycles.

    Example:

    let channel = socket.channel("topic")
    channel.onClose() { [weak self] message in
        self?.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func onClose(_ callback: @escaping ((Message) -> Void)) -> Int

    Parameters

    callback

    Called when the Channel closes

  • Hook into when the Channel is closed. Automatically handles retain cycles. Use onClose() to handle yourself.

    Example:

    let channel = socket.channel("topic")
    channel.delegateOnClose(to: self) { (self, message) in
        self.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOnClose<Target: AnyObject>(to owner: Target,
                                                   callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Channel closes

  • Hook into when the Channel receives an Error. Does not handle retain cycles. Use delegateOnError(to:) for automatic handling of retain cycles.

    Example:

    let channel = socket.channel("topic")
    channel.onError() { [weak self] (message) in
        self?.print("Channel \(message.topic) has errored"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func onError(_ callback: @escaping ((_ message: Message) -> Void)) -> Int

    Parameters

    callback

    Called when the Channel closes

  • Hook into when the Channel receives an Error. Automatically handles retain cycles. Use onError() to handle yourself.

    Example:

    let channel = socket.channel("topic")
    channel.delegateOnError(to: self) { (self, message) in
        self.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOnError<Target: AnyObject>(to owner: Target,
                                                   callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Channel closes

  • Subscribes on channel events. Does not handle retain cycles. Use delegateOn(_:, to:) for automatic handling of retain cycles.

    Subscription returns a ref counter, which can be used later to unsubscribe the exact event listener

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.on("event") { [weak self] (message) in
        self?.print("do stuff")
    }
    let ref2 = channel.on("event") { [weak self] (message) in
        self?.print("do other stuff")
    }
    channel.off("event", ref1)
    

    Since unsubscription of ref1, “do stuff” won’t print, but “do other stuff” will keep on printing on the “event”

    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func on(_ event: String, callback: @escaping ((Message) -> Void)) -> Int

    Parameters

    event

    Event to receive

    callback

    Called with the event’s message

  • Subscribes on channel events. Automatically handles retain cycles. Use on() to handle yourself.

    Subscription returns a ref counter, which can be used later to unsubscribe the exact event listener

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.delegateOn("event", to: self) { (self, message) in
        self?.print("do stuff")
    }
    let ref2 = channel.delegateOn("event", to: self) { (self, message) in
        self?.print("do other stuff")
    }
    channel.off("event", ref1)
    

    Since unsubscription of ref1, “do stuff” won’t print, but “do other stuff” will keep on printing on the “event”

    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOn<Target: AnyObject>(_ event: String,
                                              to owner: Target,
                                              callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    event

    Event to receive

    owner

    Class registering the callback. Usually self

    callback

    Called with the event’s message

  • Unsubscribes from a channel event. If a ref is given, only the exact listener will be removed. Else all listeners for the event will be removed.

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.on("event") { _ in print("ref1 event" }
    let ref2 = channel.on("event") { _ in print("ref2 event" }
    let ref3 = channel.on("other_event") { _ in print("ref3 other" }
    let ref4 = channel.on("other_event") { _ in print("ref4 other" }
    channel.off("event", ref1)
    channel.off("other_event")
    

    After this, only “ref2 event” will be printed if the channel receives “event” and nothing is printed if the channel receives “other_event”.

    • paramter ref: Ref counter returned when subscribing. Can be omitted

    Declaration

    Swift

    public func off(_ event: String, ref: Int? = nil)

    Parameters

    event

    Event to unsubscribe from

  • Push a payload to the Channel

    Example:

    channel
        .push("event", payload: ["message": "hello")
        .receive("ok") { _ in { print("message sent") }
    

    Declaration

    Swift

    @discardableResult
    public func push(_ event: String,
                     payload: Payload,
                     timeout: TimeInterval = Defaults.timeoutInterval) -> Push

    Parameters

    event

    Event to push

    payload

    Payload to push

    timeout

    Optional timeout

  • Leaves the channel

    Unsubscribes from server events, and instructs channel to terminate on server

    Triggers onClose() hooks

    To receive leave acknowledgements, use the a receive hook to bind to the server ack, ie:

    Example: / channel.leave().receive(“ok”) { _ in { print(“left”) }

    • return: Push that can add receive hooks

    Declaration

    Swift

    @discardableResult
    public func leave(timeout: TimeInterval = Defaults.timeoutInterval) -> Push

    Parameters

    timeout

    Optional timeout

  • Overridable message hook. Receives all events for specialized message handling before dispatching to the channel callbacks.

    • return: Must return the payload, modified or unmodified

    Declaration

    Swift

    public func onMessage(callback: @escaping (Message) -> Message)

    Parameters

    event

    The event the message was for

    payload

    The payload for the message

    ref

    The reference of the message

Public API

    • return: True if the Channel has been closed

    Declaration

    Swift

    public var isClosed: Bool { get }
    • return: True if the Channel experienced an error

    Declaration

    Swift

    public var isErrored: Bool { get }
    • return: True if the channel has joined

    Declaration

    Swift

    public var isJoined: Bool { get }
    • return: True if the channel has requested to join

    Declaration

    Swift

    public var isJoining: Bool { get }
    • return: True if the channel has requested to leave

    Declaration

    Swift

    public var isLeaving: Bool { get }
================================================ FILE: docs/Classes/Defaults.html ================================================ Defaults Class Reference

SwiftPhoenixClient Docs (72% documented)

Defaults

public class Defaults

A collection of default values and behaviors used accross the Client

  • Default timeout when sending messages

    Declaration

    Swift

    public static let timeoutInterval: TimeInterval
  • Default interval to send heartbeats on

    Declaration

    Swift

    public static let heartbeatInterval: TimeInterval
  • Default reconnect algorithm for the socket

    Declaration

    Swift

    public static let reconnectSteppedBackOff: (Int) -> TimeInterval
  • Default rejoin algorithm for individual channels

    Declaration

    Swift

    public static let rejoinSteppedBackOff: (Int) -> TimeInterval
  • Default encode function, utilizing JSONSerialization.data

    Declaration

    Swift

    public static let encode: ([String : Any]) -> Data
  • Default decode function, utilizing JSONSerialization.jsonObject

    Declaration

    Swift

    public static let decode: (Data) -> [String : Any]?
================================================ FILE: docs/Classes/Message.html ================================================ Message Class Reference

SwiftPhoenixClient Docs (72% documented)

Message

public class Message

Data that is received from the Server.

  • ref

    Reference number. Empty if missing

    Declaration

    Swift

    public let ref: String
  • Message topic

    Declaration

    Swift

    public let topic: String
  • Message event

    Declaration

    Swift

    public let event: String
  • Message payload

    Declaration

    Swift

    public var payload: Payload
  • Convenience accessor. Equivalent to getting the status as such:

    message.payload["status"]
    

    Declaration

    Swift

    public var status: String? { get }
================================================ FILE: docs/Classes/Presence/Events.html ================================================ Events Enumeration Reference

SwiftPhoenixClient Docs (72% documented)

Events

public enum Events : String

Presense Events

  • Undocumented

    Declaration

    Swift

    case state = "state"
  • Undocumented

    Declaration

    Swift

    case diff = "diff"
================================================ FILE: docs/Classes/Presence/Options.html ================================================ Options Structure Reference

SwiftPhoenixClient Docs (72% documented)

Options

public struct Options

Custom options that can be provided when creating Presence

Example:

let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)
  • Default set of Options used when creating Presence. Uses the phoenix events “presence_state” and “presence_diff”

    Declaration

    Swift

    public static let defaults: Presence.Options
================================================ FILE: docs/Classes/Presence.html ================================================ Presence Class Reference

SwiftPhoenixClient Docs (72% documented)

Presence

public final class Presence

The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

Syncing state from the server

To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

let channel = socket.channel("some:topic")
let presence = Presence(channel)

If you have custom syncing state events, you can configure the Presence object to use those instead.

let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)

Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

presence.onSync { renderUsers(presence.list()) }

Listing Presences

presence.list is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence. For example, you may have a user online from different devices with a metadata status of “online”, but they have set themselves to “away” on another device. In this case, the app may choose to use the “away” status for what appears on the UI. The example below defines a listBy function which prioritizes the first metadata which was registered for each user. This could be the first tab they opened, or the first device they came online from:

let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
    let first = pres["metas"]!.first!
    first["count"] = pres["metas"]!.count
    first["id"] = id
    return first
}
let onlineUsers = presence.list(by: listBy)

(NOTE: The underlying behavior is a map on the presence.state. You are mapping the state dictionary into whatever datastructure suites your needs)

Handling individual presence join and leave events

The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:

let presence = Presence(channel)
presence.onJoin { [weak self] (key, current, newPres) in
    if let cur = current {
        print("user additional presence", cur)
    } else {
        print("user entered for the first time", newPres)
    }
}

presence.onLeave { [weak self] (key, current, leftPres) in
    if current["metas"]?.isEmpty == true {
        print("user has left from all devices", leftPres)
    } else {
        print("user left from a device", current)
    }
}

presence.onSync { renderUsers(presence.list()) }

Enums and Structs

  • Custom options that can be provided when creating Presence

    Example:

    let options = Options(events: [.state: "my_state", .diff: "my_diff"])
    let presence = Presence(channel, opts: options)
    
    See more

    Declaration

    Swift

    public struct Options
  • Presense Events

    See more

    Declaration

    Swift

    public enum Events : String

Typaliases

  • Meta details of a Presence. Just a dictionary of properties

    Declaration

    Swift

    public typealias Meta = [String : Any]
  • Map

    A mapping of a String to an array of Metas. e.g. {“metas”: [{id: 1}]}

    Declaration

    Swift

    public typealias Map = [String : [Meta]]
  • A mapping of a Presence state to a mapping of Metas

    Declaration

    Swift

    public typealias State = [String : Map]
  • Undocumented

    Declaration

    Swift

    public typealias Diff = [String : State]
  • Closure signature of OnJoin callbacks

    Declaration

    Swift

    public typealias OnJoin = (_ key: String, _ current: Map?, _ new: Map) -> Void
  • Closure signature for OnLeave callbacks

    Declaration

    Swift

    public typealias OnLeave = (_ key: String, _ current: Map, _ left: Map) -> Void
  • / Closure signature for OnSync callbacks

    Declaration

    Swift

    public typealias OnSync = () -> Void

Properties

  • The state of the Presence

    Declaration

    Swift

    private(set) public var state: State
  • Pending join and leave diffs that need to be synced

    Declaration

    Swift

    private(set) public var pendingDiffs: [Diff]
  • The channel’s joinRef, set when state events occur

    Declaration

    Swift

    private(set) public var joinRef: String?
  • Undocumented

    Declaration

    Swift

    public var isPendingSyncState: Bool { get }
  • Callback to be informed of joins

    Declaration

    Swift

    public var onJoin: OnJoin { get set }
  • Set the OnJoin callback

    Declaration

    Swift

    public func onJoin(_ callback: @escaping OnJoin)
  • Callback to be informed of leaves

    Declaration

    Swift

    public var onLeave: OnLeave { get set }
  • Set the OnLeave callback

    Declaration

    Swift

    public func onLeave(_ callback: @escaping OnLeave)
  • Callback to be informed of synces

    Declaration

    Swift

    public var onSync: OnSync { get set }
  • Set the OnSync callback

    Declaration

    Swift

    public func onSync(_ callback: @escaping OnSync)
  • Undocumented

    Declaration

    Swift

    public init(channel: Channel, opts: Options = Options.defaults)
  • Returns the array of presences, with deault selected metadata.

    Declaration

    Swift

    public func list() -> [Map]
  • Returns the array of presences, with selected metadata

    Declaration

    Swift

    public func list<T>(by transformer: (String, Map) -> T) -> [T]
  • Filter the Presence state with a given function

    Declaration

    Swift

    public func filter(by filter: ((String, Map) -> Bool)?) -> State

Static

================================================ FILE: docs/Classes/Push.html ================================================ Push Class Reference

SwiftPhoenixClient Docs (72% documented)

Push

public class Push

Represnts pushing data to a Channel through the Socket

  • The channel sending the Push

    Declaration

    Swift

    public weak var channel: Channel?
  • The event, for example phx_join

    Declaration

    Swift

    public let event: String
  • The payload, for example [“user_id”: “abc123”]

    Declaration

    Swift

    public var payload: Payload
  • The push timeout. Default is 10.0 seconds

    Declaration

    Swift

    public var timeout: TimeInterval
  • Resets and sends the Push

    Declaration

    Swift

    public func resend(_ timeout: TimeInterval = Defaults.timeoutInterval)

    Parameters

    timeout

    Optional. The push timeout. Default is 10.0s

  • Sends the Push. If it has already timed out, then the call will be ignored and return early. Use resend in this case.

    Declaration

    Swift

    public func send()
  • Receive a specific event when sending an Outbound message. Subscribing to status events with this method does not guarantees no retain cycles. You should pass weak self in the capture list of the callback. You can call `.delegateReceive(status:, to:, callback:) and the library will handle it for you.

    Example:

    channel
        .send(event:"custom", payload: ["body": "example"])
        .receive("error") { [weak self] payload in
            print("Error: ", payload)
        }
    

    Declaration

    Swift

    @discardableResult
    public func receive(_ status: String,
                        callback: @escaping ((Message) -> ())) -> Push

    Parameters

    status

    Status to receive

    callback

    Callback to fire when the status is recevied

  • Receive a specific event when sending an Outbound message. Automatically prevents retain cycles. See manualReceive(status:, callback:) if you want to handle this yourself.

    Example:

    channel
        .send(event:"custom", payload: ["body": "example"])
        .delegateReceive("error", to: self) { payload in
            print("Error: ", payload)
        }
    

    Declaration

    Swift

    @discardableResult
    public func delegateReceive<Target: AnyObject>(_ status: String,
                                                   to owner: Target,
                                                   callback: @escaping ((Target, Message) -> ())) -> Push

    Parameters

    status

    Status to receive

    owner

    The class that is calling .receive. Usually self

    callback

    Callback to fire when the status is recevied

================================================ FILE: docs/Classes/Socket.html ================================================ Socket Class Reference

SwiftPhoenixClient Docs (72% documented)

Socket

public class Socket
extension Socket: WebSocketDelegate

Socket Connection

A single connection is established to the server and channels are multiplexed over the connection. Connect to the server using the Socket class:

let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] })
socket.connect()

The Socket constructor takes the mount point of the socket, the authentication params, as well as options that can be found in the Socket docs, such as configuring the heartbeat.

Public Attributes

  • The string WebSocket endpoint (ie "ws://example.com/socket", "wss://example.com", etc.) That was passed to the Socket during initialization. The URL endpoint will be modified by the Socket to include "/websocket" if missing.

    Declaration

    Swift

    public let endPoint: String
  • The fully qualified socket URL

    Declaration

    Swift

    public private(set) var endPointUrl: URL
  • Resolves to return the paramsClosure result at the time of calling. If the Socket was created with static params, then those will be returned every time.

    Declaration

    Swift

    public var params: Payload? { get }
  • The optional params closure used to get params whhen connecting. Must be set when initializaing the Socket.

    Declaration

    Swift

    public let paramsClosure: PayloadClosure?
  • Override to provide custom encoding of data before writing to the socket

    Declaration

    Swift

    public var encode: ([String : Any]) -> Data
  • Override to provide customd decoding of data read from the socket

    Declaration

    Swift

    public var decode: (Data) -> [String : Any]?
  • Timeout to use when opening connections

    Declaration

    Swift

    public var timeout: TimeInterval
  • Interval between sending a heartbeat

    Declaration

    Swift

    public var heartbeatInterval: TimeInterval
  • Interval between socket reconnect attempts, in seconds

    Declaration

    Swift

    public var reconnectAfter: (Int) -> TimeInterval
  • Interval between channel rejoin attempts, in seconds

    Declaration

    Swift

    public var rejoinAfter: (Int) -> TimeInterval
  • The optional function to receive logs

    Declaration

    Swift

    public var logger: ((String) -> Void)?
  • Disables heartbeats from being sent. Default is false.

    Declaration

    Swift

    public var skipHeartbeat: Bool
  • Enable/Disable SSL certificate validation. Default is false. This must be set before calling socket.connect() in order to be applied

    Declaration

    Swift

    public var disableSSLCertValidation: Bool
  • Configure custom SSL validation logic, eg. SSL pinning. This must be set before calling socket.connect() in order to apply.

    Declaration

    Swift

    public var security: SSLTrustValidator?
  • Configure the encryption used by your client by setting the allowed cipher suites supported by your server. This must be set before calling socket.connect() in order to apply.

    Declaration

    Swift

    public var enabledSSLCipherSuites: [SSLCipherSuite]?

Initialization

Public

    • return: The socket protocol, wss or ws

    Declaration

    Swift

    public var websocketProtocol: String { get }
    • return: True if the socket is connected

    Declaration

    Swift

    public var isConnected: Bool { get }
  • Connects the Socket. The params passed to the Socket on initialization will be sent through the connection. If the Socket is already connected, then this call will be ignored.

    Declaration

    Swift

    public func connect()
  • Disconnects the socket

    • paramter callback: Optional. Called when disconnected

    Declaration

    Swift

    public func disconnect(code: CloseCode = CloseCode.normal,
                           callback: (() -> Void)? = nil)

    Parameters

    code

    Optional. Closing status code

Register Socket State Callbacks

  • Registers callbacks for connection open events. Does not handle retain cycles. Use delegateOnOpen(to:) for automatic handling of retain cycles.

    Example:

    socket.onOpen() { [weak self] in
        self?.print("Socket Connection Open")
    }
    

    Declaration

    Swift

    public func onOpen(callback: @escaping () -> Void)

    Parameters

    callback

    Called when the Socket is opened

  • Registers callbacks for connection open events. Automatically handles retain cycles. Use onOpen() to handle yourself.

    Example:

    socket.delegateOnOpen(to: self) { self in
        self.print("Socket Connection Open")
    }
    

    Declaration

    Swift

    public func delegateOnOpen<T: AnyObject>(to owner: T,
                                             callback: @escaping ((T) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket is opened

  • Registers callbacks for connection close events. Does not handle retain cycles. Use delegateOnClose(_:) for automatic handling of retain cycles.

    Example:

    socket.onClose() { [weak self] in
        self?.print("Socket Connection Close")
    }
    

    Declaration

    Swift

    public func onClose(callback: @escaping () -> Void)

    Parameters

    callback

    Called when the Socket is closed

  • Registers callbacks for connection close events. Automatically handles retain cycles. Use onClose() to handle yourself.

    Example:

    socket.delegateOnClose(self) { self in
        self.print("Socket Connection Close")
    }
    

    Declaration

    Swift

    public func delegateOnClose<T: AnyObject>(to owner: T,
                                              callback: @escaping ((T) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket is closed

  • Registers callbacks for connection error events. Does not handle retain cycles. Use delegateOnError(to:) for automatic handling of retain cycles.

    Example:

    socket.onError() { [weak self] (error) in
        self?.print("Socket Connection Error", error)
    }
    

    Declaration

    Swift

    public func onError(callback: @escaping (Error) -> Void)

    Parameters

    callback

    Called when the Socket errors

  • Registers callbacks for connection error events. Automatically handles retain cycles. Use manualOnError() to handle yourself.

    Example:

    socket.delegateOnError(to: self) { (self, error) in
        self.print("Socket Connection Error", error)
    }
    

    Declaration

    Swift

    public func delegateOnError<T: AnyObject>(to owner: T,
                                              callback: @escaping ((T, Error) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket errors

  • Registers callbacks for connection message events. Does not handle retain cycles. Use delegateOnMessage(_to:) for automatic handling of retain cycles.

    Example:

    socket.onMessage() { [weak self] (message) in
        self?.print("Socket Connection Message", message)
    }
    

    Declaration

    Swift

    public func onMessage(callback: @escaping (Message) -> Void)

    Parameters

    callback

    Called when the Socket receives a message event

  • Registers callbacks for connection message events. Automatically handles retain cycles. Use onMessage() to handle yourself.

    Example:

    socket.delegateOnMessage(self) { (self, message) in
        self.print("Socket Connection Message", message)
    }
    

    Declaration

    Swift

    public func delegateOnMessage<T: AnyObject>(to owner: T,
                                                callback: @escaping ((T, Message) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket receives a message event

  • Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should call this method when you are finished when the Socket in order to release any references held by the socket.

    Declaration

    Swift

    public func releaseCallbacks()

Channel Initialization

  • Initialize a new Channel

    Example:

    let channel = socket.channel("rooms", params: ["user_id": "abc123"])
    
    • return: A new channel

    Declaration

    Swift

    public func channel(_ topic: String,
                        params: [String: Any] = [:]) -> Channel

    Parameters

    topic

    Topic of the channel

    params

    Optional. Parameters for the channel

  • Removes the Channel from the socket. This does not cause the channel to inform the server that it is leaving. You should call channel.leave() prior to removing the Channel.

    Example:

    channel.leave()
    socket.remove(channel)
    

    Declaration

    Swift

    public func remove(_ channel: Channel)

    Parameters

    channel

    Channel to remove

Sending Data

    • return: the next message ref, accounting for overflows

    Declaration

    Swift

    public func makeRef() -> String

WebSocketDelegate

================================================ FILE: docs/Classes.html ================================================ Classes Reference

SwiftPhoenixClient Docs (72% documented)

Classes

The following classes are available globally.

  • Undocumented

    See more

    Declaration

    Swift

    public class Channel
  • Data that is received from the Server.

    See more

    Declaration

    Swift

    public class Message
  • The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

    Syncing state from the server

    To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

    let channel = socket.channel("some:topic")
    let presence = Presence(channel)
    

    If you have custom syncing state events, you can configure the Presence object to use those instead.

    let options = Options(events: [.state: "my_state", .diff: "my_diff"])
    let presence = Presence(channel, opts: options)
    

    Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

    presence.onSync { renderUsers(presence.list()) }
    

    Listing Presences

    presence.list is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence. For example, you may have a user online from different devices with a metadata status of “online”, but they have set themselves to “away” on another device. In this case, the app may choose to use the “away” status for what appears on the UI. The example below defines a listBy function which prioritizes the first metadata which was registered for each user. This could be the first tab they opened, or the first device they came online from:

    let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
        let first = pres["metas"]!.first!
        first["count"] = pres["metas"]!.count
        first["id"] = id
        return first
    }
    let onlineUsers = presence.list(by: listBy)
    

    (NOTE: The underlying behavior is a map on the presence.state. You are mapping the state dictionary into whatever datastructure suites your needs)

    Handling individual presence join and leave events

    The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:

    let presence = Presence(channel)
    presence.onJoin { [weak self] (key, current, newPres) in
        if let cur = current {
            print("user additional presence", cur)
        } else {
            print("user entered for the first time", newPres)
        }
    }
    
    presence.onLeave { [weak self] (key, current, leftPres) in
        if current["metas"]?.isEmpty == true {
            print("user has left from all devices", leftPres)
        } else {
            print("user left from a device", current)
        }
    }
    
    presence.onSync { renderUsers(presence.list()) }
    
    See more

    Declaration

    Swift

    public final class Presence
  • Represnts pushing data to a Channel through the Socket

    See more

    Declaration

    Swift

    public class Push
  • Socket Connection

    A single connection is established to the server and channels are multiplexed over the connection. Connect to the server using the Socket class:

    let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] })
    socket.connect()
    

    The Socket constructor takes the mount point of the socket, the authentication params, as well as options that can be found in the Socket docs, such as configuring the heartbeat.

    See more

    Declaration

    Swift

    public class Socket
    extension Socket: WebSocketDelegate
  • A collection of default values and behaviors used accross the Client

    See more

    Declaration

    Swift

    public class Defaults
================================================ FILE: docs/Enums/ChannelState.html ================================================ ChannelState Enumeration Reference

SwiftPhoenixClient Docs (72% documented)

ChannelState

public enum ChannelState : String

Represents the multiple states that a Channel can be in throughout it’s lifecycle.

  • Undocumented

    Declaration

    Swift

    case closed = "closed"
  • Undocumented

    Declaration

    Swift

    case errored = "errored"
  • Undocumented

    Declaration

    Swift

    case joined = "joined"
  • Undocumented

    Declaration

    Swift

    case joining = "joining"
  • Undocumented

    Declaration

    Swift

    case leaving = "leaving"
================================================ FILE: docs/Enums.html ================================================ Enumerations Reference

SwiftPhoenixClient Docs (72% documented)

Enumerations

The following enumerations are available globally.

  • Represents the multiple states that a Channel can be in throughout it’s lifecycle.

    See more

    Declaration

    Swift

    public enum ChannelState : String
================================================ FILE: docs/Global Variables.html ================================================ Global Variables Reference

SwiftPhoenixClient Docs (70% documented)

Global Variables

The following global variables are available globally.

  • Default timeout when making a connection set to 10 seconds

    Declaration

    Swift

    public let PHOENIX_DEFAULT_TIMEOUT: Int
  • Undocumented

    Declaration

    Swift

    public let PHOENIX_TIMEOUT_INTERVAL: TimeInterval
  • Default heartbeat interval set to 30 seconds

    Declaration

    Swift

    public let PHOENIX_DEFAULT_HEARTBEAT: Int
  • Default heartbeat interval set to 30.0 seconds

    Declaration

    Swift

    public let PHOENIX_HEARTBEAT_INTERVAL: TimeInterval
================================================ FILE: docs/Protocols/Serializer.html ================================================ Serializer Protocol Reference

SwiftPhoenixClient Docs (50% documented)

Serializer

public protocol Serializer

Provides customization when enoding and decoding data within the Socket

  • Convert a message into Data to be sent over the Socket

    Declaration

    Swift

    func encode(_ message: [String : Any]) throws -> Data
  • Convert data from the Socket into a Message

    Declaration

    Swift

    func decode(_ data: Data) -> Message?
================================================ FILE: docs/Protocols.html ================================================ Protocols Reference

SwiftPhoenixClient Docs (50% documented)

Protocols

The following protocols are available globally.

  • Provides customization when enoding and decoding data within the Socket

    See more

    Declaration

    Swift

    public protocol Serializer
================================================ FILE: docs/Structs/ChannelEvent.html ================================================ ChannelEvent Structure Reference

SwiftPhoenixClient Docs (72% documented)

ChannelEvent

public struct ChannelEvent

Represents the different events that can be sent through a channel regarding a Channel’s lifecycle.

  • Undocumented

    Declaration

    Swift

    public static let heartbeat: String
  • Undocumented

    Declaration

    Swift

    public static let join: String
  • Undocumented

    Declaration

    Swift

    public static let leave: String
  • Undocumented

    Declaration

    Swift

    public static let reply: String
  • Undocumented

    Declaration

    Swift

    public static let error: String
  • Undocumented

    Declaration

    Swift

    public static let close: String
================================================ FILE: docs/Structs/Delegated.html ================================================ Delegated Structure Reference

SwiftPhoenixClient Docs (72% documented)

Delegated

public struct Delegated<Input, Output>

Provides a memory-safe way of passing callbacks around while not creating retain cycles. This file was copied from https://github.com/dreymonde/Delegated instead of added as a dependency to reduce the number of packages that ship with SwiftPhoenixClient

  • Undocumented

    Declaration

    Swift

    public init()
  • Undocumented

    Declaration

    Swift

    public mutating func delegate<Target : AnyObject>(to target: Target,
                                                      with callback: @escaping (Target, Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public func call(_ input: Input) -> Output?
  • Undocumented

    Declaration

    Swift

    public var isDelegateSet: Bool { get }
  • Undocumented

    Declaration

    Swift

    public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
                                                              with callback: @escaping (Target, Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func manuallyDelegate(with callback: @escaping (Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func removeDelegate()

Available where Input == Void

  • Undocumented

    Declaration

    Swift

    public mutating func delegate<Target : AnyObject>(to target: Target,
                                                      with callback: @escaping (Target) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
                                                              with callback: @escaping (Target) -> Output)
  • Undocumented

    Declaration

    Swift

    public func call() -> Output?

Available where Output == Void

  • Undocumented

    Declaration

    Swift

    public func call(_ input: Input)

Available where Input == Void, Output == Void

  • Undocumented

    Declaration

    Swift

    public func call()
================================================ FILE: docs/Structs.html ================================================ Structures Reference

SwiftPhoenixClient Docs (72% documented)

Structures

The following structures are available globally.

  • Represents the different events that can be sent through a channel regarding a Channel’s lifecycle.

    See more

    Declaration

    Swift

    public struct ChannelEvent
  • Provides a memory-safe way of passing callbacks around while not creating retain cycles. This file was copied from https://github.com/dreymonde/Delegated instead of added as a dependency to reduce the number of packages that ship with SwiftPhoenixClient

    See more

    Declaration

    Swift

    public struct Delegated<Input, Output>
================================================ FILE: docs/Typealiases.html ================================================ Type Aliases Reference

SwiftPhoenixClient Docs (72% documented)

Type Aliases

The following type aliases are available globally.

  • Alias for a JSON dictionary [String: Any]

    Declaration

    Swift

    public typealias Payload = [String : Any]
  • Alias for a function returning an optional JSON dictionary (Payload?)

    Declaration

    Swift

    public typealias PayloadClosure = () -> Payload?
================================================ FILE: docs/css/highlight.css ================================================ /* Credit to https://gist.github.com/wataru420/2048287 */ .highlight { /* Comment */ /* Error */ /* Keyword */ /* Operator */ /* Comment.Multiline */ /* Comment.Preproc */ /* Comment.Single */ /* Comment.Special */ /* Generic.Deleted */ /* Generic.Deleted.Specific */ /* Generic.Emph */ /* Generic.Error */ /* Generic.Heading */ /* Generic.Inserted */ /* Generic.Inserted.Specific */ /* Generic.Output */ /* Generic.Prompt */ /* Generic.Strong */ /* Generic.Subheading */ /* Generic.Traceback */ /* Keyword.Constant */ /* Keyword.Declaration */ /* Keyword.Pseudo */ /* Keyword.Reserved */ /* Keyword.Type */ /* Literal.Number */ /* Literal.String */ /* Name.Attribute */ /* Name.Builtin */ /* Name.Class */ /* Name.Constant */ /* Name.Entity */ /* Name.Exception */ /* Name.Function */ /* Name.Namespace */ /* Name.Tag */ /* Name.Variable */ /* Operator.Word */ /* Text.Whitespace */ /* Literal.Number.Float */ /* Literal.Number.Hex */ /* Literal.Number.Integer */ /* Literal.Number.Oct */ /* Literal.String.Backtick */ /* Literal.String.Char */ /* Literal.String.Doc */ /* Literal.String.Double */ /* Literal.String.Escape */ /* Literal.String.Heredoc */ /* Literal.String.Interpol */ /* Literal.String.Other */ /* Literal.String.Regex */ /* Literal.String.Single */ /* Literal.String.Symbol */ /* Name.Builtin.Pseudo */ /* Name.Variable.Class */ /* Name.Variable.Global */ /* Name.Variable.Instance */ /* Literal.Number.Integer.Long */ } .highlight .c { color: #999988; font-style: italic; } .highlight .err { color: #a61717; background-color: #e3d2d2; } .highlight .k { color: #000000; font-weight: bold; } .highlight .o { color: #000000; font-weight: bold; } .highlight .cm { color: #999988; font-style: italic; } .highlight .cp { color: #999999; font-weight: bold; } .highlight .c1 { color: #999988; font-style: italic; } .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } .highlight .gd { color: #000000; background-color: #ffdddd; } .highlight .gd .x { color: #000000; background-color: #ffaaaa; } .highlight .ge { color: #000000; font-style: italic; } .highlight .gr { color: #aa0000; } .highlight .gh { color: #999999; } .highlight .gi { color: #000000; background-color: #ddffdd; } .highlight .gi .x { color: #000000; background-color: #aaffaa; } .highlight .go { color: #888888; } .highlight .gp { color: #555555; } .highlight .gs { font-weight: bold; } .highlight .gu { color: #aaaaaa; } .highlight .gt { color: #aa0000; } .highlight .kc { color: #000000; font-weight: bold; } .highlight .kd { color: #000000; font-weight: bold; } .highlight .kp { color: #000000; font-weight: bold; } .highlight .kr { color: #000000; font-weight: bold; } .highlight .kt { color: #445588; } .highlight .m { color: #009999; } .highlight .s { color: #d14; } .highlight .na { color: #008080; } .highlight .nb { color: #0086B3; } .highlight .nc { color: #445588; font-weight: bold; } .highlight .no { color: #008080; } .highlight .ni { color: #800080; } .highlight .ne { color: #990000; font-weight: bold; } .highlight .nf { color: #990000; } .highlight .nn { color: #555555; } .highlight .nt { color: #000080; } .highlight .nv { color: #008080; } .highlight .ow { color: #000000; font-weight: bold; } .highlight .w { color: #bbbbbb; } .highlight .mf { color: #009999; } .highlight .mh { color: #009999; } .highlight .mi { color: #009999; } .highlight .mo { color: #009999; } .highlight .sb { color: #d14; } .highlight .sc { color: #d14; } .highlight .sd { color: #d14; } .highlight .s2 { color: #d14; } .highlight .se { color: #d14; } .highlight .sh { color: #d14; } .highlight .si { color: #d14; } .highlight .sx { color: #d14; } .highlight .sr { color: #009926; } .highlight .s1 { color: #d14; } .highlight .ss { color: #990073; } .highlight .bp { color: #999999; } .highlight .vc { color: #008080; } .highlight .vg { color: #008080; } .highlight .vi { color: #008080; } .highlight .il { color: #009999; } ================================================ FILE: docs/css/jazzy.css ================================================ html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { background: transparent; border: 0; margin: 0; outline: 0; padding: 0; vertical-align: baseline; } body { background-color: #f2f2f2; font-family: Helvetica, freesans, Arial, sans-serif; font-size: 14px; -webkit-font-smoothing: subpixel-antialiased; word-wrap: break-word; } h1, h2, h3 { margin-top: 0.8em; margin-bottom: 0.3em; font-weight: 100; color: black; } h1 { font-size: 2.5em; } h2 { font-size: 2em; border-bottom: 1px solid #e2e2e2; } h4 { font-size: 13px; line-height: 1.5; margin-top: 21px; } h5 { font-size: 1.1em; } h6 { font-size: 1.1em; color: #777; } .section-name { color: gray; display: block; font-family: Helvetica; font-size: 22px; font-weight: 100; margin-bottom: 15px; } pre, code { font: 0.95em Menlo, monospace; color: #777; word-wrap: normal; } p code, li code { background-color: #eee; padding: 2px 4px; border-radius: 4px; } a { color: #0088cc; text-decoration: none; } ul { padding-left: 15px; } li { line-height: 1.8em; } img { max-width: 100%; } blockquote { margin-left: 0; padding: 0 10px; border-left: 4px solid #ccc; } .content-wrapper { margin: 0 auto; width: 980px; } header { font-size: 0.85em; line-height: 26px; background-color: #414141; position: fixed; width: 100%; z-index: 2; } header img { padding-right: 6px; vertical-align: -4px; height: 16px; } header a { color: #fff; } header p { float: left; color: #999; } header .header-right { float: right; margin-left: 16px; } #breadcrumbs { background-color: #f2f2f2; height: 27px; padding-top: 17px; position: fixed; width: 100%; z-index: 2; margin-top: 26px; } #breadcrumbs #carat { height: 10px; margin: 0 5px; } .sidebar { background-color: #f9f9f9; border: 1px solid #e2e2e2; overflow-y: auto; overflow-x: hidden; position: fixed; top: 70px; bottom: 0; width: 230px; word-wrap: normal; } .nav-groups { list-style-type: none; background: #fff; padding-left: 0; } .nav-group-name { border-bottom: 1px solid #e2e2e2; font-size: 1.1em; font-weight: 100; padding: 15px 0 15px 20px; } .nav-group-name > a { color: #333; } .nav-group-tasks { margin-top: 5px; } .nav-group-task { font-size: 0.9em; list-style-type: none; white-space: nowrap; } .nav-group-task a { color: #888; } .main-content { background-color: #fff; border: 1px solid #e2e2e2; margin-left: 246px; position: absolute; overflow: hidden; padding-bottom: 20px; top: 70px; width: 734px; } .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { margin-bottom: 1em; } .main-content p { line-height: 1.8em; } .main-content section .section:first-child { margin-top: 0; padding-top: 0; } .main-content section .task-group-section .task-group:first-of-type { padding-top: 10px; } .main-content section .task-group-section .task-group:first-of-type .section-name { padding-top: 15px; } .main-content section .heading:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .main-content .section-name p { margin-bottom: inherit; line-height: inherit; } .main-content .section-name code { background-color: inherit; padding: inherit; color: inherit; } .section { padding: 0 25px; } .highlight { background-color: #eee; padding: 10px 12px; border: 1px solid #e2e2e2; border-radius: 4px; overflow-x: auto; } .declaration .highlight { overflow-x: initial; padding: 0 40px 40px 0; margin-bottom: -25px; background-color: transparent; border: none; } .section-name { margin: 0; margin-left: 18px; } .task-group-section { padding-left: 6px; border-top: 1px solid #e2e2e2; } .task-group { padding-top: 0px; } .task-name-container a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .section-name-container { position: relative; display: inline-block; } .section-name-container .section-name-link { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin-bottom: 0; } .section-name-container .section-name { position: relative; pointer-events: none; z-index: 1; } .section-name-container .section-name a { pointer-events: auto; } .item { padding-top: 8px; width: 100%; list-style-type: none; } .item a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .item code { background-color: transparent; padding: 0; } .item .token, .item .direct-link { padding-left: 3px; margin-left: 15px; font-size: 11.9px; transition: all 300ms; } .item .token-open { margin-left: 0px; } .item .discouraged { text-decoration: line-through; } .item .declaration-note { font-size: .85em; color: gray; font-style: italic; } .pointer-container { border-bottom: 1px solid #e2e2e2; left: -23px; padding-bottom: 13px; position: relative; width: 110%; } .pointer { background: #f9f9f9; border-left: 1px solid #e2e2e2; border-top: 1px solid #e2e2e2; height: 12px; left: 21px; top: -7px; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); position: absolute; width: 12px; } .height-container { display: none; left: -25px; padding: 0 25px; position: relative; width: 100%; overflow: hidden; } .height-container .section { background: #f9f9f9; border-bottom: 1px solid #e2e2e2; left: -25px; position: relative; width: 100%; padding-top: 10px; padding-bottom: 5px; } .aside, .language { padding: 6px 12px; margin: 12px 0; border-left: 5px solid #dddddd; overflow-y: hidden; } .aside .aside-title, .language .aside-title { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; padding-bottom: 0; margin: 0; color: #aaa; -webkit-user-select: none; } .aside p:last-child, .language p:last-child { margin-bottom: 0; } .language { border-left: 5px solid #cde9f4; } .language .aside-title { color: #4b8afb; } .aside-warning, .aside-deprecated, .aside-unavailable { border-left: 5px solid #ff6666; } .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { color: #ff0000; } .graybox { border-collapse: collapse; width: 100%; } .graybox p { margin: 0; word-break: break-word; min-width: 50px; } .graybox td { border: 1px solid #e2e2e2; padding: 5px 25px 5px 10px; vertical-align: middle; } .graybox tr td:first-of-type { text-align: right; padding: 7px; vertical-align: top; word-break: normal; width: 40px; } .slightly-smaller { font-size: 0.9em; } #footer { position: relative; top: 10px; bottom: 0px; margin-left: 25px; } #footer p { margin: 0; color: #aaa; font-size: 0.8em; } html.dash header, html.dash #breadcrumbs, html.dash .sidebar { display: none; } html.dash .main-content { width: 980px; margin-left: 0; border: none; width: 100%; top: 0; padding-bottom: 0; } html.dash .height-container { display: block; } html.dash .item .token { margin-left: 0; } html.dash .content-wrapper { width: auto; } html.dash #footer { position: static; } ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Info.plist ================================================ CFBundleIdentifier com.jazzy.swiftphoenixclient CFBundleName SwiftPhoenixClient DocSetPlatformFamily swiftphoenixclient isDashDocset dashIndexFilePath index.html isJavaScriptEnabled DashDocSetFamily dashtoc ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Channel.html ================================================ Channel Class Reference

SwiftPhoenixClient Docs (72% documented)

Channel

public class Channel

Undocumented

  • The topic of the Channel. e.g. “rooms:friends”

    Declaration

    Swift

    public let topic: String
  • The params sent when joining the channel

    Declaration

    Swift

    public var params: Payload { get set }
  • Overridable message hook. Receives all events for specialized message handling before dispatching to the channel callbacks.

    • return: Must return the message, modified or unmodified

    Declaration

    Swift

    public var onMessage: (Message) -> Message

    Parameters

    msg

    The Message received by the client from the server

  • Joins the channel

    • return: Push event

    Declaration

    Swift

    @discardableResult
    public func join(timeout: TimeInterval? = nil) -> Push

    Parameters

    timeout

    Optional. Defaults to Channel’s timeout

  • Hook into when the Channel is closed. Does not handle retain cycles. Use delegateOnClose(to:) for automatic handling of retain cycles.

    Example:

    let channel = socket.channel("topic")
    channel.onClose() { [weak self] message in
        self?.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func onClose(_ callback: @escaping ((Message) -> Void)) -> Int

    Parameters

    callback

    Called when the Channel closes

  • Hook into when the Channel is closed. Automatically handles retain cycles. Use onClose() to handle yourself.

    Example:

    let channel = socket.channel("topic")
    channel.delegateOnClose(to: self) { (self, message) in
        self.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOnClose<Target: AnyObject>(to owner: Target,
                                                   callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Channel closes

  • Hook into when the Channel receives an Error. Does not handle retain cycles. Use delegateOnError(to:) for automatic handling of retain cycles.

    Example:

    let channel = socket.channel("topic")
    channel.onError() { [weak self] (message) in
        self?.print("Channel \(message.topic) has errored"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func onError(_ callback: @escaping ((_ message: Message) -> Void)) -> Int

    Parameters

    callback

    Called when the Channel closes

  • Hook into when the Channel receives an Error. Automatically handles retain cycles. Use onError() to handle yourself.

    Example:

    let channel = socket.channel("topic")
    channel.delegateOnError(to: self) { (self, message) in
        self.print("Channel \(message.topic) has closed"
    }
    
    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOnError<Target: AnyObject>(to owner: Target,
                                                   callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Channel closes

  • Subscribes on channel events. Does not handle retain cycles. Use delegateOn(_:, to:) for automatic handling of retain cycles.

    Subscription returns a ref counter, which can be used later to unsubscribe the exact event listener

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.on("event") { [weak self] (message) in
        self?.print("do stuff")
    }
    let ref2 = channel.on("event") { [weak self] (message) in
        self?.print("do other stuff")
    }
    channel.off("event", ref1)
    

    Since unsubscription of ref1, “do stuff” won’t print, but “do other stuff” will keep on printing on the “event”

    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func on(_ event: String, callback: @escaping ((Message) -> Void)) -> Int

    Parameters

    event

    Event to receive

    callback

    Called with the event’s message

  • Subscribes on channel events. Automatically handles retain cycles. Use on() to handle yourself.

    Subscription returns a ref counter, which can be used later to unsubscribe the exact event listener

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.delegateOn("event", to: self) { (self, message) in
        self?.print("do stuff")
    }
    let ref2 = channel.delegateOn("event", to: self) { (self, message) in
        self?.print("do other stuff")
    }
    channel.off("event", ref1)
    

    Since unsubscription of ref1, “do stuff” won’t print, but “do other stuff” will keep on printing on the “event”

    • return: Ref counter of the subscription. See func off()

    Declaration

    Swift

    @discardableResult
    public func delegateOn<Target: AnyObject>(_ event: String,
                                              to owner: Target,
                                              callback: @escaping ((Target, Message) -> Void)) -> Int

    Parameters

    event

    Event to receive

    owner

    Class registering the callback. Usually self

    callback

    Called with the event’s message

  • Unsubscribes from a channel event. If a ref is given, only the exact listener will be removed. Else all listeners for the event will be removed.

    Example:

    let channel = socket.channel("topic")
    let ref1 = channel.on("event") { _ in print("ref1 event" }
    let ref2 = channel.on("event") { _ in print("ref2 event" }
    let ref3 = channel.on("other_event") { _ in print("ref3 other" }
    let ref4 = channel.on("other_event") { _ in print("ref4 other" }
    channel.off("event", ref1)
    channel.off("other_event")
    

    After this, only “ref2 event” will be printed if the channel receives “event” and nothing is printed if the channel receives “other_event”.

    • paramter ref: Ref counter returned when subscribing. Can be omitted

    Declaration

    Swift

    public func off(_ event: String, ref: Int? = nil)

    Parameters

    event

    Event to unsubscribe from

  • Push a payload to the Channel

    Example:

    channel
        .push("event", payload: ["message": "hello")
        .receive("ok") { _ in { print("message sent") }
    

    Declaration

    Swift

    @discardableResult
    public func push(_ event: String,
                     payload: Payload,
                     timeout: TimeInterval = Defaults.timeoutInterval) -> Push

    Parameters

    event

    Event to push

    payload

    Payload to push

    timeout

    Optional timeout

  • Leaves the channel

    Unsubscribes from server events, and instructs channel to terminate on server

    Triggers onClose() hooks

    To receive leave acknowledgements, use the a receive hook to bind to the server ack, ie:

    Example: / channel.leave().receive(“ok”) { _ in { print(“left”) }

    • return: Push that can add receive hooks

    Declaration

    Swift

    @discardableResult
    public func leave(timeout: TimeInterval = Defaults.timeoutInterval) -> Push

    Parameters

    timeout

    Optional timeout

  • Overridable message hook. Receives all events for specialized message handling before dispatching to the channel callbacks.

    • return: Must return the payload, modified or unmodified

    Declaration

    Swift

    public func onMessage(callback: @escaping (Message) -> Message)

    Parameters

    event

    The event the message was for

    payload

    The payload for the message

    ref

    The reference of the message

Public API

    • return: True if the Channel has been closed

    Declaration

    Swift

    public var isClosed: Bool { get }
    • return: True if the Channel experienced an error

    Declaration

    Swift

    public var isErrored: Bool { get }
    • return: True if the channel has joined

    Declaration

    Swift

    public var isJoined: Bool { get }
    • return: True if the channel has requested to join

    Declaration

    Swift

    public var isJoining: Bool { get }
    • return: True if the channel has requested to leave

    Declaration

    Swift

    public var isLeaving: Bool { get }
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Defaults.html ================================================ Defaults Class Reference

SwiftPhoenixClient Docs (72% documented)

Defaults

public class Defaults

A collection of default values and behaviors used accross the Client

  • Default timeout when sending messages

    Declaration

    Swift

    public static let timeoutInterval: TimeInterval
  • Default interval to send heartbeats on

    Declaration

    Swift

    public static let heartbeatInterval: TimeInterval
  • Default reconnect algorithm for the socket

    Declaration

    Swift

    public static let reconnectSteppedBackOff: (Int) -> TimeInterval
  • Default rejoin algorithm for individual channels

    Declaration

    Swift

    public static let rejoinSteppedBackOff: (Int) -> TimeInterval
  • Default encode function, utilizing JSONSerialization.data

    Declaration

    Swift

    public static let encode: ([String : Any]) -> Data
  • Default decode function, utilizing JSONSerialization.jsonObject

    Declaration

    Swift

    public static let decode: (Data) -> [String : Any]?
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Message.html ================================================ Message Class Reference

SwiftPhoenixClient Docs (72% documented)

Message

public class Message

Data that is received from the Server.

  • ref

    Reference number. Empty if missing

    Declaration

    Swift

    public let ref: String
  • Message topic

    Declaration

    Swift

    public let topic: String
  • Message event

    Declaration

    Swift

    public let event: String
  • Message payload

    Declaration

    Swift

    public var payload: Payload
  • Convenience accessor. Equivalent to getting the status as such:

    message.payload["status"]
    

    Declaration

    Swift

    public var status: String? { get }
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence/Events.html ================================================ Events Enumeration Reference

SwiftPhoenixClient Docs (72% documented)

Events

public enum Events : String

Presense Events

  • Undocumented

    Declaration

    Swift

    case state = "state"
  • Undocumented

    Declaration

    Swift

    case diff = "diff"
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence/Options.html ================================================ Options Structure Reference

SwiftPhoenixClient Docs (72% documented)

Options

public struct Options

Custom options that can be provided when creating Presence

Example:

let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)
  • Default set of Options used when creating Presence. Uses the phoenix events “presence_state” and “presence_diff”

    Declaration

    Swift

    public static let defaults: Presence.Options
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Presence.html ================================================ Presence Class Reference

SwiftPhoenixClient Docs (72% documented)

Presence

public final class Presence

The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

Syncing state from the server

To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

let channel = socket.channel("some:topic")
let presence = Presence(channel)

If you have custom syncing state events, you can configure the Presence object to use those instead.

let options = Options(events: [.state: "my_state", .diff: "my_diff"])
let presence = Presence(channel, opts: options)

Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

presence.onSync { renderUsers(presence.list()) }

Listing Presences

presence.list is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence. For example, you may have a user online from different devices with a metadata status of “online”, but they have set themselves to “away” on another device. In this case, the app may choose to use the “away” status for what appears on the UI. The example below defines a listBy function which prioritizes the first metadata which was registered for each user. This could be the first tab they opened, or the first device they came online from:

let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
    let first = pres["metas"]!.first!
    first["count"] = pres["metas"]!.count
    first["id"] = id
    return first
}
let onlineUsers = presence.list(by: listBy)

(NOTE: The underlying behavior is a map on the presence.state. You are mapping the state dictionary into whatever datastructure suites your needs)

Handling individual presence join and leave events

The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:

let presence = Presence(channel)
presence.onJoin { [weak self] (key, current, newPres) in
    if let cur = current {
        print("user additional presence", cur)
    } else {
        print("user entered for the first time", newPres)
    }
}

presence.onLeave { [weak self] (key, current, leftPres) in
    if current["metas"]?.isEmpty == true {
        print("user has left from all devices", leftPres)
    } else {
        print("user left from a device", current)
    }
}

presence.onSync { renderUsers(presence.list()) }

Enums and Structs

  • Custom options that can be provided when creating Presence

    Example:

    let options = Options(events: [.state: "my_state", .diff: "my_diff"])
    let presence = Presence(channel, opts: options)
    
    See more

    Declaration

    Swift

    public struct Options
  • Presense Events

    See more

    Declaration

    Swift

    public enum Events : String

Typaliases

  • Meta details of a Presence. Just a dictionary of properties

    Declaration

    Swift

    public typealias Meta = [String : Any]
  • Map

    A mapping of a String to an array of Metas. e.g. {“metas”: [{id: 1}]}

    Declaration

    Swift

    public typealias Map = [String : [Meta]]
  • A mapping of a Presence state to a mapping of Metas

    Declaration

    Swift

    public typealias State = [String : Map]
  • Undocumented

    Declaration

    Swift

    public typealias Diff = [String : State]
  • Closure signature of OnJoin callbacks

    Declaration

    Swift

    public typealias OnJoin = (_ key: String, _ current: Map?, _ new: Map) -> Void
  • Closure signature for OnLeave callbacks

    Declaration

    Swift

    public typealias OnLeave = (_ key: String, _ current: Map, _ left: Map) -> Void
  • / Closure signature for OnSync callbacks

    Declaration

    Swift

    public typealias OnSync = () -> Void

Properties

  • The state of the Presence

    Declaration

    Swift

    private(set) public var state: State
  • Pending join and leave diffs that need to be synced

    Declaration

    Swift

    private(set) public var pendingDiffs: [Diff]
  • The channel’s joinRef, set when state events occur

    Declaration

    Swift

    private(set) public var joinRef: String?
  • Undocumented

    Declaration

    Swift

    public var isPendingSyncState: Bool { get }
  • Callback to be informed of joins

    Declaration

    Swift

    public var onJoin: OnJoin { get set }
  • Set the OnJoin callback

    Declaration

    Swift

    public func onJoin(_ callback: @escaping OnJoin)
  • Callback to be informed of leaves

    Declaration

    Swift

    public var onLeave: OnLeave { get set }
  • Set the OnLeave callback

    Declaration

    Swift

    public func onLeave(_ callback: @escaping OnLeave)
  • Callback to be informed of synces

    Declaration

    Swift

    public var onSync: OnSync { get set }
  • Set the OnSync callback

    Declaration

    Swift

    public func onSync(_ callback: @escaping OnSync)
  • Undocumented

    Declaration

    Swift

    public init(channel: Channel, opts: Options = Options.defaults)
  • Returns the array of presences, with deault selected metadata.

    Declaration

    Swift

    public func list() -> [Map]
  • Returns the array of presences, with selected metadata

    Declaration

    Swift

    public func list<T>(by transformer: (String, Map) -> T) -> [T]
  • Filter the Presence state with a given function

    Declaration

    Swift

    public func filter(by filter: ((String, Map) -> Bool)?) -> State

Static

================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Push.html ================================================ Push Class Reference

SwiftPhoenixClient Docs (72% documented)

Push

public class Push

Represnts pushing data to a Channel through the Socket

  • The channel sending the Push

    Declaration

    Swift

    public weak var channel: Channel?
  • The event, for example phx_join

    Declaration

    Swift

    public let event: String
  • The payload, for example [“user_id”: “abc123”]

    Declaration

    Swift

    public var payload: Payload
  • The push timeout. Default is 10.0 seconds

    Declaration

    Swift

    public var timeout: TimeInterval
  • Resets and sends the Push

    Declaration

    Swift

    public func resend(_ timeout: TimeInterval = Defaults.timeoutInterval)

    Parameters

    timeout

    Optional. The push timeout. Default is 10.0s

  • Sends the Push. If it has already timed out, then the call will be ignored and return early. Use resend in this case.

    Declaration

    Swift

    public func send()
  • Receive a specific event when sending an Outbound message. Subscribing to status events with this method does not guarantees no retain cycles. You should pass weak self in the capture list of the callback. You can call `.delegateReceive(status:, to:, callback:) and the library will handle it for you.

    Example:

    channel
        .send(event:"custom", payload: ["body": "example"])
        .receive("error") { [weak self] payload in
            print("Error: ", payload)
        }
    

    Declaration

    Swift

    @discardableResult
    public func receive(_ status: String,
                        callback: @escaping ((Message) -> ())) -> Push

    Parameters

    status

    Status to receive

    callback

    Callback to fire when the status is recevied

  • Receive a specific event when sending an Outbound message. Automatically prevents retain cycles. See manualReceive(status:, callback:) if you want to handle this yourself.

    Example:

    channel
        .send(event:"custom", payload: ["body": "example"])
        .delegateReceive("error", to: self) { payload in
            print("Error: ", payload)
        }
    

    Declaration

    Swift

    @discardableResult
    public func delegateReceive<Target: AnyObject>(_ status: String,
                                                   to owner: Target,
                                                   callback: @escaping ((Target, Message) -> ())) -> Push

    Parameters

    status

    Status to receive

    owner

    The class that is calling .receive. Usually self

    callback

    Callback to fire when the status is recevied

================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes/Socket.html ================================================ Socket Class Reference

SwiftPhoenixClient Docs (72% documented)

Socket

public class Socket
extension Socket: WebSocketDelegate

Socket Connection

A single connection is established to the server and channels are multiplexed over the connection. Connect to the server using the Socket class:

let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] })
socket.connect()

The Socket constructor takes the mount point of the socket, the authentication params, as well as options that can be found in the Socket docs, such as configuring the heartbeat.

Public Attributes

  • The string WebSocket endpoint (ie "ws://example.com/socket", "wss://example.com", etc.) That was passed to the Socket during initialization. The URL endpoint will be modified by the Socket to include "/websocket" if missing.

    Declaration

    Swift

    public let endPoint: String
  • The fully qualified socket URL

    Declaration

    Swift

    public private(set) var endPointUrl: URL
  • Resolves to return the paramsClosure result at the time of calling. If the Socket was created with static params, then those will be returned every time.

    Declaration

    Swift

    public var params: Payload? { get }
  • The optional params closure used to get params whhen connecting. Must be set when initializaing the Socket.

    Declaration

    Swift

    public let paramsClosure: PayloadClosure?
  • Override to provide custom encoding of data before writing to the socket

    Declaration

    Swift

    public var encode: ([String : Any]) -> Data
  • Override to provide customd decoding of data read from the socket

    Declaration

    Swift

    public var decode: (Data) -> [String : Any]?
  • Timeout to use when opening connections

    Declaration

    Swift

    public var timeout: TimeInterval
  • Interval between sending a heartbeat

    Declaration

    Swift

    public var heartbeatInterval: TimeInterval
  • Interval between socket reconnect attempts, in seconds

    Declaration

    Swift

    public var reconnectAfter: (Int) -> TimeInterval
  • Interval between channel rejoin attempts, in seconds

    Declaration

    Swift

    public var rejoinAfter: (Int) -> TimeInterval
  • The optional function to receive logs

    Declaration

    Swift

    public var logger: ((String) -> Void)?
  • Disables heartbeats from being sent. Default is false.

    Declaration

    Swift

    public var skipHeartbeat: Bool
  • Enable/Disable SSL certificate validation. Default is false. This must be set before calling socket.connect() in order to be applied

    Declaration

    Swift

    public var disableSSLCertValidation: Bool
  • Configure custom SSL validation logic, eg. SSL pinning. This must be set before calling socket.connect() in order to apply.

    Declaration

    Swift

    public var security: SSLTrustValidator?
  • Configure the encryption used by your client by setting the allowed cipher suites supported by your server. This must be set before calling socket.connect() in order to apply.

    Declaration

    Swift

    public var enabledSSLCipherSuites: [SSLCipherSuite]?

Initialization

Public

    • return: The socket protocol, wss or ws

    Declaration

    Swift

    public var websocketProtocol: String { get }
    • return: True if the socket is connected

    Declaration

    Swift

    public var isConnected: Bool { get }
  • Connects the Socket. The params passed to the Socket on initialization will be sent through the connection. If the Socket is already connected, then this call will be ignored.

    Declaration

    Swift

    public func connect()
  • Disconnects the socket

    • paramter callback: Optional. Called when disconnected

    Declaration

    Swift

    public func disconnect(code: CloseCode = CloseCode.normal,
                           callback: (() -> Void)? = nil)

    Parameters

    code

    Optional. Closing status code

Register Socket State Callbacks

  • Registers callbacks for connection open events. Does not handle retain cycles. Use delegateOnOpen(to:) for automatic handling of retain cycles.

    Example:

    socket.onOpen() { [weak self] in
        self?.print("Socket Connection Open")
    }
    

    Declaration

    Swift

    public func onOpen(callback: @escaping () -> Void)

    Parameters

    callback

    Called when the Socket is opened

  • Registers callbacks for connection open events. Automatically handles retain cycles. Use onOpen() to handle yourself.

    Example:

    socket.delegateOnOpen(to: self) { self in
        self.print("Socket Connection Open")
    }
    

    Declaration

    Swift

    public func delegateOnOpen<T: AnyObject>(to owner: T,
                                             callback: @escaping ((T) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket is opened

  • Registers callbacks for connection close events. Does not handle retain cycles. Use delegateOnClose(_:) for automatic handling of retain cycles.

    Example:

    socket.onClose() { [weak self] in
        self?.print("Socket Connection Close")
    }
    

    Declaration

    Swift

    public func onClose(callback: @escaping () -> Void)

    Parameters

    callback

    Called when the Socket is closed

  • Registers callbacks for connection close events. Automatically handles retain cycles. Use onClose() to handle yourself.

    Example:

    socket.delegateOnClose(self) { self in
        self.print("Socket Connection Close")
    }
    

    Declaration

    Swift

    public func delegateOnClose<T: AnyObject>(to owner: T,
                                              callback: @escaping ((T) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket is closed

  • Registers callbacks for connection error events. Does not handle retain cycles. Use delegateOnError(to:) for automatic handling of retain cycles.

    Example:

    socket.onError() { [weak self] (error) in
        self?.print("Socket Connection Error", error)
    }
    

    Declaration

    Swift

    public func onError(callback: @escaping (Error) -> Void)

    Parameters

    callback

    Called when the Socket errors

  • Registers callbacks for connection error events. Automatically handles retain cycles. Use manualOnError() to handle yourself.

    Example:

    socket.delegateOnError(to: self) { (self, error) in
        self.print("Socket Connection Error", error)
    }
    

    Declaration

    Swift

    public func delegateOnError<T: AnyObject>(to owner: T,
                                              callback: @escaping ((T, Error) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket errors

  • Registers callbacks for connection message events. Does not handle retain cycles. Use delegateOnMessage(_to:) for automatic handling of retain cycles.

    Example:

    socket.onMessage() { [weak self] (message) in
        self?.print("Socket Connection Message", message)
    }
    

    Declaration

    Swift

    public func onMessage(callback: @escaping (Message) -> Void)

    Parameters

    callback

    Called when the Socket receives a message event

  • Registers callbacks for connection message events. Automatically handles retain cycles. Use onMessage() to handle yourself.

    Example:

    socket.delegateOnMessage(self) { (self, message) in
        self.print("Socket Connection Message", message)
    }
    

    Declaration

    Swift

    public func delegateOnMessage<T: AnyObject>(to owner: T,
                                                callback: @escaping ((T, Message) -> Void))

    Parameters

    owner

    Class registering the callback. Usually self

    callback

    Called when the Socket receives a message event

  • Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should call this method when you are finished when the Socket in order to release any references held by the socket.

    Declaration

    Swift

    public func releaseCallbacks()

Channel Initialization

  • Initialize a new Channel

    Example:

    let channel = socket.channel("rooms", params: ["user_id": "abc123"])
    
    • return: A new channel

    Declaration

    Swift

    public func channel(_ topic: String,
                        params: [String: Any] = [:]) -> Channel

    Parameters

    topic

    Topic of the channel

    params

    Optional. Parameters for the channel

  • Removes the Channel from the socket. This does not cause the channel to inform the server that it is leaving. You should call channel.leave() prior to removing the Channel.

    Example:

    channel.leave()
    socket.remove(channel)
    

    Declaration

    Swift

    public func remove(_ channel: Channel)

    Parameters

    channel

    Channel to remove

Sending Data

    • return: the next message ref, accounting for overflows

    Declaration

    Swift

    public func makeRef() -> String

WebSocketDelegate

================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Classes.html ================================================ Classes Reference

SwiftPhoenixClient Docs (72% documented)

Classes

The following classes are available globally.

  • Undocumented

    See more

    Declaration

    Swift

    public class Channel
  • Data that is received from the Server.

    See more

    Declaration

    Swift

    public class Message
  • The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

    Syncing state from the server

    To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

    let channel = socket.channel("some:topic")
    let presence = Presence(channel)
    

    If you have custom syncing state events, you can configure the Presence object to use those instead.

    let options = Options(events: [.state: "my_state", .diff: "my_diff"])
    let presence = Presence(channel, opts: options)
    

    Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

    presence.onSync { renderUsers(presence.list()) }
    

    Listing Presences

    presence.list is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence. For example, you may have a user online from different devices with a metadata status of “online”, but they have set themselves to “away” on another device. In this case, the app may choose to use the “away” status for what appears on the UI. The example below defines a listBy function which prioritizes the first metadata which was registered for each user. This could be the first tab they opened, or the first device they came online from:

    let listBy: (String, Presence.Map) -> Presence.Meta = { id, pres in
        let first = pres["metas"]!.first!
        first["count"] = pres["metas"]!.count
        first["id"] = id
        return first
    }
    let onlineUsers = presence.list(by: listBy)
    

    (NOTE: The underlying behavior is a map on the presence.state. You are mapping the state dictionary into whatever datastructure suites your needs)

    Handling individual presence join and leave events

    The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:

    let presence = Presence(channel)
    presence.onJoin { [weak self] (key, current, newPres) in
        if let cur = current {
            print("user additional presence", cur)
        } else {
            print("user entered for the first time", newPres)
        }
    }
    
    presence.onLeave { [weak self] (key, current, leftPres) in
        if current["metas"]?.isEmpty == true {
            print("user has left from all devices", leftPres)
        } else {
            print("user left from a device", current)
        }
    }
    
    presence.onSync { renderUsers(presence.list()) }
    
    See more

    Declaration

    Swift

    public final class Presence
  • Represnts pushing data to a Channel through the Socket

    See more

    Declaration

    Swift

    public class Push
  • Socket Connection

    A single connection is established to the server and channels are multiplexed over the connection. Connect to the server using the Socket class:

    let socket = new Socket("/socket", paramsClosure: { ["userToken": "123" ] })
    socket.connect()
    

    The Socket constructor takes the mount point of the socket, the authentication params, as well as options that can be found in the Socket docs, such as configuring the heartbeat.

    See more

    Declaration

    Swift

    public class Socket
    extension Socket: WebSocketDelegate
  • A collection of default values and behaviors used accross the Client

    See more

    Declaration

    Swift

    public class Defaults
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Enums/ChannelState.html ================================================ ChannelState Enumeration Reference

SwiftPhoenixClient Docs (72% documented)

ChannelState

public enum ChannelState : String

Represents the multiple states that a Channel can be in throughout it’s lifecycle.

  • Undocumented

    Declaration

    Swift

    case closed = "closed"
  • Undocumented

    Declaration

    Swift

    case errored = "errored"
  • Undocumented

    Declaration

    Swift

    case joined = "joined"
  • Undocumented

    Declaration

    Swift

    case joining = "joining"
  • Undocumented

    Declaration

    Swift

    case leaving = "leaving"
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Enums.html ================================================ Enumerations Reference

SwiftPhoenixClient Docs (72% documented)

Enumerations

The following enumerations are available globally.

  • Represents the multiple states that a Channel can be in throughout it’s lifecycle.

    See more

    Declaration

    Swift

    public enum ChannelState : String
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Global Variables.html ================================================ Global Variables Reference

SwiftPhoenixClient Docs (70% documented)

Global Variables

The following global variables are available globally.

  • Default timeout when making a connection set to 10 seconds

    Declaration

    Swift

    public let PHOENIX_DEFAULT_TIMEOUT: Int
  • Undocumented

    Declaration

    Swift

    public let PHOENIX_TIMEOUT_INTERVAL: TimeInterval
  • Default heartbeat interval set to 30 seconds

    Declaration

    Swift

    public let PHOENIX_DEFAULT_HEARTBEAT: Int
  • Default heartbeat interval set to 30.0 seconds

    Declaration

    Swift

    public let PHOENIX_HEARTBEAT_INTERVAL: TimeInterval
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Protocols/Serializer.html ================================================ Serializer Protocol Reference

SwiftPhoenixClient Docs (50% documented)

Serializer

public protocol Serializer

Provides customization when enoding and decoding data within the Socket

  • Convert a message into Data to be sent over the Socket

    Declaration

    Swift

    func encode(_ message: [String : Any]) throws -> Data
  • Convert data from the Socket into a Message

    Declaration

    Swift

    func decode(_ data: Data) -> Message?
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Protocols.html ================================================ Protocols Reference

SwiftPhoenixClient Docs (50% documented)

Protocols

The following protocols are available globally.

  • Provides customization when enoding and decoding data within the Socket

    See more

    Declaration

    Swift

    public protocol Serializer
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs/ChannelEvent.html ================================================ ChannelEvent Structure Reference

SwiftPhoenixClient Docs (72% documented)

ChannelEvent

public struct ChannelEvent

Represents the different events that can be sent through a channel regarding a Channel’s lifecycle.

  • Undocumented

    Declaration

    Swift

    public static let heartbeat: String
  • Undocumented

    Declaration

    Swift

    public static let join: String
  • Undocumented

    Declaration

    Swift

    public static let leave: String
  • Undocumented

    Declaration

    Swift

    public static let reply: String
  • Undocumented

    Declaration

    Swift

    public static let error: String
  • Undocumented

    Declaration

    Swift

    public static let close: String
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs/Delegated.html ================================================ Delegated Structure Reference

SwiftPhoenixClient Docs (72% documented)

Delegated

public struct Delegated<Input, Output>

Provides a memory-safe way of passing callbacks around while not creating retain cycles. This file was copied from https://github.com/dreymonde/Delegated instead of added as a dependency to reduce the number of packages that ship with SwiftPhoenixClient

  • Undocumented

    Declaration

    Swift

    public init()
  • Undocumented

    Declaration

    Swift

    public mutating func delegate<Target : AnyObject>(to target: Target,
                                                      with callback: @escaping (Target, Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public func call(_ input: Input) -> Output?
  • Undocumented

    Declaration

    Swift

    public var isDelegateSet: Bool { get }
  • Undocumented

    Declaration

    Swift

    public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
                                                              with callback: @escaping (Target, Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func manuallyDelegate(with callback: @escaping (Input) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func removeDelegate()

Available where Input == Void

  • Undocumented

    Declaration

    Swift

    public mutating func delegate<Target : AnyObject>(to target: Target,
                                                      with callback: @escaping (Target) -> Output)
  • Undocumented

    Declaration

    Swift

    public mutating func stronglyDelegate<Target : AnyObject>(to target: Target,
                                                              with callback: @escaping (Target) -> Output)
  • Undocumented

    Declaration

    Swift

    public func call() -> Output?

Available where Output == Void

  • Undocumented

    Declaration

    Swift

    public func call(_ input: Input)

Available where Input == Void, Output == Void

  • Undocumented

    Declaration

    Swift

    public func call()
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Structs.html ================================================ Structures Reference

SwiftPhoenixClient Docs (72% documented)

Structures

The following structures are available globally.

  • Represents the different events that can be sent through a channel regarding a Channel’s lifecycle.

    See more

    Declaration

    Swift

    public struct ChannelEvent
  • Provides a memory-safe way of passing callbacks around while not creating retain cycles. This file was copied from https://github.com/dreymonde/Delegated instead of added as a dependency to reduce the number of packages that ship with SwiftPhoenixClient

    See more

    Declaration

    Swift

    public struct Delegated<Input, Output>
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/Typealiases.html ================================================ Type Aliases Reference

SwiftPhoenixClient Docs (72% documented)

Type Aliases

The following type aliases are available globally.

  • Alias for a JSON dictionary [String: Any]

    Declaration

    Swift

    public typealias Payload = [String : Any]
  • Alias for a function returning an optional JSON dictionary (Payload?)

    Declaration

    Swift

    public typealias PayloadClosure = () -> Payload?
================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/css/highlight.css ================================================ /* Credit to https://gist.github.com/wataru420/2048287 */ .highlight { /* Comment */ /* Error */ /* Keyword */ /* Operator */ /* Comment.Multiline */ /* Comment.Preproc */ /* Comment.Single */ /* Comment.Special */ /* Generic.Deleted */ /* Generic.Deleted.Specific */ /* Generic.Emph */ /* Generic.Error */ /* Generic.Heading */ /* Generic.Inserted */ /* Generic.Inserted.Specific */ /* Generic.Output */ /* Generic.Prompt */ /* Generic.Strong */ /* Generic.Subheading */ /* Generic.Traceback */ /* Keyword.Constant */ /* Keyword.Declaration */ /* Keyword.Pseudo */ /* Keyword.Reserved */ /* Keyword.Type */ /* Literal.Number */ /* Literal.String */ /* Name.Attribute */ /* Name.Builtin */ /* Name.Class */ /* Name.Constant */ /* Name.Entity */ /* Name.Exception */ /* Name.Function */ /* Name.Namespace */ /* Name.Tag */ /* Name.Variable */ /* Operator.Word */ /* Text.Whitespace */ /* Literal.Number.Float */ /* Literal.Number.Hex */ /* Literal.Number.Integer */ /* Literal.Number.Oct */ /* Literal.String.Backtick */ /* Literal.String.Char */ /* Literal.String.Doc */ /* Literal.String.Double */ /* Literal.String.Escape */ /* Literal.String.Heredoc */ /* Literal.String.Interpol */ /* Literal.String.Other */ /* Literal.String.Regex */ /* Literal.String.Single */ /* Literal.String.Symbol */ /* Name.Builtin.Pseudo */ /* Name.Variable.Class */ /* Name.Variable.Global */ /* Name.Variable.Instance */ /* Literal.Number.Integer.Long */ } .highlight .c { color: #999988; font-style: italic; } .highlight .err { color: #a61717; background-color: #e3d2d2; } .highlight .k { color: #000000; font-weight: bold; } .highlight .o { color: #000000; font-weight: bold; } .highlight .cm { color: #999988; font-style: italic; } .highlight .cp { color: #999999; font-weight: bold; } .highlight .c1 { color: #999988; font-style: italic; } .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } .highlight .gd { color: #000000; background-color: #ffdddd; } .highlight .gd .x { color: #000000; background-color: #ffaaaa; } .highlight .ge { color: #000000; font-style: italic; } .highlight .gr { color: #aa0000; } .highlight .gh { color: #999999; } .highlight .gi { color: #000000; background-color: #ddffdd; } .highlight .gi .x { color: #000000; background-color: #aaffaa; } .highlight .go { color: #888888; } .highlight .gp { color: #555555; } .highlight .gs { font-weight: bold; } .highlight .gu { color: #aaaaaa; } .highlight .gt { color: #aa0000; } .highlight .kc { color: #000000; font-weight: bold; } .highlight .kd { color: #000000; font-weight: bold; } .highlight .kp { color: #000000; font-weight: bold; } .highlight .kr { color: #000000; font-weight: bold; } .highlight .kt { color: #445588; } .highlight .m { color: #009999; } .highlight .s { color: #d14; } .highlight .na { color: #008080; } .highlight .nb { color: #0086B3; } .highlight .nc { color: #445588; font-weight: bold; } .highlight .no { color: #008080; } .highlight .ni { color: #800080; } .highlight .ne { color: #990000; font-weight: bold; } .highlight .nf { color: #990000; } .highlight .nn { color: #555555; } .highlight .nt { color: #000080; } .highlight .nv { color: #008080; } .highlight .ow { color: #000000; font-weight: bold; } .highlight .w { color: #bbbbbb; } .highlight .mf { color: #009999; } .highlight .mh { color: #009999; } .highlight .mi { color: #009999; } .highlight .mo { color: #009999; } .highlight .sb { color: #d14; } .highlight .sc { color: #d14; } .highlight .sd { color: #d14; } .highlight .s2 { color: #d14; } .highlight .se { color: #d14; } .highlight .sh { color: #d14; } .highlight .si { color: #d14; } .highlight .sx { color: #d14; } .highlight .sr { color: #009926; } .highlight .s1 { color: #d14; } .highlight .ss { color: #990073; } .highlight .bp { color: #999999; } .highlight .vc { color: #008080; } .highlight .vg { color: #008080; } .highlight .vi { color: #008080; } .highlight .il { color: #009999; } ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/css/jazzy.css ================================================ html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { background: transparent; border: 0; margin: 0; outline: 0; padding: 0; vertical-align: baseline; } body { background-color: #f2f2f2; font-family: Helvetica, freesans, Arial, sans-serif; font-size: 14px; -webkit-font-smoothing: subpixel-antialiased; word-wrap: break-word; } h1, h2, h3 { margin-top: 0.8em; margin-bottom: 0.3em; font-weight: 100; color: black; } h1 { font-size: 2.5em; } h2 { font-size: 2em; border-bottom: 1px solid #e2e2e2; } h4 { font-size: 13px; line-height: 1.5; margin-top: 21px; } h5 { font-size: 1.1em; } h6 { font-size: 1.1em; color: #777; } .section-name { color: gray; display: block; font-family: Helvetica; font-size: 22px; font-weight: 100; margin-bottom: 15px; } pre, code { font: 0.95em Menlo, monospace; color: #777; word-wrap: normal; } p code, li code { background-color: #eee; padding: 2px 4px; border-radius: 4px; } a { color: #0088cc; text-decoration: none; } ul { padding-left: 15px; } li { line-height: 1.8em; } img { max-width: 100%; } blockquote { margin-left: 0; padding: 0 10px; border-left: 4px solid #ccc; } .content-wrapper { margin: 0 auto; width: 980px; } header { font-size: 0.85em; line-height: 26px; background-color: #414141; position: fixed; width: 100%; z-index: 2; } header img { padding-right: 6px; vertical-align: -4px; height: 16px; } header a { color: #fff; } header p { float: left; color: #999; } header .header-right { float: right; margin-left: 16px; } #breadcrumbs { background-color: #f2f2f2; height: 27px; padding-top: 17px; position: fixed; width: 100%; z-index: 2; margin-top: 26px; } #breadcrumbs #carat { height: 10px; margin: 0 5px; } .sidebar { background-color: #f9f9f9; border: 1px solid #e2e2e2; overflow-y: auto; overflow-x: hidden; position: fixed; top: 70px; bottom: 0; width: 230px; word-wrap: normal; } .nav-groups { list-style-type: none; background: #fff; padding-left: 0; } .nav-group-name { border-bottom: 1px solid #e2e2e2; font-size: 1.1em; font-weight: 100; padding: 15px 0 15px 20px; } .nav-group-name > a { color: #333; } .nav-group-tasks { margin-top: 5px; } .nav-group-task { font-size: 0.9em; list-style-type: none; white-space: nowrap; } .nav-group-task a { color: #888; } .main-content { background-color: #fff; border: 1px solid #e2e2e2; margin-left: 246px; position: absolute; overflow: hidden; padding-bottom: 20px; top: 70px; width: 734px; } .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { margin-bottom: 1em; } .main-content p { line-height: 1.8em; } .main-content section .section:first-child { margin-top: 0; padding-top: 0; } .main-content section .task-group-section .task-group:first-of-type { padding-top: 10px; } .main-content section .task-group-section .task-group:first-of-type .section-name { padding-top: 15px; } .main-content section .heading:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .main-content .section-name p { margin-bottom: inherit; line-height: inherit; } .main-content .section-name code { background-color: inherit; padding: inherit; color: inherit; } .section { padding: 0 25px; } .highlight { background-color: #eee; padding: 10px 12px; border: 1px solid #e2e2e2; border-radius: 4px; overflow-x: auto; } .declaration .highlight { overflow-x: initial; padding: 0 40px 40px 0; margin-bottom: -25px; background-color: transparent; border: none; } .section-name { margin: 0; margin-left: 18px; } .task-group-section { padding-left: 6px; border-top: 1px solid #e2e2e2; } .task-group { padding-top: 0px; } .task-name-container a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .section-name-container { position: relative; display: inline-block; } .section-name-container .section-name-link { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin-bottom: 0; } .section-name-container .section-name { position: relative; pointer-events: none; z-index: 1; } .section-name-container .section-name a { pointer-events: auto; } .item { padding-top: 8px; width: 100%; list-style-type: none; } .item a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .item code { background-color: transparent; padding: 0; } .item .token, .item .direct-link { padding-left: 3px; margin-left: 15px; font-size: 11.9px; transition: all 300ms; } .item .token-open { margin-left: 0px; } .item .discouraged { text-decoration: line-through; } .item .declaration-note { font-size: .85em; color: gray; font-style: italic; } .pointer-container { border-bottom: 1px solid #e2e2e2; left: -23px; padding-bottom: 13px; position: relative; width: 110%; } .pointer { background: #f9f9f9; border-left: 1px solid #e2e2e2; border-top: 1px solid #e2e2e2; height: 12px; left: 21px; top: -7px; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); position: absolute; width: 12px; } .height-container { display: none; left: -25px; padding: 0 25px; position: relative; width: 100%; overflow: hidden; } .height-container .section { background: #f9f9f9; border-bottom: 1px solid #e2e2e2; left: -25px; position: relative; width: 100%; padding-top: 10px; padding-bottom: 5px; } .aside, .language { padding: 6px 12px; margin: 12px 0; border-left: 5px solid #dddddd; overflow-y: hidden; } .aside .aside-title, .language .aside-title { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; padding-bottom: 0; margin: 0; color: #aaa; -webkit-user-select: none; } .aside p:last-child, .language p:last-child { margin-bottom: 0; } .language { border-left: 5px solid #cde9f4; } .language .aside-title { color: #4b8afb; } .aside-warning, .aside-deprecated, .aside-unavailable { border-left: 5px solid #ff6666; } .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { color: #ff0000; } .graybox { border-collapse: collapse; width: 100%; } .graybox p { margin: 0; word-break: break-word; min-width: 50px; } .graybox td { border: 1px solid #e2e2e2; padding: 5px 25px 5px 10px; vertical-align: middle; } .graybox tr td:first-of-type { text-align: right; padding: 7px; vertical-align: top; word-break: normal; width: 40px; } .slightly-smaller { font-size: 0.9em; } #footer { position: relative; top: 10px; bottom: 0px; margin-left: 25px; } #footer p { margin: 0; color: #aaa; font-size: 0.8em; } html.dash header, html.dash #breadcrumbs, html.dash .sidebar { display: none; } html.dash .main-content { width: 980px; margin-left: 0; border: none; width: 100%; top: 0; padding-bottom: 0; } html.dash .height-container { display: block; } html.dash .item .token { margin-left: 0; } html.dash .content-wrapper { width: auto; } html.dash #footer { position: static; } ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/index.html ================================================ SwiftPhoenixClient Reference

SwiftPhoenixClient Docs (72% documented)

Swift Phoenix Client

Swift Version License Platform Carthage compatible Open Source Helpers

About

Swift Phoenix Client is an extension of Starscream websocket client library that makes it easy to connect to Phoenix sockets in a similar manner to the phoenix.js client.

The client is currently updated to mirror phoenix.js 1.4.

Swift Versions

master currently supports Swift 5.0. You’ll need to set your target to = 1.0.1 if your project is using Swift 4.2

swift client
4.2 1.0.1
5.0 1.1.0

Installation

CocoaPods

You can install SwiftPhoenix Client via CocoaPods by adding the following to your Podfile. Keep in mind that in order to use Swift Phoenix Client, the minimum iOS target must be ‘9.0’

platform :ios, '9.0'
use_frameworks!

pod "SwiftPhoenixClient", '~> 1.0'

and running pod install. From there you will need to add import SwiftPhoenixClient in any class you want it to be used.

Carthage

If you use Carthage to manage your dependencies, simply add SwiftPhoenixClient to your Cartfile:

github "davidstump/SwiftPhoenixClient" ~> 1.0

SwiftPackageManager

SwiftPackageManager is properly supported starting in SwiftPhoenixClient v1.2.0. You can add the following to your Package.swift

.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMajor(from: "1.2.0"))

Make sure you have added SwiftPhoenixClient.framework, and Starscream.framework to the “Linked Frameworks and Libraries” section of your target, and have included them in your Carthage framework copying build phase.

Usage

Using the Swift Phoenix Client is extremely easy (and familiar if have used the phoenix.s client).

See the Usage Guide for details instructions. You can also check out the documentation

Example

Check out the ViewController in this repo for a brief example of a simple iOS chat application using the Phoenix Chat Example

Also check out both the Swift and Elixir channels on IRC.

Development

Check out the wiki page for getting started

Tested with the Phoenix Chat Server example, upgraded to Phoenix 1.2.

Thanks

Many many thanks to Daniel Rees for his many contributions and continued maintenance of this project!

License

SwiftPhoenixClient is available under the MIT license. See the LICENSE file for more info.

================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/js/jazzy.js ================================================ window.jazzy = {'docset': false} if (typeof window.dash != 'undefined') { document.documentElement.className += ' dash' window.jazzy.docset = true } if (navigator.userAgent.match(/xcode/i)) { document.documentElement.className += ' xcode' window.jazzy.docset = true } function toggleItem($link, $content) { var animationDuration = 300; $link.toggleClass('token-open'); $content.slideToggle(animationDuration); } function itemLinkToContent($link) { return $link.parent().parent().next(); } // On doc load + hash-change, open any targetted item function openCurrentItemIfClosed() { if (window.jazzy.docset) { return; } var $link = $(`.token[href="${location.hash}"]`); $content = itemLinkToContent($link); if ($content.is(':hidden')) { toggleItem($link, $content); } } $(openCurrentItemIfClosed); $(window).on('hashchange', openCurrentItemIfClosed); // On item link ('token') click, toggle its discussion $('.token').on('click', function(event) { if (window.jazzy.docset) { return; } var $link = $(this); toggleItem($link, itemLinkToContent($link)); // Keeps the document from jumping to the hash. var href = $link.attr('href'); if (history.pushState) { history.pushState({}, '', href); } else { location.hash = href; } event.preventDefault(); }); // Clicks on links to the current, closed, item need to open the item $("a:not('.token')").on('click', function() { if (location == this.href) { openCurrentItemIfClosed(); } }); ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/search.json ================================================ {"Typealiases.html#/s:18SwiftPhoenixClient7Payloada":{"name":"Payload","abstract":"

Alias for a JSON dictionary [String: Any]

"},"Typealiases.html#/s:18SwiftPhoenixClient14PayloadClosurea":{"name":"PayloadClosure","abstract":"

Alias for a function returning an optional JSON dictionary (Payload?)

"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVACyxq_Gycfc":{"name":"init()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV8delegate2to4withyqd___q_qd___xtctRld__ClF":{"name":"delegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV4callyq_SgxF":{"name":"call(_:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV13isDelegateSetSbvp":{"name":"isDelegateSet","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV16stronglyDelegate2to4withyqd___q_qd___xtctRld__ClF":{"name":"stronglyDelegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV16manuallyDelegate4withyq_xc_tF":{"name":"manuallyDelegate(with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV14removeDelegateyyF":{"name":"removeDelegate()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE8delegate2to4withyqd___q_qd__ctRld__ClF":{"name":"delegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE16stronglyDelegate2to4withyqd___q_qd__ctRld__ClF":{"name":"stronglyDelegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE4callq_SgyF":{"name":"call()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRs_rlE4callyyxF":{"name":"call(_:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszytRs_rlE4callyyF":{"name":"call()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV9heartbeatSSvpZ":{"name":"heartbeat","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV4joinSSvpZ":{"name":"join","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5leaveSSvpZ":{"name":"leave","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5replySSvpZ":{"name":"reply","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5errorSSvpZ":{"name":"error","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5closeSSvpZ":{"name":"close","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html":{"name":"ChannelEvent","abstract":"

Represents the different events that can be sent through"},"Structs/Delegated.html":{"name":"Delegated","abstract":"

Provides a memory-safe way of passing callbacks around while not creating"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO6closedyA2CmF":{"name":"closed","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7erroredyA2CmF":{"name":"errored","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO6joinedyA2CmF":{"name":"joined","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7joiningyA2CmF":{"name":"joining","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7leavingyA2CmF":{"name":"leaving","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html":{"name":"ChannelState","abstract":"

Represents the multiple states that a Channel can be in"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC15timeoutIntervalSdvpZ":{"name":"timeoutInterval","abstract":"

Default timeout when sending messages

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC17heartbeatIntervalSdvpZ":{"name":"heartbeatInterval","abstract":"

Default interval to send heartbeats on

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC23reconnectSteppedBackOffySdSicvpZ":{"name":"reconnectSteppedBackOff","abstract":"

Default reconnect algorithm for the socket

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC20rejoinSteppedBackOffySdSicvpZ":{"name":"rejoinSteppedBackOff","abstract":"

Default rejoin algorithm for individual channels

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC6encodey10Foundation4DataVSDySSypGcvpZ":{"name":"encode","abstract":"

Default encode function, utilizing JSONSerialization.data

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC6decodeySDySSypGSg10Foundation4DataVcvpZ":{"name":"decode","abstract":"

Default decode function, utilizing JSONSerialization.jsonObject

","parent_name":"Defaults"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC8endPointSSvp":{"name":"endPoint","abstract":"

The string WebSocket endpoint (ie "ws://example.com/socket",","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11endPointUrl10Foundation3URLVvp":{"name":"endPointUrl","abstract":"

The fully qualified socket URL

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6paramsSDySSypGSgvp":{"name":"params","abstract":"

Resolves to return the paramsClosure result at the time of calling.","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC13paramsClosureSDySSypGSgycSgvp":{"name":"paramsClosure","abstract":"

The optional params closure used to get params whhen connecting. Must","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6encodey10Foundation4DataVSDySSypGcvp":{"name":"encode","abstract":"

Override to provide custom encoding of data before writing to the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6decodeySDySSypGSg10Foundation4DataVcvp":{"name":"decode","abstract":"

Override to provide customd decoding of data read from the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7timeoutSdvp":{"name":"timeout","abstract":"

Timeout to use when opening connections

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17heartbeatIntervalSdvp":{"name":"heartbeatInterval","abstract":"

Interval between sending a heartbeat

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC14reconnectAfterySdSicvp":{"name":"reconnectAfter","abstract":"

Interval between socket reconnect attempts, in seconds

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11rejoinAfterySdSicvp":{"name":"rejoinAfter","abstract":"

Interval between channel rejoin attempts, in seconds

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6loggerySScSgvp":{"name":"logger","abstract":"

The optional function to receive logs

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC13skipHeartbeatSbvp":{"name":"skipHeartbeat","abstract":"

Disables heartbeats from being sent. Default is false.

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC24disableSSLCertValidationSbvp":{"name":"disableSSLCertValidation","abstract":"

Enable/Disable SSL certificate validation. Default is false. This","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC8security10Starscream17SSLTrustValidator_pSgvp":{"name":"security","abstract":"

Configure custom SSL validation logic, eg. SSL pinning. This","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC22enabledSSLCipherSuitesSays6UInt16VGSgvp":{"name":"enabledSSLCipherSuites","abstract":"

Configure the encryption used by your client by setting the","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC_6paramsACSS_SDySSypGSgtcfc":{"name":"init(_:params:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC_13paramsClosureACSS_SDySSypGSgycSgtcfc":{"name":"init(_:paramsClosure:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17websocketProtocolSSvp":{"name":"websocketProtocol","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11isConnectedSbvp":{"name":"isConnected","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7connectyyF":{"name":"connect()","abstract":"

Connects the Socket. The params passed to the Socket on initialization","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC10disconnect4code8callbacky10Starscream9CloseCodeO_yycSgtF":{"name":"disconnect(code:callback:)","abstract":"

Disconnects the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6onOpen8callbackyyyc_tF":{"name":"onOpen(callback:)","abstract":"

Registers callbacks for connection open events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC14delegateOnOpen2to8callbackyx_yxctRlzClF":{"name":"delegateOnOpen(to:callback:)","abstract":"

Registers callbacks for connection open events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7onClose8callbackyyyc_tF":{"name":"onClose(callback:)","abstract":"

Registers callbacks for connection close events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC15delegateOnClose2to8callbackyx_yxctRlzClF":{"name":"delegateOnClose(to:callback:)","abstract":"

Registers callbacks for connection close events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7onError8callbackyys0F0_pc_tF":{"name":"onError(callback:)","abstract":"

Registers callbacks for connection error events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC15delegateOnError2to8callbackyx_yx_s0G0_ptctRlzClF":{"name":"delegateOnError(to:callback:)","abstract":"

Registers callbacks for connection error events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC9onMessage8callbackyyAA0F0Cc_tF":{"name":"onMessage(callback:)","abstract":"

Registers callbacks for connection message events. Does not handle","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17delegateOnMessage2to8callbackyx_yx_AA0G0CtctRlzClF":{"name":"delegateOnMessage(to:callback:)","abstract":"

Registers callbacks for connection message events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC16releaseCallbacksyyF":{"name":"releaseCallbacks()","abstract":"

Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7channel_6paramsAA7ChannelCSS_SDySSypGtF":{"name":"channel(_:params:)","abstract":"

Initialize a new Channel

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6removeyyAA7ChannelCF":{"name":"remove(_:)","abstract":"

Removes the Channel from the socket. This does not cause the channel to","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7makeRefSSyF":{"name":"makeRef()","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC19websocketDidConnect6sockety10Starscream03WebdC0_p_tF":{"name":"websocketDidConnect(socket:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC22websocketDidDisconnect6socket5errory10Starscream03WebdC0_p_s5Error_pSgtF":{"name":"websocketDidDisconnect(socket:error:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC26websocketDidReceiveMessage6socket4texty10Starscream03WebdC0_p_SStF":{"name":"websocketDidReceiveMessage(socket:text:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC23websocketDidReceiveData6socket4datay10Starscream03WebdC0_p_10Foundation0H0VtF":{"name":"websocketDidReceiveData(socket:data:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7channelAA7ChannelCSgvp":{"name":"channel","abstract":"

The channel sending the Push

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC5eventSSvp":{"name":"event","abstract":"

The event, for example phx_join

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7payloadSDySSypGvp":{"name":"payload","abstract":"

The payload, for example [“user_id”: “abc123”]

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7timeoutSdvp":{"name":"timeout","abstract":"

The push timeout. Default is 10.0 seconds

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC6resendyySdF":{"name":"resend(_:)","abstract":"

Resets and sends the Push

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC4sendyyF":{"name":"send()","abstract":"

Sends the Push. If it has already timed out, then the call will","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7receive_8callbackACSS_yAA7MessageCctF":{"name":"receive(_:callback:)","abstract":"

Receive a specific event when sending an Outbound message. Subscribing","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC15delegateReceive_2to8callbackACSS_xyx_AA7MessageCtctRlzClF":{"name":"delegateReceive(_:to:callback:)","abstract":"

Receive a specific event when sending an Outbound message. Automatically","parent_name":"Push"},"Classes/Presence/Events.html#/s:18SwiftPhoenixClient8PresenceC6EventsO5stateyA2EmF":{"name":"state","abstract":"

Undocumented

","parent_name":"Events"},"Classes/Presence/Events.html#/s:18SwiftPhoenixClient8PresenceC6EventsO4diffyA2EmF":{"name":"diff","abstract":"

Undocumented

","parent_name":"Events"},"Classes/Presence/Options.html#/s:18SwiftPhoenixClient8PresenceC7OptionsV8defaultsAEvpZ":{"name":"defaults","abstract":"

Default set of Options used when creating Presence. Uses the","parent_name":"Options"},"Classes/Presence/Options.html":{"name":"Options","abstract":"

Custom options that can be provided when creating Presence

","parent_name":"Presence"},"Classes/Presence/Events.html":{"name":"Events","abstract":"

Presense Events

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4Metaa":{"name":"Meta","abstract":"

Meta details of a Presence. Just a dictionary of properties

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC3Mapa":{"name":"Map","abstract":"

A mapping of a String to an array of Metas. e.g. {“metas”: [{id: 1}]}

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC5Statea":{"name":"State","abstract":"

A mapping of a Presence state to a mapping of Metas

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4Diffa":{"name":"Diff","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6OnJoina":{"name":"OnJoin","abstract":"

Closure signature of OnJoin callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7OnLeavea":{"name":"OnLeave","abstract":"

Closure signature for OnLeave callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6OnSynca":{"name":"OnSync","abstract":"

/ Closure signature for OnSync callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC5stateSDySSSDySSSaySDySSypGGGGvp":{"name":"state","abstract":"

The state of the Presence

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC12pendingDiffsSaySDySSSDySSSDySSSaySDySSypGGGGGGvp":{"name":"pendingDiffs","abstract":"

Pending join and leave diffs that need to be synced

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7joinRefSSSgvp":{"name":"joinRef","abstract":"

The channel’s joinRef, set when state events occur

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC18isPendingSyncStateSbvp":{"name":"isPendingSyncState","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onJoinyySS_SDySSSaySDySSypGGGSgAGtcvp":{"name":"onJoin","abstract":"

Callback to be informed of joins

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onJoinyyySS_SDySSSaySDySSypGGGSgAGtcF":{"name":"onJoin(_:)","abstract":"

Set the OnJoin callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7onLeaveyySS_SDySSSaySDySSypGGGAGtcvp":{"name":"onLeave","abstract":"

Callback to be informed of leaves

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7onLeaveyyySS_SDySSSaySDySSypGGGAGtcF":{"name":"onLeave(_:)","abstract":"

Set the OnLeave callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onSyncyycvp":{"name":"onSync","abstract":"

Callback to be informed of synces

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onSyncyyyycF":{"name":"onSync(_:)","abstract":"

Set the OnSync callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7channel4optsAcA7ChannelC_AC7OptionsVtcfc":{"name":"init(channel:opts:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4listSaySDySSSaySDySSypGGGGyF":{"name":"list()","abstract":"

Returns the array of presences, with deault selected metadata.

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4list2bySayxGxSS_SDySSSaySDySSypGGGtXE_tlF":{"name":"list(by:)","abstract":"

Returns the array of presences, with selected metadata

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6filter2bySDySSSDySSSaySDySSypGGGGSbSS_AHtcSg_tF":{"name":"filter(by:)","abstract":"

Filter the Presence state with a given function

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC9syncState_03newF06onJoin0H5LeaveSDySSSDySSSaySDySSypGGGGAK_AKySS_AJSgAJtXEySS_A2JtXEtFZ":{"name":"syncState(_:newState:onJoin:onLeave:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC8syncDiff_4diff6onJoin0H5LeaveSDySSSDySSSaySDySSypGGGGAK_SDySSAKGySS_AJSgAJtXEySS_A2JtXEtFZ":{"name":"syncDiff(_:diff:onJoin:onLeave:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6filter_2bySDySSSDySSSaySDySSypGGGGAI_SbSS_AHtcSgtFZ":{"name":"filter(_:by:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6listBy_11transformerSayxGSDySSSDySSSaySDySSypGGGG_xSS_AItXEtlFZ":{"name":"listBy(_:transformer:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC3refSSvp":{"name":"ref","abstract":"

Reference number. Empty if missing

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC5topicSSvp":{"name":"topic","abstract":"

Message topic

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC5eventSSvp":{"name":"event","abstract":"

Message event

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC7payloadSDySSypGvp":{"name":"payload","abstract":"

Message payload

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC6statusSSSgvp":{"name":"status","abstract":"

Convenience accessor. Equivalent to getting the status as such:

","parent_name":"Message"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC5topicSSvp":{"name":"topic","abstract":"

The topic of the Channel. e.g. “rooms:friends”

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC6paramsSDySSypGvp":{"name":"params","abstract":"

The params sent when joining the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9onMessageyAA0F0CAFcvp":{"name":"onMessage","abstract":"

Overridable message hook. Receives all events for specialized message","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC4join7timeoutAA4PushCSdSg_tF":{"name":"join(timeout:)","abstract":"

Joins the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC7onCloseySiyAA7MessageCcF":{"name":"onClose(_:)","abstract":"

Hook into when the Channel is closed. Does not handle retain cycles.","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC15delegateOnClose2to8callbackSix_yx_AA7MessageCtctRlzClF":{"name":"delegateOnClose(to:callback:)","abstract":"

Hook into when the Channel is closed. Automatically handles retain","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC7onErrorySiyAA7MessageCcF":{"name":"onError(_:)","abstract":"

Hook into when the Channel receives an Error. Does not handle retain","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC15delegateOnError2to8callbackSix_yx_AA7MessageCtctRlzClF":{"name":"delegateOnError(to:callback:)","abstract":"

Hook into when the Channel receives an Error. Automatically handles","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC2on_8callbackSiSS_yAA7MessageCctF":{"name":"on(_:callback:)","abstract":"

Subscribes on channel events. Does not handle retain cycles. Use","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC10delegateOn_2to8callbackSiSS_xyx_AA7MessageCtctRlzClF":{"name":"delegateOn(_:to:callback:)","abstract":"

Subscribes on channel events. Automatically handles retain cycles. Use","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC3off_3refySS_SiSgtF":{"name":"off(_:ref:)","abstract":"

Unsubscribes from a channel event. If a ref is given, only the exact","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC4push_7payload7timeoutAA4PushCSS_SDySSypGSdtF":{"name":"push(_:payload:timeout:)","abstract":"

Push a payload to the Channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC5leave7timeoutAA4PushCSd_tF":{"name":"leave(timeout:)","abstract":"

Leaves the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9onMessage8callbackyAA0F0CAGc_tF":{"name":"onMessage(callback:)","abstract":"

Overridable message hook. Receives all events for specialized message","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC8isClosedSbvp":{"name":"isClosed","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isErroredSbvp":{"name":"isErrored","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC8isJoinedSbvp":{"name":"isJoined","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isJoiningSbvp":{"name":"isJoining","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isLeavingSbvp":{"name":"isLeaving","parent_name":"Channel"},"Classes/Channel.html":{"name":"Channel","abstract":"

Undocumented

"},"Classes/Message.html":{"name":"Message","abstract":"

Data that is received from the Server.

"},"Classes/Presence.html":{"name":"Presence","abstract":"

The Presence object provides features for syncing presence information from"},"Classes/Push.html":{"name":"Push","abstract":"

Represnts pushing data to a Channel through the Socket

"},"Classes/Socket.html":{"name":"Socket","abstract":"

Socket Connection

"},"Classes/Defaults.html":{"name":"Defaults","abstract":"

A collection of default values and behaviors used accross the Client

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Enums.html":{"name":"Enumerations","abstract":"

The following enumerations are available globally.

"},"Structs.html":{"name":"Structures","abstract":"

The following structures are available globally.

"},"Typealiases.html":{"name":"Type Aliases","abstract":"

The following type aliases are available globally.

"}} ================================================ FILE: docs/docsets/SwiftPhoenixClient.docset/Contents/Resources/Documents/undocumented.json ================================================ { "warnings": [ { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 118, "symbol": "Presence.Events.state", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 119, "symbol": "Presence.Events.diff", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 136, "symbol": "Presence.Diff", "symbol_kind": "source.lang.swift.decl.typealias", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 173, "symbol": "Presence.isPendingSyncState", "symbol_kind": "source.lang.swift.decl.var.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 212, "symbol": "Presence.init(channel:opts:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 285, "symbol": "Presence.syncState(_:newState:onJoin:onLeave:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 340, "symbol": "Presence.syncDiff(_:diff:onJoin:onLeave:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 380, "symbol": "Presence.filter(_:by:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Presence.swift", "line": 386, "symbol": "Presence.listBy(_:transformer:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Socket.swift", "line": 143, "symbol": "Socket.init(_:params:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Socket.swift", "line": 663, "symbol": "Socket.websocketDidConnect(socket:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Socket.swift", "line": 667, "symbol": "Socket.websocketDidDisconnect(socket:error:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Socket.swift", "line": 672, "symbol": "Socket.websocketDidReceiveMessage(socket:text:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Socket.swift", "line": 676, "symbol": "Socket.websocketDidReceiveData(socket:data:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 59, "symbol": "ChannelState.closed", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 60, "symbol": "ChannelState.errored", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 61, "symbol": "ChannelState.joined", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 62, "symbol": "ChannelState.joining", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 63, "symbol": "ChannelState.leaving", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 69, "symbol": "ChannelEvent.heartbeat", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 70, "symbol": "ChannelEvent.join", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 71, "symbol": "ChannelEvent.leave", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 72, "symbol": "ChannelEvent.reply", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 73, "symbol": "ChannelEvent.error", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Defaults.swift", "line": 74, "symbol": "ChannelEvent.close", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 31, "symbol": "Delegated.init()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 33, "symbol": "Delegated.delegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 43, "symbol": "Delegated.call(_:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 47, "symbol": "Delegated.isDelegateSet", "symbol_kind": "source.lang.swift.decl.var.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 55, "symbol": "Delegated.stronglyDelegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 62, "symbol": "Delegated.manuallyDelegate(with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 66, "symbol": "Delegated.removeDelegate()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 74, "symbol": "Delegated.delegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 79, "symbol": "Delegated.stronglyDelegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 88, "symbol": "Delegated.call()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 96, "symbol": "Delegated.call(_:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/client/Utilities/Delegated.swift", "line": 104, "symbol": "Delegated.call()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" } ], "source_directory": "/Users/drees/src/github/phoenix/SwiftPhoenixClient" } ================================================ FILE: docs/index.html ================================================ SwiftPhoenixClient Reference

SwiftPhoenixClient Docs (72% documented)

Swift Phoenix Client

Swift Version License Platform Carthage compatible Open Source Helpers

About

Swift Phoenix Client is an extension of Starscream websocket client library that makes it easy to connect to Phoenix sockets in a similar manner to the phoenix.js client.

The client is currently updated to mirror phoenix.js 1.4.

Swift Versions

master currently supports Swift 5.0. You’ll need to set your target to = 1.0.1 if your project is using Swift 4.2

swift client
4.2 1.0.1
5.0 1.1.0

Installation

CocoaPods

You can install SwiftPhoenix Client via CocoaPods by adding the following to your Podfile. Keep in mind that in order to use Swift Phoenix Client, the minimum iOS target must be ‘9.0’

platform :ios, '9.0'
use_frameworks!

pod "SwiftPhoenixClient", '~> 1.0'

and running pod install. From there you will need to add import SwiftPhoenixClient in any class you want it to be used.

Carthage

If you use Carthage to manage your dependencies, simply add SwiftPhoenixClient to your Cartfile:

github "davidstump/SwiftPhoenixClient" ~> 1.0

SwiftPackageManager

SwiftPackageManager is properly supported starting in SwiftPhoenixClient v1.2.0. You can add the following to your Package.swift

.package(url: "https://github.com/davidstump/SwiftPhoenixClient.git", .upToNextMajor(from: "1.2.0"))

Make sure you have added SwiftPhoenixClient.framework, and Starscream.framework to the “Linked Frameworks and Libraries” section of your target, and have included them in your Carthage framework copying build phase.

Usage

Using the Swift Phoenix Client is extremely easy (and familiar if have used the phoenix.s client).

See the Usage Guide for details instructions. You can also check out the documentation

Example

Check out the ViewController in this repo for a brief example of a simple iOS chat application using the Phoenix Chat Example

Also check out both the Swift and Elixir channels on IRC.

Development

Check out the wiki page for getting started

Tested with the Phoenix Chat Server example, upgraded to Phoenix 1.2.

Thanks

Many many thanks to Daniel Rees for his many contributions and continued maintenance of this project!

License

SwiftPhoenixClient is available under the MIT license. See the LICENSE file for more info.

================================================ FILE: docs/js/jazzy.js ================================================ window.jazzy = {'docset': false} if (typeof window.dash != 'undefined') { document.documentElement.className += ' dash' window.jazzy.docset = true } if (navigator.userAgent.match(/xcode/i)) { document.documentElement.className += ' xcode' window.jazzy.docset = true } function toggleItem($link, $content) { var animationDuration = 300; $link.toggleClass('token-open'); $content.slideToggle(animationDuration); } function itemLinkToContent($link) { return $link.parent().parent().next(); } // On doc load + hash-change, open any targetted item function openCurrentItemIfClosed() { if (window.jazzy.docset) { return; } var $link = $(`.token[href="${location.hash}"]`); $content = itemLinkToContent($link); if ($content.is(':hidden')) { toggleItem($link, $content); } } $(openCurrentItemIfClosed); $(window).on('hashchange', openCurrentItemIfClosed); // On item link ('token') click, toggle its discussion $('.token').on('click', function(event) { if (window.jazzy.docset) { return; } var $link = $(this); toggleItem($link, itemLinkToContent($link)); // Keeps the document from jumping to the hash. var href = $link.attr('href'); if (history.pushState) { history.pushState({}, '', href); } else { location.hash = href; } event.preventDefault(); }); // Clicks on links to the current, closed, item need to open the item $("a:not('.token')").on('click', function() { if (location == this.href) { openCurrentItemIfClosed(); } }); ================================================ FILE: docs/search.json ================================================ {"Typealiases.html#/s:18SwiftPhoenixClient7Payloada":{"name":"Payload","abstract":"

Alias for a JSON dictionary [String: Any]

"},"Typealiases.html#/s:18SwiftPhoenixClient14PayloadClosurea":{"name":"PayloadClosure","abstract":"

Alias for a function returning an optional JSON dictionary (Payload?)

"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVACyxq_Gycfc":{"name":"init()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV8delegate2to4withyqd___q_qd___xtctRld__ClF":{"name":"delegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV4callyq_SgxF":{"name":"call(_:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV13isDelegateSetSbvp":{"name":"isDelegateSet","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV16stronglyDelegate2to4withyqd___q_qd___xtctRld__ClF":{"name":"stronglyDelegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV16manuallyDelegate4withyq_xc_tF":{"name":"manuallyDelegate(with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedV14removeDelegateyyF":{"name":"removeDelegate()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE8delegate2to4withyqd___q_qd__ctRld__ClF":{"name":"delegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE16stronglyDelegate2to4withyqd___q_qd__ctRld__ClF":{"name":"stronglyDelegate(to:with:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszrlE4callq_SgyF":{"name":"call()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRs_rlE4callyyxF":{"name":"call(_:)","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/Delegated.html#/s:18SwiftPhoenixClient9DelegatedVAAytRszytRs_rlE4callyyF":{"name":"call()","abstract":"

Undocumented

","parent_name":"Delegated"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV9heartbeatSSvpZ":{"name":"heartbeat","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV4joinSSvpZ":{"name":"join","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5leaveSSvpZ":{"name":"leave","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5replySSvpZ":{"name":"reply","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5errorSSvpZ":{"name":"error","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html#/s:18SwiftPhoenixClient12ChannelEventV5closeSSvpZ":{"name":"close","abstract":"

Undocumented

","parent_name":"ChannelEvent"},"Structs/ChannelEvent.html":{"name":"ChannelEvent","abstract":"

Represents the different events that can be sent through"},"Structs/Delegated.html":{"name":"Delegated","abstract":"

Provides a memory-safe way of passing callbacks around while not creating"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO6closedyA2CmF":{"name":"closed","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7erroredyA2CmF":{"name":"errored","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO6joinedyA2CmF":{"name":"joined","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7joiningyA2CmF":{"name":"joining","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html#/s:18SwiftPhoenixClient12ChannelStateO7leavingyA2CmF":{"name":"leaving","abstract":"

Undocumented

","parent_name":"ChannelState"},"Enums/ChannelState.html":{"name":"ChannelState","abstract":"

Represents the multiple states that a Channel can be in"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC15timeoutIntervalSdvpZ":{"name":"timeoutInterval","abstract":"

Default timeout when sending messages

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC17heartbeatIntervalSdvpZ":{"name":"heartbeatInterval","abstract":"

Default interval to send heartbeats on

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC23reconnectSteppedBackOffySdSicvpZ":{"name":"reconnectSteppedBackOff","abstract":"

Default reconnect algorithm for the socket

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC20rejoinSteppedBackOffySdSicvpZ":{"name":"rejoinSteppedBackOff","abstract":"

Default rejoin algorithm for individual channels

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC6encodey10Foundation4DataVSDySSypGcvpZ":{"name":"encode","abstract":"

Default encode function, utilizing JSONSerialization.data

","parent_name":"Defaults"},"Classes/Defaults.html#/s:18SwiftPhoenixClient8DefaultsC6decodeySDySSypGSg10Foundation4DataVcvpZ":{"name":"decode","abstract":"

Default decode function, utilizing JSONSerialization.jsonObject

","parent_name":"Defaults"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC8endPointSSvp":{"name":"endPoint","abstract":"

The string WebSocket endpoint (ie "ws://example.com/socket",","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11endPointUrl10Foundation3URLVvp":{"name":"endPointUrl","abstract":"

The fully qualified socket URL

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6paramsSDySSypGSgvp":{"name":"params","abstract":"

Resolves to return the paramsClosure result at the time of calling.","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC13paramsClosureSDySSypGSgycSgvp":{"name":"paramsClosure","abstract":"

The optional params closure used to get params whhen connecting. Must","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6encodey10Foundation4DataVSDySSypGcvp":{"name":"encode","abstract":"

Override to provide custom encoding of data before writing to the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6decodeySDySSypGSg10Foundation4DataVcvp":{"name":"decode","abstract":"

Override to provide customd decoding of data read from the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7timeoutSdvp":{"name":"timeout","abstract":"

Timeout to use when opening connections

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17heartbeatIntervalSdvp":{"name":"heartbeatInterval","abstract":"

Interval between sending a heartbeat

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC14reconnectAfterySdSicvp":{"name":"reconnectAfter","abstract":"

Interval between socket reconnect attempts, in seconds

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11rejoinAfterySdSicvp":{"name":"rejoinAfter","abstract":"

Interval between channel rejoin attempts, in seconds

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6loggerySScSgvp":{"name":"logger","abstract":"

The optional function to receive logs

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC13skipHeartbeatSbvp":{"name":"skipHeartbeat","abstract":"

Disables heartbeats from being sent. Default is false.

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC24disableSSLCertValidationSbvp":{"name":"disableSSLCertValidation","abstract":"

Enable/Disable SSL certificate validation. Default is false. This","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC8security10Starscream17SSLTrustValidator_pSgvp":{"name":"security","abstract":"

Configure custom SSL validation logic, eg. SSL pinning. This","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC22enabledSSLCipherSuitesSays6UInt16VGSgvp":{"name":"enabledSSLCipherSuites","abstract":"

Configure the encryption used by your client by setting the","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC_6paramsACSS_SDySSypGSgtcfc":{"name":"init(_:params:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC_13paramsClosureACSS_SDySSypGSgycSgtcfc":{"name":"init(_:paramsClosure:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17websocketProtocolSSvp":{"name":"websocketProtocol","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC11isConnectedSbvp":{"name":"isConnected","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7connectyyF":{"name":"connect()","abstract":"

Connects the Socket. The params passed to the Socket on initialization","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC10disconnect4code8callbacky10Starscream9CloseCodeO_yycSgtF":{"name":"disconnect(code:callback:)","abstract":"

Disconnects the socket

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6onOpen8callbackyyyc_tF":{"name":"onOpen(callback:)","abstract":"

Registers callbacks for connection open events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC14delegateOnOpen2to8callbackyx_yxctRlzClF":{"name":"delegateOnOpen(to:callback:)","abstract":"

Registers callbacks for connection open events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7onClose8callbackyyyc_tF":{"name":"onClose(callback:)","abstract":"

Registers callbacks for connection close events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC15delegateOnClose2to8callbackyx_yxctRlzClF":{"name":"delegateOnClose(to:callback:)","abstract":"

Registers callbacks for connection close events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7onError8callbackyys0F0_pc_tF":{"name":"onError(callback:)","abstract":"

Registers callbacks for connection error events. Does not handle retain","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC15delegateOnError2to8callbackyx_yx_s0G0_ptctRlzClF":{"name":"delegateOnError(to:callback:)","abstract":"

Registers callbacks for connection error events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC9onMessage8callbackyyAA0F0Cc_tF":{"name":"onMessage(callback:)","abstract":"

Registers callbacks for connection message events. Does not handle","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC17delegateOnMessage2to8callbackyx_yx_AA0G0CtctRlzClF":{"name":"delegateOnMessage(to:callback:)","abstract":"

Registers callbacks for connection message events. Automatically handles","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC16releaseCallbacksyyF":{"name":"releaseCallbacks()","abstract":"

Releases all stored callback hooks (onError, onOpen, onClose, etc.) You should","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7channel_6paramsAA7ChannelCSS_SDySSypGtF":{"name":"channel(_:params:)","abstract":"

Initialize a new Channel

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC6removeyyAA7ChannelCF":{"name":"remove(_:)","abstract":"

Removes the Channel from the socket. This does not cause the channel to","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC7makeRefSSyF":{"name":"makeRef()","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC19websocketDidConnect6sockety10Starscream03WebdC0_p_tF":{"name":"websocketDidConnect(socket:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC22websocketDidDisconnect6socket5errory10Starscream03WebdC0_p_s5Error_pSgtF":{"name":"websocketDidDisconnect(socket:error:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC26websocketDidReceiveMessage6socket4texty10Starscream03WebdC0_p_SStF":{"name":"websocketDidReceiveMessage(socket:text:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Socket.html#/s:18SwiftPhoenixClient6SocketC23websocketDidReceiveData6socket4datay10Starscream03WebdC0_p_10Foundation0H0VtF":{"name":"websocketDidReceiveData(socket:data:)","abstract":"

Undocumented

","parent_name":"Socket"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7channelAA7ChannelCSgvp":{"name":"channel","abstract":"

The channel sending the Push

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC5eventSSvp":{"name":"event","abstract":"

The event, for example phx_join

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7payloadSDySSypGvp":{"name":"payload","abstract":"

The payload, for example [“user_id”: “abc123”]

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7timeoutSdvp":{"name":"timeout","abstract":"

The push timeout. Default is 10.0 seconds

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC6resendyySdF":{"name":"resend(_:)","abstract":"

Resets and sends the Push

","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC4sendyyF":{"name":"send()","abstract":"

Sends the Push. If it has already timed out, then the call will","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC7receive_8callbackACSS_yAA7MessageCctF":{"name":"receive(_:callback:)","abstract":"

Receive a specific event when sending an Outbound message. Subscribing","parent_name":"Push"},"Classes/Push.html#/s:18SwiftPhoenixClient4PushC15delegateReceive_2to8callbackACSS_xyx_AA7MessageCtctRlzClF":{"name":"delegateReceive(_:to:callback:)","abstract":"

Receive a specific event when sending an Outbound message. Automatically","parent_name":"Push"},"Classes/Presence/Events.html#/s:18SwiftPhoenixClient8PresenceC6EventsO5stateyA2EmF":{"name":"state","abstract":"

Undocumented

","parent_name":"Events"},"Classes/Presence/Events.html#/s:18SwiftPhoenixClient8PresenceC6EventsO4diffyA2EmF":{"name":"diff","abstract":"

Undocumented

","parent_name":"Events"},"Classes/Presence/Options.html#/s:18SwiftPhoenixClient8PresenceC7OptionsV8defaultsAEvpZ":{"name":"defaults","abstract":"

Default set of Options used when creating Presence. Uses the","parent_name":"Options"},"Classes/Presence/Options.html":{"name":"Options","abstract":"

Custom options that can be provided when creating Presence

","parent_name":"Presence"},"Classes/Presence/Events.html":{"name":"Events","abstract":"

Presense Events

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4Metaa":{"name":"Meta","abstract":"

Meta details of a Presence. Just a dictionary of properties

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC3Mapa":{"name":"Map","abstract":"

A mapping of a String to an array of Metas. e.g. {“metas”: [{id: 1}]}

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC5Statea":{"name":"State","abstract":"

A mapping of a Presence state to a mapping of Metas

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4Diffa":{"name":"Diff","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6OnJoina":{"name":"OnJoin","abstract":"

Closure signature of OnJoin callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7OnLeavea":{"name":"OnLeave","abstract":"

Closure signature for OnLeave callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6OnSynca":{"name":"OnSync","abstract":"

/ Closure signature for OnSync callbacks

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC5stateSDySSSDySSSaySDySSypGGGGvp":{"name":"state","abstract":"

The state of the Presence

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC12pendingDiffsSaySDySSSDySSSDySSSaySDySSypGGGGGGvp":{"name":"pendingDiffs","abstract":"

Pending join and leave diffs that need to be synced

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7joinRefSSSgvp":{"name":"joinRef","abstract":"

The channel’s joinRef, set when state events occur

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC18isPendingSyncStateSbvp":{"name":"isPendingSyncState","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onJoinyySS_SDySSSaySDySSypGGGSgAGtcvp":{"name":"onJoin","abstract":"

Callback to be informed of joins

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onJoinyyySS_SDySSSaySDySSypGGGSgAGtcF":{"name":"onJoin(_:)","abstract":"

Set the OnJoin callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7onLeaveyySS_SDySSSaySDySSypGGGAGtcvp":{"name":"onLeave","abstract":"

Callback to be informed of leaves

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7onLeaveyyySS_SDySSSaySDySSypGGGAGtcF":{"name":"onLeave(_:)","abstract":"

Set the OnLeave callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onSyncyycvp":{"name":"onSync","abstract":"

Callback to be informed of synces

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6onSyncyyyycF":{"name":"onSync(_:)","abstract":"

Set the OnSync callback

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC7channel4optsAcA7ChannelC_AC7OptionsVtcfc":{"name":"init(channel:opts:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4listSaySDySSSaySDySSypGGGGyF":{"name":"list()","abstract":"

Returns the array of presences, with deault selected metadata.

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC4list2bySayxGxSS_SDySSSaySDySSypGGGtXE_tlF":{"name":"list(by:)","abstract":"

Returns the array of presences, with selected metadata

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6filter2bySDySSSDySSSaySDySSypGGGGSbSS_AHtcSg_tF":{"name":"filter(by:)","abstract":"

Filter the Presence state with a given function

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC9syncState_03newF06onJoin0H5LeaveSDySSSDySSSaySDySSypGGGGAK_AKySS_AJSgAJtXEySS_A2JtXEtFZ":{"name":"syncState(_:newState:onJoin:onLeave:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC8syncDiff_4diff6onJoin0H5LeaveSDySSSDySSSaySDySSypGGGGAK_SDySSAKGySS_AJSgAJtXEySS_A2JtXEtFZ":{"name":"syncDiff(_:diff:onJoin:onLeave:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6filter_2bySDySSSDySSSaySDySSypGGGGAI_SbSS_AHtcSgtFZ":{"name":"filter(_:by:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Presence.html#/s:18SwiftPhoenixClient8PresenceC6listBy_11transformerSayxGSDySSSDySSSaySDySSypGGGG_xSS_AItXEtlFZ":{"name":"listBy(_:transformer:)","abstract":"

Undocumented

","parent_name":"Presence"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC3refSSvp":{"name":"ref","abstract":"

Reference number. Empty if missing

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC5topicSSvp":{"name":"topic","abstract":"

Message topic

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC5eventSSvp":{"name":"event","abstract":"

Message event

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC7payloadSDySSypGvp":{"name":"payload","abstract":"

Message payload

","parent_name":"Message"},"Classes/Message.html#/s:18SwiftPhoenixClient7MessageC6statusSSSgvp":{"name":"status","abstract":"

Convenience accessor. Equivalent to getting the status as such:

","parent_name":"Message"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC5topicSSvp":{"name":"topic","abstract":"

The topic of the Channel. e.g. “rooms:friends”

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC6paramsSDySSypGvp":{"name":"params","abstract":"

The params sent when joining the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9onMessageyAA0F0CAFcvp":{"name":"onMessage","abstract":"

Overridable message hook. Receives all events for specialized message","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC4join7timeoutAA4PushCSdSg_tF":{"name":"join(timeout:)","abstract":"

Joins the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC7onCloseySiyAA7MessageCcF":{"name":"onClose(_:)","abstract":"

Hook into when the Channel is closed. Does not handle retain cycles.","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC15delegateOnClose2to8callbackSix_yx_AA7MessageCtctRlzClF":{"name":"delegateOnClose(to:callback:)","abstract":"

Hook into when the Channel is closed. Automatically handles retain","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC7onErrorySiyAA7MessageCcF":{"name":"onError(_:)","abstract":"

Hook into when the Channel receives an Error. Does not handle retain","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC15delegateOnError2to8callbackSix_yx_AA7MessageCtctRlzClF":{"name":"delegateOnError(to:callback:)","abstract":"

Hook into when the Channel receives an Error. Automatically handles","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC2on_8callbackSiSS_yAA7MessageCctF":{"name":"on(_:callback:)","abstract":"

Subscribes on channel events. Does not handle retain cycles. Use","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC10delegateOn_2to8callbackSiSS_xyx_AA7MessageCtctRlzClF":{"name":"delegateOn(_:to:callback:)","abstract":"

Subscribes on channel events. Automatically handles retain cycles. Use","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC3off_3refySS_SiSgtF":{"name":"off(_:ref:)","abstract":"

Unsubscribes from a channel event. If a ref is given, only the exact","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC4push_7payload7timeoutAA4PushCSS_SDySSypGSdtF":{"name":"push(_:payload:timeout:)","abstract":"

Push a payload to the Channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC5leave7timeoutAA4PushCSd_tF":{"name":"leave(timeout:)","abstract":"

Leaves the channel

","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9onMessage8callbackyAA0F0CAGc_tF":{"name":"onMessage(callback:)","abstract":"

Overridable message hook. Receives all events for specialized message","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC8isClosedSbvp":{"name":"isClosed","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isErroredSbvp":{"name":"isErrored","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC8isJoinedSbvp":{"name":"isJoined","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isJoiningSbvp":{"name":"isJoining","parent_name":"Channel"},"Classes/Channel.html#/s:18SwiftPhoenixClient7ChannelC9isLeavingSbvp":{"name":"isLeaving","parent_name":"Channel"},"Classes/Channel.html":{"name":"Channel","abstract":"

Undocumented

"},"Classes/Message.html":{"name":"Message","abstract":"

Data that is received from the Server.

"},"Classes/Presence.html":{"name":"Presence","abstract":"

The Presence object provides features for syncing presence information from"},"Classes/Push.html":{"name":"Push","abstract":"

Represnts pushing data to a Channel through the Socket

"},"Classes/Socket.html":{"name":"Socket","abstract":"

Socket Connection

"},"Classes/Defaults.html":{"name":"Defaults","abstract":"

A collection of default values and behaviors used accross the Client

"},"Classes.html":{"name":"Classes","abstract":"

The following classes are available globally.

"},"Enums.html":{"name":"Enumerations","abstract":"

The following enumerations are available globally.

"},"Structs.html":{"name":"Structures","abstract":"

The following structures are available globally.

"},"Typealiases.html":{"name":"Type Aliases","abstract":"

The following type aliases are available globally.

"}} ================================================ FILE: docs/undocumented.json ================================================ { "warnings": [ { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Channel.swift", "line": 61, "symbol": "Channel", "symbol_kind": "source.lang.swift.decl.class", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Channel.swift", "line": 601, "symbol": "Channel", "symbol_kind": "source.lang.swift.decl.extension", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 118, "symbol": "Presence.Events.state", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 119, "symbol": "Presence.Events.diff", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 136, "symbol": "Presence.Diff", "symbol_kind": "source.lang.swift.decl.typealias", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 173, "symbol": "Presence.isPendingSyncState", "symbol_kind": "source.lang.swift.decl.var.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 212, "symbol": "Presence.init(channel:opts:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 285, "symbol": "Presence.syncState(_:newState:onJoin:onLeave:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 340, "symbol": "Presence.syncDiff(_:diff:onJoin:onLeave:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 380, "symbol": "Presence.filter(_:by:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Presence.swift", "line": 386, "symbol": "Presence.listBy(_:transformer:)", "symbol_kind": "source.lang.swift.decl.function.method.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 161, "symbol": "Socket.init(_:params:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 168, "symbol": "Socket.init(_:paramsClosure:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 723, "symbol": "Socket.websocketDidConnect(socket:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 727, "symbol": "Socket.websocketDidDisconnect(socket:error:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 732, "symbol": "Socket.websocketDidReceiveMessage(socket:text:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Socket.swift", "line": 736, "symbol": "Socket.websocketDidReceiveData(socket:data:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 66, "symbol": "ChannelState.closed", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 67, "symbol": "ChannelState.errored", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 68, "symbol": "ChannelState.joined", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 69, "symbol": "ChannelState.joining", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 70, "symbol": "ChannelState.leaving", "symbol_kind": "source.lang.swift.decl.enumelement", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 76, "symbol": "ChannelEvent.heartbeat", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 77, "symbol": "ChannelEvent.join", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 78, "symbol": "ChannelEvent.leave", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 79, "symbol": "ChannelEvent.reply", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 80, "symbol": "ChannelEvent.error", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Defaults.swift", "line": 81, "symbol": "ChannelEvent.close", "symbol_kind": "source.lang.swift.decl.var.static", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 31, "symbol": "Delegated.init()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 33, "symbol": "Delegated.delegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 43, "symbol": "Delegated.call(_:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 47, "symbol": "Delegated.isDelegateSet", "symbol_kind": "source.lang.swift.decl.var.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 55, "symbol": "Delegated.stronglyDelegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 62, "symbol": "Delegated.manuallyDelegate(with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 66, "symbol": "Delegated.removeDelegate()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 74, "symbol": "Delegated.delegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 79, "symbol": "Delegated.stronglyDelegate(to:with:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 88, "symbol": "Delegated.call()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 96, "symbol": "Delegated.call(_:)", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" }, { "file": "/Users/drees/src/github/phoenix/SwiftPhoenixClient/Sources/Utilities/Delegated.swift", "line": 104, "symbol": "Delegated.call()", "symbol_kind": "source.lang.swift.decl.function.method.instance", "warning": "undocumented" } ], "source_directory": "/Users/drees/src/github/phoenix/SwiftPhoenixClient" } ================================================ FILE: fastlane/Appfile ================================================ # app_identifier "[[APP_IDENTIFIER]]" # The bundle identifier of your app # apple_id "[[APPLE_ID]]" # Your Apple email address # For more information about the Appfile, see: # https://docs.fastlane.tools/advanced/#appfile ================================================ FILE: fastlane/Fastfile ================================================ # This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # # Uncomment the line if you want fastlane to automatically update itself # update_fastlane default_platform(:ios) platform :ios do desc "Runs tests for the project" lane :test do execute_tests gather_coverage end # PRIVATE LANES private_lane :execute_tests do scan( scheme: "SwiftPhoenixClient", code_coverage: true ) end private_lane :gather_coverage do slather( use_bundle_exec: true, cobertura_xml: true, travis: true ) end end ================================================ FILE: fastlane/README.md ================================================ fastlane documentation ================ # Installation Make sure you have the latest version of the Xcode command line tools installed: ``` xcode-select --install ``` Install _fastlane_ using ``` [sudo] gem install fastlane -NV ``` or alternatively using `brew cask install fastlane` # Available Actions ## iOS ### ios test ``` fastlane ios test ``` Runs tests for the project ---- This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). ================================================ FILE: sourcery/MockableClass.stencil ================================================ // swiftlint:disable line_length // swiftlint:disable variable_name @testable import SwiftPhoenixClient {% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %} {% macro methodThrowableErrorDeclaration method %} var {% call swiftifyMethodName method.selectorName %}ThrowableError: Error? {% endmacro %} {% macro argumentsBlock arguments %}{% filter removeNewlines:"leading" %} {% for argument in arguments %} {{argument.name}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {% macro methodThrowableErrorUsage method %} if let error = {% call swiftifyMethodName method.selectorName %}ThrowableError { throw error } {% endmacro %} {% macro methodReceivedParameters method %} {%if method.parameters.count == 1 %} {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %} {% else %} {% if not method.parameters.count == 0 %} {% call swiftifyMethodName method.selectorName %}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}) {% endif %} {% endif %} {% endmacro %} {% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %} {% macro methodClosureDeclaration method %} var {% call methodClosureName method %}: (({% for param in method.parameters %}{{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{{ method.returnTypeName }}{% endif %})? {% endmacro %} {% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %} {% macro mockMethod method %} //MARK: - {{ method.shortName }} {% if method.throws %} {% call methodThrowableErrorDeclaration method %} {% endif %} {% if not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}CallsCount = 0 var {% call swiftifyMethodName method.selectorName %}Called: Bool { return {% call swiftifyMethodName method.selectorName %}CallsCount > 0 } {% endif %} {% if method.parameters.count == 1 and method.isGeneric == false %} var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {% if param.isClosure %}({% endif %}{{ param.typeName.unwrappedTypeName }}{% if param.isClosure %}){% endif %}?{% endfor %} {% else %}{% if not method.parameters.count == 0 and method.isGeneric == false %} var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})? {% endif %}{% endif %} {% if not method.returnTypeName.isVoid and not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ method.returnTypeName }}! {% endif %} {% if method.isGeneric == false %} {% call methodClosureDeclaration method %} {% endif %} {% if method.isInitializer or method.isDeinitializer %} {#{% if not method.isConvenienceInitializer %} override {{ method.name }} { super.{{ method.callName }}({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% ifnot forloop.last %}, {% endif %}{% endfor %}) {% call methodReceivedParameters method %} {% call methodClosureName method %}?({% call methodClosureCallParameters method %}) } {% endif %}#} {% else %} override func {{ method.name }}{% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { {% if method.throws %} {% call methodThrowableErrorUsage method %} {% endif %} {% call swiftifyMethodName method.selectorName %}CallsCount += 1 {% if method.isGeneric == false %} {% call methodReceivedParameters method %} {% endif %} {% if method.returnTypeName.isVoid %} {% if method.isGeneric == false %} {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) {% endif %} {% else %} {% if method.isGeneric == true %} return {% call swiftifyMethodName method.selectorName %}ReturnValue {% else %} return {% if method.throws %}try {% endif %}{% call methodClosureName method %}.map({ {% if method.throws %}try {% endif %}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue {% endif %} {% endif %} } {% endif %} {% endmacro %} {% macro mockOptionalVariable variable %} {% if variable.writeAccess == "" %} var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}! override var {% call mockedVariableName variable %}: {{ variable.typeName }} { return {% call underlyingMockedVariableName variable %} } {% else %} var {{ variable.name }}SetCount: Int = 0 var {{ variable.name }}DidGetSet: Bool { return {{ variable.name }}SetCount > 0 } override var {% call mockedVariableName variable %}: {{ variable.typeName }} { didSet { {{ variable.name }}SetCount += 1 } } {% endif %} {% endmacro %} {% macro mockNonOptionalArrayOrDictionaryVariable variable %} {# var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} #} {% endmacro %} {% macro mockNonOptionalVariable variable %} override var {% call mockedVariableName variable %}: {{ variable.typeName }} { get { return {% call underlyingMockedVariableName variable %} } set(value) { {% call underlyingMockedVariableName variable %} = value } } var {% call underlyingMockedVariableName variable %}: ({{ variable.typeName }})! {% endmacro %} {% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} {% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %} {% for type in types.classes where type.name == "TimeoutTimer" or type.name == "Socket" or type.name == "Push" or type.name == "Channel" %} class {{ type.name }}Mock: {{ type.name }} { {# Generate all variable mocks #} {% for variable in type.allVariables|!definedInExtension where variable.readAccess != "private" and variable.isMutable %} {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} {% endfor %} {% for method in type.allMethods|!definedInExtension where method.accessLevel != "private" and method.actualDefinedInTypeName.name != "WebSocketDelegate" %} {% call mockMethod method %} {% endfor %} } {% endfor %} ================================================ FILE: sourcery/MockableProtocol.stencil ================================================ // swiftlint:disable line_length // swiftlint:disable variable_name import Foundation #if os(iOS) || os(tvOS) || os(watchOS) import UIKit #elseif os(OSX) import AppKit #endif @testable import SwiftPhoenixClient {% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %} {% macro methodThrowableErrorDeclaration method %} var {% call swiftifyMethodName method.selectorName %}ThrowableError: Error? {% endmacro %} {% macro methodThrowableErrorUsage method %} if let error = {% call swiftifyMethodName method.selectorName %}ThrowableError { throw error } {% endmacro %} {% macro methodReceivedParameters method %} {%if method.parameters.count == 1 %} {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %} {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append({% for param in method.parameters %}{{ param.name }}){% endfor %} {% else %} {% if not method.parameters.count == 0 %} {% call swiftifyMethodName method.selectorName %}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}) {% call swiftifyMethodName method.selectorName %}ReceivedInvocations.append(({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %})) {% endif %} {% endif %} {% endmacro %} {% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %} {% macro closureReturnTypeName method %}{% if method.isOptionalReturnType %}{{ method.unwrappedReturnTypeName }}?{% else %}{{ method.returnTypeName }}{% endif %}{% endmacro %} {% macro methodClosureDeclaration method %} var {% call methodClosureName method %}: (({% for param in method.parameters %}{{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{% call closureReturnTypeName method %}{% endif %})? {% endmacro %} {% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %} {% macro mockMethod method %} //MARK: - {{ method.shortName }} {% if method.throws %} {% call methodThrowableErrorDeclaration method %} {% endif %} {% if not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}CallsCount = 0 var {% call swiftifyMethodName method.selectorName %}Called: Bool { return {% call swiftifyMethodName method.selectorName %}CallsCount > 0 } {% endif %} {% if method.parameters.count == 1 %} var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}?{% endfor %} var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations{% for param in method.parameters %}: [{{ '(' if param.isClosure }}{{ param.typeName.unwrappedTypeName }}{{ ')' if param.isClosure }}{%if param.typeName.isOptional%}?{%endif%}]{% endfor %} = [] {% elif not method.parameters.count == 0 %} var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{{ ', ' if not forloop.last }}{% endfor %})? var {% call swiftifyMethodName method.selectorName %}ReceivedInvocations: [({% for param in method.parameters %}{{ param.name }}: {{ param.unwrappedTypeName if param.typeAttributes.escaping else param.typeName }}{{ ', ' if not forloop.last }}{% endfor %})] = [] {% endif %} {% if not method.returnTypeName.isVoid and not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ '(' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ method.returnTypeName }}{{ ')' if method.returnTypeName.isClosure and not method.isOptionalReturnType }}{{ '!' if not method.isOptionalReturnType }} {% endif %} {% call methodClosureDeclaration method %} {% if method.isInitializer %} required {{ method.name }} { {% call methodReceivedParameters method %} {% call methodClosureName method %}?({% call methodClosureCallParameters method %}) } {% else %} func {{ method.name }}{{ ' throws' if method.throws }}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { {% if method.throws %} {% call methodThrowableErrorUsage method %} {% endif %} {% call swiftifyMethodName method.selectorName %}CallsCount += 1 {% call methodReceivedParameters method %} {% if method.returnTypeName.isVoid %} {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) {% else %} return {{ 'try ' if method.throws }}{% call methodClosureName method %}.map({ {{ 'try ' if method.throws }}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue {% endif %} } {% endif %} {% endmacro %} {% macro mockOptionalVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} {% endmacro %} {% macro mockNonOptionalArrayOrDictionaryVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} {% endmacro %} {% macro mockNonOptionalVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} { get { return {% call underlyingMockedVariableName variable %} } set(value) { {% call underlyingMockedVariableName variable %} = value } } var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}! {% endmacro %} {% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} {% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %} {% for type in types.protocols where type.based.AutoMockable or type|annotated:"AutoMockable" %}{% if type.name != "AutoMockable" %} class {{ type.name }}Mock: {{ type.name }} { {% for variable in type.allVariables|!definedInExtension %} {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} {% endfor %} {% for method in type.allMethods|!definedInExtension %} {% call mockMethod method %} {% endfor %} } {% endif %}{% endfor %} ================================================ FILE: sourcery/MockableWebSocketClient.stencil ================================================ // swiftlint:disable line_length // swiftlint:disable variable_name import Starscream @testable import SwiftPhoenixClient {% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %} {% macro methodThrowableErrorDeclaration method %} var {% call swiftifyMethodName method.selectorName %}ThrowableError: Error? {% endmacro %} {% macro methodThrowableErrorUsage method %} if let error = {% call swiftifyMethodName method.selectorName %}ThrowableError { throw error } {% endmacro %} {% macro methodReceivedParameters method %} {%if method.parameters.count == 1 %} {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %} {% else %} {% if not method.parameters.count == 0 %} {% call swiftifyMethodName method.selectorName %}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}) {% endif %} {% endif %} {% endmacro %} {% macro methodClosureName method %}{% call swiftifyMethodName method.selectorName %}Closure{% endmacro %} {% macro methodClosureDeclaration method %} var {% call methodClosureName method %}: (({% for param in method.parameters %}{{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% if method.throws %}throws {% endif %}-> {% if method.isInitializer %}Void{% else %}{{ method.returnTypeName }}{% endif %})? {% endmacro %} {% macro methodClosureCallParameters method %}{% for param in method.parameters %}{{ param.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endmacro %} {% macro mockMethod method %} //MARK: - {{ method.shortName }} {% if method.throws %} {% call methodThrowableErrorDeclaration method %} {% endif %} {% if not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}CallsCount = 0 var {% call swiftifyMethodName method.selectorName %}Called: Bool { return {% call swiftifyMethodName method.selectorName %}CallsCount > 0 } {% endif %} {% if method.parameters.count == 1 %} var {% call swiftifyMethodName method.selectorName %}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {% if param.isClosure %}({% endif %}{{ param.typeName.unwrappedTypeName }}{% if param.isClosure %}){% endif %}?{% endfor %} {% else %}{% if not method.parameters.count == 0 %} var {% call swiftifyMethodName method.selectorName %}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})? {% endif %}{% endif %} {% if not method.returnTypeName.isVoid and not method.isInitializer %} var {% call swiftifyMethodName method.selectorName %}ReturnValue: {{ method.returnTypeName }}! {% endif %} {% call methodClosureDeclaration method %} {% if method.isInitializer %} required {{ method.name }} { {% call methodReceivedParameters method %} {% call methodClosureName method %}?({% call methodClosureCallParameters method %}) } {% else %} func {{ method.name }}{% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { {% if method.throws %} {% call methodThrowableErrorUsage method %} {% endif %} {% call swiftifyMethodName method.selectorName %}CallsCount += 1 {% call methodReceivedParameters method %} {% if method.returnTypeName.isVoid %} {% if method.throws %}try {% endif %}{% call methodClosureName method %}?({% call methodClosureCallParameters method %}) {% else %} return {% if method.throws %}try {% endif %}{% call methodClosureName method %}.map({ {% if method.throws %}try {% endif %}$0({% call methodClosureCallParameters method %}) }) ?? {% call swiftifyMethodName method.selectorName %}ReturnValue {% endif %} } {% endif %} {% endmacro %} {% macro mockOptionalVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} {% endmacro %} {% macro mockNonOptionalArrayOrDictionaryVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} = {% if variable.isArray %}[]{% elif variable.isDictionary %}[:]{% endif %} {% endmacro %} {% macro mockNonOptionalVariable variable %} var {% call mockedVariableName variable %}: {{ variable.typeName }} { get { return {% call underlyingMockedVariableName variable %} } set(value) { {% call underlyingMockedVariableName variable %} = value } } var {% call underlyingMockedVariableName variable %}: {{ variable.typeName }}! {% endmacro %} {% macro underlyingMockedVariableName variable %}underlying{{ variable.name|upperFirstLetter }}{% endmacro %} {% macro mockedVariableName variable %}{{ variable.name }}{% endmacro %} {% for type in types.protocols where type.name == "WebSocketClient" or type.name == "TimeoutTimer" %} class {{ type.name }}Mock: {{ type.name }} { {% for variable in type.allVariables|!definedInExtension %} {% if variable.isOptional %}{% call mockOptionalVariable variable %}{% elif variable.isArray or variable.isDictionary %}{% call mockNonOptionalArrayOrDictionaryVariable variable %}{% else %}{% call mockNonOptionalVariable variable %}{% endif %} {% endfor %} {% for method in type.allMethods|!definedInExtension %} {% call mockMethod method %} {% endfor %} } {% endfor %}