Skip to content

Commit a61d5e4

Browse files
authored
fix: SubscriptionCallback missing unchecked sendable (#226)
* fix: SubscriptionCallback missing unchecked sendable * test on oldest * protect all callbacks with NSLock * Add protected counter in tests * Update CI configuration for Xcode version and test command Updated Xcode version in CI configuration and modified build-test command. * Change _count to nonisolated and unsafe * Update Xcode version and macOS runner in CI workflow * Update Xcode version in CI workflow * Update Xcode version in CI workflow * Update Xcode version and improve test logging Updated Xcode path and enhanced test output formatting.
1 parent 531857f commit a61d5e4

5 files changed

Lines changed: 103 additions & 29 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ on:
88
branches: [ main ]
99

1010
env:
11-
CI_XCODE_OLDEST: '/Applications/Xcode_16.1.app/Contents/Developer'
12-
CI_XCODE_LATEST: '/Applications/Xcode_26.1.app/Contents/Developer'
11+
CI_XCODE_OLDEST: '/Applications/Xcode_16.3.app/Contents/Developer'
12+
CI_XCODE_LATEST: '/Applications/Xcode_26.2.app/Contents/Developer'
1313

1414
concurrency:
1515
group: ${{ github.workflow }}-${{ github.ref }}
@@ -103,7 +103,7 @@ jobs:
103103
xcode-test-oldest:
104104
timeout-minutes: 25
105105
needs: linux
106-
runs-on: macos-14
106+
runs-on: macos-15
107107
steps:
108108
- uses: actions/checkout@v6
109109
- name: Create and set the default keychain
@@ -113,7 +113,7 @@ jobs:
113113
security unlock-keychain -p "" temporary
114114
security set-keychain-settings -lut 7200 temporary
115115
- name: Build-Test
116-
run: set -o pipefail && env NSUnbufferedIO=YES swift build 2>&1 | xcbeautify --renderer github-actions
116+
run: set -o pipefail && env NSUnbufferedIO=YES swift test --enable-code-coverage 2>&1 | xcbeautify --renderer github-actions
117117
env:
118118
DEVELOPER_DIR: ${{ env.CI_XCODE_OLDEST }}
119119
- name: Prepare codecov

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
# Parse-Swift Changelog
33

44
### main
5-
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/6.0.0...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
5+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/6.0.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
66
* _Contributing to this repo? Add info about your change here to be included in the next release_
77

8+
### 6.0.1
9+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/6.0.0...6.0.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/6.0.1/documentation/parseswift)
10+
11+
__Fixes__
12+
* Fix `SubscriptionCallback` causing issue in Swift 6.1 because of missing `@unchecked Sendable` ([#226](https://github.com/netreconlab/Parse-Swift/pull/226)), thanks to [Corey Baker](https://github.com/cbaker6).
13+
814
### 6.0.0
915
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.12.3...6.0.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/6.0.0/documentation/parseswift)
1016

Sources/ParseSwift/LiveQuery/SubscriptionCallback.swift

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,53 @@ import FoundationNetworking
1414
/**
1515
A default implementation of the `QuerySubscribable` protocol using closures for callbacks.
1616
*/
17-
open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
17+
open class SubscriptionCallback<T: ParseObject>: QuerySubscribable, @unchecked Sendable {
1818

1919
private let lock = NSLock()
20+
private let eventLock = NSLock()
21+
private let subscribedLock = NSLock()
22+
private let unsubscribedLock = NSLock()
2023
private var _query: Query<T>
21-
fileprivate var eventHandlers = [(Query<T>, Event<T>) -> Void]()
22-
fileprivate var subscribeHandlers = [(Query<T>, Bool) -> Void]()
23-
fileprivate var unsubscribeHandlers = [(Query<T>) -> Void]()
24+
fileprivate var eventHandlers: [(Query<T>, Event<T>) -> Void] {
25+
get {
26+
eventLock.lock()
27+
defer { eventLock.unlock() }
28+
return _eventHandlers
29+
}
30+
set {
31+
eventLock.lock()
32+
defer { eventLock.unlock() }
33+
_eventHandlers = newValue
34+
}
35+
}
36+
fileprivate var subscribeHandlers: [(Query<T>, Bool) -> Void] {
37+
get {
38+
subscribedLock.lock()
39+
defer { subscribedLock.unlock() }
40+
return _subscribeHandlers
41+
}
42+
set {
43+
subscribedLock.lock()
44+
defer { subscribedLock.unlock() }
45+
_subscribeHandlers = newValue
46+
}
47+
}
48+
fileprivate var unsubscribeHandlers: [(Query<T>) -> Void] {
49+
get {
50+
unsubscribedLock.lock()
51+
defer { unsubscribedLock.unlock() }
52+
return _unsubscribeHandlers
53+
}
54+
set {
55+
unsubscribedLock.lock()
56+
defer { unsubscribedLock.unlock() }
57+
_unsubscribeHandlers = newValue
58+
}
59+
}
60+
61+
fileprivate var _eventHandlers = [(Query<T>, Event<T>) -> Void]()
62+
fileprivate var _subscribeHandlers = [(Query<T>, Bool) -> Void]()
63+
fileprivate var _unsubscribeHandlers = [(Query<T>) -> Void]()
2464

2565
public var query: Query<T> {
2666
get {
@@ -49,8 +89,9 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
4989
- parameter handler: The callback to register.
5090
- returns: The same subscription, for easy chaining.
5191
*/
52-
@discardableResult open func handleEvent(_ handler: @escaping (Query<T>,
53-
Event<T>) -> Void) -> SubscriptionCallback {
92+
@discardableResult open func handleEvent(
93+
_ handler: @escaping @Sendable (Query<T>, Event<T>) -> Void
94+
) -> SubscriptionCallback {
5495
eventHandlers.append(handler)
5596
return self
5697
}
@@ -60,8 +101,9 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
60101
- parameter handler: The callback to register.
61102
- returns: The same subscription, for easy chaining.
62103
*/
63-
@discardableResult open func handleSubscribe(_ handler: @escaping (Query<T>,
64-
Bool) -> Void) -> SubscriptionCallback {
104+
@discardableResult open func handleSubscribe(
105+
_ handler: @escaping @Sendable (Query<T>, Bool) -> Void
106+
) -> SubscriptionCallback {
65107
subscribeHandlers.append(handler)
66108
return self
67109
}
@@ -71,7 +113,9 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
71113
- parameter handler: The callback to register.
72114
- returns: The same subscription, for easy chaining.
73115
*/
74-
@discardableResult open func handleUnsubscribe(_ handler: @escaping (Query<T>) -> Void) -> SubscriptionCallback {
116+
@discardableResult open func handleUnsubscribe(
117+
_ handler: @escaping @Sendable (Query<T>) -> Void
118+
) -> SubscriptionCallback {
75119
unsubscribeHandlers.append(handler)
76120
return self
77121
}
@@ -86,8 +130,10 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
86130
- parameter handler: The callback to register.
87131
- returns: The same subscription, for easy chaining.
88132
*/
89-
@discardableResult public func handle(_ eventType: @escaping (T) -> Event<T>,
90-
_ handler: @escaping (Query<T>, T) -> Void) -> SubscriptionCallback {
133+
@discardableResult public func handle(
134+
_ eventType: @escaping @Sendable (T) -> Event<T>,
135+
_ handler: @escaping @Sendable (Query<T>, T) -> Void
136+
) -> SubscriptionCallback {
91137
return handleEvent { query, event in
92138
switch event {
93139
case .entered(let obj) where eventType(obj) == event: handler(query, obj)
@@ -102,7 +148,9 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
102148

103149
// MARK: QuerySubscribable
104150

105-
open func didReceive(_ eventData: Data) throws {
151+
open func didReceive(
152+
_ eventData: Data
153+
) throws {
106154
// Need to decode the event with respect to the `ParseObject`.
107155
let eventMessage = try ParseCoding.jsonDecoder().decode(EventResponse<T>.self, from: eventData)
108156
guard let event = Event(event: eventMessage) else {
@@ -111,7 +159,9 @@ open class SubscriptionCallback<T: ParseObject>: QuerySubscribable {
111159
eventHandlers.forEach { $0(query, event) }
112160
}
113161

114-
open func didSubscribe(_ new: Bool) {
162+
open func didSubscribe(
163+
_ new: Bool
164+
) {
115165
subscribeHandlers.forEach { $0(query, new) }
116166
}
117167

Sources/ParseSwift/ParseConstants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "6.0.0"
13+
static let version = "6.0.1"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

Tests/ParseSwiftTests/ParseLiveQueryTests.swift

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ class ParseLiveQueryTests: XCTestCase, @unchecked Sendable {
8484
}
8585
}
8686

87+
class KeepCount: @unchecked Sendable {
88+
var count: Int {
89+
get {
90+
lock.lock()
91+
defer { lock.unlock() }
92+
return _count
93+
}
94+
set {
95+
lock.lock()
96+
defer { lock.unlock() }
97+
_count = newValue
98+
}
99+
}
100+
101+
let lock = NSLock()
102+
nonisolated(unsafe) var _count = 0
103+
}
104+
87105
override func setUp() async throws {
88106
try await super.setUp()
89107
guard let url = URL(string: "http://localhost:1337/parse") else {
@@ -1140,15 +1158,15 @@ class ParseLiveQueryTests: XCTestCase, @unchecked Sendable {
11401158

11411159
let expectation1 = XCTestExpectation(description: "Subscribe Handler")
11421160
let expectation2 = XCTestExpectation(description: "Resubscribe Handler")
1143-
var count = 0
1144-
var originalTask: URLSessionWebSocketTask?
1161+
let keepCount = KeepCount()
11451162
subscription.handleSubscribe { subscribedQuery, isNew in
11461163
XCTAssertEqual(query, subscribedQuery)
1147-
if count == 0 {
1164+
var originalTask: URLSessionWebSocketTask?
1165+
if keepCount.count == 0 {
11481166
XCTAssertTrue(isNew)
11491167
XCTAssertNotNil(ParseLiveQuery.client?.task)
11501168
originalTask = ParseLiveQuery.client?.task
1151-
count += 1
1169+
keepCount.count += 1
11521170
Task {
11531171
let current = await client.subscriptions.current
11541172
let pending = await client.subscriptions.pending
@@ -2211,12 +2229,12 @@ class ParseLiveQueryTests: XCTestCase, @unchecked Sendable {
22112229
XCTAssertEqual(subscription.query, query)
22122230

22132231
let expectation1 = XCTestExpectation(description: "Subscribe Handler")
2214-
var count = 0
2232+
let keepCount = KeepCount()
22152233
subscription.handleSubscribe { subscribedQuery, isNew in
22162234
XCTAssertEqual(query, subscribedQuery)
2217-
if count == 0 {
2235+
if keepCount.count == 0 {
22182236
XCTAssertTrue(isNew)
2219-
count += 1
2237+
keepCount.count += 1
22202238
expectation1.fulfill()
22212239
} else {
22222240
XCTAssertFalse(isNew)
@@ -2272,12 +2290,12 @@ class ParseLiveQueryTests: XCTestCase, @unchecked Sendable {
22722290

22732291
let expectation1 = XCTestExpectation(description: "Subscribe Handler")
22742292
let expectation2 = XCTestExpectation(description: "Unsubscribe Handler")
2275-
var count = 0
2293+
let keepCount = KeepCount()
22762294
subscription.handleSubscribe { subscribedQuery, isNew in
22772295
XCTAssertEqual(query, subscribedQuery)
2278-
if count == 0 {
2296+
if keepCount.count == 0 {
22792297
XCTAssertTrue(isNew)
2280-
count += 1
2298+
keepCount.count += 1
22812299
expectation1.fulfill()
22822300
} else {
22832301
XCTAssertTrue(isNew)

0 commit comments

Comments
 (0)