Skip to content

Commit 88c3eef

Browse files
authored
Concurrency overhaul (#1)
* Bump Swift min version, use tagged async algorithms, add concurrency checking * Fix lots of concurrency issues and clean up all the Sendable warnings. The URLSession WebScoket transport is now much more robust. Tests now work when run in parallel and/or repetitively. * Fix numerous warnings * Add CI and so forth * Enable the various Swift feature flags and update accordingly * Update README * Refactor JSONMessageRegistryTransport into multiple files * Clean up naming a little * Rename and refactor JSONLoggingTransport -> JSONLoggingMiddleware. Now provides much more control over logging. * Improve naming of the JSONMessage stuff * Bump swift-algorithms dependency * Add missing `final`
1 parent 6a9643e commit 88c3eef

19 files changed

+892
-488
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
name: ISSUE_TEMPLATE
3+
about: Template for all issues
4+
title: ''
5+
labels: ''
6+
assignees: ''
7+
8+
---
9+
10+
### Expected behavior
11+
_[what you expected to happen]_
12+
13+
### Actual behavior
14+
_[what actually happened]_
15+
16+
### Steps to reproduce
17+
18+
1. ...
19+
2. ...
20+
21+
### If possible, minimal yet complete reproducer code (or URL to code)
22+
23+
_[anything to help us reproducing the issue]_
24+
25+
### Swift & OS version (output of `swift --version && uname -a`)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
_[One line description of your change]_
2+
3+
### Motivation:
4+
5+
_[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_
6+
7+
### Modifications:
8+
9+
_[Describe the modifications you've done.]_
10+
11+
### Result:
12+
13+
_[After your change, what will change.]_

.github/dependabot.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: 2
2+
enable-beta-ecosystems: true
3+
updates:
4+
- package-ecosystem: "github-actions"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
allow:
9+
- dependency-type: all
10+
groups:
11+
dependencies:
12+
patterns:
13+
- "*"
14+
- package-ecosystem: "swift"
15+
directory: "/"
16+
schedule:
17+
interval: "weekly"
18+
allow:
19+
- dependency-type: all
20+
groups:
21+
all-dependencies:
22+
patterns:
23+
- "*"

.github/workflows/test.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Tests
2+
concurrency:
3+
group: ${{ github.workflow }}-${{ github.ref }}
4+
cancel-in-progress: true
5+
on:
6+
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
7+
push: { branches: [ main ] }
8+
env:
9+
LOG_LEVEL: info
10+
11+
jobs:
12+
appleos:
13+
if: ${{ !(github.event.pull_request.draft || false) }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
xcode:
18+
- latest
19+
#- latest-stable
20+
platform:
21+
- 'macOS'
22+
- 'iOS Simulator'
23+
- 'tvOS Simulator'
24+
- 'watchOS Simulator'
25+
include:
26+
- platform: 'macOS'
27+
destination: 'arch=x86_64'
28+
- platform: 'iOS Simulator'
29+
destination: 'OS=latest,name=iPhone 15 Pro'
30+
- platform: 'tvOS Simulator'
31+
destination: 'OS=latest,name=Apple TV 4K (3rd generation)'
32+
- platform: 'watchOS Simulator'
33+
destination: 'OS=latest,name=Apple Watch Series 9 (45mm)'
34+
name: ${{ matrix.platform }} Tests
35+
runs-on: macos-13
36+
steps:
37+
- name: Select latest available Xcode
38+
uses: maxim-lobanov/setup-xcode@v1
39+
with:
40+
xcode-version: ${{ matrix.xcode }}
41+
- name: Install xcbeautify
42+
run: brew install xcbeautify
43+
- name: Checkout code
44+
uses: actions/checkout@v4
45+
- name: Run tests
46+
env:
47+
DESTINATION: ${{ format('platform={0},{1}', matrix.platform, matrix.destination) }}
48+
run: |
49+
set -o pipefail && \
50+
xcodebuild test -scheme StructuredWebSocketClient-Package \
51+
-enableThreadSanitizer YES \
52+
-enableCodeCoverage YES \
53+
-disablePackageRepositoryCache \
54+
-resultBundlePath "${GITHUB_WORKSPACE}/results.resultBundle" \
55+
-destination "${DESTINATION}" |
56+
xcbeautify --is-ci --quiet --renderer github-actions
57+
- name: Upload coverage data
58+
uses: codecov/codecov-action@v3
59+
with:
60+
token: ${{ secrets.CODECOV_TOKEN }}
61+
swift: true
62+
verbose: true
63+
xcode: true
64+
xcode_archive_path: ${{ github.workspace }}/results.resultBundle
65+
66+
# linux:
67+
# if: ${{ !(github.event.pull_request.draft || false) }}
68+
# strategy:
69+
# fail-fast: false
70+
# matrix:
71+
# swift-image:
72+
# - swift:5.9-jammy
73+
# - swiftlang/swift:nightly-5.10-jammy
74+
# - swiftlang/swift:nightly-main-jammy
75+
# name: Linux ${{ matrix.swift-image }} Tests
76+
# runs-on: ubuntu-latest
77+
# container: ${{ matrix.swift-image }}
78+
# steps:
79+
# - name: Checkout code
80+
# uses: actions/checkout@v4
81+
# - name: Install xcbeautify
82+
# run: |
83+
# DEBIAN_FRONTEND=noninteractive apt-get update
84+
# DEBIAN_FRONTEND=noninteractive apt-get install -y curl
85+
# curl -fsSLO 'https://github.com/tuist/xcbeautify/releases/download/1.0.1/xcbeautify-1.0.1-x86_64-unknown-linux-gnu.tar.xz'
86+
# tar -x -J -f xcbeautify-1.0.1-x86_64-unknown-linux-gnu.tar.xz
87+
# - name: Run tests
88+
# shell: bash
89+
# run: |
90+
# set -o pipefail && \
91+
# swift test --sanitize=thread --enable-code-coverage |
92+
# ./xcbeautify --is-ci --quiet --renderer github-actions
93+
# - name: Upload coverage data
94+
# uses: vapor/[email protected]
95+
# with:
96+
# cc_token: ${{ secrets.CODECOV_TOKEN }}
97+
# cc_verbose: true
98+
99+
codeql:
100+
if: ${{ !(github.event.pull_request.draft || false) }}
101+
name: CodeQL Analysis
102+
runs-on: ubuntu-latest
103+
container:
104+
image: swift:5.9-jammy
105+
permissions: { actions: write, contents: read, security-events: write }
106+
steps:
107+
- name: Checkout code
108+
uses: actions/checkout@v4
109+
- name: Mark repo safe
110+
run: |
111+
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
112+
- name: Initialize CodeQL
113+
uses: github/codeql-action/init@v2
114+
with: { languages: swift }
115+
- name: Perform build
116+
run: swift build
117+
- name: Run CodeQL analyze
118+
uses: github/codeql-action/analyze@v2
119+
120+
dependency-graph:
121+
if: ${{ github.event_name == 'push' }}
122+
runs-on: ubuntu-latest
123+
container: swift:jammy
124+
permissions:
125+
contents: write
126+
steps:
127+
- name: Check out code
128+
uses: actions/checkout@v4
129+
- name: Set up dependencies
130+
run: |
131+
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
132+
apt-get update && apt-get install -y curl
133+
- name: Submit dependency graph
134+
uses: vapor-community/[email protected]

Package.swift

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.7
1+
// swift-tools-version:5.9
22
//===----------------------------------------------------------------------===//
33
//
44
// This source file is part of the StructuredWebSocketClient open source project
@@ -14,38 +14,57 @@
1414

1515
import PackageDescription
1616

17+
let swiftSettings: [SwiftSetting] = [
18+
.enableUpcomingFeature("ForwardTrailingClosures"),
19+
.enableUpcomingFeature("ExistentialAny"),
20+
.enableUpcomingFeature("ConciseMagicFile"),
21+
.enableUpcomingFeature("DisableOutwardActorInference"),
22+
.enableExperimentalFeature("StrictConcurrency=complete"),
23+
]
24+
1725
let package = Package(
1826
name: "StructuredWebSocketClient",
19-
platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v8), .tvOS(.v15)],
27+
platforms: [
28+
.macOS(.v12),
29+
.iOS(.v15),
30+
.watchOS(.v8),
31+
.tvOS(.v15),
32+
],
2033
products: [
21-
.library(
22-
name: "StructuredWebSocketClient",
23-
targets: ["StructuredWebSocketClient"]),
24-
.library(
25-
name: "StructuredWebSocketClientTestSupport",
26-
targets: ["StructuredWebSocketClientTestSupport"]),
34+
.library(name: "StructuredWebSocketClient", targets: ["StructuredWebSocketClient"]),
35+
.library(name: "StructuredWebSocketClientTestSupport", targets: ["StructuredWebSocketClientTestSupport"]),
2736
],
2837
dependencies: [
2938
// Swift logging API
30-
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.5.2")),
39+
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.2"),
3140
// AsyncChannel with backpressure
32-
.package(url: "https://github.com/apple/swift-async-algorithms", branch: "main"),
41+
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
42+
.package(url: "https://github.com/stairtree/async-helpers.git", from: "0.2.0"),
3343
],
3444
targets: [
3545
.target(
3646
name: "StructuredWebSocketClient",
3747
dependencies: [
3848
.product(name: "Logging", package: "swift-log"),
3949
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
40-
]),
50+
.product(name: "AsyncHelpers", package: "async-helpers"),
51+
],
52+
swiftSettings: swiftSettings
53+
),
4154
.target(
4255
name: "StructuredWebSocketClientTestSupport",
43-
dependencies: [.target(name: "StructuredWebSocketClient")]),
56+
dependencies: [
57+
.target(name: "StructuredWebSocketClient"),
58+
],
59+
swiftSettings: swiftSettings
60+
),
4461
.testTarget(
4562
name: "StructuredWebSocketClientTests",
4663
dependencies: [
4764
.target(name: "StructuredWebSocketClient"),
4865
.target(name: "StructuredWebSocketClientTestSupport"),
49-
]),
66+
],
67+
swiftSettings: swiftSettings
68+
),
5069
]
5170
)

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1-
# StructuredWebSocketClient
1+
# <div align="center">StructuredWebSocketClient</div>
2+
3+
<p align="center">
4+
5+
<a href="LICENSE.txt">
6+
<img src="https://img.shields.io/badge/license-MIT-skyblue?style=plastic&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSJza3libHVlIiBkPSJNNzAuNTQsMTEuNWMtLjEtMy44Ny0xMi0zLjg3LTEyLDB2MTBjLTUuMjcuMi0yMC4zNCw3Ljg3LTI0LjQ0LDhoLTE4LjRjLTcuMSwwLTguMywxMy45NSwzLjUsMTIsMCwwLTE0Ljk2LDMzLjItMTYuNywzNy41NC04LjE1LDE4LjQ2LDUzLjkzLDE3LjMsNDUuOC44LTIuNS01LjA3LTE2LjctMzguMzQtMTYuNy0zOC4zNCw1LjcsMCwxOC40LTcuODMsMjcuMTQtOHY3Ni43OGgtMjBjLTMuOSwwLTMuOSwxMiwwLDEyaDUyYzMuOSwwLDMuOS0xMiwwLTEyaC0yMHYtNzYuNzhjOC43LS4xLDIxLjE2LDgsMjcuMzYsOCwwLDAtMTQuNDMsMzIuODYtMTYuNywzOC4zNC03LjEsMTUuOTYsNTMuNTYsMTguMyw0NS44LDAtMi4yLTUuMy0xNi43LTM4LjM0LTE2LjctMzguMzQsMTIuNiwxLjIsMTEuNS0xMiwzLjUtMTIsMCwwLTIyLjgtLjItMTguNCwwLDAsMC0xOS03LjktMjQuNDQtOHptMzIuODYsNDQuNjYsMTAuNCwyNGMtMy45LDEuNzQtMTguNiwxLjItMjAuODQsMHptLTc3LjcsMCwxMC40LDI0Yy04LDMuMy0xNSwyLjktMjAuODQsMCwzLjcyLTcuODcsNi41Ny0xNi4yNSwxMC40NC0yNHoiLz48L3N2Zz4%3D" alt="MIT License">
7+
</a>
8+
9+
<a href="https://github.com/stairtree/StructuredWebSocketClient/actions/workflows/test.yml">
10+
<img src="https://img.shields.io/github/actions/workflow/status/stairtree/StructuredWebSocketClient/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="CI">
11+
</a>
12+
13+
<a href="https://codecov.io/gh/stairtree/StructuredWebSocketClient">
14+
<img src="https://img.shields.io/codecov/c/gh/stairtree/StructuredWebSocketClient?style=plastic&logo=codecov&label=codecov&token=JV5WJIYWHG">
15+
</a>
16+
17+
<a href="https://swift.org">
18+
<img src="https://img.shields.io/badge/swift-5.9%2b-white?style=plastic&logoColor=%23f07158&labelColor=gray&color=%23f07158&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BPHBhdGggZD0iTTYsMjRjLTMsMC02LTMtNi02di0xMmMwLTMsMy02LDYtNmgxMmMzLDAsNiwzLDYsNnYxMmMwLDMtMyw2LTYsNnoiIGZpbGw9IiNmMDcxNTgiLz48cGF0aCBkPSJNMTMuNiwzLjRjNC4yLDIuNCw2LjMsNy41LDUuMywxMS41LDEuOSwyLjgsMS42LDUuMiwxLjQsNC43LTEuMi0yLjMtMy4zLTEuNC00LjQtLjctMy45LDEuOC0xMC4yLjItMTMuNS01LDMsMi4yLDcuMiwzLjEsMTAuMywxLjItNC42LTMuNi04LjUtOS4yLTguNS05LjMsMi4zLDIuMSw2LDQuOCw3LjMsNS43LTIuOC0zLjEtNS4zLTYuNy01LjItNi43LDIuNywyLjcsNS43LDUuMiw4LjksNy4yLjQtLjgsMS40LTQuNS0xLjYtOC43eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg%3D%3D" alt="Swift 5.9">
19+
</a>
20+
21+
</p>
222

