diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..d1ba664
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,36 @@
+---
+name: Bug report
+about: Report a problem with StompClientLib
+title: "[Bug] "
+labels: bug
+assignees: ''
+---
+
+## Describe the bug
+
+A clear and concise description of what the bug is.
+
+## To reproduce
+
+Steps (or a minimal code snippet) that triggers the issue.
+
+```swift
+// your setup
+```
+
+## Expected behaviour
+
+What you expected to happen.
+
+## Environment
+
+- StompClientLib version:
+- Installation: CocoaPods / Carthage
+- iOS deployment target:
+- Xcode / Swift version:
+- STOMP broker (e.g. Spring Boot, ActiveMQ, RabbitMQ):
+- `ws://` or `wss://`, and whether `certificateCheckEnabled` is on/off:
+
+## Logs
+
+Set `StompClientLib.isLoggingEnabled = true` and paste any relevant console output.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..b5c0e81
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Question / usage help
+ url: https://github.com/kuraydev/StompClientLib/discussions
+ about: Ask questions about using StompClientLib here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..6982bc5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,23 @@
+---
+name: Feature request
+about: Suggest an idea for StompClientLib
+title: "[Feature] "
+labels: enhancement
+assignees: ''
+---
+
+## Is your feature request related to a problem?
+
+A clear and concise description of the problem.
+
+## Describe the solution you'd like
+
+What you want to happen.
+
+## Describe alternatives you've considered
+
+Any alternative solutions or features you've considered.
+
+## Additional context
+
+Add any other context or references here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..92af34c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+# Description
+
+
+
+## Type of change
+
+- [ ] Bug fix (non-breaking change that fixes an issue)
+- [ ] New feature (non-breaking change that adds functionality)
+- [ ] Breaking change (fix or feature that changes the public API or default behaviour)
+- [ ] Documentation / tooling
+
+## Checklist
+
+- [ ] Public API (type names, delegate signatures, public methods, default values) is unchanged, **or** the breaking change is documented and a major version bump is recommended.
+- [ ] Objective-C run-time compatibility is preserved.
+- [ ] Tests added/updated and passing (`xcodebuild test`).
+- [ ] `swiftlint lint --strict` passes.
+- [ ] `pod lib lint StompClientLib.podspec --allow-warnings` passes.
+- [ ] `CHANGELOG.md` updated under `## [Unreleased]`.
+
+## Related issues
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..761c66f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: CI
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+ branches: [master, main]
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ swiftlint:
+ name: SwiftLint
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install SwiftLint
+ run: brew install swiftlint
+ - name: Run SwiftLint
+ run: swiftlint lint --strict --reporter github-actions-logging
+
+ podspec-lint:
+ name: pod lib lint
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4
+ - name: Lint podspec
+ run: pod lib lint StompClientLib.podspec --allow-warnings
+
+ test:
+ name: Build & Test (example)
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pods
+ working-directory: StompClientLibExample
+ run: pod install --repo-update
+ - name: Select an available iOS simulator
+ id: sim
+ run: |
+ UDID=$(xcrun simctl list devices available | grep -m1 -E "iPhone" | grep -oE "[0-9A-Fa-f-]{36}")
+ if [ -z "$UDID" ]; then echo "No iOS simulator available"; xcrun simctl list devices; exit 1; fi
+ echo "Using simulator $UDID"
+ echo "udid=$UDID" >> "$GITHUB_OUTPUT"
+ - name: Build & test
+ working-directory: StompClientLibExample
+ run: |
+ set -o pipefail
+ xcodebuild test \
+ -workspace StompClientLibExample.xcworkspace \
+ -scheme StompClientLibExample \
+ -sdk iphonesimulator \
+ -destination "id=${{ steps.sim.outputs.udid }}" \
+ -configuration Debug \
+ CODE_SIGNING_ALLOWED=NO
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..99ba924
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,49 @@
+# SwiftLint configuration for StompClientLib
+# https://github.com/realm/SwiftLint
+
+included:
+ - StompClientLib/Classes
+
+excluded:
+ - StompClientLibExample/Pods
+ - StompClientLibExample/StompClientLib.swift # stray duplicate of the library source
+
+# The public Obj-C-bridged API intentionally uses Cocoa-style names
+# (e.g. AutoMode, ClientMode) that predate Swift naming guidelines and
+# cannot be renamed without breaking the published contract.
+disabled_rules:
+ - identifier_name
+ - type_name
+ - todo
+
+opt_in_rules:
+ - empty_count
+ - closure_spacing
+ - redundant_nil_coalescing
+
+line_length:
+ warning: 200
+ error: 300
+ ignores_comments: true
+
+file_length:
+ warning: 600
+ error: 1000
+
+function_body_length:
+ warning: 80
+ error: 150
+
+type_body_length:
+ warning: 500
+ error: 700
+
+# StompFrame.decode returns a labelled (command, headers, body) tuple.
+large_tuple:
+ warning: 3
+ error: 4
+
+# receiveFrame is a flat dispatch over STOMP frame commands.
+cyclomatic_complexity:
+ warning: 15
+ error: 25
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100755
index c79be57..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-# references:
-# * http://www.objc.io/issue-6/travis-ci.html
-# * https://github.com/supermarin/xcpretty#usage
-
-osx_image: xcode7.3
-language: swift
-# cache: cocoapods
-# podfile: Example/Podfile
-# before_install:
-# - gem install cocoapods # Since Travis is not always on latest version
-# - pod install --project-directory=Example
-script:
-- set -o pipefail && xcodebuild test -workspace Example/StompClientLib.xcworkspace -scheme StompClientLib-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty
-- pod lib lint
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66f0738..ab4eeb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,59 @@
# Changelog
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Public `StompFrame` helpers (`encode`, `decode`, `ackHeaderValue`) — the pure,
+ transport-agnostic STOMP wire-format core that backs the client and is now unit
+ tested independently of a live WebSocket.
+- `StompClientLib.isLoggingEnabled` toggle (default `false`) so the library no
+ longer prints diagnostics to consumer consoles unless opted in.
+- Real XCTest suite covering frame encode/decode (including the `:`-in-header-value
+ case), ack-mode mapping, and client defaults.
+- GitHub Actions CI (SwiftLint, `pod lib lint`, and `xcodebuild test` of the
+ example) replacing the dead Travis configuration.
+- SwiftLint configuration (`.swiftlint.yml`).
+- `CONTRIBUTING.md`, issue templates, and a pull-request template.
+
+### Changed
+
+- The pod now ships `PrivacyInfo.xcprivacy` via `resource_bundles` (previously the
+ manifest was committed but never packaged because it sat outside `source_files`).
+- Podspec `homepage`/`source` repointed to `github.com/kuraydev/StompClientLib`;
+ Swift version pinned to 5.0; `SocketRocket` dependency pinned to `~> 0.5`.
+- `protocol StompClientLibDelegate` now refines `AnyObject` instead of the
+ deprecated `class` keyword.
+- Reconnect now honours the `exponentialBackoff` parameter (previously ignored):
+ with the default `true`, the interval grows geometrically (capped at 60s) and
+ resets on reconnect. Pass `exponentialBackoff: false` for the legacy fixed
+ interval.
+- Console `print` diagnostics replaced by a gated logger (off by default).
+
+### Fixed
+
+- `serverDidSendError` now reports a real `detailedErrorMessage` on transport
+ failures; it previously always passed `nil` (`error as? String` never succeeds).
+- `connection` is now reset to `false` when the socket fails or the server closes
+ it, so `isConnected()` is accurate and auto-reconnect actually fires
+ (issues #123, #125).
+
+### Breaking (recommend a major version, e.g. 2.0.0)
+
+- Minimum deployment target raised from iOS 9.0 to iOS 12.0.
+
+---
+
+## Historical releases
+
+The entries below were auto-generated against the original `WrathChaos/StompClientLib`
+repository and are retained for reference.
+
[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.8...HEAD)
**Implemented enhancements:**
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..60d4b02
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,55 @@
+# Contributing to StompClientLib
+
+Thanks for taking the time to contribute! This document explains how to set up
+the project, the conventions we follow, and how to get a change merged.
+
+## Getting started
+
+```bash
+git clone https://github.com/kuraydev/StompClientLib.git
+cd StompClientLib/StompClientLibExample
+pod install
+open StompClientLibExample.xcworkspace
+```
+
+The library source lives in `StompClientLib/Classes/`. The wire-format core
+(`StompFrame.swift`) is pure Foundation and is unit tested in
+`StompClientLibExample/StompClientLibExampleTests/`.
+
+## Running the checks locally
+
+The same checks run in CI (`.github/workflows/ci.yml`):
+
+```bash
+# Lint
+swiftlint lint --strict
+
+# Validate the podspec
+pod lib lint StompClientLib.podspec --allow-warnings
+
+# Build & test the example app
+cd StompClientLibExample
+xcodebuild test \
+ -workspace StompClientLibExample.xcworkspace \
+ -scheme StompClientLibExample \
+ -destination 'platform=iOS Simulator,name=iPhone 16'
+```
+
+## Pull request guidelines
+
+- Branch off `master` and open the PR against `master`.
+- Keep the **public API stable.** Exported type names, the `StompClientLibDelegate`
+ method signatures, public method names, and their default values are a contract
+ for existing CocoaPods consumers. Any source-breaking change must be called out
+ explicitly and warrants a major version bump.
+- Preserve **Objective-C run-time compatibility** (`@objc` / `@objcMembers`) — it
+ is an advertised feature.
+- Use [Conventional Commits](https://www.conventionalcommits.org/) for commit
+ messages (e.g. `fix:`, `feat:`, `docs:`, `chore:`).
+- Add or update tests for any behavioural change.
+- Update `CHANGELOG.md` under `## [Unreleased]`.
+
+## Reporting bugs
+
+Open an issue using the bug-report template and include the STOMP broker you are
+connecting to (e.g. Spring Boot), the deployment target, and a minimal repro.
diff --git a/LICENSE b/LICENSE
index 3e4da3f..5bc87b8 100755
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2017 wrathchaos
+MIT License
+
+Copyright (c) 2017-2026 Kuray Ogun (FreakyCoder)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 03f9748..cac1ef4 100755
--- a/README.md
+++ b/README.md
@@ -5,506 +5,272 @@
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
-
+
## Introduction
-StompClientLib is a stomp client in Swift. It uses Facebook's [ SocketRocket ](https://github.com/facebook/SocketRocket) as a websocket dependency. SocketRocket is written in Objective-C but StompClientLib's STOMP part is written in Swift and its usage is Swift. You can use this library in your Swift 5+, 4+ and 3+ projects.
+StompClientLib is a [STOMP](https://stomp.github.io/) (Simple Text Oriented
+Messaging Protocol) client for iOS, written in Swift with Objective-C run-time
+compatibility. It opens a WebSocket connection and layers STOMP 1.1 / 1.2
+(connect, subscribe/unsubscribe, send, ack, transactions, receipts, ping,
+auto-reconnect/disconnect) on top, surfaced through a delegate protocol.
-## Supported Stomp Versions
+The WebSocket transport is provided by Facebook's
+[SocketRocket](https://github.com/facebookincubator/SocketRocket), which is
+pulled in automatically as a dependency.
-Stomp version
-- **1.1**
-- **1.2**
-- Might **not** be worked with 1.0 (Never tested)
+> **Note:** SocketRocket is in maintenance mode. A migration to Apple's
+> first-party `URLSessionWebSocketTask` / `NWProtocolWebSocket` (which would also
+> unblock Swift Package Manager support — see issue
+> [#128](https://github.com/kuraydev/StompClientLib/issues/128)) is planned for a
+> future major release.
-## Example
+## Supported STOMP versions
-To run the example project, clone the repo, and run `pod install` from the Example directory first.
+- **1.1**
+- **1.2**
+- 1.0 is not supported (never tested)
+
+The client automatically negotiates `accept-version: 1.1,1.2` on connect, which
+is what brokers such as Spring Boot expect.
## Requirements
-- iOS 8.0+
-- XCode 8.1, 8.2, 8.3
-- XCode 9.0+
-- **XCode 10.0 +**
-- **XCode 12.1 +**
-- Swift 3.0, 3.1, 3.2
-- **Swift 4.0, Swift 4.1, Swift 4.2, Swift 5.0**
+| | Minimum |
+|---|---|
+| iOS deployment target | 12.0+ |
+| Swift | 5.0+ |
+| Xcode | 14+ (built & tested on Xcode 16 / 26) |
## Installation
-StompClientLib is available through [CocoaPods](http://cocoapods.org). To install
-it, simply add the following line to your Podfile:
+### CocoaPods
-#### Cocoapods
+StompClientLib is available through [CocoaPods](https://cocoapods.org). Add the
+following line to your `Podfile`:
```ruby
-pod "StompClientLib"
+pod 'StompClientLib'
```
-#### Carthage
+Then run `pod install`. This also pulls in the transitive `SocketRocket`
+dependency.
-```ruby
-github "WrathChaos/StompClientLib"
+### Carthage
+
+```ogdl
+github "kuraydev/StompClientLib"
```
+### Swift Package Manager
+
+SPM is **not yet supported** because the underlying SocketRocket dependency does
+not ship a `Package.swift`. It is tracked in issue
+[#128](https://github.com/kuraydev/StompClientLib/issues/128) and is gated on the
+planned migration to `URLSessionWebSocketTask`.
+
## Usage
```swift
import StompClientLib
```
-Once imported, you can open a connection to your WebSocket server.
+### Open a connection
```swift
var socketClient = StompClientLib()
-let url = NSURL(string: "your-socket-url-is-here")!
-socketClient.openSocketWithURLRequest(request: NSURLRequest(url: url as URL) , delegate: self)
+let url = URL(string: "ws://your-broker-host/your-endpoint")!
+let request = NSURLRequest(url: url)
+socketClient.openSocketWithURLRequest(request: request, delegate: self)
```
-After you are connected, there are some delegate methods that you need to implement.
-
-# StompClientLibDelegate
-
-## stompClientDidConnect
+For self-signed / untrusted TLS certificates (e.g. a local `wss://` dev broker),
+set `certificateCheckEnabled = false` **before** opening the socket. Leave it at
+its default (`true`) in production so the system performs normal TLS validation:
```swift
-func stompClientDidConnect(client: StompClientLib!) {
-print("Socket is connected")
-// Stomp subscribe will be here!
-socketClient.subscribe(destination: topic)
-// Note : topic needs to be a String object
-}
+socketClient.certificateCheckEnabled = false // accepts untrusted certs — dev only
```
-## stompClientDidDisconnect
-
-```swift
-func stompClientDidDisconnect(client: StompClientLib!) {
-print("Socket is Disconnected")
-}
-```
+### Enable logging (optional)
-## didReceiveMessageWithJSONBody ( Message Received via STOMP )
-
-Your json message will be converted to JSON Body as AnyObject and you will receive your message in this function
+Diagnostics are silent by default. Turn them on while debugging:
```swift
-func stompClient(client: StompClientLib!, didReceiveMessageWithJSONBody jsonBody: AnyObject?, akaStringBody stringBody: String?, withHeader header: [String : String]?, withDestination destination: String) {
-print("Destination : \(destination)")
-print("JSON Body : \(String(describing: jsonBody))")
-print("String Body : \(stringBody ?? "nil")")
-}
+StompClientLib.isLoggingEnabled = true
```
-## didReceiveMessageWithJSONBody ( Message Received via STOMP as String )
+## StompClientLibDelegate
+
+Conform to `StompClientLibDelegate` to receive connection and message events:
-Your json message will be converted to JSON Body as AnyObject and you will receive your message in this function
+| Method | Called when |
+|---|---|
+| `stompClientDidConnect(client:)` | The STOMP `CONNECTED` frame is received |
+| `stompClientDidDisconnect(client:)` | The socket disconnects or is closed |
+| `stompClient(client:didReceiveMessageWithJSONBody:akaStringBody:withHeader:withDestination:)` | A `MESSAGE` frame arrives (body provided as parsed JSON **and** raw String) |
+| `serverDidSendReceipt(client:withReceiptId:)` | A `RECEIPT` frame arrives |
+| `serverDidSendError(client:withErrorMessage:detailedErrorMessage:)` | An `ERROR` frame or a transport failure occurs |
+| `serverDidSendPing()` | The server sends a heart-beat / ping |
```swift
-func stompClientJSONBody(client: StompClientLib!, didReceiveMessageWithJSONBody jsonBody: String?, withHeader header: [String : String]?, withDestination destination: String) {
- print("DESTINATION : \(destination)")
- print("String JSON BODY : \(String(describing: jsonBody))")
+func stompClientDidConnect(client: StompClientLib!) {
+ print("Socket connected")
+ socketClient.subscribe(destination: "/topic/greetings")
}
-```
-## serverDidSendReceipt
+func stompClientDidDisconnect(client: StompClientLib!) {
+ print("Socket disconnected")
+}
-If you will use STOMP for in-app purchase, you might need to use this function to get receipt
+func stompClient(client: StompClientLib!,
+ didReceiveMessageWithJSONBody jsonBody: AnyObject?,
+ akaStringBody stringBody: String?,
+ withHeader header: [String: String]?,
+ withDestination destination: String) {
+ print("Destination: \(destination)")
+ print("JSON Body: \(String(describing: jsonBody))")
+ print("String Body: \(stringBody ?? "nil")")
+}
-```swift
func serverDidSendReceipt(client: StompClientLib!, withReceiptId receiptId: String) {
- print("Receipt : \(receiptId)")
+ print("Receipt: \(receiptId)")
}
-```
-
-## serverDidSendError
-
-Your error message will be received in this function
-```swift
-func serverDidSendError(client: StompClientLib!, withErrorMessage description: String, detailedErrorMessage message: String?) {
- print("Error Send : \(String(describing: message))")
+func serverDidSendError(client: StompClientLib!,
+ withErrorMessage description: String,
+ detailedErrorMessage message: String?) {
+ print("Error: \(description) — \(message ?? "")")
}
-```
-## serverDidSendPing
-
-If you need to control your server's ping, here is your part
-
-```swift
func serverDidSendPing() {
- print("Server ping")
+ print("Server ping")
}
```
-## How to subscribe and unsubscribe
-
-There are functions for subscribing and unsubscribing.
-Note : You should handle your subscribe and unsubscribe methods !
-Suggestion : Subscribe to your topic in "stompClientDidConnect" function and unsubcribe to your topic in stompClientWillDisconnect method.
+## Public API reference
-## Subscribe
+### Connection
-```swift
-socketClient.subscribe(destination: topic)
-// Note : topic needs to be a String object
-```
+| Method | Description |
+|---|---|
+| `openSocketWithURLRequest(request:delegate:connectionHeaders:)` | Open the WebSocket and connect |
+| `disconnect()` | Send `DISCONNECT` and close the socket |
+| `isConnected() -> Bool` | Current connection state |
+| `reconnect(request:delegate:connectionHeaders:time:exponentialBackoff:)` | Auto-reconnect while down (see below) |
+| `stopReconnect()` | Cancel auto-reconnect |
+| `autoDisconnect(time:)` | Disconnect after `time` seconds |
+| `var certificateCheckEnabled: Bool` | `false` accepts untrusted SSL certificates (default `true`) |
+| `static var isLoggingEnabled: Bool` | Console logging toggle (default `false`) |
-## Unsubscribe
+### Subscriptions
-```swift
-socketClient.unsubscribe(destination: topic)
-```
+| Method | Description |
+|---|---|
+| `subscribe(destination:)` | Subscribe with auto ack mode |
+| `subscribeToDestination(destination:ackMode:)` | Subscribe with an explicit `StompAckMode` |
+| `subscribeWithHeader(destination:withHeader:)` | Subscribe with custom headers |
+| `unsubscribe(destination:)` | Unsubscribe from a destination |
-Important : You have to send your destination for both subscribe or unsubscribe!
+`StompAckMode` is one of `.AutoMode`, `.ClientMode`, `.ClientIndividualMode`.
-## Unsubsribe with header
+### Messaging & transactions
-```swift
-let destination = "/topic/your_topic"
-let ack = destination
-let id = destination
-let header = ["destination": destination, "ack": ack, "id": id]
+| Method | Description |
+|---|---|
+| `sendMessage(message:toDestination:withHeaders:withReceipt:)` | Send a text message |
+| `sendJSONForDict(dict:toDestination:)` | Serialize a dictionary to JSON and send it |
+| `ack(messageId:)` / `ack(messageId:withSubscription:)` | Acknowledge a message |
+| `begin(transactionId:)` | Begin a transaction |
+| `commit(transactionId:)` | Commit a transaction |
+| `abort(transactionId:)` | Abort a transaction |
-// subscribe
-socketClient?.subscribeWithHeader(destination: destination, withHeader: header)
+### Wire-format helpers
-// unsubscribe
-socketClient?.unsubscribe(destination: subsId)
-```
+The pure STOMP frame core is exposed via `StompFrame` for advanced use and
+testing:
-## Auto Reconnect with a given time
+- `StompFrame.encode(command:headers:body:) -> String`
+- `StompFrame.decode(_:) -> (command: String, headers: [String: String], body: String)?`
+- `StompFrame.ackHeaderValue(for:) -> String`
-You can use this feature if you need to auto reconnect with a specific time or it will just try to reconnect every second.
+## Auto reconnect
```swift
-// Reconnect after 4 sec
-socketClient.reconnect(request: NSURLRequest(url: url as URL) , delegate: self as StompClientLibDelegate, time: 4.0)
-```
-
-## Auto Disconnect with a given time
+// Reconnect with exponential backoff starting at 1s (default), capped at 60s.
+socketClient.reconnect(request: request, delegate: self)
-```swift
-// Auto Disconnect after 3 sec
-socketClient.autoDisconnect(time: 3)
+// Or with a fixed 4s interval:
+socketClient.reconnect(request: request, delegate: self, time: 4.0, exponentialBackoff: false)
```
-## Login Passcode Implementation
+When `exponentialBackoff` is `true` (the default), the wait between attempts
+grows geometrically (1s, 2s, 4s, …) up to 60s and resets once the connection is
+re-established. Call `stopReconnect()` to cancel.
-This is just an example. You need to convert to your implementation.
-[#42](https://github.com/WrathChaos/StompClientLib/issues/42)
+## Auto disconnect
```swift
-let connectFrame = "CONNECT\n login:admin\n passcode:password\n\n\n\0"
-socket.write(string: connectFrame)
+socketClient.autoDisconnect(time: 3) // disconnect after 3 seconds
```
-## Future Enhancements
-
-- [x] ~~Complete a working Example~~
-- [x] ~~Add Carthage installation option~~
-- [x] ~~Add Swift Package Manager installation option~~
-- [x] ~~XCode 9 compatibility~~
-- [x] ~~Swift 4 compatibility and tests~~
-- [ ] [Apple's New Socket for iOS 13](https://developer.apple.com/documentation/network/nwprotocolwebsocket) Implementation from stratch
-
-# Changelog
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.8...HEAD)
-
-**Implemented enhancements:**
-
-- SUBSCRIBE and UNSUBSCRIBE Delegate is missing [\#16](https://github.com/WrathChaos/StompClientLib/issues/16)
-
-**Closed issues:**
-
-- Error Domain=SRWebSocketErrorDomain Code=2132 "received bad response code from server 403" [\#86](https://github.com/WrathChaos/StompClientLib/issues/86)
-- the delegate should be weak [\#83](https://github.com/WrathChaos/StompClientLib/issues/83)
-
-## [1.3.8](https://github.com/WrathChaos/StompClientLib/tree/1.3.8) (2020-03-08)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.7...1.3.8)
-
-**Implemented enhancements:**
-
-- Socket stopped working once I moved from ws//: to wss//: [\#67](https://github.com/WrathChaos/StompClientLib/issues/67)
-- Self sign certificate, 400 error [\#66](https://github.com/WrathChaos/StompClientLib/issues/66)
-- How to increase output buffer [\#37](https://github.com/WrathChaos/StompClientLib/issues/37)
-
-**Fixed bugs:**
-
-- unable to install [\#78](https://github.com/WrathChaos/StompClientLib/issues/78)
-- Number of received messages is limited [\#76](https://github.com/WrathChaos/StompClientLib/issues/76)
-
-**Closed issues:**
-
-- Can't find header in initial call [\#81](https://github.com/WrathChaos/StompClientLib/issues/81)
-- Can't connect to websocket : received bad response code from server 422 [\#80](https://github.com/WrathChaos/StompClientLib/issues/80)
-- I just closed the issue because of the stale & reproducible problem [\#79](https://github.com/WrathChaos/StompClientLib/issues/79)
-- unable to install [\#77](https://github.com/WrathChaos/StompClientLib/issues/77)
-- stompClientDidConnect not called with Spring boot [\#73](https://github.com/WrathChaos/StompClientLib/issues/73)
-- Issue with Cookies in header [\#71](https://github.com/WrathChaos/StompClientLib/issues/71)
-- Can't see any Websocket traffic in Charles Proxy [\#70](https://github.com/WrathChaos/StompClientLib/issues/70)
-- End of stream error [\#69](https://github.com/WrathChaos/StompClientLib/issues/69)
-- Cannot connect with Stomp Websocket when custom header [\#64](https://github.com/WrathChaos/StompClientLib/issues/64)
-- Can't connect Socket with Spring Boot 2.x.x [\#63](https://github.com/WrathChaos/StompClientLib/issues/63)
-- Multiple clients [\#48](https://github.com/WrathChaos/StompClientLib/issues/48)
-- IPV6 [\#38](https://github.com/WrathChaos/StompClientLib/issues/38)
-
-**Merged pull requests:**
-
-- fix delegate [\#84](https://github.com/WrathChaos/StompClientLib/pull/84) ([soledue](https://github.com/soledue))
-- Fix typo [\#74](https://github.com/WrathChaos/StompClientLib/pull/74) ([wanbok](https://github.com/wanbok))
-
-## [1.3.7](https://github.com/WrathChaos/StompClientLib/tree/1.3.7) (2019-08-26)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.6...1.3.7)
-
-**Closed issues:**
-
-- The problem when receiving the messages from the Spring boot. [\#68](https://github.com/WrathChaos/StompClientLib/issues/68)
-
-## [1.3.6](https://github.com/WrathChaos/StompClientLib/tree/1.3.6) (2019-08-06)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.5...1.3.6)
-
-**Implemented enhancements:**
-
-- Refactor code and fix support spring 2.1.x [\#65](https://github.com/WrathChaos/StompClientLib/pull/65) ([baonguyena1](https://github.com/baonguyena1))
-
-**Fixed bugs:**
-
-- v1.3.4 replaced my custom header in open socket? [\#58](https://github.com/WrathChaos/StompClientLib/issues/58)
-
-**Closed issues:**
+## Login / passcode
-- Json error: The data couldn’t be read because it isn’t in the correct format. [\#60](https://github.com/WrathChaos/StompClientLib/issues/60)
+The library connects anonymously by default. To send credentials, supply them as
+connection headers when opening the socket:
-**Merged pull requests:**
-
-- Fixed ACK id header and added new ACK type [\#62](https://github.com/WrathChaos/StompClientLib/pull/62) ([rodmytro](https://github.com/rodmytro))
-
-## [1.3.5](https://github.com/WrathChaos/StompClientLib/tree/1.3.5) (2019-07-25)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.4...1.3.5)
-
-**Fixed bugs:**
-
-- StompClientDidConnect not called on a fresh project [\#51](https://github.com/WrathChaos/StompClientLib/issues/51)
-
-**Closed issues:**
-
-- SendJSONForDict error [\#57](https://github.com/WrathChaos/StompClientLib/issues/57)
-- Can't connect to websocket with authorization header [\#39](https://github.com/WrathChaos/StompClientLib/issues/39)
-
-**Merged pull requests:**
-
-- \#58 fix open socket with custom header issue [\#59](https://github.com/WrathChaos/StompClientLib/pull/59) ([marain87](https://github.com/marain87))
-
-## [1.3.4](https://github.com/WrathChaos/StompClientLib/tree/1.3.4) (2019-07-19)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.3...1.3.4)
-
-## [1.3.3](https://github.com/WrathChaos/StompClientLib/tree/1.3.3) (2019-07-18)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.2...1.3.3)
-
-**Closed issues:**
-
-- Missing merge [\#55](https://github.com/WrathChaos/StompClientLib/issues/55)
-- Data garbled problem,help [\#41](https://github.com/WrathChaos/StompClientLib/issues/41)
-
-**Merged pull requests:**
-
-- String body parameter and ':'-in-header-value fix [\#56](https://github.com/WrathChaos/StompClientLib/pull/56) ([Erhannis](https://github.com/Erhannis))
-
-## [1.3.2](https://github.com/WrathChaos/StompClientLib/tree/1.3.2) (2019-07-10)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.1...1.3.2)
-
-**Implemented enhancements:**
-
-- stompClientWillDisconnect missing [\#44](https://github.com/WrathChaos/StompClientLib/issues/44)
-
-## [1.3.1](https://github.com/WrathChaos/StompClientLib/tree/1.3.1) (2019-06-14)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.3.0...1.3.1)
-
-**Closed issues:**
-
-- Can I subscribe to multiple topics with one stomp client? [\#47](https://github.com/WrathChaos/StompClientLib/issues/47)
-- After subscribing to a topic, how to handle messages from server side? [\#46](https://github.com/WrathChaos/StompClientLib/issues/46)
-- socket?.readyState is .OPEN and it never goes to my own "stompClientDidDisconnect" method [\#45](https://github.com/WrathChaos/StompClientLib/issues/45)
-- Didconnect function cannot be callback after successful connection [\#43](https://github.com/WrathChaos/StompClientLib/issues/43)
-- Login, passcode [\#42](https://github.com/WrathChaos/StompClientLib/issues/42)
-- App goes in background lock the kepad the socket disconnected [\#40](https://github.com/WrathChaos/StompClientLib/issues/40)
-- stompClientDidConnect not called with Spring boot [\#35](https://github.com/WrathChaos/StompClientLib/issues/35)
-- Delegate StompClientDidConnect not called after connect-\>disconnect-\>connect [\#15](https://github.com/WrathChaos/StompClientLib/issues/15)
-
-## [1.3.0](https://github.com/WrathChaos/StompClientLib/tree/1.3.0) (2019-04-30)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.7...1.3.0)
-
-**Implemented enhancements:**
-
-- invalidate reconnect [\#36](https://github.com/WrathChaos/StompClientLib/issues/36)
-
-**Closed issues:**
-
-- Should add Carthage [\#33](https://github.com/WrathChaos/StompClientLib/issues/33)
-- Invalid Sec-WebSocket-Accept response [\#32](https://github.com/WrathChaos/StompClientLib/issues/32)
-- Socket is disconnected with 1007 code as soon as it connected [\#31](https://github.com/WrathChaos/StompClientLib/issues/31)
-- enable Assert \(self.readyState != SR_CONNECTING\) [\#24](https://github.com/WrathChaos/StompClientLib/issues/24)
-
-## [1.2.7](https://github.com/WrathChaos/StompClientLib/tree/1.2.7) (2018-10-23)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.6...1.2.7)
-
-## [1.2.6](https://github.com/WrathChaos/StompClientLib/tree/1.2.6) (2018-10-23)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.5...1.2.6)
-
-**Fixed bugs:**
-
-- Error when connected to socket [\#23](https://github.com/WrathChaos/StompClientLib/issues/23)
-
-**Closed issues:**
-
-- Auto disconnects [\#11](https://github.com/WrathChaos/StompClientLib/issues/11)
-
-## [1.2.5](https://github.com/WrathChaos/StompClientLib/tree/1.2.5) (2018-10-22)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.4...1.2.5)
-
-**Closed issues:**
-
-- Value for "message-id" is always "1" [\#22](https://github.com/WrathChaos/StompClientLib/issues/22)
-- Multiple subscription to topics [\#20](https://github.com/WrathChaos/StompClientLib/issues/20)
-- I think there is a memory leak for the delegate [\#19](https://github.com/WrathChaos/StompClientLib/issues/19)
-- Getting error when framwork is installed in Objective c project [\#9](https://github.com/WrathChaos/StompClientLib/issues/9)
-
-## [1.2.4](https://github.com/WrathChaos/StompClientLib/tree/1.2.4) (2018-10-17)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.3...1.2.4)
-
-**Closed issues:**
-
-- didCloseWithCode 1000, reason: nil [\#21](https://github.com/WrathChaos/StompClientLib/issues/21)
-
-## [1.2.3](https://github.com/WrathChaos/StompClientLib/tree/1.2.3) (2018-10-17)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.2...1.2.3)
-
-**Implemented enhancements:**
-
-- How to receive heartbeat? [\#18](https://github.com/WrathChaos/StompClientLib/issues/18)
-
-**Closed issues:**
-
-- Socket is not getting connected [\#30](https://github.com/WrathChaos/StompClientLib/issues/30)
-- didCloseWithCode 1002 [\#29](https://github.com/WrathChaos/StompClientLib/issues/29)
-- No response [\#27](https://github.com/WrathChaos/StompClientLib/issues/27)
-- didCloseWithCode 1001, reason: "Stream end encountered" [\#26](https://github.com/WrathChaos/StompClientLib/issues/26)
-- Stream end encountered [\#17](https://github.com/WrathChaos/StompClientLib/issues/17)
-- unsubscribe socketclient [\#14](https://github.com/WrathChaos/StompClientLib/issues/14)
-- It not able to connect web socket. [\#13](https://github.com/WrathChaos/StompClientLib/issues/13)
-- One of the delegate method is not being called. [\#12](https://github.com/WrathChaos/StompClientLib/issues/12)
-- StompClient Disconnection. [\#10](https://github.com/WrathChaos/StompClientLib/issues/10)
-- Unable to find a specification for 'StompClientLib' [\#8](https://github.com/WrathChaos/StompClientLib/issues/8)
-
-## [1.2.2](https://github.com/WrathChaos/StompClientLib/tree/1.2.2) (2017-11-03)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.1...1.2.2)
-
-## [1.2.1](https://github.com/WrathChaos/StompClientLib/tree/1.2.1) (2017-10-31)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.2.0...1.2.1)
-
-## [1.2.0](https://github.com/WrathChaos/StompClientLib/tree/1.2.0) (2017-10-29)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.1.7...1.2.0)
-
-**Closed issues:**
-
-- Let client decide what to do with stomp frame body [\#4](https://github.com/WrathChaos/StompClientLib/issues/4)
-- Send message support [\#3](https://github.com/WrathChaos/StompClientLib/issues/3)
-- Error when calling delegate [\#1](https://github.com/WrathChaos/StompClientLib/issues/1)
-
-## [1.1.7](https://github.com/WrathChaos/StompClientLib/tree/1.1.7) (2017-10-02)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/1.1.6...1.1.7)
-
-## [1.1.6](https://github.com/WrathChaos/StompClientLib/tree/1.1.6) (2017-08-08)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.5...1.1.6)
-
-## [0.1.5](https://github.com/WrathChaos/StompClientLib/tree/0.1.5) (2017-07-10)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.4...0.1.5)
-
-## [0.1.4](https://github.com/WrathChaos/StompClientLib/tree/0.1.4) (2017-07-10)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.3...0.1.4)
-
-## [0.1.3](https://github.com/WrathChaos/StompClientLib/tree/0.1.3) (2017-07-10)
-
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.2...0.1.3)
+```swift
+let headers = ["login": "admin", "passcode": "password"]
+socketClient.openSocketWithURLRequest(request: request, delegate: self, connectionHeaders: headers)
+```
-## [0.1.2](https://github.com/WrathChaos/StompClientLib/tree/0.1.2) (2017-07-08)
+## Example app
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.1...0.1.2)
+`StompClientLibExample/` contains a UIKit demo. By default it points at a local
+Spring Boot broker (`ws://localhost:8080`); change the URL in `ViewController.swift`
+to target your own broker.
-## [0.1.1](https://github.com/WrathChaos/StompClientLib/tree/0.1.1) (2017-07-08)
+```bash
+cd StompClientLibExample
+pod install
+open StompClientLibExample.xcworkspace
+```
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/0.1.0...0.1.1)
+## Contributing
-## [0.1.0](https://github.com/WrathChaos/StompClientLib/tree/0.1.0) (2017-07-08)
+Contributions are welcome — please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
+Keep in mind that the public API and Objective-C compatibility are a contract for
+existing consumers.
-[Full Changelog](https://github.com/WrathChaos/StompClientLib/compare/cbd49d3cad9a33ae96ff1708f7e0d3e975455325...0.1.0)
+## Changelog
-\* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_
+See [CHANGELOG.md](CHANGELOG.md).
## Author
-FreakyCoder, kurayogun@gmail.com
+Kuray Ogun (FreakyCoder) — kurayogun@gmail.com
## License
-StompClientLib is available under the MIT license. See the LICENSE file for more info.
+StompClientLib is available under the MIT license. See [LICENSE](LICENSE) for
+more information.
diff --git a/StompClientLib.podspec b/StompClientLib.podspec
index a6433bc..be03bd1 100755
--- a/StompClientLib.podspec
+++ b/StompClientLib.podspec
@@ -9,8 +9,8 @@
Pod::Spec.new do |s|
s.name = 'StompClientLib'
s.version = '1.4.1'
- s.summary = 'Simple STOMP Client library. Swift 3, 4, 4.2, 5 compatible. Objective-C Run-time compatibility'
- s.swift_version = '4.0', '4.2', '5.0'
+ s.summary = 'Simple STOMP client library for iOS. Swift 5 with Objective-C run-time compatibility.'
+ s.swift_versions = ['5.0']
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
@@ -19,25 +19,23 @@ Pod::Spec.new do |s|
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
-Simple STOMP Client library, Swift 3, 4, 4.2, 5 compatible. STOMP Protocol let the program subscribe or unsubscribe the topic. It connects the websocket and use the STOMP protocol to subscribe the topic and recieve the message, receipt or even a ping.
+Simple STOMP client library for iOS, written in Swift with Objective-C run-time compatibility. The STOMP protocol lets your app subscribe/unsubscribe to topics over a WebSocket connection and send, receive, ack, and receipt messages (STOMP 1.1 / 1.2).
DESC
- s.homepage = 'https://github.com/wrathchaos/StompClientLib'
+ s.homepage = 'https://github.com/kuraydev/StompClientLib'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'FreakyCoder' => 'kurayogun@gmail.com' }
- s.source = { :git => 'https://github.com/wrathchaos/StompClientLib.git', :tag => s.version.to_s }
- s.social_media_url = 'https://twitter.com/freakycodercom'
+ s.source = { :git => 'https://github.com/kuraydev/StompClientLib.git', :tag => s.version.to_s }
- s.ios.deployment_target = '9.0'
+ s.ios.deployment_target = '12.0'
s.source_files = 'StompClientLib/Classes/**/*'
-
- # s.resource_bundles = {
- # 'StompClientLib' => ['StompClientLib/Assets/*.png']
- # }
-
- # s.public_header_files = 'Pod/Classes/**/*.h'
- # s.frameworks = 'UIKit', 'MapKit'
- s.dependency 'SocketRocket'
+
+ # Ship the App Store privacy manifest alongside the pod.
+ s.resource_bundles = {
+ 'StompClientLib' => ['StompClientLib/PrivacyInfo.xcprivacy']
+ }
+
+ s.dependency 'SocketRocket', '~> 0.5'
end
diff --git a/StompClientLib/Classes/StompClientLib.swift b/StompClientLib/Classes/StompClientLib.swift
index 50e7f44..9d6d7c9 100755
--- a/StompClientLib/Classes/StompClientLib.swift
+++ b/StompClientLib/Classes/StompClientLib.swift
@@ -1,69 +1,23 @@
//
-// StompClient.swift
-// Pods
+// StompClientLib.swift
+// StompClientLib
//
// Created by Kuray (FreakyCoder)
// Created at July 07, 2017
-// Updated at March 07, 2021
//
-import UIKit
+import Foundation
import SocketRocket
-struct StompCommands {
- // Basic Commands
- static let commandConnect = "CONNECT"
- static let commandSend = "SEND"
- static let commandSubscribe = "SUBSCRIBE"
- static let commandUnsubscribe = "UNSUBSCRIBE"
- static let commandBegin = "BEGIN"
- static let commandCommit = "COMMIT"
- static let commandAbort = "ABORT"
- static let commandAck = "ACK"
- static let commandDisconnect = "DISCONNECT"
- static let commandPing = "\n"
-
- static let controlChar = String(format: "%C", arguments: [0x00])
-
- // Ack Mode
- static let ackClientIndividual = "client-individual"
- static let ackClient = "client"
- static let ackAuto = "auto"
- // Header Commands
- static let commandHeaderReceipt = "receipt"
- static let commandHeaderDestination = "destination"
- static let commandHeaderDestinationId = "id"
- static let commandHeaderContentLength = "content-length"
- static let commandHeaderContentType = "content-type"
- static let commandHeaderAck = "ack"
- static let commandHeaderTransaction = "transaction"
- static let commandHeaderMessageId = "id"
- static let commandHeaderSubscription = "subscription"
- static let commandHeaderDisconnected = "disconnected"
- static let commandHeaderHeartBeat = "heart-beat"
- static let commandHeaderAcceptVersion = "accept-version"
- // Header Response Keys
- static let responseHeaderSession = "session"
- static let responseHeaderReceiptId = "receipt-id"
- static let responseHeaderErrorMessage = "message"
- // Frame Response Keys
- static let responseFrameConnected = "CONNECTED"
- static let responseFrameMessage = "MESSAGE"
- static let responseFrameReceipt = "RECEIPT"
- static let responseFrameError = "ERROR"
-}
-
-public enum StompAckMode {
- case AutoMode
- case ClientMode
- case ClientIndividualMode
-}
-
// Fundamental Protocols
@objc
-public protocol StompClientLibDelegate: class {
- func stompClient(client: StompClientLib!, didReceiveMessageWithJSONBody jsonBody: AnyObject?, akaStringBody stringBody: String?, withHeader header:[String:String]?, withDestination destination: String)
-
+public protocol StompClientLibDelegate: AnyObject {
+ func stompClient(client: StompClientLib!,
+ didReceiveMessageWithJSONBody jsonBody: AnyObject?,
+ akaStringBody stringBody: String?,
+ withHeader header: [String: String]?,
+ withDestination destination: String)
+
func stompClientDidDisconnect(client: StompClientLib!)
func stompClientDidConnect(client: StompClientLib!)
func serverDidSendReceipt(client: StompClientLib!, withReceiptId receiptId: String)
@@ -73,6 +27,13 @@ public protocol StompClientLibDelegate: class {
@objcMembers
public class StompClientLib: NSObject, SRWebSocketDelegate {
+ /// When `true`, the library prints diagnostic information to the console.
+ /// Defaults to `false` so the library no longer pollutes consumer logs.
+ public static var isLoggingEnabled = false
+
+ /// Upper bound for the exponential reconnect interval, in seconds.
+ private static let maxReconnectInterval: TimeInterval = 60
+
var socket: SRWebSocket?
var sessionId: String?
weak var delegate: StompClientLibDelegate?
@@ -80,21 +41,27 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
public var connection: Bool = false
public var certificateCheckEnabled = true
private var urlRequest: NSURLRequest?
-
- private var reconnectTimer : Timer?
-
+
+ private var reconnectTimer: Timer?
+ private var reconnectAttempts = 0
+
+ private static func log(_ message: @autoclosure () -> String) {
+ if isLoggingEnabled {
+ print("StompClientLib: \(message())")
+ }
+ }
+
public func sendJSONForDict(dict: AnyObject, toDestination destination: String) {
do {
let theJSONData = try JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions())
let theJSONText = String(data: theJSONData, encoding: String.Encoding.utf8)
- //print(theJSONText!)
- let header = [StompCommands.commandHeaderContentType:"application/json;charset=UTF-8"]
+ let header = [StompCommands.commandHeaderContentType: "application/json;charset=UTF-8"]
sendMessage(message: theJSONText!, toDestination: destination, withHeaders: header, withReceipt: nil)
} catch {
- print("error serializing JSON: \(error)")
+ StompClientLib.log("error serializing JSON: \(error)")
}
}
-
+
public func openSocketWithURLRequest(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String]? = nil) {
self.connectionHeaders = connectionHeaders
self.delegate = delegate
@@ -103,7 +70,7 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
openSocket()
self.connection = true
}
-
+
private func openSocket() {
if socket == nil || socket?.readyState == .CLOSED {
if certificateCheckEnabled == true {
@@ -111,13 +78,13 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
} else {
self.socket = SRWebSocket(urlRequest: urlRequest! as URLRequest, protocols: [], allowsUntrustedSSLCertificates: true)
}
-
+
socket!.delegate = self
socket!.open()
}
}
-
- private func closeSocket(){
+
+ private func closeSocket() {
if let delegate = delegate {
DispatchQueue.main.async(execute: {
delegate.stompClientDidDisconnect(client: self)
@@ -130,7 +97,7 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
})
}
}
-
+
/*
Main Connection Method to open socket
*/
@@ -138,7 +105,7 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
if socket?.readyState == .OPEN {
// Support for Spring Boot 2.1.x
if connectionHeaders == nil {
- connectionHeaders = [StompCommands.commandHeaderAcceptVersion:"1.1,1.2"]
+ connectionHeaders = [StompCommands.commandHeaderAcceptVersion: "1.1,1.2"]
} else {
connectionHeaders?[StompCommands.commandHeaderAcceptVersion] = "1.1,1.2"
}
@@ -148,111 +115,63 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
self.openSocket()
}
}
-
+
public func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {
-
- func processString(string: String) {
- var contents = string.components(separatedBy: "\n")
- if contents.first == "" {
- contents.removeFirst()
- }
-
- if let command = contents.first {
- var headers = [String: String]()
- var body = ""
- var hasHeaders = false
-
- contents.removeFirst()
- for line in contents {
- if hasHeaders == true {
- body += line
- } else {
- if line == "" {
- hasHeaders = true
- } else {
- let parts = line.components(separatedBy: ":")
- if let key = parts.first {
- headers[key] = parts.dropFirst().joined(separator: ":")
- }
- }
- }
- }
-
- // Remove the garbage from body
- if body.hasSuffix("\0") {
- body = body.replacingOccurrences(of: "\0", with: "")
- }
-
- receiveFrame(command: command, headers: headers, body: body)
+ func handle(string: String) {
+ if let frame = StompFrame.decode(string) {
+ receiveFrame(command: frame.command, headers: frame.headers, body: frame.body)
}
}
-
+
if let strData = message as? NSData {
if let msg = String(data: strData as Data, encoding: String.Encoding.utf8) {
- processString(string: msg)
+ handle(string: msg)
}
} else if let str = message as? String {
- processString(string: str)
+ handle(string: str)
}
}
-
+
public func webSocketDidOpen(_ webSocket: SRWebSocket!) {
- print("WebSocket is connected")
+ StompClientLib.log("WebSocket is connected")
connect()
}
-
+
public func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {
- print("didFailWithError: \(String(describing: error))")
-
+ StompClientLib.log("didFailWithError: \(String(describing: error))")
+ // The socket failed, so we are no longer connected. Without this the
+ // auto-reconnect timer (which only fires while !isConnected) never runs.
+ connection = false
+
if let delegate = delegate {
+ let nsError = error as NSError?
DispatchQueue.main.async(execute: {
- delegate.serverDidSendError(client: self, withErrorMessage: error.localizedDescription, detailedErrorMessage: error as? String)
+ delegate.serverDidSendError(client: self,
+ withErrorMessage: error.localizedDescription,
+ detailedErrorMessage: nsError?.debugDescription)
})
}
}
-
+
public func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {
- print("didCloseWithCode \(code), reason: \(String(describing: reason))")
+ StompClientLib.log("didCloseWithCode \(code), reason: \(String(describing: reason))")
+ // A server-initiated close means we are no longer connected (issue #123).
+ connection = false
if let delegate = delegate {
DispatchQueue.main.async(execute: {
delegate.stompClientDidDisconnect(client: self)
})
}
}
-
+
public func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
- print("didReceivePong")
+ StompClientLib.log("didReceivePong")
}
-
+
private func sendFrame(command: String?, header: [String: String]?, body: AnyObject?) {
if socket?.readyState == .OPEN {
- var frameString = ""
- if command != nil {
- frameString = command! + "\n"
- }
-
- if let header = header {
- for (key, value) in header {
- frameString += key
- frameString += ":"
- frameString += value
- frameString += "\n"
- }
- }
-
- if let body = body as? String {
- frameString += "\n"
- frameString += body
- } else if let _ = body as? NSData {
-
- }
-
- if body == nil {
- frameString += "\n"
- }
-
- frameString += StompCommands.controlChar
-
+ let frameString = StompFrame.encode(command: command, headers: header, body: body as? String)
+
if socket?.readyState == .OPEN {
socket?.send(frameString)
} else {
@@ -264,17 +183,11 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
}
}
}
-
+
private func destinationFromHeader(header: [String: String]) -> String {
- for (key, _) in header {
- if key == "destination" {
- let destination = header[key]!
- return destination
- }
- }
- return ""
+ return header[StompCommands.commandHeaderDestination] ?? ""
}
-
+
private func dictForJSONString(jsonStr: String?) -> AnyObject? {
if let jsonStr = jsonStr {
do {
@@ -283,19 +196,19 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
return json as AnyObject
}
} catch {
- print("error serializing JSON: \(error)")
+ StompClientLib.log("error serializing JSON: \(error)")
}
}
return nil
}
-
+
private func receiveFrame(command: String, headers: [String: String], body: String?) {
if command == StompCommands.responseFrameConnected {
// Connected
if let sessId = headers[StompCommands.responseHeaderSession] {
sessionId = sessId
}
-
+
if let delegate = delegate {
DispatchQueue.main.async(execute: {
delegate.stompClientDidConnect(client: self)
@@ -305,7 +218,11 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
// Response
if let delegate = delegate {
DispatchQueue.main.async(execute: {
- delegate.stompClient(client: self, didReceiveMessageWithJSONBody: self.dictForJSONString(jsonStr: body), akaStringBody: body, withHeader: headers, withDestination: self.destinationFromHeader(header: headers))
+ delegate.stompClient(client: self,
+ didReceiveMessageWithJSONBody: self.dictForJSONString(jsonStr: body),
+ akaStringBody: body,
+ withHeader: headers,
+ withDestination: self.destinationFromHeader(header: headers))
})
}
} else if command == StompCommands.responseFrameReceipt { //
@@ -317,7 +234,7 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
})
}
}
- } else if command.count == 0 {
+ } else if command.isEmpty {
// Pong from the server
socket?.send(StompCommands.commandPing)
if let delegate = delegate {
@@ -336,38 +253,38 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
}
}
}
-
+
public func sendMessage(message: String, toDestination destination: String, withHeaders headers: [String: String]?, withReceipt receipt: String?) {
var headersToSend = [String: String]()
if let headers = headers {
headersToSend = headers
}
-
+
// Setting up the receipt.
if let receipt = receipt {
headersToSend[StompCommands.commandHeaderReceipt] = receipt
}
-
+
headersToSend[StompCommands.commandHeaderDestination] = destination
-
+
// Setting up the content length.
let contentLength = message.utf8.count
headersToSend[StompCommands.commandHeaderContentLength] = "\(contentLength)"
-
+
// Setting up content type as plain text.
if headersToSend[StompCommands.commandHeaderContentType] == nil {
headersToSend[StompCommands.commandHeaderContentType] = "text/plain"
}
sendFrame(command: StompCommands.commandSend, header: headersToSend, body: message as AnyObject)
}
-
+
/*
Main Connection Check Method
*/
- public func isConnected() -> Bool{
+ public func isConnected() -> Bool {
return connection
}
-
+
/*
Main Subscribe Method with topic name
*/
@@ -375,33 +292,22 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
connection = true
subscribeToDestination(destination: destination, ackMode: .AutoMode)
}
-
+
public func subscribeToDestination(destination: String, ackMode: StompAckMode) {
- var ack = ""
- switch ackMode {
- case StompAckMode.ClientMode:
- ack = StompCommands.ackClient
- break
- case StompAckMode.ClientIndividualMode:
- ack = StompCommands.ackClientIndividual
- break
- default:
- ack = StompCommands.ackAuto
- break
- }
+ let ack = StompFrame.ackHeaderValue(for: ackMode)
var headers = [StompCommands.commandHeaderDestination: destination, StompCommands.commandHeaderAck: ack, StompCommands.commandHeaderDestinationId: ""]
if destination != "" {
headers = [StompCommands.commandHeaderDestination: destination, StompCommands.commandHeaderAck: ack, StompCommands.commandHeaderDestinationId: destination]
}
self.sendFrame(command: StompCommands.commandSubscribe, header: headers, body: nil)
}
-
+
public func subscribeWithHeader(destination: String, withHeader header: [String: String]) {
var headerToSend = header
headerToSend[StompCommands.commandHeaderDestination] = destination
sendFrame(command: StompCommands.commandSubscribe, header: headerToSend, body: nil)
}
-
+
/*
Main Unsubscribe Method with topic name
*/
@@ -411,83 +317,96 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
headerToSend[StompCommands.commandHeaderDestinationId] = destination
sendFrame(command: StompCommands.commandUnsubscribe, header: headerToSend, body: nil)
}
-
+
public func begin(transactionId: String) {
var headerToSend = [String: String]()
headerToSend[StompCommands.commandHeaderTransaction] = transactionId
sendFrame(command: StompCommands.commandBegin, header: headerToSend, body: nil)
}
-
+
public func commit(transactionId: String) {
var headerToSend = [String: String]()
headerToSend[StompCommands.commandHeaderTransaction] = transactionId
sendFrame(command: StompCommands.commandCommit, header: headerToSend, body: nil)
}
-
+
public func abort(transactionId: String) {
var headerToSend = [String: String]()
headerToSend[StompCommands.commandHeaderTransaction] = transactionId
sendFrame(command: StompCommands.commandAbort, header: headerToSend, body: nil)
}
-
+
public func ack(messageId: String) {
var headerToSend = [String: String]()
headerToSend[StompCommands.commandHeaderMessageId] = messageId
sendFrame(command: StompCommands.commandAck, header: headerToSend, body: nil)
}
-
+
public func ack(messageId: String, withSubscription subscription: String) {
var headerToSend = [String: String]()
headerToSend[StompCommands.commandHeaderMessageId] = messageId
headerToSend[StompCommands.commandHeaderSubscription] = subscription
sendFrame(command: StompCommands.commandAck, header: headerToSend, body: nil)
}
-
+
/*
Main Disconnection Method to close the socket
*/
public func disconnect() {
connection = false
var headerToSend = [String: String]()
- headerToSend[StompCommands.commandDisconnect] = String(Int(NSDate().timeIntervalSince1970))
+ headerToSend[StompCommands.commandDisconnect] = String(Int(Date().timeIntervalSince1970))
sendFrame(command: StompCommands.commandDisconnect, header: headerToSend, body: nil)
// Close the socket to allow recreation
self.closeSocket()
}
-
- // Reconnect after one sec or arg, if reconnect is available
- // TODO: MAKE A VARIABLE TO CHECK RECONNECT OPTION IS AVAILABLE OR NOT
- public func reconnect(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String] = [String: String](), time: Double = 1.0, exponentialBackoff: Bool = true){
- if #available(iOS 10.0, *) {
- reconnectTimer = Timer.scheduledTimer(withTimeInterval: time, repeats: true, block: { _ in
- self.reconnectLogic(request: request, delegate: delegate
- , connectionHeaders: connectionHeaders)
- })
- } else {
- // Fallback on earlier versions
- // Swift >=3 selector syntax
- // Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.reconnectFallback), userInfo: nil, repeats: true)
- print("Reconnect Feature has no support for below iOS 10, it is going to be available soon!")
+
+ /// Auto reconnect after the given interval while the connection is down.
+ ///
+ /// When `exponentialBackoff` is `true` (the default) the wait between
+ /// attempts grows geometrically from `time` (1s, 2s, 4s, …) up to a 60s
+ /// cap and resets once a connection is re-established. Pass `false` to keep
+ /// the legacy fixed-interval behaviour. Call `stopReconnect()` to cancel.
+ public func reconnect(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String] = [String: String](), time: Double = 1.0, exponentialBackoff: Bool = true) {
+ reconnectAttempts = 0
+ scheduleReconnect(request: request, delegate: delegate, connectionHeaders: connectionHeaders, time: time, exponentialBackoff: exponentialBackoff)
+ }
+
+ private func scheduleReconnect(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String], time: Double, exponentialBackoff: Bool) {
+ reconnectTimer?.invalidate()
+ let interval: TimeInterval = exponentialBackoff
+ ? min(time * pow(2.0, Double(reconnectAttempts)), StompClientLib.maxReconnectInterval)
+ : time
+ reconnectTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
+ guard let self = self else { return }
+ if self.isConnected() {
+ self.reconnectAttempts = 0
+ } else {
+ self.reconnectAttempts += 1
+ self.reconnectLogic(request: request, delegate: delegate, connectionHeaders: connectionHeaders)
+ }
+ self.scheduleReconnect(request: request, delegate: delegate, connectionHeaders: connectionHeaders, time: time, exponentialBackoff: exponentialBackoff)
}
}
- // @objc func reconnectFallback() {
- // reconnectLogic(request: request, delegate: delegate, connectionHeaders: connectionHeaders)
- // }
-
- private func reconnectLogic(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String] = [String: String]()){
+
+ private func reconnectLogic(request: NSURLRequest, delegate: StompClientLibDelegate, connectionHeaders: [String: String] = [String: String]()) {
// Check if connection is alive or dead
- if (!self.isConnected()){
- self.checkConnectionHeader(connectionHeaders: connectionHeaders) ? self.openSocketWithURLRequest(request: request, delegate: delegate, connectionHeaders: connectionHeaders) : self.openSocketWithURLRequest(request: request, delegate: delegate)
+ guard !self.isConnected() else { return }
+ if checkConnectionHeader(connectionHeaders: connectionHeaders) {
+ openSocketWithURLRequest(request: request, delegate: delegate, connectionHeaders: connectionHeaders)
+ } else {
+ openSocketWithURLRequest(request: request, delegate: delegate)
}
}
-
+
public func stopReconnect() {
reconnectTimer?.invalidate()
reconnectTimer = nil
+ reconnectAttempts = 0
}
-
- private func checkConnectionHeader(connectionHeaders: [String: String] = [String: String]()) -> Bool{
- if (connectionHeaders.isEmpty){
+
+ private func checkConnectionHeader(connectionHeaders: [String: String] = [String: String]()) -> Bool {
+ if connectionHeaders.isEmpty {
// No connection header
return false
} else {
@@ -495,13 +414,12 @@ public class StompClientLib: NSObject, SRWebSocketDelegate {
return true
}
}
-
+
// Autodisconnect with a given time
- public func autoDisconnect(time: Double){
+ public func autoDisconnect(time: Double) {
DispatchQueue.main.asyncAfter(deadline: .now() + time) {
// Disconnect the socket
self.disconnect()
}
}
}
-
diff --git a/StompClientLib/Classes/StompFrame.swift b/StompClientLib/Classes/StompFrame.swift
new file mode 100644
index 0000000..943fb56
--- /dev/null
+++ b/StompClientLib/Classes/StompFrame.swift
@@ -0,0 +1,147 @@
+//
+// StompFrame.swift
+// StompClientLib
+//
+// Pure, transport-agnostic STOMP frame encoding/decoding helpers.
+// Depends only on Foundation, so it stays unit-testable without a live
+// WebSocket and without UIKit.
+//
+
+import Foundation
+
+struct StompCommands {
+ // Basic Commands
+ static let commandConnect = "CONNECT"
+ static let commandSend = "SEND"
+ static let commandSubscribe = "SUBSCRIBE"
+ static let commandUnsubscribe = "UNSUBSCRIBE"
+ static let commandBegin = "BEGIN"
+ static let commandCommit = "COMMIT"
+ static let commandAbort = "ABORT"
+ static let commandAck = "ACK"
+ static let commandDisconnect = "DISCONNECT"
+ static let commandPing = "\n"
+
+ static let controlChar = String(format: "%C", arguments: [0x00])
+
+ // Ack Mode
+ static let ackClientIndividual = "client-individual"
+ static let ackClient = "client"
+ static let ackAuto = "auto"
+ // Header Commands
+ static let commandHeaderReceipt = "receipt"
+ static let commandHeaderDestination = "destination"
+ static let commandHeaderDestinationId = "id"
+ static let commandHeaderContentLength = "content-length"
+ static let commandHeaderContentType = "content-type"
+ static let commandHeaderAck = "ack"
+ static let commandHeaderTransaction = "transaction"
+ static let commandHeaderMessageId = "id"
+ static let commandHeaderSubscription = "subscription"
+ static let commandHeaderDisconnected = "disconnected"
+ static let commandHeaderHeartBeat = "heart-beat"
+ static let commandHeaderAcceptVersion = "accept-version"
+ // Header Response Keys
+ static let responseHeaderSession = "session"
+ static let responseHeaderReceiptId = "receipt-id"
+ static let responseHeaderErrorMessage = "message"
+ // Frame Response Keys
+ static let responseFrameConnected = "CONNECTED"
+ static let responseFrameMessage = "MESSAGE"
+ static let responseFrameReceipt = "RECEIPT"
+ static let responseFrameError = "ERROR"
+}
+
+public enum StompAckMode {
+ case AutoMode
+ case ClientMode
+ case ClientIndividualMode
+}
+
+/// Pure helpers for building and parsing STOMP frames. Exposed publicly so the
+/// wire format can be unit-tested (and reused) independently of the WebSocket
+/// transport. The behaviour mirrors what `StompClientLib` sends/receives.
+public enum StompFrame {
+
+ /// Builds a STOMP frame string for the given command, headers and optional
+ /// text body, terminated by the STOMP NULL control character.
+ public static func encode(command: String?, headers: [String: String]?, body: String?) -> String {
+ var frame = ""
+
+ if let command = command {
+ frame += command + "\n"
+ }
+
+ if let headers = headers {
+ for (key, value) in headers {
+ frame += key
+ frame += ":"
+ frame += value
+ frame += "\n"
+ }
+ }
+
+ if let body = body {
+ frame += "\n"
+ frame += body
+ } else {
+ frame += "\n"
+ }
+
+ frame += StompCommands.controlChar
+ return frame
+ }
+
+ /// Parses a raw STOMP frame string into its command, headers and body.
+ /// Header values that themselves contain ':' are preserved (e.g. URLs).
+ /// Returns `nil` if no command line can be found.
+ public static func decode(_ string: String) -> (command: String, headers: [String: String], body: String)? {
+ var contents = string.components(separatedBy: "\n")
+ if contents.first == "" {
+ contents.removeFirst()
+ }
+
+ guard let command = contents.first else {
+ return nil
+ }
+
+ var headers = [String: String]()
+ var body = ""
+ var hasHeaders = false
+
+ contents.removeFirst()
+ for line in contents {
+ if hasHeaders == true {
+ body += line
+ } else {
+ if line == "" {
+ hasHeaders = true
+ } else {
+ let parts = line.components(separatedBy: ":")
+ if let key = parts.first {
+ headers[key] = parts.dropFirst().joined(separator: ":")
+ }
+ }
+ }
+ }
+
+ // Remove the trailing NULL control character from the body, if present.
+ if body.hasSuffix("\0") {
+ body = body.replacingOccurrences(of: "\0", with: "")
+ }
+
+ return (command, headers, body)
+ }
+
+ /// Maps a public `StompAckMode` to its STOMP `ack` header value.
+ public static func ackHeaderValue(for mode: StompAckMode) -> String {
+ switch mode {
+ case .ClientMode:
+ return StompCommands.ackClient
+ case .ClientIndividualMode:
+ return StompCommands.ackClientIndividual
+ case .AutoMode:
+ return StompCommands.ackAuto
+ }
+ }
+}
diff --git a/StompClientLibExample/Podfile b/StompClientLibExample/Podfile
index 8fa83fd..4844b1d 100644
--- a/StompClientLibExample/Podfile
+++ b/StompClientLibExample/Podfile
@@ -1,5 +1,4 @@
-# Uncomment the next line to define a global platform for your project
-# platform :ios, '9.0'
+platform :ios, '12.0'
target 'StompClientLibExample' do
# Comment the next line if you don't want to use dynamic frameworks
@@ -8,8 +7,6 @@ target 'StompClientLibExample' do
# Pods for StompClientLibExample
pod 'StompClientLib', :path => '../'
-
-
target 'StompClientLibExampleTests' do
inherit! :search_paths
# Pods for testing
@@ -21,3 +18,13 @@ target 'StompClientLibExample' do
end
end
+
+# Ensure every pod (notably SocketRocket) builds with a modern deployment
+# target so recent Xcode toolchains (which dropped libarclite) can compile them.
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
+ end
+ end
+end
diff --git a/StompClientLibExample/StompClientLibExample.xcodeproj/project.pbxproj b/StompClientLibExample/StompClientLibExample.xcodeproj/project.pbxproj
index 7fb3d57..4be2ffb 100644
--- a/StompClientLibExample/StompClientLibExample.xcodeproj/project.pbxproj
+++ b/StompClientLibExample/StompClientLibExample.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
4D7A2EDF252F0A3400C25C02 /* StompClientLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D7A2EDE252F0A3400C25C02 /* StompClientLib.swift */; };
+ 4D7A2EE0252F0A3400C25C02 /* StompFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D7A2EE1252F0A3400C25C02 /* StompFrame.swift */; };
4DDC24BE252EFF0200E14704 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC24BD252EFF0200E14704 /* AppDelegate.swift */; };
4DDC24C0252EFF0200E14704 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC24BF252EFF0200E14704 /* SceneDelegate.swift */; };
4DDC24C2252EFF0200E14704 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC24C1252EFF0200E14704 /* ViewController.swift */; };
@@ -44,6 +45,7 @@
2420D6AC0CC10C10A0D9CB8C /* Pods-StompClientLibExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StompClientLibExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-StompClientLibExampleTests/Pods-StompClientLibExampleTests.debug.xcconfig"; sourceTree = ""; };
4CB0E35D0539542EEC8D550E /* Pods-StompClientLibExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StompClientLibExample.release.xcconfig"; path = "Target Support Files/Pods-StompClientLibExample/Pods-StompClientLibExample.release.xcconfig"; sourceTree = ""; };
4D7A2EDE252F0A3400C25C02 /* StompClientLib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StompClientLib.swift; path = ../StompClientLib/Classes/StompClientLib.swift; sourceTree = ""; };
+ 4D7A2EE1252F0A3400C25C02 /* StompFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StompFrame.swift; path = ../StompClientLib/Classes/StompFrame.swift; sourceTree = ""; };
4DDC24BA252EFF0200E14704 /* StompClientLibExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StompClientLibExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
4DDC24BD252EFF0200E14704 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
4DDC24BF252EFF0200E14704 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
@@ -107,6 +109,7 @@
isa = PBXGroup;
children = (
4D7A2EDE252F0A3400C25C02 /* StompClientLib.swift */,
+ 4D7A2EE1252F0A3400C25C02 /* StompFrame.swift */,
4DDC24BC252EFF0200E14704 /* StompClientLibExample */,
4DDC24D3252EFF0400E14704 /* StompClientLibExampleTests */,
4DDC24DE252EFF0400E14704 /* StompClientLibExampleUITests */,
@@ -428,6 +431,7 @@
buildActionMask = 2147483647;
files = (
4D7A2EDF252F0A3400C25C02 /* StompClientLib.swift in Sources */,
+ 4D7A2EE0252F0A3400C25C02 /* StompFrame.swift in Sources */,
4DDC24C2252EFF0200E14704 /* ViewController.swift in Sources */,
4DDC24BE252EFF0200E14704 /* AppDelegate.swift in Sources */,
4DDC24C0252EFF0200E14704 /* SceneDelegate.swift in Sources */,
diff --git a/StompClientLibExample/StompClientLibExample.xcodeproj/xcshareddata/xcschemes/StompClientLibExample.xcscheme b/StompClientLibExample/StompClientLibExample.xcodeproj/xcshareddata/xcschemes/StompClientLibExample.xcscheme
new file mode 100644
index 0000000..cdaccfe
--- /dev/null
+++ b/StompClientLibExample/StompClientLibExample.xcodeproj/xcshareddata/xcschemes/StompClientLibExample.xcscheme
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StompClientLibExample/StompClientLibExampleTests/StompClientLibExampleTests.swift b/StompClientLibExample/StompClientLibExampleTests/StompClientLibExampleTests.swift
index cbdc4bc..c9e8a31 100644
--- a/StompClientLibExample/StompClientLibExampleTests/StompClientLibExampleTests.swift
+++ b/StompClientLibExample/StompClientLibExampleTests/StompClientLibExampleTests.swift
@@ -2,32 +2,107 @@
// StompClientLibExampleTests.swift
// StompClientLibExampleTests
//
-// Created by Kuray on 8.10.2020.
+// STOMP wire-format unit tests for StompClientLib. These exercise the pure
+// frame encode/decode/ack helpers that back the public client, without
+// needing a live WebSocket connection.
//
import XCTest
-@testable import StompClientLibExample
+import StompClientLib
class StompClientLibExampleTests: XCTestCase {
- override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
+ private let NUL = "\u{00}"
+
+ // MARK: encode
+
+ func testEncodeCommandHeadersAndBody() {
+ let frame = StompFrame.encode(command: "SEND",
+ headers: ["destination": "/app/hello"],
+ body: "hi")
+ XCTAssertTrue(frame.hasPrefix("SEND\n"))
+ XCTAssertTrue(frame.contains("destination:/app/hello\n"))
+ XCTAssertTrue(frame.contains("\nhi"))
+ XCTAssertTrue(frame.hasSuffix(NUL))
+ }
+
+ func testEncodeNilBodyStillAddsBlankLineAndTerminator() {
+ let frame = StompFrame.encode(command: "DISCONNECT", headers: ["id": "1"], body: nil)
+ XCTAssertEqual(frame, "DISCONNECT\nid:1\n\n" + NUL)
+ }
+
+ func testEncodeNilCommandOmitsCommandLine() {
+ let frame = StompFrame.encode(command: nil, headers: nil, body: nil)
+ XCTAssertEqual(frame, "\n" + NUL)
}
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
+ // MARK: decode
+
+ func testDecodeBasicFrame() {
+ let raw = "MESSAGE\ndestination:/topic/x\nmessage-id:42\n\nhello world" + NUL
+ let frame = StompFrame.decode(raw)
+ XCTAssertEqual(frame?.command, "MESSAGE")
+ XCTAssertEqual(frame?.headers["destination"], "/topic/x")
+ XCTAssertEqual(frame?.headers["message-id"], "42")
+ XCTAssertEqual(frame?.body, "hello world")
+ }
+
+ func testDecodeStripsLeadingEmptyLine() {
+ let raw = "\nCONNECTED\nsession:abc\n\n" + NUL
+ XCTAssertEqual(StompFrame.decode(raw)?.command, "CONNECTED")
+ XCTAssertEqual(StompFrame.decode(raw)?.headers["session"], "abc")
}
- func testExample() throws {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct results.
+ // Regression: header values that contain ':' must be preserved (PR #56).
+ func testDecodeKeepsColonInHeaderValue() {
+ let raw = "MESSAGE\nlocation:ws://example.com:8080/path\n\nbody" + NUL
+ let frame = StompFrame.decode(raw)
+ XCTAssertEqual(frame?.headers["location"], "ws://example.com:8080/path")
}
- func testPerformanceExample() throws {
- // This is an example of a performance test case.
- self.measure {
- // Put the code you want to measure the time of here.
- }
+ func testDecodeStripsTrailingNullFromBody() {
+ let raw = "MESSAGE\n\npayload" + NUL
+ XCTAssertEqual(StompFrame.decode(raw)?.body, "payload")
+ XCTAssertFalse(StompFrame.decode(raw)?.body.contains(NUL) ?? true)
}
+ func testDecodeEmptyStringReturnsNil() {
+ XCTAssertNil(StompFrame.decode(""))
+ }
+
+ func testDecodePingFrameHasEmptyCommand() {
+ // A server heart-beat is a lone newline; decode yields an empty command,
+ // which the client treats as a pong.
+ XCTAssertEqual(StompFrame.decode("\n")?.command, "")
+ }
+
+ // MARK: round trip
+
+ func testEncodeDecodeRoundTrip() {
+ let headers = ["destination": "/queue/a", "content-type": "text/plain"]
+ let encoded = StompFrame.encode(command: "SEND", headers: headers, body: "payload")
+ let decoded = StompFrame.decode(encoded)
+ XCTAssertEqual(decoded?.command, "SEND")
+ XCTAssertEqual(decoded?.headers["destination"], "/queue/a")
+ XCTAssertEqual(decoded?.headers["content-type"], "text/plain")
+ XCTAssertEqual(decoded?.body, "payload")
+ }
+
+ // MARK: ack mode mapping
+
+ func testAckHeaderValueMapping() {
+ XCTAssertEqual(StompFrame.ackHeaderValue(for: .AutoMode), "auto")
+ XCTAssertEqual(StompFrame.ackHeaderValue(for: .ClientMode), "client")
+ XCTAssertEqual(StompFrame.ackHeaderValue(for: .ClientIndividualMode), "client-individual")
+ }
+
+ // MARK: client defaults
+
+ func testLoggingDisabledByDefault() {
+ XCTAssertFalse(StompClientLib.isLoggingEnabled)
+ }
+
+ func testFreshClientIsNotConnected() {
+ XCTAssertFalse(StompClientLib().isConnected())
+ }
}