From 57427e984cf56da3fe25c5f9886717310db45680 Mon Sep 17 00:00:00 2001 From: kmcbride Date: Fri, 16 Feb 2024 08:22:55 -0800 Subject: [PATCH] Add asynchronous effect handling routes --- .../EffectRouterDSL+Concurrency.swift | 334 ++++++++++++++++++ .../EffectRouterDSL+ConcurrencyTests.swift | 315 +++++++++++++++++ 2 files changed, 649 insertions(+) create mode 100644 MobiusCore/Source/EffectHandlers/EffectRouterDSL+Concurrency.swift create mode 100644 MobiusCore/Test/EffectHandlers/EffectRouterDSL+ConcurrencyTests.swift diff --git a/MobiusCore/Source/EffectHandlers/EffectRouterDSL+Concurrency.swift b/MobiusCore/Source/EffectHandlers/EffectRouterDSL+Concurrency.swift new file mode 100644 index 00000000..98406863 --- /dev/null +++ b/MobiusCore/Source/EffectHandlers/EffectRouterDSL+Concurrency.swift @@ -0,0 +1,334 @@ +// Copyright 2019-2024 Spotify AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an asynchronous closure. + /// + /// - Parameter handler: An asynchronous closure receiving the `Effect`'s parameters as input. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping (EffectParameters) async -> Void) -> EffectRouter { + to { parameters, _ in + await handler(parameters) + } + } + + /// Routes the `Effect` to an asynchronous throwing closure. + /// + /// - Parameter handler: An asynchronous throwing closure receiving the `Effect`'s parameters as input. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping (EffectParameters) async throws -> Void) -> EffectRouter { + to { parameters, _ in + try await handler(parameters) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an asynchronous closure. + /// + /// - Parameter handler: An asynchronous closure. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping () async -> Void) -> EffectRouter { + to { _, _ in + await handler() + } + } + + /// Routes the `Effect` to an asynchronous throwing closure. + /// + /// - Parameter handler: An asynchronous throwing closure. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping () async throws -> Void) -> EffectRouter { + to { _, _ in + try await handler() + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an asynchronous closure producing a single `Event`. + /// + /// - Parameter handler: An asynchronous closure receiving the `Effect`'s parameters as input and producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping (EffectParameters) async -> Event) -> EffectRouter { + to { parameters, callback in + await callback(handler(parameters)) + } + } + + /// Routes the `Effect` to an asynchronous throwing closure producing a single `Event`. + /// + /// - Parameter handler: An asynchronous throwing closure receiving the `Effect`'s parameters as input and producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping (EffectParameters) async throws -> Event) -> EffectRouter { + to { parameters, callback in + try await callback(handler(parameters)) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an asynchronous closure producing a single `Event`. + /// + /// - Parameter handler: An asynchronous closure producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping () async -> Event) -> EffectRouter { + to { _, callback in + await callback(handler()) + } + } + + /// Routes the `Effect` to an asynchronous throwing closure producing a single `Event`. + /// + /// - Parameter handler: An asynchronous throwing closure producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to(_ handler: @escaping () async throws -> Event) -> EffectRouter { + to { _, callback in + try await callback(handler()) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an asynchronous closure producing a sequence of `Event`s. + /// + /// - Parameter handler: An asynchronous closure receiving the `Effect`'s parameters as input and producing an `AsyncSequence` of `Event`s as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ handler: @escaping (EffectParameters) async -> S + ) -> EffectRouter where S.Element == Event { + to { parameters, callback in + for try await output in await handler(parameters) { + callback(output) + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an asynchronous closure producing a sequence of `Event`s. + /// + /// - Parameter handler: An asynchronous closure producing an `AsyncSequence` of `Event`s as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ handler: @escaping () async -> S + ) -> EffectRouter where S.Element == Event { + to { _, callback in + for try await output in await handler() { + callback(output) + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an `Actor`'s closure. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure receiving the `Effect`'s parameters as input. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> (EffectParameters) async -> Void + ) -> EffectRouter { + to { parameters, _ in + await handler(actor)(parameters) + } + } + + /// Routes the `Effect` to an `Actor`'s throwing closure. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling throwing closure. + /// - Parameter handler: An isolated asynchronous throwing closure receiving the `Effect`'s parameters as input. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> (EffectParameters) async throws -> Void + ) -> EffectRouter { + to { parameters, _ in + try await handler(actor)(parameters) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an `Actor`'s closure. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> () async -> Void + ) -> EffectRouter { + to { _, _ in + await handler(actor)() + } + } + + /// Routes the `Effect` to an `Actor`'s closure. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling throwing closure. + /// - Parameter handler: An isolated asynchronous throwing closure. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> () async throws -> Void + ) -> EffectRouter { + to { _, _ in + try await handler(actor)() + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an `Actor`'s closure producing a single `Event`. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure receiving the `Effect`'s parameters as input and producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> (EffectParameters) async -> Event + ) -> EffectRouter { + to { parameters, callback in + await callback(handler(actor)(parameters)) + } + } + + /// Routes the `Effect` to an `Actor`'s throwing closure producing a single `Event`. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling throwing closure. + /// - Parameter handler: An isolated asynchronous throwing closure receiving the `Effect`'s parameters as input and producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> (EffectParameters) async throws -> Event + ) -> EffectRouter { + to { parameters, callback in + try await callback(handler(actor)(parameters)) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an `Actor`'s closure producing a single `Event`. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> () async -> Event + ) -> EffectRouter { + to { _, callback in + await callback(handler(actor)()) + } + } + + /// Routes the `Effect` to an `Actor`'s throwing closure producing a single `Event`. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling throwing closure. + /// - Parameter handler: An isolated asynchronous throwing closure producing a single `Event` as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> () async throws -> Event + ) -> EffectRouter { + to { _, callback in + try await callback(handler(actor)()) + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter { + /// Routes the `Effect` to an `Actor`'s closure producing a sequence of `Event`s. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure receiving the `Effect`'s parameters as input and producing an `AsyncSequence` of `Event`s as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> (EffectParameters) async -> S + ) -> EffectRouter where S.Element == Event { + to { parameters, callback in + for try await output in await handler(actor)(parameters) { + callback(output) + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension _PartialEffectRouter where EffectParameters == Void { + /// Routes the `Effect` to an `Actor`'s closure producing a sequence of `Event`s. + /// + /// - Parameter actor: An `Actor` providing the `Effect` handling closure. + /// - Parameter handler: An isolated asynchronous closure producing an `AsyncSequence` of `Event`s as output. + /// - Returns: An `EffectRouter` that includes a handler for the given `Effect`. + func to( + _ actor: A, + _ handler: @escaping (isolated A) -> () async -> S + ) -> EffectRouter where S.Element == Event { + to { _, callback in + for try await output in await handler(actor)() { + callback(output) + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +private extension _PartialEffectRouter { + func to( + _ handler: @Sendable @escaping (EffectParameters, Consumer) async -> Void + ) -> EffectRouter { + to { parameters, callback in + let task = Task.detached { + defer { callback.end() } + await handler(parameters, callback.send) + } + + return AnonymousDisposable { + task.cancel() + } + } + } + + func to( + _ handler: @Sendable @escaping (EffectParameters, Consumer) async throws -> Void + ) -> EffectRouter { + to { parameters, callback in + let task = Task.detached { + defer { callback.end() } + try await handler(parameters, callback.send) + } + + return AnonymousDisposable { + task.cancel() + } + } + } +} diff --git a/MobiusCore/Test/EffectHandlers/EffectRouterDSL+ConcurrencyTests.swift b/MobiusCore/Test/EffectHandlers/EffectRouterDSL+ConcurrencyTests.swift new file mode 100644 index 00000000..c948a165 --- /dev/null +++ b/MobiusCore/Test/EffectHandlers/EffectRouterDSL+ConcurrencyTests.swift @@ -0,0 +1,315 @@ +// Copyright 2019-2024 Spotify AB. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import MobiusCore +import Nimble +import Quick + +// swiftlint:disable type_body_length file_length + +private enum Effect { + case effect1 + case effect2(param1: Int) + case effect3(param1: Int, param2: String) +} + +private enum Event { + case eventForEffect1 + case eventForEffect2 + case eventForEffect3 +} + +final class EffectRouterDSL_ConcurrencyTests: QuickSpec { + // swiftlint:disable function_body_length + override func spec() { + describe("EffectRouter DSL") { + beforeEach { + effectPerformedCount.value = 0 + } + + context("routing to a side-effecting function") { + it("supports async handlers") { + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(handlerForEffect1) + .routeCase(Effect.effect2).to(handlerForEffect2) + .routeCase(Effect.effect3).to(handlerForEffect3) + .asConnectable + .connect { _ in } + + routerConnection.accept(.effect1) + expect(effectPerformedCount.value).toEventually(equal(1)) + + routerConnection.accept(.effect2(param1: 123)) + expect(effectPerformedCount.value).toEventually(equal(2)) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(effectPerformedCount.value).toEventually(equal(3)) + } + + it("supports async throwing handlers") { + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(throwingHandlerForEffect1) + .routeCase(Effect.effect2).to(throwingHandlerForEffect2) + .routeCase(Effect.effect3).to(throwingHandlerForEffect3) + .asConnectable + .connect { _ in } + + routerConnection.accept(.effect1) + expect(effectPerformedCount.value).toEventually(equal(1)) + + routerConnection.accept(.effect2(param1: 123)) + expect(effectPerformedCount.value).toEventually(equal(2)) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(effectPerformedCount.value).toEventually(equal(3)) + } + + it("supports isolated async handlers") { + let actor = TestActor() + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(actor, TestActor.handlerForEffect1) + .routeCase(Effect.effect2).to(actor, TestActor.handlerForEffect2) + .routeCase(Effect.effect3).to(actor, TestActor.handlerForEffect3) + .asConnectable + .connect { _ in } + + routerConnection.accept(.effect1) + expect(effectPerformedCount.value).toEventually(equal(1)) + + routerConnection.accept(.effect2(param1: 123)) + expect(effectPerformedCount.value).toEventually(equal(2)) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(effectPerformedCount.value).toEventually(equal(3)) + } + + it("supports isolated async throwing handlers") { + let actor = TestActor() + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(actor, TestActor.throwingHandlerForEffect1) + .routeCase(Effect.effect2).to(actor, TestActor.throwingHandlerForEffect2) + .routeCase(Effect.effect3).to(actor, TestActor.throwingHandlerForEffect3) + .asConnectable + .connect { _ in } + + routerConnection.accept(.effect1) + expect(effectPerformedCount.value).toEventually(equal(1)) + + routerConnection.accept(.effect2(param1: 123)) + expect(effectPerformedCount.value).toEventually(equal(2)) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(effectPerformedCount.value).toEventually(equal(3)) + } + } + + context("routing to a event-returning function") { + it("supports async handlers") { + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(producingHandlerForEffect1) + .routeCase(Effect.effect2).to(producingHandlerForEffect2) + .routeCase(Effect.effect3).to(producingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2, .eventForEffect3])) + } + + it("supports async throwing handlers") { + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(throwingProducingHandlerForEffect1) + .routeCase(Effect.effect2).to(throwingProducingHandlerForEffect2) + .routeCase(Effect.effect3).to(throwingProducingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2, .eventForEffect3])) + } + + it("supports isolated async handlers") { + let actor = TestActor() + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(actor, TestActor.producingHandlerForEffect1) + .routeCase(Effect.effect2).to(actor, TestActor.producingHandlerForEffect2) + .routeCase(Effect.effect3).to(actor, TestActor.producingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2, .eventForEffect3])) + } + + it("supports isolated async throwing handlers") { + let actor = TestActor() + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(actor, TestActor.throwingProducingHandlerForEffect1) + .routeCase(Effect.effect2).to(actor, TestActor.throwingProducingHandlerForEffect2) + .routeCase(Effect.effect3).to(actor, TestActor.throwingProducingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect2, .eventForEffect3])) + } + } + + context("routing to a sequence-returning function") { + it("supports async handlers") { + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(sequencingHandlerForEffect1) + .routeCase(Effect.effect2).to(sequencingHandlerForEffect2) + .routeCase(Effect.effect3).to(sequencingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1, .eventForEffect2, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1, .eventForEffect2, .eventForEffect2, .eventForEffect3, .eventForEffect3])) + } + + it("supports isolated async handlers") { + let actor = TestActor() + let events: Synchronized<[Event]> = .init(value: []) + let routerConnection = EffectRouter() + .routeCase(Effect.effect1).to(actor, TestActor.sequencingHandlerForEffect1) + .routeCase(Effect.effect2).to(actor, TestActor.sequencingHandlerForEffect2) + .routeCase(Effect.effect3).to(actor, TestActor.sequencingHandlerForEffect3) + .asConnectable + .connect { event in events.mutate { events in events.append(event) } } + + routerConnection.accept(.effect1) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1])) + + routerConnection.accept(.effect2(param1: 123)) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1, .eventForEffect2, .eventForEffect2])) + + routerConnection.accept(.effect3(param1: 123, param2: "Foo")) + expect(events.value).toEventually(equal([.eventForEffect1, .eventForEffect1, .eventForEffect2, .eventForEffect2, .eventForEffect3, .eventForEffect3])) + } + } + } + } +} + +private var effectPerformedCount = Synchronized(value: 0) + +private func handlerForEffect1() async { effectPerformedCount.value += 1 } +private func throwingHandlerForEffect1() async throws { effectPerformedCount.value += 1 } +private func producingHandlerForEffect1() async -> Event { .eventForEffect1 } +private func throwingProducingHandlerForEffect1() async throws -> Event { .eventForEffect1 } +private func sequencingHandlerForEffect1() async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect1) + continuation.yield(.eventForEffect1) + continuation.finish() + } +} + +private func handlerForEffect2(param1: Int) async { effectPerformedCount.value += 1 } +private func throwingHandlerForEffect2(param1: Int) async throws { effectPerformedCount.value += 1 } +private func producingHandlerForEffect2(param1: Int) async -> Event { .eventForEffect2 } +private func throwingProducingHandlerForEffect2(param1: Int) async throws -> Event { .eventForEffect2 } +private func sequencingHandlerForEffect2(param1: Int) async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect2) + continuation.yield(.eventForEffect2) + continuation.finish() + } +} + +private func handlerForEffect3(params: (one: Int, two: String)) async { effectPerformedCount.value += 1 } +private func throwingHandlerForEffect3(params: (one: Int, two: String)) async throws { effectPerformedCount.value += 1 } +private func producingHandlerForEffect3(params: (one: Int, two: String)) async throws -> Event { .eventForEffect3 } +private func throwingProducingHandlerForEffect3(params: (one: Int, two: String)) async -> Event { .eventForEffect3 } +private func sequencingHandlerForEffect3(params: (one: Int, two: String)) async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect3) + continuation.yield(.eventForEffect3) + continuation.finish() + } +} + +private final actor TestActor { + func handlerForEffect1() async { effectPerformedCount.value += 1 } + func throwingHandlerForEffect1() async throws { effectPerformedCount.value += 1 } + func producingHandlerForEffect1() async -> Event { .eventForEffect1 } + func throwingProducingHandlerForEffect1() async throws -> Event { .eventForEffect1 } + func sequencingHandlerForEffect1() async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect1) + continuation.yield(.eventForEffect1) + continuation.finish() + } + } + + func handlerForEffect2(param1: Int) async { effectPerformedCount.value += 1 } + func throwingHandlerForEffect2(param1: Int) async throws { effectPerformedCount.value += 1 } + func producingHandlerForEffect2(param1: Int) async -> Event { .eventForEffect2 } + func throwingProducingHandlerForEffect2(param1: Int) async throws -> Event { .eventForEffect2 } + func sequencingHandlerForEffect2(param1: Int) async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect2) + continuation.yield(.eventForEffect2) + continuation.finish() + } + } + + func handlerForEffect3(params: (one: Int, two: String)) async { effectPerformedCount.value += 1 } + func throwingHandlerForEffect3(params: (one: Int, two: String)) async throws { effectPerformedCount.value += 1 } + func producingHandlerForEffect3(params: (one: Int, two: String)) async throws -> Event { .eventForEffect3 } + func throwingProducingHandlerForEffect3(params: (one: Int, two: String)) async -> Event { .eventForEffect3 } + func sequencingHandlerForEffect3(params: (one: Int, two: String)) async -> AsyncStream { + AsyncStream { continuation in + continuation.yield(.eventForEffect3) + continuation.yield(.eventForEffect3) + continuation.finish() + } + } +}