diff --git a/Sources/NIOCore/NIOLoopBound.swift b/Sources/NIOCore/NIOLoopBound.swift index ad216fd5c7..2d0bf87874 100644 --- a/Sources/NIOCore/NIOLoopBound.swift +++ b/Sources/NIOCore/NIOLoopBound.swift @@ -175,4 +175,24 @@ public final class NIOLoopBoundBox: @unchecked Sendable { yield &self._value } } + + /// Safely access and potentially modify the contained value with a closure. + /// + /// This method provides a way to perform operations on the contained value while ensuring + /// thread safety through EventLoop verification. The closure receives an `inout` parameter + /// allowing both read and write access to the value. + /// + /// - Parameter handler: A closure that receives an `inout` reference to the contained value. + /// The closure can read from and write to this value. Any modifications made within the + /// closure will be reflected in the box after the closure completes, even if the closure throws. + /// - Returns: The value returned by the `handler` closure. + /// - Note: This method is particularly useful when you need to perform read and write operations + /// on the value because it reduces the on EventLoop checks. + @inlinable + public func withValue( + _ handler: (inout Value) throws(Failure) -> Success + ) throws(Failure) -> Success { + self.eventLoop.preconditionInEventLoop() + return try handler(&self._value) + } } diff --git a/Tests/NIOPosixTests/NIOLoopBoundTests.swift b/Tests/NIOPosixTests/NIOLoopBoundTests.swift index e50ccf79c4..30f260ff33 100644 --- a/Tests/NIOPosixTests/NIOLoopBoundTests.swift +++ b/Tests/NIOPosixTests/NIOLoopBoundTests.swift @@ -116,6 +116,36 @@ final class NIOLoopBoundTests: XCTestCase { XCTAssertTrue(loopBoundBox.value.mutateInPlace()) } + #if compiler(>=6.0) + func testWithValue() { + var expectedValue = 0 + let loopBound = NIOLoopBoundBox(expectedValue, eventLoop: loop) + for value in 1...100 { + loopBound.withValue { boundValue in + XCTAssertEqual(boundValue, expectedValue) + boundValue = value + expectedValue = value + } + } + XCTAssertEqual(100, loopBound.value) + } + + func testWithValueRethrows() { + struct TestError: Error {} + + let loopBound = NIOLoopBoundBox(0, eventLoop: loop) + XCTAssertThrowsError( + try loopBound.withValue { boundValue in + XCTAssertEqual(0, boundValue) + boundValue = 10 + throw TestError() + } + ) + + XCTAssertEqual(10, loopBound.value, "Ensure value is set even if we throw") + } + #endif + // MARK: - Helpers func sendableBlackhole(_ sendableThing: S) {}