Skip to content

Commit 8815ea8

Browse files
committed
Swift 6.0
1 parent ef8615f commit 8815ea8

File tree

6 files changed

+170
-72
lines changed

6 files changed

+170
-72
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ on:
66
workflow_dispatch:
77

88
jobs:
9-
xcode_15_2:
9+
xcode_15_4:
1010
runs-on: macos-14
1111
env:
12-
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
12+
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
1313
steps:
1414
- name: Checkout
1515
uses: actions/checkout@v4
@@ -27,59 +27,6 @@ jobs:
2727
token: ${{ secrets.CODECOV_TOKEN }}
2828
files: ./coverage_report.lcov
2929

30-
xcode_14_3_1:
31-
runs-on: macos-13
32-
env:
33-
DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer
34-
steps:
35-
- name: Checkout
36-
uses: actions/checkout@v4
37-
- name: Version
38-
run: swift --version
39-
- name: Build
40-
run: swift build --build-tests
41-
- name: Test
42-
run: swift test --skip-build
43-
44-
linux_swift_5_7:
45-
runs-on: ubuntu-latest
46-
container: swift:5.7
47-
steps:
48-
- name: Checkout
49-
uses: actions/checkout@v4
50-
- name: Version
51-
run: swift --version
52-
- name: Build
53-
run: swift build --build-tests
54-
- name: Test
55-
run: swift test --skip-build
56-
57-
linux_swift_5_8:
58-
runs-on: ubuntu-latest
59-
container: swift:5.8
60-
steps:
61-
- name: Checkout
62-
uses: actions/checkout@v4
63-
- name: Version
64-
run: swift --version
65-
- name: Build
66-
run: swift build --build-tests
67-
- name: Test
68-
run: swift test --skip-build
69-
70-
linux_swift_5_9:
71-
runs-on: ubuntu-latest
72-
container: swift:5.9
73-
steps:
74-
- name: Checkout
75-
uses: actions/checkout@v4
76-
- name: Version
77-
run: swift --version
78-
- name: Build
79-
run: swift build --build-tests
80-
- name: Test
81-
run: swift test --skip-build
82-
8330
linux_swift_5_10:
8431
runs-on: ubuntu-latest
8532
container: swift:5.10

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.8
1+
// swift-tools-version:6.0
22

33
import PackageDescription
44

@@ -33,7 +33,8 @@ extension Array where Element == SwiftSetting {
3333
static var upcomingFeatures: [SwiftSetting] {
3434
[
3535
.enableUpcomingFeature("ExistentialAny"),
36-
.enableExperimentalFeature("StrictConcurrency")
36+
.enableExperimentalFeature("StrictConcurrency"),
37+
.swiftLanguageVersion(.v6)
3738
]
3839
}
3940
}
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.7
1+
// swift-tools-version:5.10
22

33
import PackageDescription
44