323
A testable generic websocket client
424

525
To integrate the package:
626

727
```swift
828
dependencies: [
9-
.package(url: "https://github.com/Stairtree/StructuredWebSocketClient.git", branch: "main"),
29+
.package(url: "https://github.com/stairtree/StructuredWebSocketClient.git", from: "0.1.0"),
1030
]
1131
```

Sources/StructuredWebSocketClient/InterceptingTransport.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,22 @@ import FoundationNetworking
1818
#endif
1919

2020
public final class InterceptingTransport: WebSocketMessageInboundMiddleware {
21-
22-
public let nextIn: WebSocketMessageInboundMiddleware?
23-
let _handle: (_ message: URLSessionWebSocketTask.Message) async throws -> MessageHandling
21+
public let nextIn: (any WebSocketMessageInboundMiddleware)?
22+
let _handle: @Sendable (_ message: URLSessionWebSocketTask.Message) async throws -> MessageHandling
2423

2524
public init(
26-
next: WebSocketMessageInboundMiddleware?,
27-
handle: @escaping (_ message: URLSessionWebSocketTask.Message) async throws -> MessageHandling
25+
next: (any WebSocketMessageInboundMiddleware)?,
26+
handle: @escaping @Sendable (_ message: URLSessionWebSocketTask.Message) async throws -> MessageHandling
2827
) {
2928
self.nextIn = next
3029
self._handle = handle
3130
}
3231

3332
public func handle(_ received: URLSessionWebSocketTask.Message, metadata: MessageMetadata) async throws -> MessageHandling {
3433
switch try await self._handle(received) {
35-
case .handled: return .handled
34+
case .handled: .handled
3635
case .unhandled(let message):
37-
if let nextIn { return try await nextIn.handle(message, metadata: metadata) }
38-
return .unhandled(message)
36+
try await self.nextIn?.handle(message, metadata: metadata) ?? .unhandled(message)
3937
}
4038
}
4139
}

0 commit comments

Comments
 (0)