Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 4 additions & 5 deletions WorkflowCombine/Sources/PublisherWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ import Combine
import Foundation
import Workflow

struct PublisherWorkflow<WorkflowPublisher: Publisher>: Workflow where WorkflowPublisher.Failure == Never {
typealias Output = WorkflowPublisher.Output
struct PublisherWorkflow<Output>: Workflow {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This change has the side effect of making this worker only unique against the output of a publisher rather than the concrete publisher type. This can result in two different workers overlapping if they do not specify a key and have the same output type (e.g. Bool).

This is already true for WorkflowReactiveSwift but is a new change for WorkflowCombine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some ai suggestions related to this:

I dug further on this and added a repro test showing that this change really does alter runtime behavior: switching between different concrete publisher types with the same output/key no longer forces a resubscribe.

That said, I couldn’t find a strong real-world breakage in ios-register, and WorkflowReactiveSwift already has effectively this behavior today. So I think the important question is whether we want WorkflowCombine to intentionally match those semantics.

If yes, I think this is probably fine, but it should be treated as an explicit behavior change rather than incidental test cleanup:

  • keep/add the repro test
  • document the caveat similarly to WorkflowReactiveSwift

If no, then we should preserve the existing concrete-publisher identity and keep this PR scoped to testing ergonomics.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - this 100% would break something relying on this, but since we haven't used WorkflowCombine nearly as much as we use WorkflowReactiveSwift it should be relatively safe. The framework will throw an assertion if there's overlap

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also asked Claude to investigate and found that every usage in register erases to AnyPublisher:

So the behavioral concern — two different concrete publisher types collapsing into the same workflow identity — doesn't apply in practice. Everyone is already erasing to AnyPublisher before calling .running(), which means the concrete type parameter is already AnyPublisher<Output, Never> at runtime today.

typealias State = Void
typealias Rendering = Void

let publisher: WorkflowPublisher
let publisher: AnyPublisher<Output, Never>

init(publisher: WorkflowPublisher) {
self.publisher = publisher
init<WorkflowPublisher: Publisher>(publisher: WorkflowPublisher) where WorkflowPublisher.Failure == Never, WorkflowPublisher.Output == Output {
self.publisher = publisher.eraseToAnyPublisher()
}

func render(state: State, context: RenderContext<Self>) -> Rendering {
Expand Down
25 changes: 23 additions & 2 deletions WorkflowCombine/Testing/PublisherTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ import XCTest
@testable import WorkflowCombine

extension RenderTester {
/// Expect a `Publisher`-based Workflow.
///
/// `PublisherWorkflow` is used to subscribe to `Publisher`s.
///
/// - Parameters:
/// - producingOutput: An output that will be returned when this worker is requested, if any.
/// - key: Key to expect this `Workflow` to be rendered with.
public func expectPublisher<Output>(
producingOutput output: Output? = nil,
key: String = ""
) -> RenderTester<WorkflowType> {
expectWorkflow(
type: PublisherWorkflow<Output>.self,
key: key,
producingRendering: (),
producingOutput: output,
assertions: { _ in }
)
}

/// Expect a `Publisher`-based Workflow.
///
/// `PublisherWorkflow` is used to subscribe to `Publisher`s.
Expand All @@ -31,21 +51,22 @@ extension RenderTester {
/// - publisher: Type of the Publisher-based Workflow to expect
/// - producingOutput: An output that will be returned when this worker is requested, if any.
/// - key: Key to expect this `Workflow` to be rendered with.
@available(*, deprecated, message: "Use expectPublisher(producingOutput:key:) instead")
public func expect<PublisherType: Publisher>(
publisher: PublisherType.Type,
producingOutput output: PublisherType.Output? = nil,
key: String = ""
) -> RenderTester<WorkflowType> where PublisherType.Failure == Never {
expectWorkflow(
type: PublisherWorkflow<PublisherType>.self,
type: PublisherWorkflow<PublisherType.Output>.self,
key: key,
producingRendering: (),
producingOutput: output,
assertions: { _ in }
)
}

@available(*, deprecated, renamed: "expect(publisher:producingOutput:key:)")
@available(*, deprecated, message: "Use expectPublisher(producingOutput:key:) instead")
public func expect<PublisherType: Publisher>(
publisher: PublisherType.Type,
output: PublisherType.Output,
Expand Down
17 changes: 17 additions & 0 deletions WorkflowCombine/TestingTests/PublisherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,41 @@
func testPublisherWorkflow() {
TestWorkflow()
.renderTester()
.expect(

Check warning on line 19 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 17.5]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead

Check warning on line 19 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 16.4]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead

Check warning on line 19 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 26.2]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead
publisher: Publishers.Sequence<[Int], Never>.self,
producingOutput: 1,
key: "123"
)
.render {}

TestWorkflow()
.renderTester()
.expectPublisher(
producingOutput: 1,
key: "123"
)
.render {}
}

func test_publisher_no_output() {
TestWorkflow()
.renderTester()
.expect(

Check warning on line 38 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 17.5]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead

Check warning on line 38 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 16.4]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead

Check warning on line 38 in WorkflowCombine/TestingTests/PublisherTests.swift

View workflow job for this annotation

GitHub Actions / development-tests [iOS 26.2]

'expect(publisher:producingOutput:key:)' is deprecated: Use expectPublisher(producingOutput:key:) instead
publisher: Publishers.Sequence<[Int], Never>.self,
producingOutput: nil,
key: "123"
)
.render {}
.assertNoAction()

TestWorkflow()
.renderTester()
.expectPublisher(
producingOutput: Int?.none,
key: "123"
)
.render {}
.assertNoAction()
}

struct TestWorkflow: Workflow {
Expand Down
Loading