@@ -16,12 +16,24 @@ let package = Package(
1616
targets: [
1717
.target(
1818
name: "IdentifiableContinuation",
19-
path: "Sources"
19+
path: "Sources",
20+
swiftSettings: .upcomingFeatures
2021
),
2122
.testTarget(
2223
name: "IdentifiableContinuationTests",
2324
dependencies: ["IdentifiableContinuation"],
24-
path: "Tests"
25+
path: "Tests",
26+
swiftSettings: .upcomingFeatures
2527
)
2628
]
2729
)
30+
31+
extension Array where Element == SwiftSetting {
32+
33+
static var upcomingFeatures: [SwiftSetting] {
34+
[
35+
.enableUpcomingFeature("ExistentialAny"),
36+
.enableExperimentalFeature("StrictConcurrency")
37+
]
38+
}
39+
}

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[![Build](https://github.com/swhitty/IdentifiableContinuation/actions/workflows/build.yml/badge.svg)](https://github.com/swhitty/IdentifiableContinuation/actions/workflows/build.yml)
22
[![Codecov](https://codecov.io/gh/swhitty/IdentifiableContinuation/graphs/badge.svg)](https://codecov.io/gh/swhitty/IdentifiableContinuation)
33
[![Platforms](https://img.shields.io/badge/platforms-iOS%20|%20Mac%20|%20tvOS%20|%20Linux%20|%20Windows-lightgray.svg)](https://github.com/swhitty/IdentifiableContinuation/blob/main/Package.swift)
4-
[![Swift 5.10](https://img.shields.io/badge/swift-5.7%20–%205.10-red.svg?style=flat)](https://developer.apple.com/swift)
4+
[![Swift 6.0](https://img.shields.io/badge/swift-5.10%20–%206.0-red.svg?style=flat)](https://developer.apple.com/swift)
55
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://opensource.org/licenses/MIT)
66
[![Twitter](https://img.shields.io/badge/[email protected])](http://twitter.com/simonwhitty)
77

@@ -22,26 +22,35 @@ To install using Swift Package Manager, add this to the `dependencies:` section
2222

2323
# Usage
2424

25-
Usage is similar to existing continuations, but requires an `Actor` to ensure the closure is executed within the actors isolation:
25+
With Swift 6, usage is similar to existing continuations where the closure is executed syncronously within the current isolation allowing actors to mutate their isolated state.
2626

2727
```swift
28-
let val: String = await withIdentifiableContinuation(isolation: self) {
29-
$0.resume(returning: "bar")
28+
let val: String = await withIdentifiableContinuation {
29+
continuations[$0.id] = $0
3030
}
3131
```
3232

33-
This allows actors to synchronously start continuations and mutate their isolated state _before_ suspension occurs. The `onCancel:` handler is `@Sendable` and can be called at any time _after_ the body has completed. Manually check `Task.isCancelled` before creating the continuation to prevent performing unrequired work.
33+
An optional cancellation handler is called when the task is cancelled. The handler is `@Sendable` and can be called at any time _after_ the body has completed.
3434

3535
```swift
36-
let val: String = await withIdentifiableContinuation(isolation: self) {
37-
// executed within actor isolation so can immediately mutate actor state
36+
let val: String = await withIdentifiableContinuation {
3837
continuations[$0.id] = $0
39-
} onCancel: { id in
38+
} onCancel { id in
4039
// @Sendable closure executed outside of actor isolation requires `await` to mutate actor state
4140
Task { await self.cancelContinuation(with: id) }
4241
}
4342
```
4443

44+
## Swift 5
45+
46+
While behaviour is identical, Swift 5 is unable to automatically inherit actor isolation through the new `#isolation` keyword ([SE-420](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md)) so an `isolated` reference to the current actor must always be passed.
47+
48+
```swift
49+
let val: String = await withIdentifiableContinuation(isolation: self) {
50+
continuations[$0.id] = $0
51+
}
52+
```
53+
4554
# Credits
4655

4756
IdentifiableContinuation is primarily the work of [Simon Whitty](https://github.com/swhitty).

Sources/IdentifiableContinuation.swift

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,99 @@
2929
// SOFTWARE.
3030
//
3131

32+
#if compiler(>=6.0)
33+
/// Invokes the passed in closure with an `IdentifableContinuation` for the current task.
34+
///
35+
/// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended.
36+
/// It is possible to immediately resume the task, or escape the continuation in order to complete it afterwards, which
37+
/// will then resume the suspended task.
38+
///
39+
/// You must invoke the continuation's `resume` method exactly once.
40+
/// - Parameters:
41+
/// - function: A string identifying the declaration that is the notional
42+
/// source for the continuation, used to identify the continuation in
43+
/// runtime diagnostics related to misuse of this continuation.
44+
/// - body: A closure that takes a `IdentifiableContinuation` parameter.
45+
/// - handler: Cancellation closure executed when the current Task is cancelled. Handler is always called _after_ the body closure is compeled.
46+
/// - Returns: The value continuation is resumed with.
47+
public func withIdentifiableContinuation<T>(
48+
isolation: isolated (any Actor)? = #isolation,
49+
function: String = #function,
50+
body: (IdentifiableContinuation<T, Never>) -> Void,
51+
onCancel handler: @Sendable (IdentifiableContinuation<T, Never>.ID) -> Void
52+
) async -> T {
53+
let id = IdentifiableContinuation<T, Never>.ID()
54+
let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false))
55+
return await withTaskCancellationHandler {
56+
await withCheckedContinuation(function: function) {
57+
let continuation = IdentifiableContinuation(id: id, continuation: $0)
58+
body(continuation)
59+
let sendCancel = state.withLock {
60+
$0.isStarted = true
61+
return $0.isCancelled
62+
}
63+
if sendCancel {
64+
handler(id)
65+
}
66+
_ = isolation
67+
}
68+
} onCancel: {
69+
let sendCancel = state.withLock {
70+
$0.isCancelled = true
71+
return $0.isStarted
72+
}
73+
if sendCancel {
74+
handler(id)
75+
}
76+
}
77+
}
78+
79+
/// Invokes the passed in closure with an `IdentifableContinuation` for the current task.
80+
///
81+
/// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended.
82+
/// It is possible to immediately resume the task, or escape the continuation in order to complete it afterwards, which
83+
/// will then resume the suspended task.
84+
///
85+
/// You must invoke the continuation's `resume` method exactly once.
86+
/// - Parameters:
87+
/// - function: A string identifying the declaration that is the notional
88+
/// source for the continuation, used to identify the continuation in
89+
/// runtime diagnostics related to misuse of this continuation.
90+
/// - body: A closure that takes a `IdentifiableContinuation` parameter.
91+
/// - handler: Cancellation closure executed when the current Task is cancelled. Handler is always called _after_ the body closure is compeled.
92+
/// - Returns: The value continuation is resumed with.
93+
public func withIdentifiableThrowingContinuation<T>(
94+
isolation: isolated (any Actor)? = #isolation,
95+
function: String = #function,
96+
body: (IdentifiableContinuation<T, any Error>) -> Void,
97+
onCancel handler: @Sendable (IdentifiableContinuation<T, any Error>.ID) -> Void
98+
) async throws -> T {
99+
let id = IdentifiableContinuation<T, any Error>.ID()
100+
let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false))
101+
return try await withTaskCancellationHandler {
102+
try await withCheckedThrowingContinuation(function: function) {
103+
let continuation = IdentifiableContinuation(id: id, continuation: $0)
104+
body(continuation)
105+
let sendCancel = state.withLock {
106+
$0.isStarted = true
107+
return $0.isCancelled
108+
}
109+
if sendCancel {
110+
handler(id)
111+
}
112+
_ = isolation
113+
}
114+
} onCancel: {
115+
let sendCancel = state.withLock {
116+
$0.isCancelled = true
117+
return $0.isStarted
118+
}
119+
if sendCancel {
120+
handler(id)
121+
}
122+
}
123+
}
124+
#else
32125
/// Invokes the passed in closure with an `IdentifableContinuation` for the current task.
33126
///
34127
/// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended.
@@ -124,6 +217,7 @@ public func withIdentifiableThrowingContinuation<T>(
124217
}
125218
}
126219
}
220+
#endif
127221

128222
public struct IdentifiableContinuation<T, E>: Sendable, Identifiable where E: Error {
129223

@@ -149,17 +243,27 @@ public struct IdentifiableContinuation<T, E>: Sendable, Identifiable where E: Er
149243

150244
private let continuation: CheckedContinuation<T, E>
151245

152-
public func resume(returning value: T) {
246+
#if compiler(>=6.0)
247+
public func resume(returning value: sending T) {
153248
continuation.resume(returning: value)
154249
}
155250

156-
public func resume(throwing error: E) {
157-
continuation.resume(throwing: error)
251+
public func resume(with result: sending Result<T, E>) {
252+
continuation.resume(with: result)
253+
}
254+
#else
255+
public func resume(returning value: T) {
256+
continuation.resume(returning: value)
158257
}
159258

160259
public func resume(with result: Result<T, E>) {
161260
continuation.resume(with: result)
162261
}
262+
#endif
263+
264+
public func resume(throwing error: E) {
265+
continuation.resume(throwing: error)
266+
}
163267

164268
public func resume() where T == () {
165269
continuation.resume()

Tests/IdentifiableContinuationTests.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ final class IdentifiableContinuationAsyncTests: XCTestCase {
154154
}
155155
}
156156

157-
private actor Waiter<T, E: Error> {
157+
private actor Waiter<T: Sendable, E: Error> {
158158
typealias Continuation = IdentifiableContinuation<T, E>
159159

160160
private var waiting = [Continuation.ID: Continuation]()
@@ -166,22 +166,38 @@ private actor Waiter<T, E: Error> {
166166
func makeTask(delay: TimeInterval = 0, onCancel: T) -> Task<T, Never> where E == Never {
167167
Task {
168168
await Task.sleep(seconds: delay)
169+
#if compiler(>=6.0)
170+
return await withIdentifiableContinuation {
171+
addContinuation($0)
172+
} onCancel: { id in
173+
Task { await self.resumeID(id, returning: onCancel) }
174+
}
175+
#else
169176
return await withIdentifiableContinuation(isolation: self) {
170177
addContinuation($0)
171178
} onCancel: { id in
172179
Task { await self.resumeID(id, returning: onCancel) }
173180
}
181+
#endif
174182
}
175183
}
176184

177185
func makeTask(delay: TimeInterval = 0, onCancel: Result<T, E>) -> Task<T, any Error> where E == any Error {
178186
Task {
179187
await Task.sleep(seconds: delay)
188+
#if compiler(>=6.0)
189+
return try await withIdentifiableThrowingContinuation {
190+
addContinuation($0)
191+
} onCancel: { id in
192+
Task { await self.resumeID(id, with: onCancel) }
193+
}
194+
#else
180195
return try await withIdentifiableThrowingContinuation(isolation: self) {
181196
addContinuation($0)
182197
} onCancel: { id in
183198
Task { await self.resumeID(id, with: onCancel) }
184199
}
200+
#endif
185201
}
186202
}
187203

@@ -234,13 +250,22 @@ private extension Actor {
234250
body: @Sendable (IdentifiableContinuation<T, Never>) -> Void,
235251
onCancel handler: @Sendable (IdentifiableContinuation<T, Never>.ID) -> Void = { _ in }
236252
) async -> T {
253+
#if compiler(>=6.0)
254+
await withIdentifiableContinuation(body: body, onCancel: handler)
255+
#else
237256
await withIdentifiableContinuation(isolation: self, body: body, onCancel: handler)
257+
#endif
238258
}
239259

240260
func throwingIdentifiableContinuation<T: Sendable>(
241261
body: @Sendable (IdentifiableContinuation<T, any Error>) -> Void,
242262
onCancel handler: @Sendable (IdentifiableContinuation<T, any Error>.ID) -> Void = { _ in }
243263
) async throws -> T {
264+
#if compiler(>=6.0)
265+
try await withIdentifiableThrowingContinuation(body: body, onCancel: handler)
266+
#else
244267
try await withIdentifiableThrowingContinuation(isolation: self, body: body, onCancel: handler)
268+
#endif
269+
245270
}
246271
}

0 commit comments

Comments
 (0)