diff --git a/Playgrounds/Operators.playground/Contents.swift b/Playgrounds/01 - Protocol Free Design.playground/Pages/01-Type Level vs Value Level.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Contents.swift rename to Playgrounds/01 - Protocol Free Design.playground/Pages/01-Type Level vs Value Level.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/timeline.xctimeline b/Playgrounds/01 - Protocol Free Design.playground/Pages/01-Type Level vs Value Level.xcplaygroundpage/timeline.xctimeline similarity index 100% rename from Playgrounds/Operators.playground/timeline.xctimeline rename to Playgrounds/01 - Protocol Free Design.playground/Pages/01-Type Level vs Value Level.xcplaygroundpage/timeline.xctimeline diff --git a/Playgrounds/Operators.playground/Pages/01-Map.xcplaygroundpage/Contents.swift b/Playgrounds/01 - Protocol Free Design.playground/Pages/02-Witnesses.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Pages/01-Map.xcplaygroundpage/Contents.swift rename to Playgrounds/01 - Protocol Free Design.playground/Pages/02-Witnesses.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/contents.xcplayground b/Playgrounds/01 - Protocol Free Design.playground/contents.xcplayground similarity index 100% rename from Playgrounds/Operators.playground/contents.xcplayground rename to Playgrounds/01 - Protocol Free Design.playground/contents.xcplayground diff --git a/Playgrounds/FreeCombine.playground/playground.xcworkspace/contents.xcworkspacedata b/Playgrounds/01 - Protocol Free Design.playground/playground.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Playgrounds/FreeCombine.playground/playground.xcworkspace/contents.xcworkspacedata rename to Playgrounds/01 - Protocol Free Design.playground/playground.xcworkspace/contents.xcworkspacedata diff --git a/Playgrounds/Publishers.playground/Contents.swift b/Playgrounds/02 - Continuation Passing Style.playground/Pages/01 - Future.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Contents.swift rename to Playgrounds/02 - Continuation Passing Style.playground/Pages/01 - Future.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/Pages/02-FlatMap.xcplaygroundpage/Contents.swift b/Playgrounds/02 - Continuation Passing Style.playground/Pages/02 - Continuation.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Pages/02-FlatMap.xcplaygroundpage/Contents.swift rename to Playgrounds/02 - Continuation Passing Style.playground/Pages/02 - Continuation.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/Pages/03-Zip.xcplaygroundpage/Contents.swift b/Playgrounds/02 - Continuation Passing Style.playground/Pages/03 - Modifications Of Continuation.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Pages/03-Zip.xcplaygroundpage/Contents.swift rename to Playgrounds/02 - Continuation Passing Style.playground/Pages/03 - Modifications Of Continuation.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/Pages/04-Reduce.xcplaygroundpage/Contents.swift b/Playgrounds/02 - Continuation Passing Style.playground/Pages/04 - Publishers.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Pages/04-Reduce.xcplaygroundpage/Contents.swift rename to Playgrounds/02 - Continuation Passing Style.playground/Pages/04 - Publishers.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Operators.playground/Pages/05-Filter.xcplaygroundpage/Contents.swift b/Playgrounds/02 - Continuation Passing Style.playground/Pages/05 - Duality of Direct Style vs Continuation Passing Style.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Operators.playground/Pages/05-Filter.xcplaygroundpage/Contents.swift rename to Playgrounds/02 - Continuation Passing Style.playground/Pages/05 - Duality of Direct Style vs Continuation Passing Style.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/02 - Continuation Passing Style.playground/contents.xcplayground b/Playgrounds/02 - Continuation Passing Style.playground/contents.xcplayground new file mode 100644 index 0000000..7627317 --- /dev/null +++ b/Playgrounds/02 - Continuation Passing Style.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Playgrounds/Operators.playground/playground.xcworkspace/contents.xcworkspacedata b/Playgrounds/02 - Continuation Passing Style.playground/playground.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Playgrounds/Operators.playground/playground.xcworkspace/contents.xcworkspacedata rename to Playgrounds/02 - Continuation Passing Style.playground/playground.xcworkspace/contents.xcworkspacedata diff --git a/Playgrounds/Publishers.playground/timeline.xctimeline b/Playgrounds/02 - Continuation Passing Style.playground/timeline.xctimeline similarity index 100% rename from Playgrounds/Publishers.playground/timeline.xctimeline rename to Playgrounds/02 - Continuation Passing Style.playground/timeline.xctimeline diff --git a/Playgrounds/FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift similarity index 98% rename from Playgrounds/FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift index 5ed7df4..0295176 100644 --- a/Playgrounds/FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift +++ b/Playgrounds/03 - FreeCombine.playground/Pages/00a-Introduction (Examples).xcplaygroundpage/Contents.swift @@ -55,8 +55,8 @@ import FreeCombine import _Concurrency Task { - let fsubject1 = await FreeCombine.PassthroughSubject(Int.self) - let fsubject2 = await FreeCombine.PassthroughSubject(String.self) + let fsubject1 = await PassthroughSubject(Int.self) + let fsubject2 = await PassthroughSubject(String.self) let fseq1 = "abcdefghijklmnopqrstuvwxyz".asyncPublisher let fseq2 = (1 ... 100).asyncPublisher diff --git a/Playgrounds/FreeCombine.playground/Pages/00b-Introduction (Requirements).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/00b-Introduction (Requirements).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/00b-Introduction (Requirements).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/00b-Introduction (Requirements).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/00c-Introduction (Supply and Demand).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/00c-Introduction (Supply and Demand).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/00c-Introduction (Supply and Demand).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/00c-Introduction (Supply and Demand).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01a-Deriving FreeCombine (Base).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01a-Deriving FreeCombine (Base).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01a-Deriving FreeCombine (Base).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01a-Deriving FreeCombine (Base).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01b-Deriving FreeCombine (enum Demand).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01b-Deriving FreeCombine (enum Demand).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01b-Deriving FreeCombine (enum Demand).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01b-Deriving FreeCombine (enum Demand).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01c-Deriving FreeCombine (async eliminates Subscription).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01c-Deriving FreeCombine (async eliminates Subscription).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01c-Deriving FreeCombine (async eliminates Subscription).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01c-Deriving FreeCombine (async eliminates Subscription).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01d-Deriving FreeCombine (simplify Subscriber to one func).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01d-Deriving FreeCombine (simplify Subscriber to one func).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01d-Deriving FreeCombine (simplify Subscriber to one func).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01d-Deriving FreeCombine (simplify Subscriber to one func).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01e-Deriving FreeCombine (eliminate Subscriber into Publisher).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01e-Deriving FreeCombine (eliminate Subscriber into Publisher).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01e-Deriving FreeCombine (eliminate Subscriber into Publisher).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01e-Deriving FreeCombine (eliminate Subscriber into Publisher).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01f-Deriving FreeCombine (add Task as Cancellation).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01f-Deriving FreeCombine (add Task as Cancellation).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01f-Deriving FreeCombine (add Task as Cancellation).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01f-Deriving FreeCombine (add Task as Cancellation).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01g-Deriving FreeCombine (cleanup).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01g-Deriving FreeCombine (cleanup).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01g-Deriving FreeCombine (cleanup).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01g-Deriving FreeCombine (cleanup).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Pages/01h-Deriving FreeCombine (convert Publisher protocol to struct).xcplaygroundpage/Contents.swift b/Playgrounds/03 - FreeCombine.playground/Pages/01h-Deriving FreeCombine (convert Publisher protocol to struct).xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/FreeCombine.playground/Pages/01h-Deriving FreeCombine (convert Publisher protocol to struct).xcplaygroundpage/Contents.swift rename to Playgrounds/03 - FreeCombine.playground/Pages/01h-Deriving FreeCombine (convert Publisher protocol to struct).xcplaygroundpage/Contents.swift diff --git a/Playgrounds/FreeCombine.playground/Resources/CombineReturnTypes.png b/Playgrounds/03 - FreeCombine.playground/Resources/CombineReturnTypes.png similarity index 100% rename from Playgrounds/FreeCombine.playground/Resources/CombineReturnTypes.png rename to Playgrounds/03 - FreeCombine.playground/Resources/CombineReturnTypes.png diff --git a/Playgrounds/FreeCombine.playground/contents.xcplayground b/Playgrounds/03 - FreeCombine.playground/contents.xcplayground similarity index 100% rename from Playgrounds/FreeCombine.playground/contents.xcplayground rename to Playgrounds/03 - FreeCombine.playground/contents.xcplayground diff --git a/Playgrounds/Publishers.playground/playground.xcworkspace/contents.xcworkspacedata b/Playgrounds/03 - FreeCombine.playground/playground.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Playgrounds/Publishers.playground/playground.xcworkspace/contents.xcworkspacedata rename to Playgrounds/03 - FreeCombine.playground/playground.xcworkspace/contents.xcworkspacedata diff --git a/Playgrounds/04 - Publishers.playground/Contents.swift b/Playgrounds/04 - Publishers.playground/Contents.swift new file mode 100644 index 0000000..9d86688 --- /dev/null +++ b/Playgrounds/04 - Publishers.playground/Contents.swift @@ -0,0 +1,3 @@ +import UIKit + +var greeting = "Hello, playground" diff --git a/Playgrounds/Publishers.playground/Pages/01-Just.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/01-Just.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/01-Just.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/01-Just.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/02-Empty.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/02-Empty.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/02-Empty.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/02-Empty.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/03-Fail.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/03-Fail.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/03-Fail.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/03-Fail.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/04-Unfolded.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/04-Unfolded.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/04-Unfolded.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/04-Unfolded.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/05-Deferred.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/05-Deferred.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/05-Deferred.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/05-Deferred.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/06-Concat.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/06-Concat.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/06-Concat.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/06-Concat.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/07-Zipped.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/07-Zipped.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/07-Zipped.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/07-Zipped.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/08-Merged.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/08-Merged.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/08-Merged.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/08-Merged.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/09-CombineLatest.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/09-CombineLatest.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/09-CombineLatest.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/09-CombineLatest.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/10-Heartbeat.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/10-Heartbeat.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/10-Heartbeat.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/10-Heartbeat.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/Pages/11-FireAndForget.xcplaygroundpage/Contents.swift b/Playgrounds/04 - Publishers.playground/Pages/11-FireAndForget.xcplaygroundpage/Contents.swift similarity index 100% rename from Playgrounds/Publishers.playground/Pages/11-FireAndForget.xcplaygroundpage/Contents.swift rename to Playgrounds/04 - Publishers.playground/Pages/11-FireAndForget.xcplaygroundpage/Contents.swift diff --git a/Playgrounds/Publishers.playground/contents.xcplayground b/Playgrounds/04 - Publishers.playground/contents.xcplayground similarity index 100% rename from Playgrounds/Publishers.playground/contents.xcplayground rename to Playgrounds/04 - Publishers.playground/contents.xcplayground diff --git a/Playgrounds/04 - Publishers.playground/playground.xcworkspace/contents.xcworkspacedata b/Playgrounds/04 - Publishers.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Playgrounds/04 - Publishers.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Playgrounds/04 - Publishers.playground/timeline.xctimeline b/Playgrounds/04 - Publishers.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/Playgrounds/04 - Publishers.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/Playgrounds/05 - Operators.playground/Contents.swift b/Playgrounds/05 - Operators.playground/Contents.swift new file mode 100644 index 0000000..9d86688 --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Contents.swift @@ -0,0 +1,3 @@ +import UIKit + +var greeting = "Hello, playground" diff --git a/Playgrounds/05 - Operators.playground/Pages/01-Map.xcplaygroundpage/Contents.swift b/Playgrounds/05 - Operators.playground/Pages/01-Map.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5275afb --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Pages/01-Map.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/Playgrounds/05 - Operators.playground/Pages/02-FlatMap.xcplaygroundpage/Contents.swift b/Playgrounds/05 - Operators.playground/Pages/02-FlatMap.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5275afb --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Pages/02-FlatMap.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/Playgrounds/05 - Operators.playground/Pages/03-Zip.xcplaygroundpage/Contents.swift b/Playgrounds/05 - Operators.playground/Pages/03-Zip.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5275afb --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Pages/03-Zip.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/Playgrounds/05 - Operators.playground/Pages/04-Reduce.xcplaygroundpage/Contents.swift b/Playgrounds/05 - Operators.playground/Pages/04-Reduce.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5275afb --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Pages/04-Reduce.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/Playgrounds/05 - Operators.playground/Pages/05-Filter.xcplaygroundpage/Contents.swift b/Playgrounds/05 - Operators.playground/Pages/05-Filter.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5275afb --- /dev/null +++ b/Playgrounds/05 - Operators.playground/Pages/05-Filter.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/Playgrounds/05 - Operators.playground/contents.xcplayground b/Playgrounds/05 - Operators.playground/contents.xcplayground new file mode 100644 index 0000000..7627317 --- /dev/null +++ b/Playgrounds/05 - Operators.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Playgrounds/05 - Operators.playground/playground.xcworkspace/contents.xcworkspacedata b/Playgrounds/05 - Operators.playground/playground.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Playgrounds/05 - Operators.playground/playground.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Playgrounds/05 - Operators.playground/timeline.xctimeline b/Playgrounds/05 - Operators.playground/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/Playgrounds/05 - Operators.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/Sources/FreeCombine/Channel/Channel.swift b/Sources/FreeCombine/Channel/Channel.swift index c5ae632..77e72d3 100644 --- a/Sources/FreeCombine/Channel/Channel.swift +++ b/Sources/FreeCombine/Channel/Channel.swift @@ -19,13 +19,11 @@ public struct Channel: AsyncSequence { public init( _: Element.Type = Element.self, - buffering: AsyncStream.Continuation.BufferingPolicy = .bufferingOldest(1), - onTermination: (@Sendable (AsyncStream.Continuation.Termination) -> Void)? = .none + buffering: AsyncStream.Continuation.BufferingPolicy = .bufferingOldest(1) ) { var localContinuation: AsyncStream.Continuation! = .none stream = .init(bufferingPolicy: buffering) { continuation in localContinuation = continuation - localContinuation.onTermination = onTermination } continuation = localContinuation } @@ -79,7 +77,6 @@ public extension Channel { func stateTask( initialState: @escaping (Self) async -> State, - onCancel: @Sendable @escaping () -> Void = { }, reducer: Reducer ) async -> StateTask { var stateTask: StateTask! @@ -88,7 +85,6 @@ public extension Channel { channel: self, initialState: initialState, onStartup: stateTaskContinuation, - onCancel: onCancel, reducer: reducer ) } @@ -98,14 +94,12 @@ public extension Channel { func stateTask( initialState: @escaping (Self) async -> State, onStartup: UnsafeContinuation? = .none, - onCancel: @Sendable @escaping () -> Void = { }, reducer: Reducer ) -> StateTask { .init( channel: self, initialState: initialState, onStartup: onStartup, - onCancel: onCancel, reducer: reducer ) } diff --git a/Sources/FreeCombine/Combinator/CombineLatestState.swift b/Sources/FreeCombine/Combinator/CombineLatestState.swift new file mode 100644 index 0000000..b53458e --- /dev/null +++ b/Sources/FreeCombine/Combinator/CombineLatestState.swift @@ -0,0 +1,120 @@ +// +// CombineLatestState.swift +// +// +// Created by Van Simmons on 6/4/22. +// +struct CombineLatestState: CombinatorState { + typealias CombinatorAction = Self.Action + enum Action { + case setLeft(AsyncStream.Result, UnsafeContinuation) + case setRight(AsyncStream.Result, UnsafeContinuation) + } + + let downstream: (AsyncStream<(Left?, Right?)>.Result) async throws -> Demand + let leftCancellable: Cancellable + let rightCancellable: Cancellable + + var mostRecentDemand: Demand + var left: (value: Left?, continuation: UnsafeContinuation)? = .none + var right: (value: Right?, continuation: UnsafeContinuation)? = .none + var leftComplete = false + var rightComplete = false + + init( + channel: Channel.Action>, + downstream: @escaping (AsyncStream<(Left?, Right?)>.Result) async throws -> Demand, + mostRecentDemand: Demand = .more, + left: Publisher, + right: Publisher + ) async { + self.downstream = downstream + self.mostRecentDemand = mostRecentDemand + self.leftCancellable = await channel.consume(publisher: left, using: CombineLatestState.Action.setLeft) + self.rightCancellable = await channel.consume(publisher: right, using: CombineLatestState.Action.setRight) + } + + static func create( + mostRecentDemand: Demand = .more, + left: Publisher, + right: Publisher + ) -> (@escaping (AsyncStream<(Left?, Right?)>.Result) async throws -> Demand) -> (Channel.Action>) async -> Self { + { downstream in { channel in + await .init(channel: channel, downstream: downstream, left: left, right: right) + } } + } + + static func complete(state: inout Self, completion: Reducer.Completion) async -> Void { + state.leftCancellable.cancel() + state.left?.continuation.resume(returning: .done) + state.rightCancellable.cancel() + state.right?.continuation.resume(returning: .done) + switch completion { + case .cancel: + _ = try? await state.downstream(.completion(.cancelled)) + case .exit, .termination: + _ = try? await state.downstream(.completion(.finished)) + case let .failure(error): + _ = try? await state.downstream(.completion(.failure(error))) + } + } + + static func reduce(`self`: inout Self, action: Self.Action) async throws -> Reducer.Effect { + guard !Task.isCancelled else { return .completion(.cancel) } + return try await `self`.reduce(action: action) + } + + private mutating func reduce( + action: Self.Action + ) async throws -> Reducer.Effect { + switch action { + case let .setLeft(leftResult, leftContinuation): + if mostRecentDemand == .done { leftContinuation.resume(returning: .done) } + else { return try await handleLeft(leftResult, leftContinuation) } + case let .setRight(rightResult, rightContinuation): + if mostRecentDemand == .done { rightContinuation.resume(returning: .done) } + else { return try await handleRight(rightResult, rightContinuation) } + } + return .none + } + + private mutating func handleLeft( + _ leftResult: AsyncStream.Result, + _ leftContinuation: UnsafeContinuation + ) async throws -> Reducer.Effect { + switch leftResult { + case let .value((value)): + left = (value, leftContinuation) + guard !Task.isCancelled else { + leftContinuation.resume(returning: .done) + return .completion(.cancel) + } + mostRecentDemand = try await downstream(.value((value, right?.value))) + leftContinuation.resume(returning: mostRecentDemand) + return .none + case .completion(_): + leftComplete = true + return leftComplete && rightComplete ? .completion(.exit) : .none + } + } + + private mutating func handleRight( + _ rightResult: AsyncStream.Result, + _ rightContinuation: UnsafeContinuation + ) async throws -> Reducer.Effect { + switch rightResult { + case let .value((value)): + right = (value, rightContinuation) + guard !Task.isCancelled else { + rightContinuation.resume(returning: .done) + return .completion(.cancel) + } + mostRecentDemand = try await downstream(.value((left?.value, value))) + rightContinuation.resume(returning: mostRecentDemand) + return .none + case .completion(_) : + rightComplete = true + return leftComplete && rightComplete ? .completion(.exit) : .none + } + } +} diff --git a/Sources/FreeCombine/Filter/DropFirst.swift b/Sources/FreeCombine/Filter/DropFirst.swift new file mode 100644 index 0000000..45dd66b --- /dev/null +++ b/Sources/FreeCombine/Filter/DropFirst.swift @@ -0,0 +1,25 @@ +// +// File.swift +// +// +// Created by Van Simmons on 6/4/22. +// +public extension Publisher { + func dropFirst( + _ count: Int = 1 + ) -> Self { + .init { continuation, downstream in + let currentValue: ValueRef = ValueRef(value: count + 1) + return self(onStartup: continuation) { r in + let current = await currentValue.value - 1 + await currentValue.set(value: max(0, current)) + switch r { + case .value: + guard current <= 0 else { return .more } + return try await downstream(r) + case let .completion(value): + return try await downstream(.completion(value)) + } } + } + } +} diff --git a/Sources/FreeCombine/Publishers/Combinator.swift b/Sources/FreeCombine/Publishers/Combinator.swift index 08c1a91..e445716 100644 --- a/Sources/FreeCombine/Publishers/Combinator.swift +++ b/Sources/FreeCombine/Publishers/Combinator.swift @@ -12,13 +12,11 @@ public protocol CombinatorState { public func Combinator( initialState: @escaping (@escaping (AsyncStream.Result) async throws -> Demand) -> (Channel) async -> State, buffering: AsyncStream.Continuation.BufferingPolicy, - onCancel: @escaping () -> Void, reducer: Reducer ) -> Publisher where State.CombinatorAction == Action { .init( initialState: initialState, buffering: buffering, - onCancel: onCancel, reducer: reducer ) } @@ -27,7 +25,6 @@ public extension Publisher { init( initialState: @escaping (@escaping (AsyncStream.Result) async throws -> Demand) -> (Channel) async -> State, buffering: AsyncStream.Continuation.BufferingPolicy, - onCancel: @escaping () -> Void, reducer: Reducer ) where State.CombinatorAction == Action { self = .init { continuation, downstream in @@ -37,10 +34,7 @@ public extension Publisher { reducer: reducer ) - return try await withTaskCancellationHandler(handler: { - stateTask.cancel() - onCancel() - }) { + return try await withTaskCancellationHandler(handler: stateTask.cancel) { continuation?.resume() guard !Task.isCancelled else { throw PublisherError.cancelled diff --git a/Sources/FreeCombine/Publishers/Combined.swift b/Sources/FreeCombine/Publishers/Combined.swift new file mode 100644 index 0000000..240e027 --- /dev/null +++ b/Sources/FreeCombine/Publishers/Combined.swift @@ -0,0 +1,104 @@ +// +// Combined.swift +// +// +// Created by Van Simmons on 6/4/22. +// + +public extension Publisher { + func combineLatest( + _ other: Publisher + ) -> Publisher<(Output?, Other?)> { + Combined(self, other) + } +} + +public func Combined( + _ left: Publisher, + _ right: Publisher +) -> Publisher<(Left?, Right?)> { + combineLatest(left, right) +} + +public func combineLatest( + _ left: Publisher, + _ right: Publisher +) -> Publisher<(Left?, Right?)> { + .init( + initialState: CombineLatestState.create(left: left, right: right), + buffering: .bufferingOldest(2), + reducer: Reducer( + onCompletion: CombineLatestState.complete, + reducer: CombineLatestState.reduce + ) + ) +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher +) -> Publisher<(A?, B?, C?)> { + combineLatest(combineLatest(one, two), three) + .map { ($0.0?.0, $0.0?.1, $0.1) } +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher, + _ four: Publisher +) -> Publisher<(A?, B?, C?, D?)> { + combineLatest(combineLatest(one, two), combineLatest(three, four)) + .map { ($0.0?.0, $0.0?.1, $0.1?.0, $0.1?.1) } +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher, + _ four: Publisher, + _ five: Publisher +) -> Publisher<(A?, B?, C?, D?, E?)> { + combineLatest(combineLatest(combineLatest(one, two), combineLatest(three, four)), five) + .map { ($0.0?.0?.0, $0.0?.0?.1, $0.0?.1?.0, $0.0?.1?.1, $0.1) } +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher, + _ four: Publisher, + _ five: Publisher, + _ six: Publisher +) -> Publisher<(A?, B?, C?, D?, E?, F?)> { + combineLatest(combineLatest(combineLatest(one, two), combineLatest(three, four)), combineLatest(five, six)) + .map { ($0.0?.0?.0, $0.0?.0?.1, $0.0?.1?.0, $0.0?.1?.1, $0.1?.0, $0.1?.1) } +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher, + _ four: Publisher, + _ five: Publisher, + _ six: Publisher, + _ seven: Publisher +) -> Publisher<(A?, B?, C?, D?, E?, F?, G?)> { + combineLatest(combineLatest(combineLatest(one, two), combineLatest(three, four)), combineLatest(combineLatest(five, six), seven)) + .map { ($0.0?.0?.0, $0.0?.0?.1, $0.0?.1?.0, $0.0?.1?.1, $0.1?.0?.0, $0.1?.0?.1, $0.1?.1) } +} + +public func combineLatest( + _ one: Publisher, + _ two: Publisher, + _ three: Publisher, + _ four: Publisher, + _ five: Publisher, + _ six: Publisher, + _ seven: Publisher, + _ eight: Publisher +) -> Publisher<(A?, B?, C?, D?, E?, F?, G?, H?)> { + combineLatest(combineLatest(combineLatest(one, two), combineLatest(three, four)), combineLatest(combineLatest(five, six), combineLatest(seven, eight))) + .map { ($0.0?.0?.0, $0.0?.0?.1, $0.0?.1?.0, $0.0?.1?.1, $0.1?.0?.0, $0.1?.0?.1, $0.1?.1?.0, $0.1?.1?.1) } +} diff --git a/Sources/FreeCombine/Publishers/Concat.swift b/Sources/FreeCombine/Publishers/Concat.swift index 4e8c59b..d785512 100644 --- a/Sources/FreeCombine/Publishers/Concat.swift +++ b/Sources/FreeCombine/Publishers/Concat.swift @@ -11,7 +11,6 @@ public extension Publisher { } public func Concat( - onCancel: @escaping () -> Void = { }, _ publishers: S ) -> Publisher where S.Element == Publisher{ .init(concatenating: publishers) @@ -19,25 +18,23 @@ public func Concat( public extension Publisher { init( - onCancel: @Sendable @escaping () -> Void = { }, concatenating publishers: S ) where S.Element == Publisher { self = .init { continuation, downstream in let flattenedDownstream = flattener(downstream) - return .init { try await withTaskCancellationHandler(handler: onCancel) { + return .init { continuation?.resume() for p in publishers { let t = await p(flattenedDownstream) guard try await t.value == .more else { return .done } } return try await downstream(.completion(.finished)) - } } + } } } } public func Concat( - onCancel: @escaping () -> Void = { }, _ publishers: Publisher... ) -> Publisher { .init(concatenating: publishers) @@ -45,15 +42,13 @@ public func Concat( public extension Publisher { init( - onCancel: @Sendable @escaping () -> Void = { }, concatenating publishers: Publisher... ) { - self = .init(onCancel: onCancel, concatenating: publishers) + self = .init(concatenating: publishers) } } public func Concat( - onCancel: @escaping () -> Void = { }, _ publishers: @escaping () async -> Publisher? ) -> Publisher { .init(flattening: publishers) @@ -61,19 +56,18 @@ public func Concat( public extension Publisher { init( - onCancel: @Sendable @escaping () -> Void = { }, flattening: @escaping () async -> Publisher? ) { self = .init { continuation, downstream in let flattenedDownstream = flattener(downstream) - return .init { try await withTaskCancellationHandler(handler: onCancel) { + return .init { continuation?.resume() while let p = await flattening() { let t = await p(flattenedDownstream) guard try await t.value == .more else { return .done } } return try await downstream(.completion(.finished)) - } } + } } } } diff --git a/Sources/FreeCombine/Publishers/Decombinator.swift b/Sources/FreeCombine/Publishers/Decombinator.swift index 86bb7ae..13fd1c8 100644 --- a/Sources/FreeCombine/Publishers/Decombinator.swift +++ b/Sources/FreeCombine/Publishers/Decombinator.swift @@ -6,22 +6,19 @@ // public extension StateTask { func publisher( - onCancel: @Sendable @escaping () -> Void = { } ) -> Publisher where State == DistributorState, Action == DistributorState.Action { - .init(onCancel: onCancel, stateTask: self) + .init(stateTask: self) } } public func Decombinator( - onCancel: @Sendable @escaping () -> Void = { }, stateTask: StateTask, DistributorState.Action> ) -> Publisher { - .init(onCancel: onCancel, stateTask: stateTask) + .init(stateTask: stateTask) } public extension Publisher { init( - onCancel: @Sendable @escaping () -> Void = { }, stateTask: StateTask, DistributorState.Action> ) { self = .init { continuation, downstream in diff --git a/Sources/FreeCombine/Publishers/Deferred.swift b/Sources/FreeCombine/Publishers/Deferred.swift index 8419924..184f58c 100644 --- a/Sources/FreeCombine/Publishers/Deferred.swift +++ b/Sources/FreeCombine/Publishers/Deferred.swift @@ -4,46 +4,36 @@ // // Created by Van Simmons on 5/18/22. // -public func Deferred( - onCancel: @Sendable @escaping () -> Void = { }, - from flattable: Publisher -) -> Publisher { - .init(onCancel: onCancel, from: flattable) +public func Deferred(from flattable: Publisher) -> Publisher { + .init(from: flattable) } extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void = { }, - from flattable: Publisher - ) { + init(from flattable: Publisher) { self = .init { continuation, downstream in - .init{ try await withTaskCancellationHandler(handler: onCancel) { + .init { continuation?.resume() return try await flattable(downstream).value - } } + } } } } public func Deferred( - onCancel: @Sendable @escaping () -> Void = { }, flattener: @escaping () async -> Publisher ) -> Publisher { - .init(onCancel: onCancel, from: flattener) + .init(from: flattener) } extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void = { }, - from flattener: @escaping () async throws -> Publisher - ) { + init(from flattener: @escaping () async throws -> Publisher) { self = .init { continuation, downstream in - .init { try await withTaskCancellationHandler(handler: onCancel) { + .init { continuation?.resume() let p = try await flattener() let c = await p(downstream) return try await c.value - } } + } } } } diff --git a/Sources/FreeCombine/Publishers/Empty.swift b/Sources/FreeCombine/Publishers/Empty.swift index 271ebb0..a56550e 100644 --- a/Sources/FreeCombine/Publishers/Empty.swift +++ b/Sources/FreeCombine/Publishers/Empty.swift @@ -5,10 +5,9 @@ // Created by Van Simmons on 4/10/22. // public func Empty( - onCancel: @Sendable @escaping () -> Void = { }, _ elementType: Element.Type = Element.self ) -> Publisher { - .init(onCancel: onCancel, elementType) + .init(elementType) } public extension Publisher { @@ -16,10 +15,7 @@ public extension Publisher { Empty(Output.self) } - init( - onCancel: @Sendable @escaping () -> Void = { }, - _: Output.Type = Output.self - ) { + init( _: Output.Type = Output.self) { self = .init { continuation, downstream in .init { continuation?.resume() diff --git a/Sources/FreeCombine/Publishers/Fail.swift b/Sources/FreeCombine/Publishers/Fail.swift index fa249fe..9e9d76c 100644 --- a/Sources/FreeCombine/Publishers/Fail.swift +++ b/Sources/FreeCombine/Publishers/Fail.swift @@ -5,24 +5,20 @@ // Created by Van Simmons on 5/18/22. // public func Fail( - onCancel: @Sendable @escaping () -> Void = { }, _ t: Element.Type = Element.self, _ e: Swift.Error ) -> Publisher { - .init(onCancel: onCancel, t, e) + .init(t, e) } public extension Publisher { init( - onCancel: @Sendable @escaping () -> Void = { }, _: Output.Type = Output.self, _ error: Swift.Error ) { self = .init { continuation, downstream in .init { - try await withTaskCancellationHandler(handler: onCancel) { - return try await downstream(.completion(.failure(error))) - } + try await downstream(.completion(.failure(error))) } } } diff --git a/Sources/FreeCombine/Publishers/FireAndForget.swift b/Sources/FreeCombine/Publishers/FireAndForget.swift index cc4a07c..5edc1bd 100644 --- a/Sources/FreeCombine/Publishers/FireAndForget.swift +++ b/Sources/FreeCombine/Publishers/FireAndForget.swift @@ -5,11 +5,10 @@ // Created by Van Simmons on 5/26/22. // public func FireAndForget( - onCancel: @Sendable @escaping () -> Void = { }, _ elementType: Element.Type = Element.self, operation: @escaping () async throws -> Void ) -> Publisher { - .init(onCancel: onCancel, elementType) + .init(elementType) } public extension Publisher { @@ -18,7 +17,6 @@ public extension Publisher { } init( - onCancel: @Sendable @escaping () -> Void = { }, _: Output.Type = Output.self, operation: @escaping () async throws -> Void ) { diff --git a/Sources/FreeCombine/Publishers/Heartbeat.swift b/Sources/FreeCombine/Publishers/Heartbeat.swift index 8005dbc..ab3a8c9 100644 --- a/Sources/FreeCombine/Publishers/Heartbeat.swift +++ b/Sources/FreeCombine/Publishers/Heartbeat.swift @@ -6,19 +6,17 @@ // import Dispatch public func Heartbeat( - onCancel: @Sendable @escaping () -> Void = { }, interval: Duration ) -> Publisher { - .init(onCancel: onCancel, interval: interval) + .init(interval: interval) } public extension Publisher where Output == UInt64 { init( - onCancel: @Sendable @escaping () -> Void = { }, interval: Duration ) { self = Publisher { continuation, downstream in - .init { try await withTaskCancellationHandler(handler: onCancel) { + .init { let startTime = DispatchTime.now().uptimeNanoseconds var ticks: UInt64 = 0 continuation?.resume() @@ -35,7 +33,7 @@ public extension Publisher where Output == UInt64 { case .more: try? await Task.sleep(nanoseconds: nextTime - currentTime) } } - } } + } } } } diff --git a/Sources/FreeCombine/Publishers/Just.swift b/Sources/FreeCombine/Publishers/Just.swift index a9c2148..af8b67b 100644 --- a/Sources/FreeCombine/Publishers/Just.swift +++ b/Sources/FreeCombine/Publishers/Just.swift @@ -4,46 +4,32 @@ // // Created by Van Simmons on 3/15/22. // -public func Just( - onCancel: @Sendable @escaping () -> Void = { }, - _ a: Element -) -> Publisher { - .init(onCancel: onCancel, a) +public func Just(_ a: Element) -> Publisher { + .init(a) } public extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void = { }, - _ a: Output - ) { + init(_ a: Output) { self = .init { continuation, downstream in .init { continuation?.resume() - return try await withTaskCancellationHandler(handler: onCancel) { - return try await downstream(.value(a)) == .more ? try await downstream(.completion(.finished)) : .done - } + return try await downstream(.value(a)) == .more ? try await downstream(.completion(.finished)) : .done } } } } -public func Just( - onCancel: @Sendable @escaping () -> Void = { }, - _ a: AsyncStream.Result -) -> Publisher { - .init(onCancel: onCancel, a) +public func Just(_ a: AsyncStream.Result) -> Publisher { + .init(a) } public extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void = { }, - _ result: AsyncStream.Result - ) { + init(_ result: AsyncStream.Result) { self = .init { continuation, downstream in - .init { try await withTaskCancellationHandler(handler: onCancel) { + .init { continuation?.resume() return try await downstream(result) == .more ? try await downstream(.completion(.finished)) : .done - } } + } } } } diff --git a/Sources/FreeCombine/Publishers/Merged.swift b/Sources/FreeCombine/Publishers/Merged.swift index b03d834..84c514f 100644 --- a/Sources/FreeCombine/Publishers/Merged.swift +++ b/Sources/FreeCombine/Publishers/Merged.swift @@ -7,43 +7,38 @@ public extension Publisher { func merge( - onCancel: @escaping () -> Void = { }, with upstream2: Publisher, _ otherUpstreams: Publisher... ) -> Publisher { - Merged(onCancel: onCancel, self, upstream2, otherUpstreams) + Merged(self, upstream2, otherUpstreams) } } public func Merged( - onCancel: @escaping () -> Void = { }, _ upstream1: Publisher, _ upstream2: Publisher, _ otherUpstreams: [Publisher] ) -> Publisher { - merge(onCancel: onCancel, publishers: upstream1, upstream2, otherUpstreams) + merge(publishers: upstream1, upstream2, otherUpstreams) } public func Merged( - onCancel: @escaping () -> Void = { }, _ upstream1: Publisher, _ upstream2: Publisher, _ otherUpstreams: Publisher... ) -> Publisher { - merge(onCancel: onCancel, publishers: upstream1, upstream2, otherUpstreams) + merge(publishers: upstream1, upstream2, otherUpstreams) } public func merge( - onCancel: @escaping () -> Void = { }, publishers upstream1: Publisher, _ upstream2: Publisher, _ otherUpstreams: Publisher... ) -> Publisher { - merge(onCancel: onCancel, publishers: upstream1, upstream2, otherUpstreams) + merge(publishers: upstream1, upstream2, otherUpstreams) } public func merge( - onCancel: @escaping () -> Void = { }, publishers upstream1: Publisher, _ upstream2: Publisher, _ otherUpstreams: [Publisher] @@ -51,7 +46,6 @@ public func merge( .init( initialState: MergeState.create(upstreams: upstream1, upstream2, otherUpstreams), buffering: .bufferingOldest(2 + otherUpstreams.count), - onCancel: onCancel, reducer: Reducer( onCompletion: MergeState.complete, disposer: { action, error in @@ -63,4 +57,3 @@ public func merge( ) ) } - diff --git a/Sources/FreeCombine/Publishers/Unfolded.swift b/Sources/FreeCombine/Publishers/Unfolded.swift index cefd1bf..cde4309 100644 --- a/Sources/FreeCombine/Publishers/Unfolded.swift +++ b/Sources/FreeCombine/Publishers/Unfolded.swift @@ -10,54 +10,41 @@ public extension Sequence { } } -public func Unfolded( - onCancel: @Sendable @escaping () -> Void = { }, - _ sequence: S -) -> Publisher { - .init(onCancel: onCancel, sequence) +public func Unfolded(_ sequence: S) -> Publisher { + .init(sequence) } public extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void, - _ sequence: S - ) where S.Element == Output { + init(_ sequence: S) where S.Element == Output { self = .init { continuation, downstream in Cancellable { continuation?.resume() - return try await withTaskCancellationHandler(handler: onCancel) { - for a in sequence { - guard !Task.isCancelled else { - return try await downstream(.completion(.failure(PublisherError.cancelled))) - } - guard try await downstream(.value(a)) == .more else { return .done } - } + for a in sequence { guard !Task.isCancelled else { return try await downstream(.completion(.failure(PublisherError.cancelled))) } - return try await downstream(.completion(.finished)) + guard try await downstream(.value(a)) == .more else { return .done } + } + guard !Task.isCancelled else { + return try await downstream(.completion(.failure(PublisherError.cancelled))) } + return try await downstream(.completion(.finished)) } } } } public func Unfolded( - onCancel: @Sendable @escaping () -> Void = { }, _ generator: @escaping () -> Element? ) -> Publisher { - .init(onCancel: onCancel, generator) + .init(generator) } public extension Publisher { - init( - onCancel: @Sendable @escaping () -> Void = { }, - _ generator: @escaping () async throws -> Output? - ) { + init(_ generator: @escaping () async throws -> Output?) { self = .init { continuation, downstream in - .init { - continuation?.resume() - return try await withTaskCancellationHandler(handler: onCancel) { + .init { + continuation?.resume() while let a = try await generator() { guard !Task.isCancelled else { return try await downstream(.completion(.failure(PublisherError.cancelled))) @@ -69,7 +56,6 @@ public extension Publisher { } return try await downstream(.completion(.finished)) } - } } } } diff --git a/Sources/FreeCombine/Publishers/Zipped.swift b/Sources/FreeCombine/Publishers/Zipped.swift index a9622b4..c01d865 100644 --- a/Sources/FreeCombine/Publishers/Zipped.swift +++ b/Sources/FreeCombine/Publishers/Zipped.swift @@ -7,30 +7,26 @@ public extension Publisher { func zip( - onCancel: @escaping () -> Void = { }, _ other: Publisher ) -> Publisher<(Output, Other)> { - Zipped(onCancel: onCancel, self, other) + Zipped(self, other) } } public func Zipped( - onCancel: @escaping () -> Void = { }, _ left: Publisher, _ right: Publisher ) -> Publisher<(Left, Right)> { - zip(onCancel: onCancel, left, right) + zip(left, right) } public func zip( - onCancel: @escaping () -> Void = { }, _ left: Publisher, _ right: Publisher ) -> Publisher<(Left, Right)> { .init( initialState: ZipState.create(left: left, right: right), buffering: .bufferingOldest(2), - onCancel: onCancel, reducer: Reducer( onCompletion: ZipState.complete, reducer: ZipState.reduce @@ -39,40 +35,36 @@ public func zip( } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher ) -> Publisher<(A, B, C)> { - zip(onCancel: onCancel, zip(one, two), three) + zip(zip(one, two), three) .map { ($0.0.0, $0.0.1, $0.1) } } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher, _ four: Publisher ) -> Publisher<(A, B, C, D)> { - zip(onCancel: onCancel, zip(one, two), zip(three, four)) + zip(zip(one, two), zip(three, four)) .map { ($0.0.0, $0.0.1, $0.1.0, $0.1.1) } } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher, _ four: Publisher, _ five: Publisher ) -> Publisher<(A, B, C, D, E)> { - zip(onCancel: onCancel, zip(zip(one, two), zip(three, four)), five) + zip(zip(zip(one, two), zip(three, four)), five) .map { ($0.0.0.0, $0.0.0.1, $0.0.1.0, $0.0.1.1, $0.1) } } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher, @@ -80,12 +72,11 @@ public func zip( _ five: Publisher, _ six: Publisher ) -> Publisher<(A, B, C, D, E, F)> { - zip(onCancel: onCancel, zip(zip(one, two), zip(three, four)), zip(five, six)) + zip(zip(zip(one, two), zip(three, four)), zip(five, six)) .map { ($0.0.0.0, $0.0.0.1, $0.0.1.0, $0.0.1.1, $0.1.0, $0.1.1) } } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher, @@ -94,12 +85,11 @@ public func zip( _ six: Publisher, _ seven: Publisher ) -> Publisher<(A, B, C, D, E, F, G)> { - zip(onCancel: onCancel, zip(zip(one, two), zip(three, four)), zip(zip(five, six), seven)) + zip(zip(zip(one, two), zip(three, four)), zip(zip(five, six), seven)) .map { ($0.0.0.0, $0.0.0.1, $0.0.1.0, $0.0.1.1, $0.1.0.0, $0.1.0.1, $0.1.1) } } public func zip( - onCancel: @escaping () -> Void = { }, _ one: Publisher, _ two: Publisher, _ three: Publisher, @@ -109,6 +99,6 @@ public func zip( _ seven: Publisher, _ eight: Publisher ) -> Publisher<(A, B, C, D, E, F, G, H)> { - zip(onCancel: onCancel, zip(zip(one, two), zip(three, four)), zip(zip(five, six), zip(seven, eight))) + zip(zip(zip(one, two), zip(three, four)), zip(zip(five, six), zip(seven, eight))) .map { ($0.0.0.0, $0.0.0.1, $0.0.1.0, $0.0.1.1, $0.1.0.0, $0.1.0.1, $0.1.1.0, $0.1.1.1) } } diff --git a/Sources/FreeCombine/State/StateTask.swift b/Sources/FreeCombine/State/StateTask.swift index c60c973..5c25479 100644 --- a/Sources/FreeCombine/State/StateTask.swift +++ b/Sources/FreeCombine/State/StateTask.swift @@ -64,12 +64,11 @@ extension StateTask { channel: Channel, initialState: @escaping (Channel) async -> State, onStartup: UnsafeContinuation? = .none, - onCancel: @Sendable @escaping () -> Void = { }, reducer: Reducer ) { self.init( channel: channel, - cancellable: .init { try await withTaskCancellationHandler(handler: onCancel) { + cancellable: .init { var state = await initialState(channel) onStartup?.resume() do { @@ -109,7 +108,7 @@ extension StateTask { } } return state - } } + } ) } @@ -128,7 +127,6 @@ public extension StateTask { static func stateTask( channel: Channel, initialState: @escaping (Channel) async -> State, - onCancel: @Sendable @escaping () -> Void = { }, reducer: Reducer ) async -> Self { var stateTask: Self! @@ -137,7 +135,6 @@ public extension StateTask { channel: channel, initialState: initialState, onStartup: stateTaskContinuation, - onCancel: onCancel, reducer: reducer ) } diff --git a/Sources/FreeCombine/Subject/CurrentValueSubject.swift b/Sources/FreeCombine/Subject/CurrentValueSubject.swift index 5a5ea0d..ea3f7a6 100644 --- a/Sources/FreeCombine/Subject/CurrentValueSubject.swift +++ b/Sources/FreeCombine/Subject/CurrentValueSubject.swift @@ -8,14 +8,12 @@ public func CurrentValueSubject( currentValue: Output, buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1), - onStartup: UnsafeContinuation? = .none, - onCancel: @Sendable @escaping () -> Void = { } + onStartup: UnsafeContinuation? = .none ) -> StateTask, DistributorState.Action> { .init( channel: .init(buffering: buffering), initialState: { _ in .init(currentValue: currentValue, nextKey: 0, downstreams: [:]) }, onStartup: onStartup, - onCancel: onCancel, reducer: Reducer( onCompletion: DistributorState.complete, reducer: DistributorState.reduce @@ -25,13 +23,11 @@ public func CurrentValueSubject( public func CurrentValueSubject( currentValue: Output, - buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1), - onCancel: @Sendable @escaping () -> Void = { } + buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1) ) async -> StateTask, DistributorState.Action> { await .stateTask( channel: .init(buffering: buffering), initialState: { channel in .init(currentValue: currentValue, nextKey: 0, downstreams: [:]) }, - onCancel: onCancel, reducer: Reducer( onCompletion: DistributorState.complete, reducer: DistributorState.reduce diff --git a/Sources/FreeCombine/Subject/PassthroughSubject.swift b/Sources/FreeCombine/Subject/PassthroughSubject.swift index c617567..7cb5b0e 100644 --- a/Sources/FreeCombine/Subject/PassthroughSubject.swift +++ b/Sources/FreeCombine/Subject/PassthroughSubject.swift @@ -7,14 +7,12 @@ public func PassthroughSubject( _ type: Output.Type = Output.self, buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1), - onStartup: UnsafeContinuation? = .none, - onCancel: @Sendable @escaping () -> Void = { } + onStartup: UnsafeContinuation? = .none ) -> StateTask, DistributorState.Action> { Channel.init(buffering: buffering) .stateTask( initialState: { channel in .init(currentValue: .none, nextKey: 0, downstreams: [:]) }, onStartup: onStartup, - onCancel: onCancel, reducer: Reducer( onCompletion: DistributorState.complete, reducer: DistributorState.reduce @@ -24,13 +22,11 @@ public func PassthroughSubject( public func PassthroughSubject( _ type: Output.Type = Output.self, - buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1), - onCancel: @Sendable @escaping () -> Void = { } + buffering: AsyncStream.Action>.Continuation.BufferingPolicy = .bufferingOldest(1) ) async -> StateTask, DistributorState.Action> { await Channel(buffering: buffering) .stateTask( initialState: { channel in .init(currentValue: .none, nextKey: 0, downstreams: [:]) }, - onCancel: onCancel, reducer: Reducer( onCompletion: DistributorState.complete, reducer: DistributorState.reduce diff --git a/Tests/FreeCombineTests/CombineLatestTests.swift b/Tests/FreeCombineTests/CombineLatestTests.swift new file mode 100644 index 0000000..da3a954 --- /dev/null +++ b/Tests/FreeCombineTests/CombineLatestTests.swift @@ -0,0 +1,271 @@ +// +// CombineLatestTests.swift +// +// +// Created by Van Simmons on 6/4/22. +// +import XCTest +@testable import FreeCombine + +class CombineLatestTests: XCTestCase { + + override func setUpWithError() throws { } + + override func tearDownWithError() throws { } + + func testSimpleJustCombineLatest() async throws { + let expectation = await CheckedExpectation() + + let publisher1 = Just(100) + let publisher2 = Just("abcdefghijklmnopqrstuvwxyz") + + let counter = Counter() + + let c1 = await combineLatest(publisher1, publisher2) + .sink { (result: AsyncStream<(Int?, String?)>.Result) in + let count = await counter.count + switch result { + case .value: + _ = await counter.increment() + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + case .completion(.finished): + XCTAssert(count == 2, "wrong number of values sent: \(count)") + do { try await expectation.complete() } + catch { XCTFail("Failed to complete: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + return .more + } + + do { try await FreeCombine.wait(for: expectation, timeout: 100_000_000) } + catch { + let count = await counter.count + XCTFail("Timed out, count = \(count)") + } + c1.cancel() + } + + func testEmptyCombineLatest() async throws { + let expectation = await CheckedExpectation() + + let publisher1 = Just(100) + let publisher2 = Empty(String.self) + + let counter = Counter() + + let z1 = await combineLatest(publisher1, publisher2) + .sink { (result: AsyncStream<(Int?, String?)>.Result) in + let count = await counter.count + switch result { + case .value: + _ = await counter.increment() + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + case .completion(.finished): + XCTAssert(count == 1, "wrong number of values sent: \(count)") + do { try await expectation.complete() } + catch { XCTFail("Failed to complete: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + return .more + } + + do { try await FreeCombine.wait(for: expectation, timeout: 100_000_000) } + catch { + let count = await counter.count + XCTFail("Timed out, count = \(count)") + } + z1.cancel() + } + + func testSimpleSequenceCombineLatest() async throws { + let expectation = await CheckedExpectation() + + let publisher1 = (0 ..< 100).asyncPublisher + let publisher2 = "abcdefghijklmnopqrstuvwxyz".asyncPublisher + + let counter = Counter() + let z1 = await combineLatest(publisher1, publisher2) + .sink { (result: AsyncStream<(Int?, Character?)>.Result) in + let count = await counter.count + switch result { + case .value: + _ = await counter.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + XCTAssert(count == 126, "wrong number of values sent: \(count)") + do { try await expectation.complete() } + catch { XCTFail("Failed to complete: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + } + + do { try await FreeCombine.wait(for: expectation, timeout: 100_000_000) } + catch { + let count = await counter.count + XCTFail("Timed out, count = \(count)") + } + z1.cancel() + } + + func testSimpleCombineLatest() async throws { + let expectation = await CheckedExpectation() + + let publisher1 = (0 ..< 100).asyncPublisher + let publisher2 = Unfolded("abcdefghijklmnopqrstuvwxyz") + + let counter = Counter() + + let z1 = await combineLatest(publisher1, publisher2) + .map {value in ((value.0 ?? 0) + 100, (value.1 ?? Character(" ")).uppercased()) } + .sink({ result in + switch result { + case .value: + await counter.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + let count = await counter.count + XCTAssert(count == 126, "wrong number of values sent: \(count)") + do { try await expectation.complete() } + catch { XCTFail("Failed to complete: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + }) + + do { try await FreeCombine.wait(for: expectation, timeout: 100_000_000) } + catch { + let count = await counter.count + XCTFail("Timed out, count = \(count)") + } + z1.cancel() + } + + func testComplexCombineLatest() async throws { + let expectation = await CheckedExpectation() + + let p1 = Unfolded(0 ... 100) + let p2 = Unfolded("abcdefghijklmnopqrstuvwxyz") + let p3 = Unfolded(0 ... 100) + let p4 = Unfolded("abcdefghijklmnopqrstuvwxyz") + + let counter = Counter() + let z1 = await combineLatest(p1, p2, p3, p4) + .map { v in + ((v.0 ?? 0) + 100, (v.1 ?? Character(" ")).uppercased(), (v.2 ?? 0) + 110, (v.3 ?? Character(" "))) + } + .sink({ result in + switch result { + case .value: + await counter.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + let count = await counter.count + XCTAssert(count == 254, "wrong number of values sent: \(count)") + do { try await expectation.complete() } + catch { XCTFail("Multiple terminations sent: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + }) + + do { try await FreeCombine.wait(for: expectation, timeout: 100_000_000) } + catch { + let count = await counter.count + XCTFail("Timed out, count = \(count)") + } + z1.cancel() + } + + func testMultiComplexCombineLatest() async throws { + let expectation1 = await CheckedExpectation() + let expectation2 = await CheckedExpectation() + + let p1 = Unfolded(0 ..< 100) + let p2 = Unfolded("abcdefghijklmnopqrstuvwxyz") + let p3 = Unfolded(0 ..< 100) + let p4 = Unfolded("abcdefghijklmnopqrstuvwxyz") + + let combineLatest = combineLatest(p1, p2, p3, p4) + + let count1 = Counter() + let z1 = await combineLatest + .map { v in + ((v.0 ?? 0) + 100, (v.1 ?? Character(" ")).uppercased(), (v.2 ?? 0) + 110, v.3 ?? Character(" ")) + } + .sink({ result in + switch result { + case .value: + await count1.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + let count = await count1.count + XCTAssert(count == 252, "wrong number of values sent: \(count)") + try await expectation1.complete() + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + }) + + let count2 = Counter() + let z2 = await combineLatest + .map { v in + ((v.0 ?? 0) + 100, (v.1 ?? Character(" ")).uppercased(), (v.2 ?? 0) + 110, v.3 ?? Character(" ")) + } + .sink({ result in + switch result { + case .value: + await count2.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .more + case .completion(.finished): + let count = await count2.count + XCTAssert(count == 252, "wrong number of values sent: \(count)") + try await expectation2.complete() + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + }) + + do { + try await FreeCombine.wait(for: expectation1, timeout: 100_000_000) + try await FreeCombine.wait(for: expectation2, timeout: 100_000_000) + } catch { + XCTFail("Timed out") + } + z1.cancel() + z2.cancel() + } +} diff --git a/Tests/FreeCombineTests/DropFirstTests.swift b/Tests/FreeCombineTests/DropFirstTests.swift new file mode 100644 index 0000000..7a5206a --- /dev/null +++ b/Tests/FreeCombineTests/DropFirstTests.swift @@ -0,0 +1,60 @@ +// +// DropFirstTests.swift +// +// +// Created by Van Simmons on 6/4/22. +// + +import XCTest +@testable import FreeCombine + +class DropFirstTests: XCTestCase { + + override func setUpWithError() throws { } + + override func tearDownWithError() throws { } + + func testSimpleDropFirst() async throws { + let expectation1 = await CheckedExpectation() + let unfolded = (0 ..< 100).asyncPublisher + + let counter1 = Counter() + let u1 = await unfolded + .dropFirst(27) + .sink { (result: AsyncStream.Result) in + switch result { + case .value: + await counter1.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + let count = await counter1.count + guard count == 73 else { + XCTFail("Incorrect count: \(count) in subscription 1") + return .done + } + do { + try await expectation1.complete() + } + catch { XCTFail("Failed to complete with error: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + } + do { + try await FreeCombine.wait(for: expectation1, timeout: 100_000_000) + } catch { + XCTFail("Timed out") + } + do { + let finalValue = try await u1.value + XCTAssert(finalValue == .done, "Did not complete") + } catch { + XCTFail("Failed to get final value") + } + } +} diff --git a/Tests/FreeCombineTests/FilterTests.swift b/Tests/FreeCombineTests/FilterTests.swift new file mode 100644 index 0000000..0afd0d4 --- /dev/null +++ b/Tests/FreeCombineTests/FilterTests.swift @@ -0,0 +1,59 @@ +// +// FilterTests.swift +// +// +// Created by Van Simmons on 6/4/22. +// +import XCTest +@testable import FreeCombine + +class FilterTests: XCTestCase { + + override func setUpWithError() throws { } + + override func tearDownWithError() throws { } + + func testSimpleFilter() async throws { + let expectation1 = await CheckedExpectation() + let unfolded = (0 ..< 100).asyncPublisher + + let counter1 = Counter() + let u1 = await unfolded + .filter { $0 % 3 == 0 } + .sink { (result: AsyncStream.Result) in + switch result { + case .value: + await counter1.increment() + return .more + case let .completion(.failure(error)): + XCTFail("Got an error? \(error)") + return .done + case .completion(.finished): + let count = await counter1.count + guard count == 34 else { + XCTFail("Incorrect count: \(count) in subscription 1") + return .done + } + do { + try await expectation1.complete() + } + catch { XCTFail("Failed to complete with error: \(error)") } + return .done + case .completion(.cancelled): + XCTFail("Should not have cancelled") + return .done + } + } + do { + try await FreeCombine.wait(for: expectation1, timeout: 100_000_000) + } catch { + XCTFail("Timed out") + } + do { + let finalValue = try await u1.value + XCTAssert(finalValue == .done, "Did not complete") + } catch { + XCTFail("Failed to get final value") + } + } +}