diff --git a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift index 25dbd49f..99056c34 100644 --- a/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift +++ b/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift @@ -65,6 +65,22 @@ public struct AsyncAdjacentPairsSequence: AsyncSequence { self.base = base } + @available(AsyncAlgorithms 1.1, *) + mutating public func next( + isolation actor: isolated (any Actor)? + ) async throws(Base.Failure) -> (Base.Element, Base.Element)? { + if previousElement == nil { + previousElement = try await base.next(isolation: actor) + } + + guard let previous = previousElement, let next = try await base.next(isolation: actor) else { + return nil + } + + previousElement = next + return (previous, next) + } + @inlinable public mutating func next() async rethrows -> (Base.Element, Base.Element)? { if previousElement == nil { diff --git a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift index 57d81836..d8ee7093 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ReportingSequence.swift @@ -46,11 +46,13 @@ final class ReportingSequence: Sequence, IteratorProtocol { final class ReportingAsyncSequence: AsyncSequence, AsyncIteratorProtocol, @unchecked Sendable { enum Event: Equatable, CustomStringConvertible { case next + case nextIsolation case makeAsyncIterator var description: String { switch self { case .next: return "next()" + case .nextIsolation: return "next(isolation:)" case .makeAsyncIterator: return "makeAsyncIterator()" } } @@ -63,6 +65,14 @@ final class ReportingAsyncSequence: AsyncSequence, AsyncItera self.elements = elements } + func next(isolation actor: isolated (any Actor)?) async -> Element? { + events.append(.nextIsolation) + guard elements.count > 0 else { + return nil + } + return elements.removeFirst() + } + func next() async -> Element? { events.append(.next) guard elements.count > 0 else { diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift index 9b6fdf3c..6adb4def 100644 --- a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -95,4 +95,34 @@ final class TestAdjacentPairs: XCTestCase { task.cancel() await fulfillment(of: [finished], timeout: 1.0) } + + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) + @MainActor + func test_adjacentPairs_next_with_nil_isolation_produces_correct_pairs() async throws { + let reportingSequence = ReportingAsyncSequence([1, 2]) + let adjacentPairs = reportingSequence.adjacentPairs() + let iterated = expectation(description: "iterates once") + + let t = Task.immediate { + for await (previous, current) in adjacentPairs { + XCTAssertEqual(previous, 1) + XCTAssertEqual(current, 2) + iterated.fulfill() + } + } + + XCTAssert(reportingSequence.elements.isEmpty, "With a proper implementation of next(isolaton:), elements should be immediately consumed") + let nonIsolatedNextCalled = reportingSequence.events.contains { + switch $0 { + case .next: + true + default: + false + } + } + XCTAssertFalse(nonIsolatedNextCalled, "Next without isolation should not be called with a fully isolated pipeline") + + await fulfillment(of: [iterated], timeout: 1.0) + t.cancel() + } }