Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
wip
  • Loading branch information
stephencelis committed Jul 17, 2023
commit 1bc99b0c2815a1c199f7672d371f157dc20a24c8
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ let package = Package(
targets: ["ConcurrencyExtras"]
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
],
targets: [
.target(
name: "ConcurrencyExtras"
Expand All @@ -32,6 +29,13 @@ let package = Package(
]
)

#if !os(Windows)
// Add the documentation compiler plugin if possible
package.dependencies.append(
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
)
#endif

//for target in package.targets {
// target.swiftSettings = target.swiftSettings ?? []
// target.swiftSettings?.append(
Expand Down
224 changes: 112 additions & 112 deletions Sources/ConcurrencyExtras/ActorIsolated.swift
Original file line number Diff line number Diff line change
@@ -1,112 +1,112 @@
///// A generic wrapper for isolating a mutable value to an actor.
/////
///// This type is most useful when writing tests for when you want to inspect what happens inside an
///// async operation.
/////
///// For example, suppose you have a feature such that when a button is tapped you track some
///// analytics:
/////
///// ```swift
///// struct AnalyticsClient {
///// var track: (String) async -> Void
///// }
/////
///// class FeatureModel: ObservableObject {
///// let analytics: AnalyticsClient
///// // ...
///// func buttonTapped() {
///// // ...
///// await self.analytics.track("Button tapped")
///// }
///// }
///// ```
/////
///// Then, in tests we can construct an analytics client that appends events to a mutable array
///// rather than actually sending events to an analytics server. However, in order to do this in a
///// safe way we should use an actor, and `ActorIsolated` makes this easy:
/////
///// ```swift
///// func testAnalytics() async {
///// let events = ActorIsolated<[String]>([])
///// let analytics = AnalyticsClient(
///// track: { event in await events.withValue { $0.append(event) } }
///// )
///// let model = FeatureModel(analytics: analytics)
///// model.buttonTapped()
///// await events.withValue {
///// XCTAssertEqual($0, ["Button tapped"])
///// }
///// }
///// ```
/////
///// To synchronously isolate a value, see ``LockIsolated``.
//public final actor ActorIsolated<Value> {
// /// The actor-isolated value.
// public var value: Value
//
// /// Initializes actor-isolated state around a value.
// ///
// /// - Parameter value: A value to isolate in an actor.
// public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
// self.value = try value()
// }
//
// /// Perform an operation with isolated access to the underlying value.
// ///
// /// Useful for modifying a value in a single transaction.
// ///
// /// ```swift
// /// // Isolate an integer for concurrent read/write access:
// /// let count = ActorIsolated(0)
// ///
// /// func increment() async {
// /// // Safely increment it:
// /// await self.count.withValue { $0 += 1 }
// /// }
// /// ```
// ///
// /// > Tip: Because XCTest assertions don't play nicely with Swift concurrency, `withValue` also
// /// > provides a handy interface to peek at an actor-isolated value and assert against it:
// /// >
// /// > ```swift
// /// > let didOpenSettings = ActorIsolated(false)
// /// > let model = withDependencies {
// /// > $0.openSettings = { await didOpenSettings.setValue(true) }
// /// > } operation: {
// /// > FeatureModel()
// /// > }
// /// > await model.settingsButtonTapped()
// /// > await didOpenSettings.withValue { XCTAssertTrue($0) }
// /// > ```
// ///
// /// - Parameters: operation: An operation to be performed on the actor with the underlying value.
// /// - Returns: The result of the operation.
// public func withValue<T>(
// _ operation: @Sendable (inout Value) throws -> T
// ) rethrows -> T {
// var value = self.value
// defer { self.value = value }
// return try operation(&value)
// }
//
// /// Overwrite the isolated value with a new value.
// ///
// /// ```swift
// /// // Isolate an integer for concurrent read/write access:
// /// let count = ActorIsolated(0)
// ///
// /// func reset() async {
// /// // Reset it:
// /// await self.count.setValue(0)
// /// }
// /// ```
// ///
// /// > Tip: Use ``withValue(_:)`` instead of `setValue` if the value being set is derived from the
// /// > current value. This isolates the entire transaction and avoids data races between reading
// /// > and writing the value.
// ///
// /// - Parameter newValue: The value to replace the current isolated value with.
// public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
// self.value = try newValue()
// }
//}
/// A generic wrapper for isolating a mutable value to an actor.
///
/// This type is most useful when writing tests for when you want to inspect what happens inside an
/// async operation.
///
/// For example, suppose you have a feature such that when a button is tapped you track some
/// analytics:
///
/// ```swift
/// struct AnalyticsClient {
/// var track: (String) async -> Void
/// }
///
/// class FeatureModel: ObservableObject {
/// let analytics: AnalyticsClient
/// // ...
/// func buttonTapped() {
/// // ...
/// await self.analytics.track("Button tapped")
/// }
/// }
/// ```
///
/// Then, in tests we can construct an analytics client that appends events to a mutable array
/// rather than actually sending events to an analytics server. However, in order to do this in a
/// safe way we should use an actor, and `ActorIsolated` makes this easy:
///
/// ```swift
/// func testAnalytics() async {
/// let events = ActorIsolated<[String]>([])
/// let analytics = AnalyticsClient(
/// track: { event in await events.withValue { $0.append(event) } }
/// )
/// let model = FeatureModel(analytics: analytics)
/// model.buttonTapped()
/// await events.withValue {
/// XCTAssertEqual($0, ["Button tapped"])
/// }
/// }
/// ```
///
/// To synchronously isolate a value, see ``LockIsolated``.
public final actor ActorIsolated<Value> {
/// The actor-isolated value.
public var value: Value

/// Initializes actor-isolated state around a value.
///
/// - Parameter value: A value to isolate in an actor.
public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self.value = try value()
}

/// Perform an operation with isolated access to the underlying value.
///
/// Useful for modifying a value in a single transaction.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// let count = ActorIsolated(0)
///
/// func increment() async {
/// // Safely increment it:
/// await self.count.withValue { $0 += 1 }
/// }
/// ```
///
/// > Tip: Because XCTest assertions don't play nicely with Swift concurrency, `withValue` also
/// > provides a handy interface to peek at an actor-isolated value and assert against it:
/// >
/// > ```swift
/// > let didOpenSettings = ActorIsolated(false)
/// > let model = withDependencies {
/// > $0.openSettings = { await didOpenSettings.setValue(true) }
/// > } operation: {
/// > FeatureModel()
/// > }
/// > await model.settingsButtonTapped()
/// > await didOpenSettings.withValue { XCTAssertTrue($0) }
/// > ```
///
/// - Parameters: operation: An operation to be performed on the actor with the underlying value.
/// - Returns: The result of the operation.
public func withValue<T>(
_ operation: @Sendable (inout Value) throws -> T
) rethrows -> T {
var value = self.value
defer { self.value = value }
return try operation(&value)
}

/// Overwrite the isolated value with a new value.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// let count = ActorIsolated(0)
///
/// func reset() async {
/// // Reset it:
/// await self.count.setValue(0)
/// }
/// ```
///
/// > Tip: Use ``withValue(_:)`` instead of `setValue` if the value being set is derived from the
/// > current value. This isolates the entire transaction and avoids data races between reading
/// > and writing the value.
///
/// - Parameter newValue: The value to replace the current isolated value with.
public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
self.value = try newValue()
}
}
Loading